You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The current CLI has a few structural issues that hurt discoverability and consistency:
Verbs and nouns are mixed at the root. Some top-level commands are actions (ssh, cp, port-forward), others are resource groups (playground, content, challenge, expose, auth). There is no single mental model for where to look for a command.
Content types are fragmented across top-level groups. Playgrounds are managed under playground (create, update, remove), but every other authorable kind lives under content (create, push, pull, remove). Challenges are split again: catalog and runtime under challenge, with no authoring verbs at all. Users have to remember which container a given content kind lives in.
Author workflows and user workflows share the same groups, and so do content definitions and running instances. These are two angles on the same problem. Under playground today, authoring commands (create, update, remove, manifest) operate on a content definition for an author audience, while user-facing commands (start, stop, list, machines) operate on a running instance for a consumer audience. One group, two audiences, two different resources that happen to share a name. challenge has the same split (definition vs. running attempt).
Commands are not grouped in the CLI help output.labctl --help lists every top-level command in a single flat list. A purpose-based grouping (authoring vs. runtime vs. access, etc.) would make the help dramatically easier to scan, and would teach the structure just by looking at it.
Note
The current command reference is available at the bottom of this issue in the Reference section.
Ideas
A few directions worth considering:
Pure verb-based structure: start, stop, create, remove, get, ... at the root; nouns become arguments.
Pure noun-based structure: playground, challenge, tutorial, play, ... at the root; verbs live underneath. Matches the current direction but applied consistently.
Group all content-authoring commands together (playground definitions and every other content kind under one verb set or group).
Group all runtime commands together (start/stop/ssh/cp/expose all operate on a running play).
Group commands by purpose in --help so the entry point itself teaches the structure.
Proposal: kubectl-style interface
Use a kubectl-like design:
A small set of universal verbs at the root: get, create, remove, start, stop, push, pull, expose, unexpose, ...
Resources as arguments: get playgrounds, start playground <id>, remove tutorial <id>.
Introduce play as the runtime noun, distinct from playground (the content/template).
get subsumes list, catalog, and manifest inspection (via -o yaml).
--help groups commands by purpose (content authoring, runtime, access, meta).
This gives one mental model, separates template from instance, and removes the verb/noun split at the root.
Content kinds in scope: playground, challenge, tutorial, training, course, skill-path.
Brainstorm: content vs. running content
This is the trickiest part of the redesign. A challenge (or playground, tutorial, etc.) has two lives: the piece of content someone authored, and the running instance someone is using. The same noun means different things to different audiences. From a user's perspective a challenge is "something I am solving." From an author's perspective it is content they wrote. Any naming we pick has to support both without losing clarity.
Option A: Collapse runtime into a single play noun
get plays lists all running instances. A row shows what content backs each play: kind=challenge, content=kubernetes-pod-networking.
Filter shortcuts: get plays --kind challenge.
Content commands stay kind-named: get challenges means challenge content, get playgrounds means playground content.
Pro: one runtime noun, minimal new surface.
Con: breaks the user's mental model ("I am solving a challenge," not "I have a play of kind challenge"). The noun they care about disappears from the verb they use.
Option B: Keep a per-kind runtime noun (play, attempt, ...)
get plays for running playground instances.
get attempts for running challenge attempts.
Content commands stay kind-named as before.
Pro: matches each audience's mental model.
Con: every startable content kind needs its own runtime noun. Doesn't scale cleanly if tutorial/training/course also instantiate runtime.
Option C: Namespace content and runtime under the same verb
get <kind> means content; a prefix flags runtime: get running <kind>, or get running --kind challenge.
Alternatively explicit on both sides: get content challenges vs. get running challenges.
Pro: both perspectives get a first-class name; same verb works for both.
Con: "running" is an awkward word in this role, and more words per command. Less kubectl-native.
Option D: Use different verbs for content and runtime
Keep get for runtime resources only: get plays, get ports, get shells, get machines, and get challenges means running challenge attempts.
Use a different verb for content: a reading verb that pairs naturally with the author-facing verb set (create, remove, push, pull). Candidates: list, show, browse, find, describe, or keep catalog.
User verbs: start, stop, get, expose, ssh, cp, ...
Pro: cleanest separation of author and user command surfaces. Each verb has one meaning. Solves problem no. 3 at the vocabulary level, not just grouping.
Con: two verbs for "look at something" is more to teach, and the public catalog (what users browse to pick something to start) has to land on one side. Arguably browsing the catalog is a user action, so the content verb has to cover both "my authored items" and "public catalog" via a filter.
Option E: Context-sensitive defaults
get challenges means one thing to users (running attempts) and another to authors (content), based on role or flag.
Pro: short commands for the common case.
Con: magical; hard to teach; hard to script against safely.
Important
Author's preference: Option D. It turns the "author vs. user" problem into a vocabulary split rather than a grouping or filtering convention, and keeps each verb with a single meaning. The exact wording of the content-reading verb (list, show, browse, ...) is still up for debate. Option A stays the simplest fallback if we can accept "play" covering all runtime instances.
Proposed structure
The tables below assume Option D from the brainstorm. The content-reading verb is written as list for concreteness, but the exact word is open (see the brainstorm and open questions).
Top-level groups (all commands live at the root; grouping is only for documentation and --help rendering):
Auth: auth login, auth logout, auth whoami
Content management (author): create, remove, push, pull, list <kind>
Catalog & inspection (mixed): sits between authoring and use (browsing the public catalog is a user action; browsing --mine is an author action)
Runtime (plays) (user): start, stop, get plays, get machines
Play access (user, name TBD): expose, unexpose, cp, ssh, ssh-proxy, port-forward
Meta: completion, version
An alternative framing to consider: group explicitly by audience (common, author, user) rather than by purpose. Common = auth, catalog, meta. Author = content management. User = runtime + play access.
Runtime and play access are closely related (both operate on a running play), so they are listed adjacently. get is used on both sides (get plays, get machines in runtime; get ports, get shells in play access); we could split them out into a dedicated get section if that reads better, or just keep them where each resource belongs.
Auth
Keep auth as its own group. Strictly, a verbs-only root would promote these, but auth is a small, self-contained set and the grouping is worth the exception.
New
Old
Notes
auth login
auth login
unchanged
auth logout
auth logout
unchanged
auth whoami
auth whoami
unchanged
Alternative (not preferred, but worth listing): promote to login / logout / whoami at the root, or alias the root forms to auth *.
Content management
Author-facing verbs. Each operates on a content kind.
push and the ID argument.push playground reads the name from the manifest today, while push challenge <id> would need the ID explicitly. Since each kind gets its own subcommand, per-kind signatures are fine: push playground takes no ID, push challenge <id> does. Flagging as a side issue, not a blocker.
push/pull naming is generic. Alternatives: upload/download, apply/fetch, sync. Precedent for push/pull exists (helm, docker, git), so the terms are familiar, but they do not convey "content authoring" on their own.
push vs. playground update remains open: collapse into one, or keep both with distinct semantics (iterative sync vs. one-shot manifest apply)?
Catalog & inspection
Under Option D, the catalog is content and uses the content-reading verb (shown as list here). Browsing the public catalog is a user action, and viewing --mine is an author action, so this group straddles audiences. If Option A wins, these all collapse to get <kind>.
New
Old
Notes
list playgrounds
playground catalog
catalog joins the content-reading verb
list challenges
challenge catalog
list tutorials
(none)
new kind surfaced at root
list trainings
(none)
new kind surfaced at root
list courses
(none)
new kind surfaced at root
list skill-paths
(none)
new kind surfaced at root
list <kind> --mine
content list
filter to content the caller authored. Open: also support list content --mine as a cross-kind aggregator?
list <kind> <id> -o yaml
playground manifest
manifest view of a content definition. Open: also surface runtime state via get play <id> -o yaml (runtime side)?
Runtime (plays)
Under Option D, get is the user-facing read verb for runtime resources. get challenges therefore means "my running challenge attempts", not the challenge catalog.
New
Old
Notes
start <kind> <id>
playground start, challenge start
content kind + id produces a play. Open: do tutorial/training/course/skill-path instantiate runtime, or are they read-only?
stop <play-id>
playground stop, challenge stop
always a play id
get plays
playground list
running plays
get challenges
challenge list
running challenge attempts (user-facing naming)
get machines --play <id>
playground machines
scoped list; could fold into get play <id> output
Play access
The name needs work. "Exposure" is too narrow (doesn't cover ssh/cp). "Connectivity" doesn't cover expose. This group is really about interacting with a running play: exposing endpoints, connecting to the machine, moving files. Candidates: Play access, Interacting with plays, Play I/O.
New
Old
Notes
expose port
expose port
unchanged
expose shell
expose shell
unchanged
unexpose port <id>
expose remove
parallel to expose port; split by type. Alternative verb: close port <id>
unexpose shell <id>
expose remove
parallel to expose shell; split by type. Alternative verb: close shell <id>
get ports
expose list
split by type
get shells
expose list
split by type
cp
cp
unchanged
ssh
ssh
unchanged
ssh-proxy
ssh-proxy
unchanged
port-forward
port-forward
unchanged
Meta
New
Old
Notes
completion {bash|zsh|fish|powershell}
completion ...
unchanged
version
version
unchanged
Backwards compatibility
This redesign is almost entirely a renaming and regrouping exercise; no existing command's behavior needs to change. That opens a low-risk migration path:
Keep existing commands in place.labctl playground start, labctl expose list, etc. keep working exactly as they do today. Scripts and pipelines that depend on the current CLI continue to function without any change.
Hide the old commands from help output. New structure shows in labctl --help and the per-group help pages; old commands are accessible but hidden (e.g., marked hidden in the cobra command definitions). New users see only the new structure; existing users are not broken.
Deprecation period. Old commands emit a deprecation notice on stderr pointing to the new equivalent. Length of the period is a product decision (one minor release, two releases, a fixed calendar window, ...).
Removal. After the deprecation period, old commands are deleted. Any scripts still using them break loudly at that point, with a clear error that names the replacement.
This way we get the clean new structure in docs and tab completion immediately, without breaking anyone on day one.
Open: length of the deprecation window, and whether a runtime flag (or env var) can re-enable hidden commands in help for users who want to see both during the transition.
Open questions
Content vs. runtime strategy: which of Options A/B/C/D/E from the brainstorm? Author's lean is Option D.
push/pull naming: keep (helm, docker, git precedent), or rename to upload/download, apply/fetch, sync?
push vs. update: collapse playground update into push playground, or keep both with distinct semantics (iterative file sync vs. one-shot manifest apply)?
Manifest view target: content definition via the content verb (list <kind> <id> -o yaml), runtime state via get play <id> -o yaml, or both?
Content cross-kind listing: keep a list content --mine aggregator, or force a specific kind?
Startable kinds: do tutorial/training/course/skill-path instantiate plays, or are they read-only content?
Grouping layout: group by purpose (auth / content / catalog / runtime / play access / meta) or by audience (common / author / user)?
Dedicated get section: get appears in both runtime and play access groups. Should the docs/help pull all get * commands into one section, or keep them colocated with their resources?
auth at root: keep as a group (author's lean) or promote to login/logout/whoami?
Play access group naming: what should we call the cluster that includes expose, unexpose, cp, ssh, ssh-proxy, port-forward?
unexpose vs. close: which verb for tearing down an exposed port or shell?
Aliases: preserve today's aliases (ch, p, e, ex, ...) during a deprecation period, or drop them with the rewrite?
Reference
Current command list
Command
Aliases
Description
labctl auth login
Log in via browser one-time URL
labctl auth logout
Log out current CLI session
labctl auth whoami
Print current user info
labctl challenge catalog
ch, challenges
List available challenges (filter by category/status)
labctl challenge list
List running challenge attempts
labctl challenge start
Start a challenge attempt
labctl challenge stop
Stop current challenge attempt
labctl completion bash
Generate bash autocompletion script
labctl completion fish
Generate fish autocompletion script
labctl completion powershell
Generate powershell autocompletion script
labctl completion zsh
Generate zsh autocompletion script
labctl content create
c, contents
Create a new piece of content
labctl content list
List authored content (filter by kind)
labctl content pull
Pull remote content files to local dir
labctl content push
Push local content files to remote (inner author loop)
labctl content remove
Remove authored content
labctl cp
Copy files to/from target playground
labctl expose list
e, ex
List exposed ports and web terminals
labctl expose port
Expose an HTTP(s) service from a playground
labctl expose remove
Un-expose a previously exposed port/shell by ID
labctl expose shell
Expose a web terminal session with a URL
labctl playground catalog
p, playgrounds
List playgrounds from catalog (filter by type)
labctl playground create
Create a new playground from base + manifest
labctl playground list
List current/recent playgrounds (up to 50)
labctl playground machines
List machines of a playground by ID
labctl playground manifest
View playground manifest
labctl playground remove
Remove an authored playground
labctl playground start
Start a new playground (optionally open in browser)
labctl playground stop
Stop one or more playgrounds
labctl playground update
Update existing playground from manifest
labctl port-forward
Forward local/remote ports to a running playground
Problems
The current CLI has a few structural issues that hurt discoverability and consistency:
Verbs and nouns are mixed at the root. Some top-level commands are actions (
ssh,cp,port-forward), others are resource groups (playground,content,challenge,expose,auth). There is no single mental model for where to look for a command.Content types are fragmented across top-level groups. Playgrounds are managed under
playground(create,update,remove), but every other authorable kind lives undercontent(create,push,pull,remove). Challenges are split again: catalog and runtime underchallenge, with no authoring verbs at all. Users have to remember which container a given content kind lives in.Author workflows and user workflows share the same groups, and so do content definitions and running instances. These are two angles on the same problem. Under
playgroundtoday, authoring commands (create,update,remove,manifest) operate on a content definition for an author audience, while user-facing commands (start,stop,list,machines) operate on a running instance for a consumer audience. One group, two audiences, two different resources that happen to share a name.challengehas the same split (definition vs. running attempt).Commands are not grouped in the CLI help output.
labctl --helplists every top-level command in a single flat list. A purpose-based grouping (authoring vs. runtime vs. access, etc.) would make the help dramatically easier to scan, and would teach the structure just by looking at it.Note
The current command reference is available at the bottom of this issue in the Reference section.
Ideas
A few directions worth considering:
start,stop,create,remove,get, ... at the root; nouns become arguments.playground,challenge,tutorial,play, ... at the root; verbs live underneath. Matches the current direction but applied consistently.start/stop/ssh/cp/exposeall operate on a running play).--helpso the entry point itself teaches the structure.Proposal: kubectl-style interface
Use a kubectl-like design:
get,create,remove,start,stop,push,pull,expose,unexpose, ...get playgrounds,start playground <id>,remove tutorial <id>.getsubsumeslist,catalog, and manifest inspection (via-o yaml).--helpgroups commands by purpose (content authoring, runtime, access, meta).This gives one mental model, separates template from instance, and removes the verb/noun split at the root.
Content kinds in scope: playground, challenge, tutorial, training, course, skill-path.
Brainstorm: content vs. running content
This is the trickiest part of the redesign. A challenge (or playground, tutorial, etc.) has two lives: the piece of content someone authored, and the running instance someone is using. The same noun means different things to different audiences. From a user's perspective a challenge is "something I am solving." From an author's perspective it is content they wrote. Any naming we pick has to support both without losing clarity.
Option A: Collapse runtime into a single
playnounget playslists all running instances. A row shows what content backs each play:kind=challenge, content=kubernetes-pod-networking.get plays --kind challenge.get challengesmeans challenge content,get playgroundsmeans playground content.Option B: Keep a per-kind runtime noun (
play,attempt, ...)get playsfor running playground instances.get attemptsfor running challenge attempts.Option C: Namespace content and runtime under the same verb
get <kind>means content; a prefix flags runtime:get running <kind>, orget running --kind challenge.get content challengesvs.get running challenges.Option D: Use different verbs for content and runtime
getfor runtime resources only:get plays,get ports,get shells,get machines, andget challengesmeans running challenge attempts.create,remove,push,pull). Candidates:list,show,browse,find,describe, or keepcatalog.create,remove,push,pull,list/show/browse<kind>.start,stop,get,expose,ssh,cp, ...Option E: Context-sensitive defaults
get challengesmeans one thing to users (running attempts) and another to authors (content), based on role or flag.Important
Author's preference: Option D. It turns the "author vs. user" problem into a vocabulary split rather than a grouping or filtering convention, and keeps each verb with a single meaning. The exact wording of the content-reading verb (
list,show,browse, ...) is still up for debate. Option A stays the simplest fallback if we can accept "play" covering all runtime instances.Proposed structure
The tables below assume Option D from the brainstorm. The content-reading verb is written as
listfor concreteness, but the exact word is open (see the brainstorm and open questions).Top-level groups (all commands live at the root; grouping is only for documentation and
--helprendering):auth login,auth logout,auth whoamicreate,remove,push,pull,list <kind>--mineis an author action)start,stop,get plays,get machinesexpose,unexpose,cp,ssh,ssh-proxy,port-forwardcompletion,versionAn alternative framing to consider: group explicitly by audience (common, author, user) rather than by purpose. Common = auth, catalog, meta. Author = content management. User = runtime + play access.
Runtime and play access are closely related (both operate on a running play), so they are listed adjacently.
getis used on both sides (get plays,get machinesin runtime;get ports,get shellsin play access); we could split them out into a dedicatedgetsection if that reads better, or just keep them where each resource belongs.Auth
Keep
authas its own group. Strictly, a verbs-only root would promote these, but auth is a small, self-contained set and the grouping is worth the exception.auth loginauth loginauth logoutauth logoutauth whoamiauth whoamiAlternative (not preferred, but worth listing): promote to
login/logout/whoamiat the root, or alias the root forms toauth *.Content management
Author-facing verbs. Each operates on a content kind.
create <kind>content create,playground createremove <kind> <id>content remove,playground removestoppush <kind> [<id>]content push,playground update?pull <kind> <id>content pulllist <kind>content list(--minefor authored)list,show,browse,find,describe,catalog)Notes:
pushand the ID argument.push playgroundreads the name from the manifest today, whilepush challenge <id>would need the ID explicitly. Since each kind gets its own subcommand, per-kind signatures are fine:push playgroundtakes no ID,push challenge <id>does. Flagging as a side issue, not a blocker.push/pullnaming is generic. Alternatives:upload/download,apply/fetch,sync. Precedent forpush/pullexists (helm, docker, git), so the terms are familiar, but they do not convey "content authoring" on their own.pushvs.playground updateremains open: collapse into one, or keep both with distinct semantics (iterative sync vs. one-shot manifest apply)?Catalog & inspection
Under Option D, the catalog is content and uses the content-reading verb (shown as
listhere). Browsing the public catalog is a user action, and viewing--mineis an author action, so this group straddles audiences. If Option A wins, these all collapse toget <kind>.list playgroundsplayground cataloglist challengeschallenge cataloglist tutorialslist trainingslist courseslist skill-pathslist <kind> --minecontent listlist content --mineas a cross-kind aggregator?list <kind> <id> -o yamlplayground manifestget play <id> -o yaml(runtime side)?Runtime (plays)
Under Option D,
getis the user-facing read verb for runtime resources.get challengestherefore means "my running challenge attempts", not the challenge catalog.start <kind> <id>playground start,challenge startstop <play-id>playground stop,challenge stopget playsplayground listget challengeschallenge listget machines --play <id>playground machinesget play <id>outputPlay access
The name needs work. "Exposure" is too narrow (doesn't cover ssh/cp). "Connectivity" doesn't cover
expose. This group is really about interacting with a running play: exposing endpoints, connecting to the machine, moving files. Candidates: Play access, Interacting with plays, Play I/O.expose portexpose portexpose shellexpose shellunexpose port <id>expose removeexpose port; split by type. Alternative verb:close port <id>unexpose shell <id>expose removeexpose shell; split by type. Alternative verb:close shell <id>get portsexpose listget shellsexpose listcpcpsshsshssh-proxyssh-proxyport-forwardport-forwardMeta
completion {bash|zsh|fish|powershell}completion ...versionversionBackwards compatibility
This redesign is almost entirely a renaming and regrouping exercise; no existing command's behavior needs to change. That opens a low-risk migration path:
labctl playground start,labctl expose list, etc. keep working exactly as they do today. Scripts and pipelines that depend on the current CLI continue to function without any change.labctl --helpand the per-group help pages; old commands are accessible but hidden (e.g., marked hidden in the cobra command definitions). New users see only the new structure; existing users are not broken.This way we get the clean new structure in docs and tab completion immediately, without breaking anyone on day one.
Open: length of the deprecation window, and whether a runtime flag (or env var) can re-enable hidden commands in help for users who want to see both during the transition.
Open questions
list,show,browse,find,describe, or keepcatalog?push/pullnaming: keep (helm, docker, git precedent), or rename toupload/download,apply/fetch,sync?pushvs.update: collapseplayground updateintopush playground, or keep both with distinct semantics (iterative file sync vs. one-shot manifest apply)?list <kind> <id> -o yaml), runtime state viaget play <id> -o yaml, or both?list content --mineaggregator, or force a specific kind?getsection:getappears in both runtime and play access groups. Should the docs/help pull allget *commands into one section, or keep them colocated with their resources?authat root: keep as a group (author's lean) or promote tologin/logout/whoami?expose,unexpose,cp,ssh,ssh-proxy,port-forward?unexposevs.close: which verb for tearing down an exposed port or shell?ch,p,e,ex, ...) during a deprecation period, or drop them with the rewrite?Reference
Current command list
labctl auth loginlabctl auth logoutlabctl auth whoamilabctl challenge catalogch,challengeslabctl challenge listlabctl challenge startlabctl challenge stoplabctl completion bashlabctl completion fishlabctl completion powershelllabctl completion zshlabctl content createc,contentslabctl content listlabctl content pulllabctl content pushlabctl content removelabctl cplabctl expose liste,exlabctl expose portlabctl expose removelabctl expose shelllabctl playground catalogp,playgroundslabctl playground createlabctl playground listlabctl playground machineslabctl playground manifestlabctl playground removelabctl playground startlabctl playground stoplabctl playground updatelabctl port-forwardlabctl sshlabctl ssh-proxylabctl version