@harness-lab/cli 0.1.5 → 0.1.8

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
@@ -59,14 +59,14 @@ harness version
59
59
  harness --help
60
60
  ```
61
61
 
62
- Install the repo-local workshop skill bundle for Codex/OpenCode discovery:
62
+ Install the repo-local workshop skill bundle for Codex/pi discovery:
63
63
 
64
64
  ```bash
65
65
  harness skill install
66
66
  ```
67
67
 
68
68
  This creates `.agents/skills/harness-lab-workshop` in the current Harness Lab repo checkout.
69
- After install, the CLI prints the first recommended agent commands, starting with `Codex: $workshop reference` and `OpenCode: /workshop reference`.
69
+ After install, the CLI prints the first recommended agent commands, starting with `Codex: $workshop reference` and `pi: /skill:workshop`.
70
70
 
71
71
  Default device/browser login:
72
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-lab/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
4
4
  "description": "Participant-facing Harness Lab CLI for facilitator auth and workshop operations",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -33,6 +33,9 @@
33
33
  "bin": {
34
34
  "harness": "bin/harness.js"
35
35
  },
36
+ "dependencies": {
37
+ "chalk": "^5.6.2"
38
+ },
36
39
  "scripts": {
37
40
  "test": "node --test"
38
41
  }
package/src/io.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import readline from "node:readline/promises";
2
+ import { Chalk } from "chalk";
2
3
 
3
4
  export async function prompt(io, label) {
4
5
  const rl = readline.createInterface({
@@ -17,3 +18,179 @@ export async function prompt(io, label) {
17
18
  export function writeLine(stream, line = "") {
18
19
  stream.write(`${line}\n`);
19
20
  }
21
+
22
+ function supportsColor(stream, env = {}) {
23
+ if (!stream?.isTTY) {
24
+ return false;
25
+ }
26
+
27
+ if ("NO_COLOR" in env) {
28
+ return false;
29
+ }
30
+
31
+ return env.TERM !== "dumb";
32
+ }
33
+
34
+ function createStyler(stream, env) {
35
+ return new Chalk({ level: supportsColor(stream, env) ? 1 : 0 });
36
+ }
37
+
38
+ function getWrapWidth(stream) {
39
+ const width = Number(stream?.columns);
40
+ if (!Number.isFinite(width) || width <= 0) {
41
+ return 88;
42
+ }
43
+
44
+ return Math.max(60, Math.min(width, 100));
45
+ }
46
+
47
+ function wrapText(text, width, indent = "", subsequentIndent = indent) {
48
+ if (!text) {
49
+ return [indent.trimEnd()];
50
+ }
51
+
52
+ const firstLineWidth = Math.max(20, width - indent.length);
53
+ const laterLineWidth = Math.max(20, width - subsequentIndent.length);
54
+ const words = text.split(/\s+/).filter(Boolean);
55
+ const lines = [];
56
+ let currentPrefix = indent;
57
+ let currentText = "";
58
+
59
+ for (const word of words) {
60
+ const lineWidth = currentPrefix === indent ? firstLineWidth : laterLineWidth;
61
+ const candidate = currentText ? `${currentText} ${word}` : word;
62
+
63
+ if (candidate.length <= lineWidth || currentText.length === 0) {
64
+ currentText = candidate;
65
+ continue;
66
+ }
67
+
68
+ lines.push(`${currentPrefix}${currentText}`);
69
+ currentPrefix = subsequentIndent;
70
+ currentText = word;
71
+ }
72
+
73
+ lines.push(`${currentPrefix}${currentText}`);
74
+ return lines;
75
+ }
76
+
77
+ function writeWrapped(stream, text, options = {}) {
78
+ const width = options.width ?? getWrapWidth(stream);
79
+ const indent = options.indent ?? "";
80
+ const subsequentIndent = options.subsequentIndent ?? indent;
81
+
82
+ for (const line of wrapText(text, width, indent, subsequentIndent)) {
83
+ writeLine(stream, line);
84
+ }
85
+ }
86
+
87
+ export function createCliUi(io) {
88
+ function streamFor(target) {
89
+ return target === "stderr" ? io.stderr : io.stdout;
90
+ }
91
+
92
+ function blank(target = "stdout") {
93
+ writeLine(streamFor(target));
94
+ }
95
+
96
+ function heading(title, options = {}) {
97
+ const target = options.stream ?? "stdout";
98
+ const stream = streamFor(target);
99
+ const chalk = createStyler(stream, io.env);
100
+ writeLine(stream, chalk.cyan.bold(title));
101
+ writeLine(stream, chalk.dim("=".repeat(title.length)));
102
+ }
103
+
104
+ function section(title, options = {}) {
105
+ const target = options.stream ?? "stdout";
106
+ const stream = streamFor(target);
107
+ const chalk = createStyler(stream, io.env);
108
+ writeLine(stream, chalk.bold(title));
109
+ }
110
+
111
+ function paragraph(text, options = {}) {
112
+ writeWrapped(streamFor(options.stream ?? "stdout"), text, options);
113
+ }
114
+
115
+ function status(kind, text, options = {}) {
116
+ const target = options.stream ?? (kind === "error" ? "stderr" : "stdout");
117
+ const stream = streamFor(target);
118
+ const chalk = createStyler(stream, io.env);
119
+ const prefixMap = {
120
+ ok: chalk.green.bold("[ok]"),
121
+ info: chalk.cyan.bold("[info]"),
122
+ warn: chalk.yellow.bold("[warn]"),
123
+ error: chalk.red.bold("[error]"),
124
+ };
125
+ const prefix = prefixMap[kind] ?? "[info]";
126
+ writeWrapped(stream, `${prefix} ${text}`, {
127
+ indent: options.indent ?? "",
128
+ subsequentIndent: options.subsequentIndent ?? " ",
129
+ width: options.width,
130
+ });
131
+ }
132
+
133
+ function keyValue(label, value, options = {}) {
134
+ const target = options.stream ?? "stdout";
135
+ const stream = streamFor(target);
136
+ const indent = options.indent ?? " ";
137
+ const subsequentIndent = options.subsequentIndent ?? " ";
138
+ const renderedValue = String(value);
139
+
140
+ if (!/\s/.test(renderedValue)) {
141
+ writeLine(stream, `${indent}${label}: ${renderedValue}`);
142
+ return;
143
+ }
144
+
145
+ writeWrapped(stream, `${label}: ${value}`, {
146
+ indent,
147
+ subsequentIndent,
148
+ width: options.width,
149
+ });
150
+ }
151
+
152
+ function numberedList(items, options = {}) {
153
+ const target = options.stream ?? "stdout";
154
+ const stream = streamFor(target);
155
+
156
+ items.forEach((item, index) => {
157
+ const indent = ` ${index + 1}. `;
158
+ writeWrapped(stream, item, {
159
+ indent,
160
+ subsequentIndent: " ",
161
+ width: options.width,
162
+ });
163
+ });
164
+ }
165
+
166
+ function commandList(items, options = {}) {
167
+ const target = options.stream ?? "stdout";
168
+ const stream = streamFor(target);
169
+
170
+ for (const item of items) {
171
+ writeWrapped(stream, item, {
172
+ indent: " ",
173
+ subsequentIndent: " ",
174
+ width: options.width,
175
+ });
176
+ }
177
+ }
178
+
179
+ function json(title, value, options = {}) {
180
+ const target = options.stream ?? "stdout";
181
+ heading(title, { stream: target });
182
+ writeLine(streamFor(target), JSON.stringify(value, null, 2));
183
+ }
184
+
185
+ return {
186
+ blank,
187
+ heading,
188
+ section,
189
+ paragraph,
190
+ status,
191
+ keyValue,
192
+ numberedList,
193
+ commandList,
194
+ json,
195
+ };
196
+ }
package/src/run-cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getDefaultDashboardUrl } from "./config.js";
2
2
  import { createHarnessClient, HarnessApiError } from "./client.js";
3
- import { prompt, writeLine } from "./io.js";
3
+ import { createCliUi, prompt, writeLine } from "./io.js";
4
4
  import { deleteSession, readSession, sanitizeSession, writeSession, getSessionStorageMode, SessionStoreError } from "./session-store.js";
5
5
  import { installWorkshopSkill, SkillInstallError } from "./skill-install.js";
6
6
  import { createRequire } from "node:module";
@@ -64,43 +64,57 @@ async function readJson(response) {
64
64
  }
65
65
  }
66
66
 
67
- function printUsage(io) {
68
- writeLine(io.stdout, "Usage:");
69
- writeLine(io.stdout, " harness --help");
70
- writeLine(io.stdout, " harness --version");
71
- writeLine(io.stdout, " harness version");
72
- writeLine(io.stdout, " harness auth login [--auth device|basic|neon] [--dashboard-url URL] [--username USER] [--email EMAIL] [--password PASS] [--no-open]");
73
- writeLine(io.stdout, " harness auth logout");
74
- writeLine(io.stdout, " harness auth status");
75
- writeLine(io.stdout, " harness skill install [--force]");
76
- writeLine(io.stdout, " harness workshop status");
77
- writeLine(io.stdout, " harness workshop archive [--notes TEXT]");
78
- writeLine(io.stdout, " harness workshop phase set <phase-id>");
67
+ function printUsage(io, ui) {
68
+ ui.heading("Harness CLI");
69
+ ui.paragraph(`Version ${version}`);
70
+ ui.blank();
71
+ ui.section("Usage");
72
+ ui.commandList([
73
+ "harness --help",
74
+ "harness --version",
75
+ "harness version",
76
+ ]);
77
+ ui.blank();
78
+ ui.section("Commands");
79
+ ui.commandList([
80
+ "harness auth login [--auth device|basic|neon] [--dashboard-url URL] [--username USER] [--email EMAIL] [--password PASS] [--no-open]",
81
+ "harness auth logout",
82
+ "harness auth status",
83
+ "harness skill install [--force]",
84
+ "harness workshop status",
85
+ "harness workshop archive [--notes TEXT]",
86
+ "harness workshop phase set <phase-id>",
87
+ ]);
79
88
  }
80
89
 
81
90
  function printVersion(io) {
82
91
  writeLine(io.stdout, `harness ${version}`);
83
92
  }
84
93
 
85
- async function handleSkillInstall(io, deps, flags) {
94
+ async function handleSkillInstall(io, ui, deps, flags) {
86
95
  try {
87
96
  const result = await installWorkshopSkill(deps.cwd ?? process.cwd(), { force: flags.force === true });
97
+ ui.heading("Workshop Skill");
88
98
  if (result.mode === "already_bundled") {
89
- writeLine(io.stdout, `Harness Lab workshop skill is already bundled at ${result.installPath}`);
90
- writeLine(io.stdout, "Codex and OpenCode should discover it from this repo via .agents/skills.");
99
+ ui.status("ok", "Harness Lab workshop skill is already bundled in this repo.");
91
100
  } else {
92
- writeLine(io.stdout, `Installed Harness Lab workshop skill to ${result.installPath}`);
93
- writeLine(io.stdout, "Codex and OpenCode should now discover it from this repo via .agents/skills.");
101
+ ui.status("ok", "Installed the Harness Lab workshop skill bundle.");
94
102
  }
95
- writeLine(io.stdout, "Next steps:");
96
- writeLine(io.stdout, " 1. Open Codex or OpenCode in this repo.");
97
- writeLine(io.stdout, " 2. In Codex, start with `$workshop reference`. In OpenCode, use `/workshop reference`.");
98
- writeLine(io.stdout, " 3. If your environment is not ready yet, use `$workshop setup` in Codex or `/workshop setup` in OpenCode.");
99
- writeLine(io.stdout, " 4. For other help, keep the same pattern: `$workshop ...` in Codex, `/workshop ...` in OpenCode.");
103
+ ui.keyValue("Location", result.installPath);
104
+ ui.keyValue("Discovery", ".agents/skills");
105
+ ui.blank();
106
+ ui.section("Next steps");
107
+ ui.numberedList([
108
+ "Open Codex or pi in this repo.",
109
+ "Start with the workshop reference card.",
110
+ "Codex: `$workshop reference`.",
111
+ "pi: `/skill:workshop`, then ask for the workshop reference card.",
112
+ "Need setup help? Codex: `$workshop setup`. pi: `/skill:workshop`, then ask for setup help.",
113
+ ]);
100
114
  return 0;
101
115
  } catch (error) {
102
116
  if (error instanceof SkillInstallError) {
103
- writeLine(io.stderr, `Skill install failed: ${error.message}`);
117
+ ui.status("error", `Skill install failed: ${error.message}`, { stream: "stderr" });
104
118
  return 1;
105
119
  }
106
120
  throw error;
@@ -115,23 +129,23 @@ function formatStorageError(error) {
115
129
  return "Harness CLI could not access the configured session store.";
116
130
  }
117
131
 
118
- async function persistSession(io, env, session) {
132
+ async function persistSession(io, ui, env, session) {
119
133
  try {
120
134
  await writeSession(env, session);
121
135
  return true;
122
136
  } catch (error) {
123
- writeLine(io.stderr, `Session storage failed: ${formatStorageError(error)}`);
137
+ ui.status("error", `Session storage failed: ${formatStorageError(error)}`, { stream: "stderr" });
124
138
  return false;
125
139
  }
126
140
  }
127
141
 
128
- async function handleBasicAuthLogin(io, env, flags, deps) {
142
+ async function handleBasicAuthLogin(io, ui, env, flags, deps) {
129
143
  const dashboardUrl = String(flags["dashboard-url"] ?? getDefaultDashboardUrl(env));
130
144
  const username = String(flags.username ?? env.HARNESS_ADMIN_USERNAME ?? (await prompt(io, "Username: ")));
131
145
  const password = String(flags.password ?? env.HARNESS_ADMIN_PASSWORD ?? (await prompt(io, "Password: ")));
132
146
 
133
147
  if (!username || !password) {
134
- writeLine(io.stderr, "Username and password are required.");
148
+ ui.status("error", "Username and password are required.", { stream: "stderr" });
135
149
  return 1;
136
150
  }
137
151
 
@@ -148,31 +162,33 @@ async function handleBasicAuthLogin(io, env, flags, deps) {
148
162
 
149
163
  try {
150
164
  const payload = await client.verifyAccess();
151
- if (!(await persistSession(io, env, session))) {
165
+ if (!(await persistSession(io, ui, env, session))) {
152
166
  return 1;
153
167
  }
154
- writeLine(io.stdout, `Logged in to ${dashboardUrl}`);
155
- writeLine(io.stdout, `Session storage: ${getSessionStorageMode(env)}`);
168
+ ui.heading("Auth Login");
169
+ ui.status("ok", "Logged in.");
170
+ ui.keyValue("Dashboard", dashboardUrl);
171
+ ui.keyValue("Session storage", getSessionStorageMode(env));
156
172
  if (payload?.workshopId) {
157
- writeLine(io.stdout, `Workshop: ${payload.workshopId}`);
173
+ ui.keyValue("Workshop", payload.workshopId);
158
174
  }
159
175
  return 0;
160
176
  } catch (error) {
161
177
  if (error instanceof HarnessApiError) {
162
- writeLine(io.stderr, `Login failed: ${error.message}`);
178
+ ui.status("error", `Login failed: ${error.message}`, { stream: "stderr" });
163
179
  return 1;
164
180
  }
165
181
  throw error;
166
182
  }
167
183
  }
168
184
 
169
- async function handleNeonAuthLogin(io, env, flags, deps) {
185
+ async function handleNeonAuthLogin(io, ui, env, flags, deps) {
170
186
  const dashboardUrl = String(flags["dashboard-url"] ?? getDefaultDashboardUrl(env));
171
187
  const email = String(flags.email ?? env.HARNESS_FACILITATOR_EMAIL ?? (await prompt(io, "Email: ")));
172
188
  const password = String(flags.password ?? env.HARNESS_FACILITATOR_PASSWORD ?? (await prompt(io, "Password: ")));
173
189
 
174
190
  if (!email || !password) {
175
- writeLine(io.stderr, "Email and password are required.");
191
+ ui.status("error", "Email and password are required.", { stream: "stderr" });
176
192
  return 1;
177
193
  }
178
194
 
@@ -193,13 +209,13 @@ async function handleNeonAuthLogin(io, env, flags, deps) {
193
209
  : payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
194
210
  ? payload.error
195
211
  : `Login failed with status ${signInResponse.status}`;
196
- writeLine(io.stderr, `Login failed: ${message}`);
212
+ ui.status("error", `Login failed: ${message}`, { stream: "stderr" });
197
213
  return 1;
198
214
  }
199
215
 
200
216
  const setCookie = signInResponse.headers?.get?.("set-cookie");
201
217
  if (!setCookie) {
202
- writeLine(io.stderr, "Login failed: auth response did not include a session cookie.");
218
+ ui.status("error", "Login failed: auth response did not include a session cookie.", { stream: "stderr" });
203
219
  return 1;
204
220
  }
205
221
 
@@ -215,25 +231,27 @@ async function handleNeonAuthLogin(io, env, flags, deps) {
215
231
  try {
216
232
  const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
217
233
  const authSession = await client.getAuthSession();
218
- if (!(await persistSession(io, env, session))) {
234
+ if (!(await persistSession(io, ui, env, session))) {
219
235
  return 1;
220
236
  }
221
- writeLine(io.stdout, `Logged in to ${dashboardUrl}`);
222
- writeLine(io.stdout, `Session storage: ${getSessionStorageMode(env)}`);
237
+ ui.heading("Auth Login");
238
+ ui.status("ok", "Logged in.");
239
+ ui.keyValue("Dashboard", dashboardUrl);
240
+ ui.keyValue("Session storage", getSessionStorageMode(env));
223
241
  if (authSession?.user?.email) {
224
- writeLine(io.stdout, `Facilitator: ${authSession.user.email}`);
242
+ ui.keyValue("Facilitator", authSession.user.email);
225
243
  }
226
244
  return 0;
227
245
  } catch (error) {
228
246
  if (error instanceof HarnessApiError) {
229
- writeLine(io.stderr, `Session verification failed: ${error.message}`);
247
+ ui.status("error", `Session verification failed: ${error.message}`, { stream: "stderr" });
230
248
  return 1;
231
249
  }
232
250
  throw error;
233
251
  }
234
252
  }
235
253
 
236
- async function handleDeviceAuthLogin(io, env, flags, deps) {
254
+ async function handleDeviceAuthLogin(io, ui, env, flags, deps) {
237
255
  const dashboardUrl = String(flags["dashboard-url"] ?? getDefaultDashboardUrl(env));
238
256
  const client = createHarnessClient({
239
257
  fetchFn: deps.fetchFn,
@@ -242,10 +260,11 @@ async function handleDeviceAuthLogin(io, env, flags, deps) {
242
260
 
243
261
  try {
244
262
  const deviceAuth = await client.startDeviceAuthorization();
245
- writeLine(io.stdout, `Open: ${deviceAuth.verificationUriComplete ?? deviceAuth.verificationUri}`);
246
- writeLine(io.stdout, `Code: ${deviceAuth.userCode}`);
247
- writeLine(io.stdout, `Expires: ${deviceAuth.expiresAt}`);
248
- writeLine(io.stdout, "Approve the login in a browser, then the CLI will continue automatically.");
263
+ ui.heading("Device Login");
264
+ ui.status("info", "Approve the login in a browser. The CLI will continue automatically.");
265
+ ui.keyValue("Open", deviceAuth.verificationUriComplete ?? deviceAuth.verificationUri);
266
+ ui.keyValue("Code", deviceAuth.userCode);
267
+ ui.keyValue("Expires", deviceAuth.expiresAt);
249
268
 
250
269
  if (flags["no-open"] !== true && typeof deps.openUrl === "function") {
251
270
  await deps.openUrl(deviceAuth.verificationUriComplete ?? deviceAuth.verificationUri);
@@ -271,42 +290,44 @@ async function handleDeviceAuthLogin(io, env, flags, deps) {
271
290
  expiresAt: result.expiresAt,
272
291
  };
273
292
 
274
- if (!(await persistSession(io, env, session))) {
293
+ if (!(await persistSession(io, ui, env, session))) {
275
294
  return 1;
276
295
  }
277
296
 
278
- writeLine(io.stdout, `Logged in to ${dashboardUrl}`);
279
- writeLine(io.stdout, `Session storage: ${getSessionStorageMode(env)}`);
297
+ ui.blank();
298
+ ui.status("ok", "Logged in.");
299
+ ui.keyValue("Dashboard", dashboardUrl);
300
+ ui.keyValue("Session storage", getSessionStorageMode(env));
280
301
  if (result.session?.role) {
281
- writeLine(io.stdout, `Facilitator role: ${result.session.role}`);
302
+ ui.keyValue("Facilitator role", result.session.role);
282
303
  }
283
304
  return 0;
284
305
  }
285
306
 
286
307
  if (result.status === "access_denied") {
287
- writeLine(io.stderr, "Login failed: device authorization was denied.");
308
+ ui.status("error", "Login failed: device authorization was denied.", { stream: "stderr" });
288
309
  return 1;
289
310
  }
290
311
 
291
312
  if (result.status === "expired_token") {
292
- writeLine(io.stderr, "Login failed: device authorization expired.");
313
+ ui.status("error", "Login failed: device authorization expired.", { stream: "stderr" });
293
314
  return 1;
294
315
  }
295
316
 
296
- writeLine(io.stderr, "Login failed: device authorization could not be completed.");
317
+ ui.status("error", "Login failed: device authorization could not be completed.", { stream: "stderr" });
297
318
  return 1;
298
319
  }
299
320
 
300
- writeLine(io.stderr, "Login failed: device authorization expired.");
321
+ ui.status("error", "Login failed: device authorization expired.", { stream: "stderr" });
301
322
  return 1;
302
323
  } catch (error) {
303
324
  if (error instanceof HarnessApiError) {
304
- writeLine(io.stderr, `Login failed: ${error.message}`);
325
+ ui.status("error", `Login failed: ${error.message}`, { stream: "stderr" });
305
326
  return 1;
306
327
  }
307
328
 
308
329
  if (error instanceof SessionStoreError) {
309
- writeLine(io.stderr, `Session storage failed: ${error.message}`);
330
+ ui.status("error", `Session storage failed: ${error.message}`, { stream: "stderr" });
310
331
  return 1;
311
332
  }
312
333
 
@@ -314,45 +335,45 @@ async function handleDeviceAuthLogin(io, env, flags, deps) {
314
335
  }
315
336
  }
316
337
 
317
- async function handleAuthLogin(io, env, flags, deps) {
338
+ async function handleAuthLogin(io, ui, env, flags, deps) {
318
339
  const authMode = String(flags.auth ?? env.HARNESS_AUTH_MODE ?? "device");
319
340
 
320
341
  if (authMode === "device") {
321
- return handleDeviceAuthLogin(io, env, flags, deps);
342
+ return handleDeviceAuthLogin(io, ui, env, flags, deps);
322
343
  }
323
344
 
324
345
  if (authMode === "neon") {
325
- return handleNeonAuthLogin(io, env, flags, deps);
346
+ return handleNeonAuthLogin(io, ui, env, flags, deps);
326
347
  }
327
348
 
328
- return handleBasicAuthLogin(io, env, flags, deps);
349
+ return handleBasicAuthLogin(io, ui, env, flags, deps);
329
350
  }
330
351
 
331
- async function requireSession(io, env) {
352
+ async function requireSession(io, ui, env) {
332
353
  try {
333
354
  const session = await readSession(env);
334
355
  if (!session) {
335
- writeLine(io.stderr, "No active session. Run `harness auth login` first.");
356
+ ui.status("error", "No active session. Run `harness auth login` first.", { stream: "stderr" });
336
357
  return null;
337
358
  }
338
359
  return session;
339
360
  } catch (error) {
340
- writeLine(io.stderr, `Session storage failed: ${formatStorageError(error)}`);
361
+ ui.status("error", `Session storage failed: ${formatStorageError(error)}`, { stream: "stderr" });
341
362
  return null;
342
363
  }
343
364
  }
344
365
 
345
- async function handleAuthStatus(io, env, deps) {
366
+ async function handleAuthStatus(io, ui, env, deps) {
346
367
  let session;
347
368
  try {
348
369
  session = await readSession(env);
349
370
  } catch (error) {
350
- writeLine(io.stderr, `Session storage failed: ${formatStorageError(error)}`);
371
+ ui.status("error", `Session storage failed: ${formatStorageError(error)}`, { stream: "stderr" });
351
372
  return 1;
352
373
  }
353
374
 
354
375
  if (!session) {
355
- writeLine(io.stdout, "Not logged in.");
376
+ ui.status("info", "Not logged in.");
356
377
  return 0;
357
378
  }
358
379
 
@@ -360,14 +381,11 @@ async function handleAuthStatus(io, env, deps) {
360
381
  try {
361
382
  const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
362
383
  const deviceSession = await client.getDeviceSession();
363
- writeLine(
364
- io.stdout,
365
- JSON.stringify({ ok: true, session: sanitizeSession(session, env), remoteSession: deviceSession.session }, null, 2),
366
- );
384
+ ui.json("Auth Status", { ok: true, session: sanitizeSession(session, env), remoteSession: deviceSession.session });
367
385
  return 0;
368
386
  } catch (error) {
369
387
  if (error instanceof HarnessApiError) {
370
- writeLine(io.stdout, JSON.stringify({ ok: false, session: sanitizeSession(session, env), error: error.message }, null, 2));
388
+ ui.json("Auth Status", { ok: false, session: sanitizeSession(session, env), error: error.message });
371
389
  return 1;
372
390
  }
373
391
  throw error;
@@ -378,30 +396,27 @@ async function handleAuthStatus(io, env, deps) {
378
396
  try {
379
397
  const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
380
398
  const authSession = await client.getAuthSession();
381
- writeLine(
382
- io.stdout,
383
- JSON.stringify({ ok: true, session: sanitizeSession(session, env), remoteSession: authSession }, null, 2),
384
- );
399
+ ui.json("Auth Status", { ok: true, session: sanitizeSession(session, env), remoteSession: authSession });
385
400
  return 0;
386
401
  } catch (error) {
387
402
  if (error instanceof HarnessApiError) {
388
- writeLine(io.stdout, JSON.stringify({ ok: false, session: sanitizeSession(session, env), error: error.message }, null, 2));
403
+ ui.json("Auth Status", { ok: false, session: sanitizeSession(session, env), error: error.message });
389
404
  return 1;
390
405
  }
391
406
  throw error;
392
407
  }
393
408
  }
394
409
 
395
- writeLine(io.stdout, JSON.stringify({ ok: true, session: sanitizeSession(session, env) }, null, 2));
410
+ ui.json("Auth Status", { ok: true, session: sanitizeSession(session, env) });
396
411
  return 0;
397
412
  }
398
413
 
399
- async function handleAuthLogout(io, env, deps) {
414
+ async function handleAuthLogout(io, ui, env, deps) {
400
415
  let session;
401
416
  try {
402
417
  session = await readSession(env);
403
418
  } catch (error) {
404
- writeLine(io.stderr, `Session storage failed: ${formatStorageError(error)}`);
419
+ ui.status("error", `Session storage failed: ${formatStorageError(error)}`, { stream: "stderr" });
405
420
  return 1;
406
421
  }
407
422
 
@@ -430,15 +445,15 @@ async function handleAuthLogout(io, env, deps) {
430
445
  try {
431
446
  await deleteSession(env);
432
447
  } catch (error) {
433
- writeLine(io.stderr, `Session storage failed: ${formatStorageError(error)}`);
448
+ ui.status("error", `Session storage failed: ${formatStorageError(error)}`, { stream: "stderr" });
434
449
  return 1;
435
450
  }
436
- writeLine(io.stdout, "Logged out.");
451
+ ui.status("ok", "Logged out.");
437
452
  return 0;
438
453
  }
439
454
 
440
- async function handleWorkshopStatus(io, env, deps) {
441
- const session = await requireSession(io, env);
455
+ async function handleWorkshopStatus(io, ui, env, deps) {
456
+ const session = await requireSession(io, ui, env);
442
457
  if (!session) {
443
458
  return 1;
444
459
  }
@@ -446,32 +461,25 @@ async function handleWorkshopStatus(io, env, deps) {
446
461
  try {
447
462
  const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
448
463
  const [workshop, agenda] = await Promise.all([client.getWorkshopStatus(), client.getAgenda()]);
449
- writeLine(
450
- io.stdout,
451
- JSON.stringify(
452
- {
453
- ok: true,
454
- workshopId: workshop.workshopId,
455
- workshopMeta: workshop.workshopMeta,
456
- currentPhase: agenda.phase,
457
- templates: workshop.templates,
458
- },
459
- null,
460
- 2,
461
- ),
462
- );
464
+ ui.json("Workshop Status", {
465
+ ok: true,
466
+ workshopId: workshop.workshopId,
467
+ workshopMeta: workshop.workshopMeta,
468
+ currentPhase: agenda.phase,
469
+ templates: workshop.templates,
470
+ });
463
471
  return 0;
464
472
  } catch (error) {
465
473
  if (error instanceof HarnessApiError) {
466
- writeLine(io.stderr, `Workshop status failed: ${error.message}`);
474
+ ui.status("error", `Workshop status failed: ${error.message}`, { stream: "stderr" });
467
475
  return 1;
468
476
  }
469
477
  throw error;
470
478
  }
471
479
  }
472
480
 
473
- async function handleWorkshopArchive(io, env, flags, deps) {
474
- const session = await requireSession(io, env);
481
+ async function handleWorkshopArchive(io, ui, env, flags, deps) {
482
+ const session = await requireSession(io, ui, env);
475
483
  if (!session) {
476
484
  return 1;
477
485
  }
@@ -479,25 +487,25 @@ async function handleWorkshopArchive(io, env, flags, deps) {
479
487
  try {
480
488
  const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
481
489
  const result = await client.archiveWorkshop(typeof flags.notes === "string" ? flags.notes : undefined);
482
- writeLine(io.stdout, JSON.stringify(result, null, 2));
490
+ ui.json("Workshop Archive", result);
483
491
  return 0;
484
492
  } catch (error) {
485
493
  if (error instanceof HarnessApiError) {
486
- writeLine(io.stderr, `Archive failed: ${error.message}`);
494
+ ui.status("error", `Archive failed: ${error.message}`, { stream: "stderr" });
487
495
  return 1;
488
496
  }
489
497
  throw error;
490
498
  }
491
499
  }
492
500
 
493
- async function handleWorkshopPhaseSet(io, env, positionals, deps) {
501
+ async function handleWorkshopPhaseSet(io, ui, env, positionals, deps) {
494
502
  const phaseId = positionals[3];
495
503
  if (!phaseId) {
496
- writeLine(io.stderr, "Phase id is required.");
504
+ ui.status("error", "Phase id is required.", { stream: "stderr" });
497
505
  return 1;
498
506
  }
499
507
 
500
- const session = await requireSession(io, env);
508
+ const session = await requireSession(io, ui, env);
501
509
  if (!session) {
502
510
  return 1;
503
511
  }
@@ -505,11 +513,11 @@ async function handleWorkshopPhaseSet(io, env, positionals, deps) {
505
513
  try {
506
514
  const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
507
515
  const result = await client.setCurrentPhase(phaseId);
508
- writeLine(io.stdout, JSON.stringify(result, null, 2));
516
+ ui.json("Workshop Phase", result);
509
517
  return 0;
510
518
  } catch (error) {
511
519
  if (error instanceof HarnessApiError) {
512
- writeLine(io.stderr, `Phase update failed: ${error.message}`);
520
+ ui.status("error", `Phase update failed: ${error.message}`, { stream: "stderr" });
513
521
  return 1;
514
522
  }
515
523
  throw error;
@@ -519,11 +527,12 @@ async function handleWorkshopPhaseSet(io, env, positionals, deps) {
519
527
  export async function runCli(argv, io, deps = {}) {
520
528
  const fetchFn = deps.fetchFn ?? globalThis.fetch;
521
529
  const mergedDeps = { fetchFn, sleepFn: deps.sleepFn, openUrl: deps.openUrl, cwd: deps.cwd };
530
+ const ui = createCliUi(io);
522
531
  const { positionals, flags } = parseArgs(argv);
523
532
  const [scope, action, subaction] = positionals;
524
533
 
525
534
  if (flags.help === true) {
526
- printUsage(io);
535
+ printUsage(io, ui);
527
536
  return 0;
528
537
  }
529
538
 
@@ -538,7 +547,7 @@ export async function runCli(argv, io, deps = {}) {
538
547
  }
539
548
 
540
549
  if (!scope) {
541
- printUsage(io);
550
+ printUsage(io, ui);
542
551
  return 1;
543
552
  }
544
553
 
@@ -547,34 +556,34 @@ export async function runCli(argv, io, deps = {}) {
547
556
  }
548
557
 
549
558
  if (scope === "auth" && action === "login") {
550
- return handleAuthLogin(io, io.env, flags, mergedDeps);
559
+ return handleAuthLogin(io, ui, io.env, flags, mergedDeps);
551
560
  }
552
561
 
553
562
  if (scope === "auth" && action === "logout") {
554
- return handleAuthLogout(io, io.env, mergedDeps);
563
+ return handleAuthLogout(io, ui, io.env, mergedDeps);
555
564
  }
556
565
 
557
566
  if (scope === "auth" && action === "status") {
558
- return handleAuthStatus(io, io.env, mergedDeps);
567
+ return handleAuthStatus(io, ui, io.env, mergedDeps);
559
568
  }
560
569
 
561
570
  if (scope === "skill" && action === "install") {
562
- return handleSkillInstall(io, mergedDeps, flags);
571
+ return handleSkillInstall(io, ui, mergedDeps, flags);
563
572
  }
564
573
 
565
574
  if (scope === "workshop" && action === "status") {
566
- return handleWorkshopStatus(io, io.env, mergedDeps);
575
+ return handleWorkshopStatus(io, ui, io.env, mergedDeps);
567
576
  }
568
577
 
569
578
  if (scope === "workshop" && action === "archive") {
570
- return handleWorkshopArchive(io, io.env, flags, mergedDeps);
579
+ return handleWorkshopArchive(io, ui, io.env, flags, mergedDeps);
571
580
  }
572
581
 
573
582
  if (scope === "workshop" && action === "phase" && subaction === "set") {
574
- return handleWorkshopPhaseSet(io, io.env, positionals, mergedDeps);
583
+ return handleWorkshopPhaseSet(io, ui, io.env, positionals, mergedDeps);
575
584
  }
576
585
 
577
- printUsage(io);
586
+ printUsage(io, ui);
578
587
  return 1;
579
588
  }
580
589