@harness-lab/cli 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,9 +9,14 @@ Current shipped scope:
9
9
  - `harness auth login`
10
10
  - `harness auth logout`
11
11
  - `harness auth status`
12
+ - `harness workshop current-instance`
13
+ - `harness workshop select-instance`
12
14
  - `harness workshop status`
15
+ - `harness workshop list-instances`
16
+ - `harness workshop show-instance`
13
17
  - `harness workshop create-instance`
14
18
  - `harness workshop update-instance`
19
+ - `harness workshop reset-instance`
15
20
  - `harness workshop prepare`
16
21
  - `harness workshop remove-instance`
17
22
  - `harness workshop archive`
@@ -120,16 +125,37 @@ Workshop commands:
120
125
  ```bash
121
126
  harness auth status
122
127
  harness skill install
128
+ harness workshop list-instances
129
+ harness workshop select-instance sample-workshop-demo-orbit
130
+ harness workshop current-instance
123
131
  harness workshop status
132
+ harness workshop show-instance sample-workshop-demo-orbit
124
133
  harness workshop create-instance sample-workshop-demo-orbit --event-title "Sample Workshop Demo"
125
- harness workshop update-instance sample-workshop-demo-orbit --room-name Orbit
126
- harness workshop prepare sample-workshop-demo-orbit
127
- harness workshop remove-instance sample-workshop-demo-orbit
134
+ harness workshop update-instance --room-name Orbit
135
+ harness workshop reset-instance --template-id blueprint-default
136
+ harness workshop prepare
137
+ harness workshop remove-instance
128
138
  harness workshop phase set rotation
129
139
  harness workshop archive --notes "Manual archive"
140
+ harness workshop select-instance --clear
130
141
  harness auth logout
131
142
  ```
132
143
 
144
+ Targeting model:
145
+
146
+ - `harness workshop list-instances` is the discovery entrypoint for facilitator-visible workshops
147
+ - `harness workshop select-instance <instance-id>` stores a local current target for later workshop commands
148
+ - `harness workshop current-instance` reports the stored target and resolves its current server state
149
+ - `harness workshop status` and `harness workshop phase set <phase-id>` use the selected instance when present, otherwise they fall back to deployment default behavior
150
+ - `harness workshop show-instance`, `update-instance`, `reset-instance`, `prepare`, and `remove-instance` accept an explicit `<instance-id>` but may also use the stored selection as a fallback
151
+ - `harness workshop select-instance --clear` removes the stored selection
152
+ - `HARNESS_WORKSHOP_INSTANCE_ID` remains an environment fallback when no local selection is stored
153
+
154
+ Machine-readable output:
155
+
156
+ - `harness --json ...` prints strict JSON output without headings
157
+ - prefer this for agent or script consumption instead of parsing human-oriented terminal copy
158
+
133
159
  Facilitator lifecycle commands are intentionally CLI-first:
134
160
 
135
161
  - skill invokes `harness`
@@ -214,18 +214,34 @@ This is a facilitator-only command — do not surface to participants.
214
214
 
215
215
  Show the current instance state, agenda phase, facilitator list, and team count.
216
216
  Requires active facilitator session.
217
+ If a local current instance is selected, prefer that target over deployment-default status.
218
+ When an agent needs machine-readable output, prefer `harness --json workshop status`.
219
+
220
+ ### `workshop facilitator current-instance`
221
+
222
+ Show the locally selected facilitator target, where it came from, and the resolved instance record.
223
+ Prefer invoking `harness workshop current-instance`.
224
+ Use this to confirm the CLI target before update, reset, prepare, remove, or phase operations.
225
+
226
+ ### `workshop facilitator select-instance <instance-id>`
227
+
228
+ Persist a facilitator-local current target for later workshop commands.
229
+ Prefer invoking `harness workshop select-instance <instance-id>`.
230
+ Use `harness workshop select-instance --clear` to remove the stored selection.
217
231
 
218
232
  ### `workshop facilitator list-instances`
219
233
 
220
234
  List the facilitator-visible workshop instance registry.
221
235
  Prefer invoking `harness workshop list-instances` over raw API scripts or local session-file inspection.
222
236
  Use this when the facilitator needs to discover what currently exists on a shared dashboard before choosing an explicit instance for reset, update, or agenda work.
237
+ When an agent needs strict parsing, prefer `harness --json workshop list-instances`.
223
238
 
224
239
  ### `workshop facilitator show-instance <instance-id>`
225
240
 
226
241
  Inspect one explicit workshop instance.
227
242
  Prefer invoking `harness workshop show-instance <instance-id>` over raw API scripts.
228
243
  Use this when the facilitator needs the full record for one instance rather than the deployment-default runtime status returned by `workshop facilitator status`.
244
+ If a current instance is already selected, the CLI may omit `<instance-id>` and use the stored target.
229
245
 
230
246
  ### `workshop facilitator grant <email> <role>`
231
247
 
@@ -263,6 +279,7 @@ Support `contentLang` changes explicitly so facilitators can choose workshop del
263
279
 
264
280
  Facilitator discovery rule:
265
281
  - for routine discovery, use `harness workshop list-instances` and `harness workshop show-instance`
