@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 +2 -2
- package/package.json +4 -1
- package/src/io.js +177 -0
- package/src/run-cli.js +127 -118
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/
|
|
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 `
|
|
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.
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
173
|
+
ui.keyValue("Workshop", payload.workshopId);
|
|
158
174
|
}
|
|
159
175
|
return 0;
|
|
160
176
|
} catch (error) {
|
|
161
177
|
if (error instanceof HarnessApiError) {
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
242
|
+
ui.keyValue("Facilitator", authSession.user.email);
|
|
225
243
|
}
|
|
226
244
|
return 0;
|
|
227
245
|
} catch (error) {
|
|
228
246
|
if (error instanceof HarnessApiError) {
|
|
229
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
313
|
+
ui.status("error", "Login failed: device authorization expired.", { stream: "stderr" });
|
|
293
314
|
return 1;
|
|
294
315
|
}
|
|
295
316
|
|
|
296
|
-
|
|
317
|
+
ui.status("error", "Login failed: device authorization could not be completed.", { stream: "stderr" });
|
|
297
318
|
return 1;
|
|
298
319
|
}
|
|
299
320
|
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
371
|
+
ui.status("error", `Session storage failed: ${formatStorageError(error)}`, { stream: "stderr" });
|
|
351
372
|
return 1;
|
|
352
373
|
}
|
|
353
374
|
|
|
354
375
|
if (!session) {
|
|
355
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
448
|
+
ui.status("error", `Session storage failed: ${formatStorageError(error)}`, { stream: "stderr" });
|
|
434
449
|
return 1;
|
|
435
450
|
}
|
|
436
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
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
|
-
|
|
490
|
+
ui.json("Workshop Archive", result);
|
|
483
491
|
return 0;
|
|
484
492
|
} catch (error) {
|
|
485
493
|
if (error instanceof HarnessApiError) {
|
|
486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
516
|
+
ui.json("Workshop Phase", result);
|
|
509
517
|
return 0;
|
|
510
518
|
} catch (error) {
|
|
511
519
|
if (error instanceof HarnessApiError) {
|
|
512
|
-
|
|
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
|
|