@harness-lab/cli 0.1.4 → 0.1.7
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 +1 -1
- package/package.json +4 -1
- package/src/io.js +177 -0
- package/src/run-cli.js +142 -116
- package/src/skill-install.js +21 -1
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ 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
|
|
69
|
+
After install, the CLI prints the first recommended agent commands, starting with `Codex: $workshop reference` and `OpenCode: /workshop reference`.
|
|
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.7",
|
|
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,9 +1,10 @@
|
|
|
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";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
7
8
|
|
|
8
9
|
const require = createRequire(import.meta.url);
|
|
9
10
|
const { version } = require("../package.json");
|
|
@@ -63,38 +64,57 @@ async function readJson(response) {
|
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
function printUsage(io) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
]);
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
function printVersion(io) {
|
|
81
91
|
writeLine(io.stdout, `harness ${version}`);
|
|
82
92
|
}
|
|
83
93
|
|
|
84
|
-
async function handleSkillInstall(io, deps, flags) {
|
|
94
|
+
async function handleSkillInstall(io, ui, deps, flags) {
|
|
85
95
|
try {
|
|
86
96
|
const result = await installWorkshopSkill(deps.cwd ?? process.cwd(), { force: flags.force === true });
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
ui.heading("Workshop Skill");
|
|
98
|
+
if (result.mode === "already_bundled") {
|
|
99
|
+
ui.status("ok", "Harness Lab workshop skill is already bundled in this repo.");
|
|
100
|
+
} else {
|
|
101
|
+
ui.status("ok", "Installed the Harness Lab workshop skill bundle.");
|
|
102
|
+
}
|
|
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 OpenCode in this repo.",
|
|
109
|
+
"Start with the workshop reference card.",
|
|
110
|
+
"Codex: `$workshop reference`. OpenCode: `/workshop reference`.",
|
|
111
|
+
"Need setup help? Codex: `$workshop setup`. OpenCode: `/workshop setup`.",
|
|
112
|
+
"Other workshop commands follow the same pattern: `$workshop ...` in Codex and `/workshop ...` in OpenCode.",
|
|
113
|
+
]);
|
|
94
114
|
return 0;
|
|
95
115
|
} catch (error) {
|
|
96
116
|
if (error instanceof SkillInstallError) {
|
|
97
|
-
|
|
117
|
+
ui.status("error", `Skill install failed: ${error.message}`, { stream: "stderr" });
|
|
98
118
|
return 1;
|
|
99
119
|
}
|
|
100
120
|
throw error;
|
|
@@ -109,23 +129,23 @@ function formatStorageError(error) {
|
|
|
109
129
|
return "Harness CLI could not access the configured session store.";
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
async function persistSession(io, env, session) {
|
|
132
|
+
async function persistSession(io, ui, env, session) {
|
|
113
133
|
try {
|
|
114
134
|
await writeSession(env, session);
|
|
115
135
|
return true;
|
|
116
136
|
} catch (error) {
|
|
117
|
-
|
|
137
|
+
ui.status("error", `Session storage failed: ${formatStorageError(error)}`, { stream: "stderr" });
|
|
118
138
|
return false;
|
|
119
139
|
}
|
|
120
140
|
}
|
|
121
141
|
|
|
122
|
-
async function handleBasicAuthLogin(io, env, flags, deps) {
|
|
142
|
+
async function handleBasicAuthLogin(io, ui, env, flags, deps) {
|
|
123
143
|
const dashboardUrl = String(flags["dashboard-url"] ?? getDefaultDashboardUrl(env));
|
|
124
144
|
const username = String(flags.username ?? env.HARNESS_ADMIN_USERNAME ?? (await prompt(io, "Username: ")));
|
|
125
145
|
const password = String(flags.password ?? env.HARNESS_ADMIN_PASSWORD ?? (await prompt(io, "Password: ")));
|
|
126
146
|
|
|
127
147
|
if (!username || !password) {
|
|
128
|
-
|
|
148
|
+
ui.status("error", "Username and password are required.", { stream: "stderr" });
|
|
129
149
|
return 1;
|
|
130
150
|
}
|
|
131
151
|
|
|
@@ -142,31 +162,33 @@ async function handleBasicAuthLogin(io, env, flags, deps) {
|
|
|
142
162
|
|
|
143
163
|
try {
|
|
144
164
|
const payload = await client.verifyAccess();
|
|
145
|
-
if (!(await persistSession(io, env, session))) {
|
|
165
|
+
if (!(await persistSession(io, ui, env, session))) {
|
|
146
166
|
return 1;
|
|
147
167
|
}
|
|
148
|
-
|
|
149
|
-
|
|
168
|
+
ui.heading("Auth Login");
|
|
169
|
+
ui.status("ok", "Logged in.");
|
|
170
|
+
ui.keyValue("Dashboard", dashboardUrl);
|
|
171
|
+
ui.keyValue("Session storage", getSessionStorageMode(env));
|
|
150
172
|
if (payload?.workshopId) {
|
|
151
|
-
|
|
173
|
+
ui.keyValue("Workshop", payload.workshopId);
|
|
152
174
|
}
|
|
153
175
|
return 0;
|
|
154
176
|
} catch (error) {
|
|
155
177
|
if (error instanceof HarnessApiError) {
|
|
156
|
-
|
|
178
|
+
ui.status("error", `Login failed: ${error.message}`, { stream: "stderr" });
|
|
157
179
|
return 1;
|
|
158
180
|
}
|
|
159
181
|
throw error;
|
|
160
182
|
}
|
|
161
183
|
}
|
|
162
184
|
|
|
163
|
-
async function handleNeonAuthLogin(io, env, flags, deps) {
|
|
185
|
+
async function handleNeonAuthLogin(io, ui, env, flags, deps) {
|
|
164
186
|
const dashboardUrl = String(flags["dashboard-url"] ?? getDefaultDashboardUrl(env));
|
|
165
187
|
const email = String(flags.email ?? env.HARNESS_FACILITATOR_EMAIL ?? (await prompt(io, "Email: ")));
|
|
166
188
|
const password = String(flags.password ?? env.HARNESS_FACILITATOR_PASSWORD ?? (await prompt(io, "Password: ")));
|
|
167
189
|
|
|
168
190
|
if (!email || !password) {
|
|
169
|
-
|
|
191
|
+
ui.status("error", "Email and password are required.", { stream: "stderr" });
|
|
170
192
|
return 1;
|
|
171
193
|
}
|
|
172
194
|
|
|
@@ -187,13 +209,13 @@ async function handleNeonAuthLogin(io, env, flags, deps) {
|
|
|
187
209
|
: payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string"
|
|
188
210
|
? payload.error
|
|
189
211
|
: `Login failed with status ${signInResponse.status}`;
|
|
190
|
-
|
|
212
|
+
ui.status("error", `Login failed: ${message}`, { stream: "stderr" });
|
|
191
213
|
return 1;
|
|
192
214
|
}
|
|
193
215
|
|
|
194
216
|
const setCookie = signInResponse.headers?.get?.("set-cookie");
|
|
195
217
|
if (!setCookie) {
|
|
196
|
-
|
|
218
|
+
ui.status("error", "Login failed: auth response did not include a session cookie.", { stream: "stderr" });
|
|
197
219
|
return 1;
|
|
198
220
|
}
|
|
199
221
|
|
|
@@ -209,25 +231,27 @@ async function handleNeonAuthLogin(io, env, flags, deps) {
|
|
|
209
231
|
try {
|
|
210
232
|
const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
|
|
211
233
|
const authSession = await client.getAuthSession();
|
|
212
|
-
if (!(await persistSession(io, env, session))) {
|
|
234
|
+
if (!(await persistSession(io, ui, env, session))) {
|
|
213
235
|
return 1;
|
|
214
236
|
}
|
|
215
|
-
|
|
216
|
-
|
|
237
|
+
ui.heading("Auth Login");
|
|
238
|
+
ui.status("ok", "Logged in.");
|
|
239
|
+
ui.keyValue("Dashboard", dashboardUrl);
|
|
240
|
+
ui.keyValue("Session storage", getSessionStorageMode(env));
|
|
217
241
|
if (authSession?.user?.email) {
|
|
218
|
-
|
|
242
|
+
ui.keyValue("Facilitator", authSession.user.email);
|
|
219
243
|
}
|
|
220
244
|
return 0;
|
|
221
245
|
} catch (error) {
|
|
222
246
|
if (error instanceof HarnessApiError) {
|
|
223
|
-
|
|
247
|
+
ui.status("error", `Session verification failed: ${error.message}`, { stream: "stderr" });
|
|
224
248
|
return 1;
|
|
225
249
|
}
|
|
226
250
|
throw error;
|
|
227
251
|
}
|
|
228
252
|
}
|
|
229
253
|
|
|
230
|
-
async function handleDeviceAuthLogin(io, env, flags, deps) {
|
|
254
|
+
async function handleDeviceAuthLogin(io, ui, env, flags, deps) {
|
|
231
255
|
const dashboardUrl = String(flags["dashboard-url"] ?? getDefaultDashboardUrl(env));
|
|
232
256
|
const client = createHarnessClient({
|
|
233
257
|
fetchFn: deps.fetchFn,
|
|
@@ -236,10 +260,11 @@ async function handleDeviceAuthLogin(io, env, flags, deps) {
|
|
|
236
260
|
|
|
237
261
|
try {
|
|
238
262
|
const deviceAuth = await client.startDeviceAuthorization();
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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);
|
|
243
268
|
|
|
244
269
|
if (flags["no-open"] !== true && typeof deps.openUrl === "function") {
|
|
245
270
|
await deps.openUrl(deviceAuth.verificationUriComplete ?? deviceAuth.verificationUri);
|
|
@@ -265,42 +290,44 @@ async function handleDeviceAuthLogin(io, env, flags, deps) {
|
|
|
265
290
|
expiresAt: result.expiresAt,
|
|
266
291
|
};
|
|
267
292
|
|
|
268
|
-
if (!(await persistSession(io, env, session))) {
|
|
293
|
+
if (!(await persistSession(io, ui, env, session))) {
|
|
269
294
|
return 1;
|
|
270
295
|
}
|
|
271
296
|
|
|
272
|
-
|
|
273
|
-
|
|
297
|
+
ui.blank();
|
|
298
|
+
ui.status("ok", "Logged in.");
|
|
299
|
+
ui.keyValue("Dashboard", dashboardUrl);
|
|
300
|
+
ui.keyValue("Session storage", getSessionStorageMode(env));
|
|
274
301
|
if (result.session?.role) {
|
|
275
|
-
|
|
302
|
+
ui.keyValue("Facilitator role", result.session.role);
|
|
276
303
|
}
|
|
277
304
|
return 0;
|
|
278
305
|
}
|
|
279
306
|
|
|
280
307
|
if (result.status === "access_denied") {
|
|
281
|
-
|
|
308
|
+
ui.status("error", "Login failed: device authorization was denied.", { stream: "stderr" });
|
|
282
309
|
return 1;
|
|
283
310
|
}
|
|
284
311
|
|
|
285
312
|
if (result.status === "expired_token") {
|
|
286
|
-
|
|
313
|
+
ui.status("error", "Login failed: device authorization expired.", { stream: "stderr" });
|
|
287
314
|
return 1;
|
|
288
315
|
}
|
|
289
316
|
|
|
290
|
-
|
|
317
|
+
ui.status("error", "Login failed: device authorization could not be completed.", { stream: "stderr" });
|
|
291
318
|
return 1;
|
|
292
319
|
}
|
|
293
320
|
|
|
294
|
-
|
|
321
|
+
ui.status("error", "Login failed: device authorization expired.", { stream: "stderr" });
|
|
295
322
|
return 1;
|
|
296
323
|
} catch (error) {
|
|
297
324
|
if (error instanceof HarnessApiError) {
|
|
298
|
-
|
|
325
|
+
ui.status("error", `Login failed: ${error.message}`, { stream: "stderr" });
|
|
299
326
|
return 1;
|
|
300
327
|
}
|
|
301
328
|
|
|
302
329
|
if (error instanceof SessionStoreError) {
|
|
303
|
-
|
|
330
|
+
ui.status("error", `Session storage failed: ${error.message}`, { stream: "stderr" });
|
|
304
331
|
return 1;
|
|
305
332
|
}
|
|
306
333
|
|
|
@@ -308,45 +335,45 @@ async function handleDeviceAuthLogin(io, env, flags, deps) {
|
|
|
308
335
|
}
|
|
309
336
|
}
|
|
310
337
|
|
|
311
|
-
async function handleAuthLogin(io, env, flags, deps) {
|
|
338
|
+
async function handleAuthLogin(io, ui, env, flags, deps) {
|
|
312
339
|
const authMode = String(flags.auth ?? env.HARNESS_AUTH_MODE ?? "device");
|
|
313
340
|
|
|
314
341
|
if (authMode === "device") {
|
|
315
|
-
return handleDeviceAuthLogin(io, env, flags, deps);
|
|
342
|
+
return handleDeviceAuthLogin(io, ui, env, flags, deps);
|
|
316
343
|
}
|
|
317
344
|
|
|
318
345
|
if (authMode === "neon") {
|
|
319
|
-
return handleNeonAuthLogin(io, env, flags, deps);
|
|
346
|
+
return handleNeonAuthLogin(io, ui, env, flags, deps);
|
|
320
347
|
}
|
|
321
348
|
|
|
322
|
-
return handleBasicAuthLogin(io, env, flags, deps);
|
|
349
|
+
return handleBasicAuthLogin(io, ui, env, flags, deps);
|
|
323
350
|
}
|
|
324
351
|
|
|
325
|
-
async function requireSession(io, env) {
|
|
352
|
+
async function requireSession(io, ui, env) {
|
|
326
353
|
try {
|
|
327
354
|
const session = await readSession(env);
|
|
328
355
|
if (!session) {
|
|
329
|
-
|
|
356
|
+
ui.status("error", "No active session. Run `harness auth login` first.", { stream: "stderr" });
|
|
330
357
|
return null;
|
|
331
358
|
}
|
|
332
359
|
return session;
|
|
333
360
|
} catch (error) {
|
|
334
|
-
|
|
361
|
+
ui.status("error", `Session storage failed: ${formatStorageError(error)}`, { stream: "stderr" });
|
|
335
362
|
return null;
|
|
336
363
|
}
|
|
337
364
|
}
|
|
338
365
|
|
|
339
|
-
async function handleAuthStatus(io, env, deps) {
|
|
366
|
+
async function handleAuthStatus(io, ui, env, deps) {
|
|
340
367
|
let session;
|
|
341
368
|
try {
|
|
342
369
|
session = await readSession(env);
|
|
343
370
|
} catch (error) {
|
|
344
|
-
|
|
371
|
+
ui.status("error", `Session storage failed: ${formatStorageError(error)}`, { stream: "stderr" });
|
|
345
372
|
return 1;
|
|
346
373
|
}
|
|
347
374
|
|
|
348
375
|
if (!session) {
|
|
349
|
-
|
|
376
|
+
ui.status("info", "Not logged in.");
|
|
350
377
|
return 0;
|
|
351
378
|
}
|
|
352
379
|
|
|
@@ -354,14 +381,11 @@ async function handleAuthStatus(io, env, deps) {
|
|
|
354
381
|
try {
|
|
355
382
|
const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
|
|
356
383
|
const deviceSession = await client.getDeviceSession();
|
|
357
|
-
|
|
358
|
-
io.stdout,
|
|
359
|
-
JSON.stringify({ ok: true, session: sanitizeSession(session, env), remoteSession: deviceSession.session }, null, 2),
|
|
360
|
-
);
|
|
384
|
+
ui.json("Auth Status", { ok: true, session: sanitizeSession(session, env), remoteSession: deviceSession.session });
|
|
361
385
|
return 0;
|
|
362
386
|
} catch (error) {
|
|
363
387
|
if (error instanceof HarnessApiError) {
|
|
364
|
-
|
|
388
|
+
ui.json("Auth Status", { ok: false, session: sanitizeSession(session, env), error: error.message });
|
|
365
389
|
return 1;
|
|
366
390
|
}
|
|
367
391
|
throw error;
|
|
@@ -372,30 +396,27 @@ async function handleAuthStatus(io, env, deps) {
|
|
|
372
396
|
try {
|
|
373
397
|
const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
|
|
374
398
|
const authSession = await client.getAuthSession();
|
|
375
|
-
|
|
376
|
-
io.stdout,
|
|
377
|
-
JSON.stringify({ ok: true, session: sanitizeSession(session, env), remoteSession: authSession }, null, 2),
|
|
378
|
-
);
|
|
399
|
+
ui.json("Auth Status", { ok: true, session: sanitizeSession(session, env), remoteSession: authSession });
|
|
379
400
|
return 0;
|
|
380
401
|
} catch (error) {
|
|
381
402
|
if (error instanceof HarnessApiError) {
|
|
382
|
-
|
|
403
|
+
ui.json("Auth Status", { ok: false, session: sanitizeSession(session, env), error: error.message });
|
|
383
404
|
return 1;
|
|
384
405
|
}
|
|
385
406
|
throw error;
|
|
386
407
|
}
|
|
387
408
|
}
|
|
388
409
|
|
|
389
|
-
|
|
410
|
+
ui.json("Auth Status", { ok: true, session: sanitizeSession(session, env) });
|
|
390
411
|
return 0;
|
|
391
412
|
}
|
|
392
413
|
|
|
393
|
-
async function handleAuthLogout(io, env, deps) {
|
|
414
|
+
async function handleAuthLogout(io, ui, env, deps) {
|
|
394
415
|
let session;
|
|
395
416
|
try {
|
|
396
417
|
session = await readSession(env);
|
|
397
418
|
} catch (error) {
|
|
398
|
-
|
|
419
|
+
ui.status("error", `Session storage failed: ${formatStorageError(error)}`, { stream: "stderr" });
|
|
399
420
|
return 1;
|
|
400
421
|
}
|
|
401
422
|
|
|
@@ -424,15 +445,15 @@ async function handleAuthLogout(io, env, deps) {
|
|
|
424
445
|
try {
|
|
425
446
|
await deleteSession(env);
|
|
426
447
|
} catch (error) {
|
|
427
|
-
|
|
448
|
+
ui.status("error", `Session storage failed: ${formatStorageError(error)}`, { stream: "stderr" });
|
|
428
449
|
return 1;
|
|
429
450
|
}
|
|
430
|
-
|
|
451
|
+
ui.status("ok", "Logged out.");
|
|
431
452
|
return 0;
|
|
432
453
|
}
|
|
433
454
|
|
|
434
|
-
async function handleWorkshopStatus(io, env, deps) {
|
|
435
|
-
const session = await requireSession(io, env);
|
|
455
|
+
async function handleWorkshopStatus(io, ui, env, deps) {
|
|
456
|
+
const session = await requireSession(io, ui, env);
|
|
436
457
|
if (!session) {
|
|
437
458
|
return 1;
|
|
438
459
|
}
|
|
@@ -440,32 +461,25 @@ async function handleWorkshopStatus(io, env, deps) {
|
|
|
440
461
|
try {
|
|
441
462
|
const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
|
|
442
463
|
const [workshop, agenda] = await Promise.all([client.getWorkshopStatus(), client.getAgenda()]);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
currentPhase: agenda.phase,
|
|
451
|
-
templates: workshop.templates,
|
|
452
|
-
},
|
|
453
|
-
null,
|
|
454
|
-
2,
|
|
455
|
-
),
|
|
456
|
-
);
|
|
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
|
+
});
|
|
457
471
|
return 0;
|
|
458
472
|
} catch (error) {
|
|
459
473
|
if (error instanceof HarnessApiError) {
|
|
460
|
-
|
|
474
|
+
ui.status("error", `Workshop status failed: ${error.message}`, { stream: "stderr" });
|
|
461
475
|
return 1;
|
|
462
476
|
}
|
|
463
477
|
throw error;
|
|
464
478
|
}
|
|
465
479
|
}
|
|
466
480
|
|
|
467
|
-
async function handleWorkshopArchive(io, env, flags, deps) {
|
|
468
|
-
const session = await requireSession(io, env);
|
|
481
|
+
async function handleWorkshopArchive(io, ui, env, flags, deps) {
|
|
482
|
+
const session = await requireSession(io, ui, env);
|
|
469
483
|
if (!session) {
|
|
470
484
|
return 1;
|
|
471
485
|
}
|
|
@@ -473,25 +487,25 @@ async function handleWorkshopArchive(io, env, flags, deps) {
|
|
|
473
487
|
try {
|
|
474
488
|
const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
|
|
475
489
|
const result = await client.archiveWorkshop(typeof flags.notes === "string" ? flags.notes : undefined);
|
|
476
|
-
|
|
490
|
+
ui.json("Workshop Archive", result);
|
|
477
491
|
return 0;
|
|
478
492
|
} catch (error) {
|
|
479
493
|
if (error instanceof HarnessApiError) {
|
|
480
|
-
|
|
494
|
+
ui.status("error", `Archive failed: ${error.message}`, { stream: "stderr" });
|
|
481
495
|
return 1;
|
|
482
496
|
}
|
|
483
497
|
throw error;
|
|
484
498
|
}
|
|
485
499
|
}
|
|
486
500
|
|
|
487
|
-
async function handleWorkshopPhaseSet(io, env, positionals, deps) {
|
|
501
|
+
async function handleWorkshopPhaseSet(io, ui, env, positionals, deps) {
|
|
488
502
|
const phaseId = positionals[3];
|
|
489
503
|
if (!phaseId) {
|
|
490
|
-
|
|
504
|
+
ui.status("error", "Phase id is required.", { stream: "stderr" });
|
|
491
505
|
return 1;
|
|
492
506
|
}
|
|
493
507
|
|
|
494
|
-
const session = await requireSession(io, env);
|
|
508
|
+
const session = await requireSession(io, ui, env);
|
|
495
509
|
if (!session) {
|
|
496
510
|
return 1;
|
|
497
511
|
}
|
|
@@ -499,11 +513,11 @@ async function handleWorkshopPhaseSet(io, env, positionals, deps) {
|
|
|
499
513
|
try {
|
|
500
514
|
const client = createHarnessClient({ fetchFn: deps.fetchFn, session });
|
|
501
515
|
const result = await client.setCurrentPhase(phaseId);
|
|
502
|
-
|
|
516
|
+
ui.json("Workshop Phase", result);
|
|
503
517
|
return 0;
|
|
504
518
|
} catch (error) {
|
|
505
519
|
if (error instanceof HarnessApiError) {
|
|
506
|
-
|
|
520
|
+
ui.status("error", `Phase update failed: ${error.message}`, { stream: "stderr" });
|
|
507
521
|
return 1;
|
|
508
522
|
}
|
|
509
523
|
throw error;
|
|
@@ -513,11 +527,12 @@ async function handleWorkshopPhaseSet(io, env, positionals, deps) {
|
|
|
513
527
|
export async function runCli(argv, io, deps = {}) {
|
|
514
528
|
const fetchFn = deps.fetchFn ?? globalThis.fetch;
|
|
515
529
|
const mergedDeps = { fetchFn, sleepFn: deps.sleepFn, openUrl: deps.openUrl, cwd: deps.cwd };
|
|
530
|
+
const ui = createCliUi(io);
|
|
516
531
|
const { positionals, flags } = parseArgs(argv);
|
|
517
532
|
const [scope, action, subaction] = positionals;
|
|
518
533
|
|
|
519
534
|
if (flags.help === true) {
|
|
520
|
-
printUsage(io);
|
|
535
|
+
printUsage(io, ui);
|
|
521
536
|
return 0;
|
|
522
537
|
}
|
|
523
538
|
|
|
@@ -532,7 +547,7 @@ export async function runCli(argv, io, deps = {}) {
|
|
|
532
547
|
}
|
|
533
548
|
|
|
534
549
|
if (!scope) {
|
|
535
|
-
printUsage(io);
|
|
550
|
+
printUsage(io, ui);
|
|
536
551
|
return 1;
|
|
537
552
|
}
|
|
538
553
|
|
|
@@ -541,33 +556,44 @@ export async function runCli(argv, io, deps = {}) {
|
|
|
541
556
|
}
|
|
542
557
|
|
|
543
558
|
if (scope === "auth" && action === "login") {
|
|
544
|
-
return handleAuthLogin(io, io.env, flags, mergedDeps);
|
|
559
|
+
return handleAuthLogin(io, ui, io.env, flags, mergedDeps);
|
|
545
560
|
}
|
|
546
561
|
|
|
547
562
|
if (scope === "auth" && action === "logout") {
|
|
548
|
-
return handleAuthLogout(io, io.env, mergedDeps);
|
|
563
|
+
return handleAuthLogout(io, ui, io.env, mergedDeps);
|
|
549
564
|
}
|
|
550
565
|
|
|
551
566
|
if (scope === "auth" && action === "status") {
|
|
552
|
-
return handleAuthStatus(io, io.env, mergedDeps);
|
|
567
|
+
return handleAuthStatus(io, ui, io.env, mergedDeps);
|
|
553
568
|
}
|
|
554
569
|
|
|
555
570
|
if (scope === "skill" && action === "install") {
|
|
556
|
-
return handleSkillInstall(io, mergedDeps, flags);
|
|
571
|
+
return handleSkillInstall(io, ui, mergedDeps, flags);
|
|
557
572
|
}
|
|
558
573
|
|
|
559
574
|
if (scope === "workshop" && action === "status") {
|
|
560
|
-
return handleWorkshopStatus(io, io.env, mergedDeps);
|
|
575
|
+
return handleWorkshopStatus(io, ui, io.env, mergedDeps);
|
|
561
576
|
}
|
|
562
577
|
|
|
563
578
|
if (scope === "workshop" && action === "archive") {
|
|
564
|
-
return handleWorkshopArchive(io, io.env, flags, mergedDeps);
|
|
579
|
+
return handleWorkshopArchive(io, ui, io.env, flags, mergedDeps);
|
|
565
580
|
}
|
|
566
581
|
|
|
567
582
|
if (scope === "workshop" && action === "phase" && subaction === "set") {
|
|
568
|
-
return handleWorkshopPhaseSet(io, io.env, positionals, mergedDeps);
|
|
583
|
+
return handleWorkshopPhaseSet(io, ui, io.env, positionals, mergedDeps);
|
|
569
584
|
}
|
|
570
585
|
|
|
571
|
-
printUsage(io);
|
|
586
|
+
printUsage(io, ui);
|
|
572
587
|
return 1;
|
|
573
588
|
}
|
|
589
|
+
|
|
590
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
591
|
+
const exitCode = await runCli(process.argv.slice(2), {
|
|
592
|
+
stdin: process.stdin,
|
|
593
|
+
stdout: process.stdout,
|
|
594
|
+
stderr: process.stderr,
|
|
595
|
+
env: process.env,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
process.exitCode = exitCode;
|
|
599
|
+
}
|
package/src/skill-install.js
CHANGED
|
@@ -41,6 +41,10 @@ export function getInstalledSkillPath(repoRoot) {
|
|
|
41
41
|
return path.join(repoRoot, ".agents", "skills", SKILL_NAME);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
async function hasBundledRepoSkill(repoRoot) {
|
|
45
|
+
return pathExists(path.join(getInstalledSkillPath(repoRoot), "SKILL.md"));
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
export async function installWorkshopSkill(startDir, options = {}) {
|
|
45
49
|
const repoRoot = await findHarnessLabRepoRoot(startDir);
|
|
46
50
|
if (!repoRoot) {
|
|
@@ -51,6 +55,17 @@ export async function installWorkshopSkill(startDir, options = {}) {
|
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
const installPath = getInstalledSkillPath(repoRoot);
|
|
58
|
+
const bundledRepoSkill = await hasBundledRepoSkill(repoRoot);
|
|
59
|
+
|
|
60
|
+
if (bundledRepoSkill) {
|
|
61
|
+
return {
|
|
62
|
+
repoRoot,
|
|
63
|
+
installPath,
|
|
64
|
+
skillName: SKILL_NAME,
|
|
65
|
+
mode: "already_bundled",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
if ((await pathExists(installPath)) && options.force !== true) {
|
|
55
70
|
throw new SkillInstallError(
|
|
56
71
|
`Skill already installed at ${installPath}. Re-run with --force to replace it.`,
|
|
@@ -78,5 +93,10 @@ export async function installWorkshopSkill(startDir, options = {}) {
|
|
|
78
93
|
path.join(installPath, "docs", "harness-cli-foundation.md"),
|
|
79
94
|
);
|
|
80
95
|
|
|
81
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
repoRoot,
|
|
98
|
+
installPath,
|
|
99
|
+
skillName: SKILL_NAME,
|
|
100
|
+
mode: "installed",
|
|
101
|
+
};
|
|
82
102
|
}
|