282
+ - for repeated work on one live workshop, use `harness workshop select-instance <instance-id>` and `harness workshop current-instance`
266
283
  - do not read local CLI session files or improvise authenticated `node -e` fetch scripts unless you are diagnosing the CLI itself
267
284
 
268
285
  ### `workshop facilitator reset-instance <instance-id>`
@@ -270,16 +287,19 @@ Facilitator discovery rule:
270
287
  Reset one existing workshop instance from the selected blueprint template. Requires facilitator session.
271
288
  Prefer invoking `harness workshop reset-instance` over raw API scripts.
272
289
  Use this when the facilitator wants fresh canonical agenda, runner, and presenter content for a live instance and accepts that local runtime state will be reinitialized.
290
+ If a current instance is already selected, the CLI may omit `<instance-id>` and use the stored target.
273
291
 
274
292
  ### `workshop facilitator remove-instance`
275
293
 
276
294
  Remove a workshop instance from the active list without deleting its archive history. Requires facilitator session.
277
295
  Prefer invoking `harness workshop remove-instance`.
296
+ If a current instance is already selected, the CLI may omit `<instance-id>` and use the stored target.
278
297
 
279
298
  ### `workshop facilitator prepare`
280
299
 
281
300
  Set the current instance to `prepared` state. Verify event code is ready.
282
301
  Prefer invoking `harness workshop prepare`.
302
+ If a current instance is already selected, the CLI may omit `<instance-id>` and use the stored target.
283
303
 
284
304
  ### `workshop facilitator agenda`
285
305
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "manifestVersion": 1,
3
3
  "bundleName": "harness-lab-workshop",
4
- "bundleVersion": "0.2.8",
5
- "contentHash": "6194a59089ff94131b647282c1b5e7574a6a9d112c48b4b9d58b600f5775736a",
4
+ "bundleVersion": "0.2.9",
5
+ "contentHash": "671837951cf6679bea1d517c900927aab5d457806695e1a6da25574b8573f325",
6
6
  "files": [
7
7
  {
8
8
  "path": "content/challenge-cards/.gitkeep",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  {
36
36
  "path": "content/facilitation/master-guide.md",
37
- "sha256": "f0edc5eef8c7bc0efc67672e184edecce46fa5fd5e9820cfda0b28fbb8cb1a8f"
37
+ "sha256": "845adbcf9284cedd589d461b40ae1cd336d816b92bcc239df4574148a57b263a"
38
38
  },
39
39
  {
40
40
  "path": "content/project-briefs/.gitkeep",
@@ -98,11 +98,11 @@
98
98
  },
99
99
  {
100
100
  "path": "content/talks/context-is-king.md",
101
- "sha256": "4d0972fefac067de0b8803698b44bdf8ab0bee64b181d50d1ac5e26a92eb7089"
101
+ "sha256": "0bb48497b458474a546c351118ad455c00ad997c9efc0e227b388f81dd0b55e3"
102
102
  },
103
103
  {
104
104
  "path": "docs/harness-cli-foundation.md",
105
- "sha256": "f3b6125a4575c27dcb7b88474003f2a9b189a4b1b25bfaa462861571b69f7a51"
105
+ "sha256": "cff448e5c1ee4e7129724e4f8294add35cd1efb621c83182b341a1b7e5a16b86"
106
106
  },
107
107
  {
108
108
  "path": "docs/learner-reference-gallery.md",
@@ -134,7 +134,7 @@
134
134
  },
135
135
  {
136
136
  "path": "SKILL.md",
137
- "sha256": "de24b73c431855143630f6d312d9bb2af0a16178c2c70f928c5c3a426e569777"
137
+ "sha256": "d1e242a857f61bc2fae29c74a9e0efbd1ba0ed5beb25087dbaeffabc5f53b4a5"
138
138
  },
139
139
  {
140
140
  "path": "workshop-blueprint/agenda.json",
@@ -182,7 +182,7 @@
182
182
  },
183
183
  {
184
184
  "path": "workshop-skill/facilitator.md",
185
- "sha256": "f0e977df5979d0d218eabad026b66ecefa90802c71dc8a9468959334570f1c1c"
185
+ "sha256": "525fc9fdad22cf42b47854ff9ad90579d728daf2c4ac484aba13f93193eb56d3"
186
186
  },
187
187
  {
188
188
  "path": "workshop-skill/follow-up-package.md",
@@ -64,14 +64,14 @@ Pravidla:
64
64
 
65
65
  Po launchi potřebuje místnost ještě jednu konkrétní věc:
66
66
 
67
- - co má být po prvním build bloku opravdu vidět
68
- - co nestačí jen slíbit
67
+ - co má být po prvním build bloku opravdu vidět v repu
68
+ - co nestačí jen slíbit nebo dovysvětlit u stolu
69
69
 
70
70
  Do oběda má být vidět:
71
71
 
72
- - repo a `README`, které dávají smysl cizímu člověku
73
- - `AGENTS.md` jako krátká mapa
74
- - plan nebo jasně vedená implementační stopa
72
+ - `README`, které dává smysl cizímu člověku
73
+ - `AGENTS.md` jako krátká mapa, ne sklad všeho
74
+ - plan nebo jasně vedená implementační stopa, ze které je poznat další safe move
75
75
  - první explicitní check před dalším generováním
76
76
 
77
77
  ## Context is King talk
@@ -93,6 +93,8 @@ Proměnit energii z openingu v přesnou tezi a čistý most do Build fáze 1.
93
93
 
94
94
  ### Mikro-cvičení
95
95
 
96
+ Tohle je krátká facilitátorova ukázka, ne zadání pro celý room.
97
+
96
98
  Použijte stejný malý task ve dvou podmínkách:
97
99
 
98
100
  1. prompt blob
@@ -112,6 +114,7 @@ Na konci talku má být jasné:
112
114
 
113
115
  - teorie tím končí
114
116
  - tým se vrací k repu
117
+ - pokud tým ještě nemá workshop skill, teď je chvíle na `harness skill install`, pak `Codex: $workshop setup` nebo `pi: /skill:workshop`
115
118
  - nejdřív vzniká mapa a první explicitní check
116
119
  - teprve potom dává smysl další feature motion
117
120
 
@@ -30,7 +30,9 @@ Pointa analogie:
30
30
 
31
31
  ## Mikro-cvičení
32
32
 
33
- Všichni dostanou stejný malý task. Jedna varianta bude prompt blob. Druhá varianta bude krátké zadání se 4 prvky a s odkazem na kontext zapsaný v repu. Pak porovnáme výsledky. Nehledáme „nejhezčí prompt“. Hledáme způsob práce, který přenese záměr, omezení a done criteria i do dalšího kroku.
33
+ Tohle je krátká facilitátorova ukázka, ne práce pro celý room.
34
+
35
+ Vezmeme stejný malý task ve dvou podmínkách. Jedna varianta bude prompt blob. Druhá varianta bude krátké zadání se 4 prvky a s odkazem na kontext zapsaný v repu. Pak porovnáme výsledky. Nehledáme „nejhezčí prompt“. Hledáme způsob práce, který přenese záměr, omezení a done criteria i do dalšího kroku.
34
36
 
35
37
  4 prvky pro druhou variantu:
36
38
 
@@ -64,6 +66,7 @@ Všichni dostanou stejný malý task. Jedna varianta bude prompt blob. Druhá va
64
66
 
65
67
  Po tomhle talku se tým nemá vracet k repu s pocitem, že potřebuje jen chytřejší prompt. Má se vracet s jedním jasným očekáváním:
66
68
 
69
+ - pokud ještě nemá workshop skill, teď je chvíle na `harness skill install`, pak `Codex: $workshop setup` nebo `pi: /skill:workshop`
67
70
  - nejdřív krátká mapa v repu
68
71
  - potom plan
69
72
  - potom první explicitní check
@@ -8,7 +8,7 @@ Current implementation in this repo:
8
8
 
9
9
  - lives in the repository `harness-cli/` package and ships publicly as `@harness-lab/cli`
10
10
  - distributes a portable participant workshop skill bundle through `harness skill install`
11
- - covers `auth login/logout/status` plus `workshop status/list-instances/show-instance/create-instance/update-instance/reset-instance/prepare/remove-instance/archive/phase set`
11
+ - covers `auth login/logout/status` plus `workshop current-instance/select-instance/status/list-instances/show-instance/create-instance/update-instance/reset-instance/prepare/remove-instance/archive/phase set`
12
12
  - targets the existing shared dashboard facilitator APIs
13
13
  - is tested for browser/device auth, local-dev Basic Auth fallback, and cookie-backed Neon bootstrap fallback
14
14
  - stores sessions in a local file under `HARNESS_CLI_HOME` or `~/.harness` by default
@@ -38,6 +38,8 @@ Required commands:
38
38
  - `harness auth login`
39
39
  - `harness auth logout`
40
40
  - `harness auth status`
41
+ - `harness workshop current-instance`
42
+ - `harness workshop select-instance <instance-id>`
41
43
  - `harness workshop status`
42
44
  - `harness workshop list-instances`
43
45
  - `harness workshop show-instance <instance-id>`
@@ -85,33 +87,41 @@ Current auth layering:
85
87
  Current target-selection order:
86
88
 
87
89
  1. an explicit command or route instance id
88
- 2. the selected control-room instance in the dashboard
90
+ 2. the CLI-local persisted selection set by `harness workshop select-instance`
89
91
  3. the deployment default `HARNESS_WORKSHOP_INSTANCE_ID`
90
92
  4. a repository fallback only on surfaces that intentionally present a workspace-level default
91
93
 
92
94
  Current command posture:
93
95
 
94
96
  - explicit workspace-discovery commands:
97
+ - `harness workshop current-instance`
98
+ - `harness workshop select-instance <instance-id>`
95
99
  - `harness workshop list-instances`
96
- - explicit target required:
97
- - `harness workshop show-instance <instance-id>`
98
- - explicit target required:
99
- - `harness workshop update-instance <instance-id>`
100
- - `harness workshop reset-instance <instance-id>`
101
- - `harness workshop prepare <instance-id>`
102
- - `harness workshop remove-instance <instance-id>`
100
+ - explicit target preferred but selected-instance fallback allowed:
101
+ - `harness workshop show-instance [<instance-id>]`
102
+ - `harness workshop update-instance [<instance-id>]`
103
+ - `harness workshop reset-instance [<instance-id>]`
104
+ - `harness workshop prepare [<instance-id>]`
105
+ - `harness workshop remove-instance [<instance-id>]`
103
106
  - explicit target is the created resource:
104
107
  - `harness workshop create-instance [<instance-id>]`
105
- - deployment-default target:
108
+ - selected-instance target first, otherwise deployment-default target:
106
109
  - `harness workshop status`
107
- - `harness workshop archive`
108
110
  - `harness workshop phase set <phase-id>`
111
+ - deployment-default target:
112
+ - `harness workshop archive`
109
113
 
110
114
  Current discoverability path:
111
115
 
112
116
  - facilitators can inspect available workshop instances through the dashboard workspace view
113
117
  - the same platform-scoped list lives behind the authenticated instances API surface at `/api/workshop/instances`
114
118
  - the CLI should expose that list directly so facilitator skills do not need session-file inspection or raw authenticated scripts for routine discovery
119
+ - the CLI should also expose a local current-target selector so repeated facilitator work does not depend on ambient dashboard selection or deployment-default routing
120
+
121
+ Machine-readable posture:
122
+
123
+ - facilitator commands should support `--json` for strict machine parsing without headings or prose wrappers
124
+ - skills and agents should prefer `harness --json ...` over scraping human-oriented terminal formatting
115
125
 
116
126
  For the current facilitator lifecycle slice:
117
127
 
@@ -41,6 +41,20 @@ Note:
41
41
  - if the facilitator wants OS-native storage, they can use `HARNESS_SESSION_STORAGE=keychain`, `credential-manager`, or `secret-service`
42
42
  - `--auth basic` and `--auth neon` remain explicit fallbacks for local dev/bootstrap
43
43
 
44
+ Preferred operator flow after login:
45
+
46
+ ```bash
47
+ harness workshop list-instances
48
+ harness workshop select-instance sample-workshop-demo-orbit
49
+ harness workshop current-instance
50
+ harness workshop status
51
+ ```
52
+
53
+ Rules:
54
+ - prefer this flow over reading local CLI session files or composing ad hoc authenticated `node -e` scripts
55
+ - once a local selection exists, the CLI may use it as the default target for subsequent facilitator operations
56
+ - when an agent needs machine-readable output, prefer `harness --json ...`
57
+
44
58
  ### `/workshop facilitator logout`
45
59
 
46
60
  Ask the facilitator to run:
@@ -68,6 +82,48 @@ Show:
68
82
  Use this for the current default or selected workshop context.
69
83
  If the facilitator needs the full workspace registry first, use `list-instances` instead of probing local session files or writing raw authenticated fetch scripts.
70
84
 
85
+ Targeting behavior:
86
+ - if the facilitator previously ran `harness workshop select-instance <instance-id>`, `status` reports that selected instance
87
+ - if no local selection exists, `status` falls back to the deployment-default workshop context
88
+ - for exact machine parsing, prefer `harness --json workshop status`
89
+
90
+ ### `/workshop facilitator current-instance`
91
+
92
+ Preferred path:
93
+
94
+ ```bash
95
+ harness workshop current-instance
96
+ ```
97
+
98
+ Show:
99
+ - the locally selected instance id when one exists
100
+ - whether the current target came from persisted selection or `HARNESS_WORKSHOP_INSTANCE_ID`
101
+ - the resolved instance summary and full record for operator verification
102
+
103
+ Rules:
104
+ - use this after `select-instance` when the facilitator wants to confirm the CLI target before update, reset, or remove
105
+ - if nothing is selected, the command should say so clearly instead of forcing the facilitator into session-file inspection
106
+
107
+ ### `/workshop facilitator select-instance <instance-id>`
108
+
109
+ Preferred path:
110
+
111
+ ```bash
112
+ harness workshop select-instance sample-workshop-demo-orbit
113
+ ```
114
+
115
+ Clear the stored selection:
116
+
117
+ ```bash
118
+ harness workshop select-instance --clear
119
+ ```
120
+
121
+ Rules:
122
+ - use this when a facilitator will perform several operations against the same live workshop instance
123
+ - the CLI should validate the instance through the server before persisting the selection
124
+ - after selection, `status` and `phase set` should target that instance instead of the deployment default
125
+ - `show-instance`, `update-instance`, `reset-instance`, `prepare`, and `remove-instance` may omit `<instance-id>` when a valid selection already exists
126
+
71
127
  ### `/workshop facilitator list-instances`
72
128
 
73
129
  Preferred path:
@@ -93,6 +149,7 @@ Rules:
93
149
  - prefer this over inspecting local session files or composing one-off authenticated scripts
94
150
  - use it when the facilitator needs to discover which live instances exist before reset, update, or scene work
95
151
  - keep raw API usage as a diagnostic fallback, not the default operator workflow
152
+ - when an agent needs to parse the output, prefer `harness --json workshop list-instances`
96
153
 
97
154
  ### `/workshop facilitator show-instance <instance-id>`
98
155
 
@@ -117,6 +174,7 @@ Rules:
117
174
  - use this when the facilitator needs one specific instance, not the deployment-default `workshop status`
118
175
  - if the route returns `404`, the instance does not exist or is not visible to the facilitator
119
176
  - prefer this over ad hoc authenticated scripts for routine discovery
177
+ - if a local selection already exists, the facilitator may omit `<instance-id>` and let the CLI use the stored target
120
178
 
121
179
  ### `/workshop facilitator grant <email> <role>`
122
180
 
@@ -230,6 +288,7 @@ Rules:
230
288
  - do not use reset for an ordinary title, venue, or room correction
231
289
  - if the route returns `400`, the payload is wrong; if it returns `404`, the instance does not exist
232
290
  - use `--content-lang cs|en` when the facilitator intends to change the workshop delivery language for that instance
291
+ - if the facilitator already selected a local current instance, the CLI may omit `<instance-id>` and use the stored target
233
292
 
234
293
  ### `/workshop facilitator reset-instance <instance-id>`
235
294
 
@@ -256,6 +315,7 @@ Rules:
256
315
  - warn that reset archives current runtime state first and then clears live runtime state for the instance
257
316
  - prefer `update-instance` for ordinary metadata corrections; reset is the high-impact operation
258
317
  - if the facilitator does not specify a template, keep the current template unless there is a clear reason to switch
318
+ - if the facilitator already selected a local current instance, the CLI may omit `<instance-id>` and use the stored target
259
319
 
260
320
  ### `/workshop facilitator prepare`
261
321
 
@@ -275,6 +335,7 @@ Content-Type: application/json
275
335
  ```
276
336
 
277
337
  This sets the instance to `prepared` state and verifies the event code.
338
+ If a local current instance is already selected, the CLI may omit `<instance-id>` and use the stored target.
278
339
 
279
340
  ### `/workshop facilitator remove-instance <instance-id>`
280
341
 
@@ -296,6 +357,7 @@ Content-Type: application/json
296
357
  Rules:
297
358
  - remove remains an owner-only operation
298
359
  - the skill should warn the facilitator that this is destructive removal from the active list, not routine metadata editing
360
+ - if the facilitator already selected a local current instance, the CLI may omit `<instance-id>` and use the stored target
299
361
 
300
362
  ### `/workshop facilitator agenda`
301
363
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-lab/cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Participant-facing Harness Lab CLI for facilitator auth and workshop operations",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/client.js CHANGED
@@ -99,9 +99,18 @@ export function createHarnessClient({ fetchFn, session }) {
99
99
  getAgenda() {
100
100
  return request("/api/agenda");
101
101
  },
102
+ getWorkshopAgenda(instanceId) {
103
+ return request(`/api/workshop/instances/${encodeURIComponent(instanceId)}/agenda`);
104
+ },
102
105
  setCurrentPhase(currentId) {
103
106
  return request("/api/agenda", { method: "PATCH", body: { currentId } });
104
107
  },
108
+ setCurrentPhaseForInstance(instanceId, currentId) {
109
+ return request(`/api/workshop/instances/${encodeURIComponent(instanceId)}/agenda`, {
110
+ method: "PATCH",
111
+ body: { itemId: currentId },
112
+ });
113
+ },
105
114
  archiveWorkshop(notes) {
106
115
  return request("/api/workshop/archive", { method: "POST", body: notes ? { notes } : {} });
107
116
  },
package/src/io.js CHANGED
@@ -84,7 +84,9 @@ function writeWrapped(stream, text, options = {}) {
84
84
  }
85
85
  }
86
86
 
87
- export function createCliUi(io) {
87
+ export function createCliUi(io, options = {}) {
88
+ const jsonMode = options.jsonMode === true;
89
+
88
90
  function streamFor(target) {
89
91
  return target === "stderr" ? io.stderr : io.stdout;
90
92
  }
@@ -115,6 +117,10 @@ export function createCliUi(io) {
115
117
  function status(kind, text, options = {}) {
116
118
  const target = options.stream ?? (kind === "error" ? "stderr" : "stdout");
117
119
  const stream = streamFor(target);
120
+ if (jsonMode) {
121
+ writeLine(stream, JSON.stringify({ ok: kind !== "error", level: kind, message: text }));
122
+ return;
123
+ }
118
124
  const chalk = createStyler(stream, io.env);
119
125
  const prefixMap = {
120
126
  ok: chalk.green.bold("[ok]"),
@@ -178,7 +184,9 @@ export function createCliUi(io) {
178
184
 
179
185
  function json(title, value, options = {}) {
180
186
  const target = options.stream ?? "stdout";
181
- heading(title, { stream: target });
187
+ if (!jsonMode) {
188
+ heading(title, { stream: target });
189
+ }
182
190
  writeLine(streamFor(target), JSON.stringify(value, null, 2));
183
191
  }
184
192
 
package/src/run-cli.js CHANGED
@@ -16,6 +16,7 @@ function sleep(ms) {
16
16
  function parseArgs(argv) {
17
17
  const positionals = [];
18
18
  const flags = {};
19
+ const booleanFlags = new Set(["json", "help", "version", "force", "no-open", "clear"]);
19
20
 
20
21
  for (let index = 0; index < argv.length; index += 1) {
21
22
  const value = argv[index];
@@ -29,6 +30,10 @@ function parseArgs(argv) {
29
30
  }
30
31
  if (value.startsWith("--")) {
31
32
  const key = value.slice(2);
33
+ if (booleanFlags.has(key)) {
34
+ flags[key] = true;
35
+ continue;
36
+ }
32
37
  const next = argv[index + 1];
33
38
  if (!next || next.startsWith("--")) {
34
39
  flags[key] = true;
@@ -161,12 +166,34 @@ function summarizeWorkshopInstance(instance) {
161
166
  };
162
167
  }
163
168
 
169
+ function resolveCurrentInstanceTarget(session, env) {
170
+ if (typeof session?.selectedInstanceId === "string" && session.selectedInstanceId.trim().length > 0) {
171
+ return {
172
+ instanceId: session.selectedInstanceId.trim(),
173
+ source: "session",
174
+ };
175
+ }
176
+
177
+ if (typeof env?.HARNESS_WORKSHOP_INSTANCE_ID === "string" && env.HARNESS_WORKSHOP_INSTANCE_ID.trim().length > 0) {
178
+ return {
179
+ instanceId: env.HARNESS_WORKSHOP_INSTANCE_ID.trim(),
180
+ source: "env",
181
+ };
182
+ }
183
+
184
+ return {
185
+ instanceId: null,
186
+ source: "none",
187
+ };
188
+ }
189
+
164
190
  function printUsage(io, ui) {
165
191
  ui.heading("Harness CLI");
166
192
  ui.paragraph(`Version ${version}`);
167
193
  ui.blank();
168
194
  ui.section("Usage");
169
195
  ui.commandList([
196
+ "harness [--json] <command>",
170
197
  "harness --help",
171
198
  "harness --version",
172
199
  "harness version",
@@ -178,6 +205,8 @@ function printUsage(io, ui) {
178
205
  "harness auth logout",
179
206
  "harness auth status",
180
207
  "harness skill install [--target PATH] [--force]",
208
+ "harness workshop current-instance",
209
+ "harness workshop select-instance <instance-id> [--clear]",
181
210
  "harness workshop status",
182
211
  "harness workshop list-instances",
183
212
  "harness workshop show-instance <instance-id>",
@@ -571,9 +600,30 @@ async function handleWorkshopStatus(io, ui, env, deps) {
571
600
 
572
601
  try {
573
602
  const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
603
+ const target = resolveCurrentInstanceTarget(session, env);
604
+
605
+ if (target.source === "session" && target.instanceId) {
606
+ const [instanceResult, agenda] = await Promise.all([
607
+ client.getWorkshopInstance(target.instanceId),
608
+ client.getWorkshopAgenda(target.instanceId),
609
+ ]);
610
+ ui.json("Workshop Status", {
611
+ ok: true,
612
+ targetInstanceId: target.instanceId,
613
+ targetSource: target.source,
614
+ ...summarizeWorkshopInstance(instanceResult.instance),
615
+ workshopMeta: instanceResult.instance?.workshopMeta ?? null,
616
+ currentPhase: agenda.phase,
617
+ agendaItems: Array.isArray(agenda.items) ? agenda.items.length : null,
618
+ });
619
+ return 0;
620
+ }
621
+
574
622
  const [workshop, agenda] = await Promise.all([client.getWorkshopStatus(), client.getAgenda()]);
575
623
  ui.json("Workshop Status", {
576
624
  ok: true,
625
+ targetInstanceId: target.instanceId,
626
+ targetSource: target.source,
577
627
  workshopId: workshop.workshopId,
578
628
  workshopMeta: workshop.workshopMeta,
579
629
  currentPhase: agenda.phase,
@@ -589,6 +639,107 @@ async function handleWorkshopStatus(io, ui, env, deps) {
589
639
  }
590
640
  }
591
641
 
642
+ async function handleWorkshopCurrentInstance(io, ui, env, deps) {
643
+ const session = await requireSession(io, ui, env);
644
+ if (!session) {
645
+ return 1;
646
+ }
647
+
648
+ const target = resolveCurrentInstanceTarget(session, env);
649
+ if (!target.instanceId) {
650
+ ui.json("Workshop Current Instance", {
651
+ ok: true,
652
+ instanceId: null,
653
+ source: target.source,
654
+ selectedInstanceId: session.selectedInstanceId ?? null,
655
+ });
656
+ return 0;
657
+ }
658
+
659
+ try {
660
+ const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
661
+ const result = await client.getWorkshopInstance(target.instanceId);
662
+ ui.json("Workshop Current Instance", {
663
+ ok: true,
664
+ source: target.source,
665
+ selectedInstanceId: session.selectedInstanceId ?? null,
666
+ ...summarizeWorkshopInstance(result.instance),
667
+ instance: result.instance,
668
+ });
669
+ return 0;
670
+ } catch (error) {
671
+ if (error instanceof HarnessApiError) {
672
+ ui.status("error", `Current instance lookup failed: ${error.message}`, { stream: "stderr" });
673
+ return 1;
674
+ }
675
+ throw error;
676
+ }
677
+ }
678
+
679
+ async function handleWorkshopSelectInstance(io, ui, env, positionals, flags, deps) {
680
+ const session = await requireSession(io, ui, env);
681
+ if (!session) {
682
+ return 1;
683
+ }
684
+
685
+ if (flags.clear === true) {
686
+ const nextSession = { ...session };
687
+ delete nextSession.selectedInstanceId;
688
+ if (!(await persistSession(io, ui, env, nextSession))) {
689
+ return 1;
690
+ }
691
+
692
+ const target = resolveCurrentInstanceTarget(nextSession, env);
693
+ ui.json("Workshop Select Instance", {
694
+ ok: true,
695
+ selectedInstanceId: null,
696
+ currentInstanceId: target.instanceId,
697
+ source: target.source,
698
+ cleared: true,
699
+ });
700
+ return 0;
701
+ }
702
+
703
+ const instanceId = await readRequiredCommandValue(
704
+ io,
705
+ flags,
706
+ ["id", "instance-id"],
707
+ "Instance id: ",
708
+ readOptionalPositional(positionals, 2),
709
+ );
710
+ if (!instanceId) {
711
+ ui.status("error", "Instance id is required.", { stream: "stderr" });
712
+ return 1;
713
+ }
714
+
715
+ try {
716
+ const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
717
+ const result = await client.getWorkshopInstance(instanceId);
718
+ const nextSession = {
719
+ ...session,
720
+ selectedInstanceId: result.instance?.id ?? instanceId,
721
+ };
722
+ if (!(await persistSession(io, ui, env, nextSession))) {
723
+ return 1;
724
+ }
725
+
726
+ ui.json("Workshop Select Instance", {
727
+ ok: true,
728
+ source: "session",
729
+ selectedInstanceId: nextSession.selectedInstanceId,
730
+ ...summarizeWorkshopInstance(result.instance),
731
+ instance: result.instance,
732
+ });
733
+ return 0;
734
+ } catch (error) {
735
+ if (error instanceof HarnessApiError) {
736
+ ui.status("error", `Select instance failed: ${error.message}`, { stream: "stderr" });
737
+ return 1;
738
+ }
739
+ throw error;
740
+ }
741
+ }
742
+
592
743
  async function handleWorkshopListInstances(io, ui, env, deps) {
593
744
  const session = await requireSession(io, ui, env);
594
745
  if (!session) {
@@ -625,7 +776,7 @@ async function handleWorkshopShowInstance(io, ui, env, positionals, flags, deps)
625
776
  flags,
626
777
  ["id", "instance-id"],
627
778
  "Instance id: ",
628
- readOptionalPositional(positionals, 2),
779
+ readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
629
780
  );
630
781
  if (!instanceId) {
631
782
  ui.status("error", "Instance id is required.", { stream: "stderr" });
@@ -724,7 +875,7 @@ async function handleWorkshopUpdateInstance(io, ui, env, positionals, flags, dep
724
875
  flags,
725
876
  ["id"],
726
877
  "Instance id: ",
727
- readOptionalPositional(positionals, 2),
878
+ readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
728
879
  );
729
880
  if (!instanceId) {
730
881
  ui.status("error", "Instance id is required.", { stream: "stderr" });
@@ -739,7 +890,7 @@ async function handleWorkshopUpdateInstance(io, ui, env, positionals, flags, dep
739
890
  if (!hasWorkshopMetadataInput(payload)) {
740
891
  ui.status(
741
892
  "error",
742
- "At least one metadata field is required. Use flags such as --event-title, --date-range, --venue-name, or --room-name.",
893
+ "At least one metadata field is required. Use flags such as --content-lang, --event-title, --date-range, --venue-name, or --room-name.",
743
894
  { stream: "stderr" },
744
895
  );
745
896
  return 1;
@@ -774,7 +925,7 @@ async function handleWorkshopPrepare(io, ui, env, positionals, flags, deps) {
774
925
  flags,
775
926
  ["id", "instance-id"],
776
927
  "Instance id: ",
777
- readOptionalPositional(positionals, 2),
928
+ readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
778
929
  );
779
930
  if (!instanceId) {
780
931
  ui.status("error", "Instance id is required.", { stream: "stderr" });
@@ -810,7 +961,7 @@ async function handleWorkshopResetInstance(io, ui, env, positionals, flags, deps
810
961
  flags,
811
962
  ["id", "instance-id"],
812
963
  "Instance id: ",
813
- readOptionalPositional(positionals, 2),
964
+ readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
814
965
  );
815
966
  if (!instanceId) {
816
967
  ui.status("error", "Instance id is required.", { stream: "stderr" });
@@ -845,7 +996,7 @@ async function handleWorkshopRemoveInstance(io, ui, env, positionals, flags, dep
845
996
  flags,
846
997
  ["id", "instance-id"],
847
998
  "Instance id: ",
848
- readOptionalPositional(positionals, 2),
999
+ readOptionalPositional(positionals, 2) ?? session.selectedInstanceId,
849
1000
  );
850
1001
  if (!instanceId) {
851
1002
  ui.status("error", "Instance id is required.", { stream: "stderr" });
@@ -884,7 +1035,11 @@ async function handleWorkshopPhaseSet(io, ui, env, positionals, deps) {
884
1035
 
885
1036
  try {
886
1037
  const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
887
- const result = await client.setCurrentPhase(phaseId);
1038
+ const target = resolveCurrentInstanceTarget(session, env);
1039
+ const result =
1040
+ target.source === "session" && target.instanceId
1041
+ ? await client.setCurrentPhaseForInstance(target.instanceId, phaseId)
1042
+ : await client.setCurrentPhase(phaseId);
888
1043
  ui.json("Workshop Phase", result);
889
1044
  return 0;
890
1045
  } catch (error) {
@@ -899,8 +1054,8 @@ async function handleWorkshopPhaseSet(io, ui, env, positionals, deps) {
899
1054
  export async function runCli(argv, io, deps = {}) {
900
1055
  const fetchFn = deps.fetchFn ?? globalThis.fetch;
901
1056
  const mergedDeps = { fetchFn, sleepFn: deps.sleepFn, openUrl: deps.openUrl, cwd: deps.cwd };
902
- const ui = createCliUi(io);
903
1057
  const { positionals, flags } = parseArgs(argv);
1058
+ const ui = createCliUi(io, { jsonMode: flags.json === true || flags.output === "json" });
904
1059
  const [scope, action, subaction] = positionals;
905
1060
 
906
1061
  if (flags.help === true) {
@@ -943,6 +1098,14 @@ export async function runCli(argv, io, deps = {}) {
943
1098
  return handleSkillInstall(io, ui, mergedDeps, flags);
944
1099
  }
945
1100
 
1101
+ if (scope === "workshop" && action === "current-instance") {
1102
+ return handleWorkshopCurrentInstance(io, ui, io.env, mergedDeps);
1103
+ }
1104
+
1105
+ if (scope === "workshop" && action === "select-instance") {
1106
+ return handleWorkshopSelectInstance(io, ui, io.env, positionals, flags, mergedDeps);
1107
+ }
1108
+
946
1109
  if (scope === "workshop" && action === "status") {
947
1110
  return handleWorkshopStatus(io, ui, io.env, mergedDeps);
948
1111
  }
@@ -314,6 +314,7 @@ export function sanitizeSession(session, env) {
314
314
  authType: session.authType,
315
315
  username: session.username ?? null,
316
316
  email: session.email ?? null,
317
+ selectedInstanceId: session.selectedInstanceId ?? null,
317
318
  loggedInAt: session.loggedInAt,
318
319
  expiresAt: session.expiresAt ?? null,
319
320
  mode: session.mode ?? "local-dev",
@@ -225,6 +225,20 @@ async function writeWorkshopBundleManifest(bundleRoot, manifest) {
225
225
  );
226
226
  }
227
227
 
228
+ async function pruneBundleFiles(bundleRoot, manifest) {
229
+ const expectedFiles = new Set([
230
+ WORKSHOP_BUNDLE_MANIFEST,
231
+ ...manifest.files.map((file) => file.path),
232
+ ]);
233
+ const currentFiles = await listFilesRecursive(bundleRoot);
234
+
235
+ for (const file of currentFiles) {
236
+ if (!expectedFiles.has(file.relativePath)) {
237
+ await fs.rm(file.absolutePath, { force: true });
238
+ }
239
+ }
240
+ }
241
+
228
242
  export async function createWorkshopBundleFromSource(sourceRoot, targetRoot, options = {}) {
229
243
  if (options.clean === true) {
230
244
  await fs.rm(targetRoot, { recursive: true, force: true });
@@ -234,6 +248,9 @@ export async function createWorkshopBundleFromSource(sourceRoot, targetRoot, opt
234
248
  await fs.rm(path.join(targetRoot, "workshop-skill", "SKILL.md"), { force: true });
235
249
  await copyBundleFiles(sourceRoot, targetRoot);
236
250
  const manifest = await createWorkshopBundleManifestFromSource(sourceRoot);
251
+ if (options.prune === true) {
252
+ await pruneBundleFiles(targetRoot, manifest);
253
+ }
237
254
  await writeWorkshopBundleManifest(targetRoot, manifest);
238
255
  }
239
256
 
@@ -250,7 +267,7 @@ export async function syncPackagedWorkshopBundle() {
250
267
  export async function syncRepoBundledWorkshopSkill() {
251
268
  const sourceRoot = getRepoWorkshopSourceRoot();
252
269
  const bundleRoot = getRepoBundledWorkshopSkillPath();
253
- await createWorkshopBundleFromSource(sourceRoot, bundleRoot, { clean: true });
270
+ await createWorkshopBundleFromSource(sourceRoot, bundleRoot, { prune: true });
254
271
  return {
255
272
  sourceRoot,
256
273
  bundleRoot,