@harness-lab/cli 0.1.1 → 0.1.3
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 +23 -4
- package/package.json +1 -1
- package/src/run-cli.js +55 -5
- package/src/session-store.js +1 -17
- package/src/skill-install.js +81 -0
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Harness CLI
|
|
2
2
|
|
|
3
|
-
Small
|
|
3
|
+
Small Harness Lab CLI for facilitator auth, workshop operations, and repo-local skill installation.
|
|
4
4
|
|
|
5
5
|
Current shipped scope:
|
|
6
6
|
|
|
7
|
+
- `harness version`
|
|
8
|
+
- `harness skill install`
|
|
7
9
|
- `harness auth login`
|
|
8
10
|
- `harness auth logout`
|
|
9
11
|
- `harness auth status`
|
|
@@ -16,8 +18,8 @@ Current implementation posture:
|
|
|
16
18
|
- targets the existing shared dashboard facilitator APIs
|
|
17
19
|
- defaults to a browser/device approval flow backed by dashboard-side facilitator broker sessions
|
|
18
20
|
- keeps `--auth basic` and `--auth neon` as explicit local-dev/bootstrap fallback modes
|
|
19
|
-
- stores session material in
|
|
20
|
-
-
|
|
21
|
+
- stores session material in a local file under `HARNESS_CLI_HOME` or `~/.harness` by default
|
|
22
|
+
- supports macOS Keychain, Windows Credential Manager, and Linux Secret Service as explicit `HARNESS_SESSION_STORAGE` overrides
|
|
21
23
|
- supports brokered facilitator commands over the same workshop APIs used by the dashboard
|
|
22
24
|
|
|
23
25
|
## Usage
|
|
@@ -33,6 +35,7 @@ npm install -g @harness-lab/cli
|
|
|
33
35
|
Verify the binary:
|
|
34
36
|
|
|
35
37
|
```bash
|
|
38
|
+
harness --version
|
|
36
39
|
harness --help
|
|
37
40
|
```
|
|
38
41
|
|
|
@@ -49,6 +52,21 @@ cd harness-cli
|
|
|
49
52
|
npm link
|
|
50
53
|
```
|
|
51
54
|
|
|
55
|
+
Verify the local install:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
harness version
|
|
59
|
+
harness --help
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Install the repo-local workshop skill bundle for Codex/OpenCode discovery:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
harness skill install
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This creates `.agents/skills/harness-lab-workshop` in the current Harness Lab repo checkout.
|
|
69
|
+
|
|
52
70
|
Default device/browser login:
|
|
53
71
|
|
|
54
72
|
```bash
|
|
@@ -81,6 +99,7 @@ Workshop commands:
|
|
|
81
99
|
|
|
82
100
|
```bash
|
|
83
101
|
harness auth status
|
|
102
|
+
harness skill install
|
|
84
103
|
harness workshop status
|
|
85
104
|
harness workshop phase set rotation
|
|
86
105
|
harness workshop archive --notes "Manual archive"
|
|
@@ -96,7 +115,7 @@ Environment variables:
|
|
|
96
115
|
- `HARNESS_FACILITATOR_EMAIL`
|
|
97
116
|
- `HARNESS_FACILITATOR_PASSWORD`
|
|
98
117
|
- `HARNESS_CLI_HOME`
|
|
99
|
-
- `HARNESS_SESSION_STORAGE` (`keychain`, `credential-manager`, `secret-service
|
|
118
|
+
- `HARNESS_SESSION_STORAGE` (`file`, `keychain`, `credential-manager`, or `secret-service`)
|
|
100
119
|
|
|
101
120
|
## Release Gate
|
|
102
121
|
|
package/package.json
CHANGED
package/src/run-cli.js
CHANGED
|
@@ -2,6 +2,11 @@ import { getDefaultDashboardUrl } from "./config.js";
|
|
|
2
2
|
import { createHarnessClient, HarnessApiError } from "./client.js";
|
|
3
3
|
import { prompt, writeLine } from "./io.js";
|
|
4
4
|
import { deleteSession, readSession, sanitizeSession, writeSession, getSessionStorageMode, SessionStoreError } from "./session-store.js";
|
|
5
|
+
import { installWorkshopSkill, SkillInstallError } from "./skill-install.js";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const { version } = require("../package.json");
|
|
5
10
|
|
|
6
11
|
function sleep(ms) {
|
|
7
12
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -13,6 +18,14 @@ function parseArgs(argv) {
|
|
|
13
18
|
|
|
14
19
|
for (let index = 0; index < argv.length; index += 1) {
|
|
15
20
|
const value = argv[index];
|
|
21
|
+
if (value === "-h") {
|
|
22
|
+
flags.help = true;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (value === "-v") {
|
|
26
|
+
flags.version = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
16
29
|
if (value.startsWith("--")) {
|
|
17
30
|
const key = value.slice(2);
|
|
18
31
|
const next = argv[index + 1];
|
|
@@ -52,14 +65,37 @@ async function readJson(response) {
|
|
|
52
65
|
|
|
53
66
|
function printUsage(io) {
|
|
54
67
|
writeLine(io.stdout, "Usage:");
|
|
68
|
+
writeLine(io.stdout, " harness --help");
|
|
69
|
+
writeLine(io.stdout, " harness --version");
|
|
70
|
+
writeLine(io.stdout, " harness version");
|
|
55
71
|
writeLine(io.stdout, " harness auth login [--auth device|basic|neon] [--dashboard-url URL] [--username USER] [--email EMAIL] [--password PASS] [--no-open]");
|
|
56
72
|
writeLine(io.stdout, " harness auth logout");
|
|
57
73
|
writeLine(io.stdout, " harness auth status");
|
|
74
|
+
writeLine(io.stdout, " harness skill install [--force]");
|
|
58
75
|
writeLine(io.stdout, " harness workshop status");
|
|
59
76
|
writeLine(io.stdout, " harness workshop archive [--notes TEXT]");
|
|
60
77
|
writeLine(io.stdout, " harness workshop phase set <phase-id>");
|
|
61
78
|
}
|
|
62
79
|
|
|
80
|
+
function printVersion(io) {
|
|
81
|
+
writeLine(io.stdout, `harness ${version}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function handleSkillInstall(io, deps, flags) {
|
|
85
|
+
try {
|
|
86
|
+
const result = await installWorkshopSkill(deps.cwd ?? process.cwd(), { force: flags.force === true });
|
|
87
|
+
writeLine(io.stdout, `Installed Harness Lab workshop skill to ${result.installPath}`);
|
|
88
|
+
writeLine(io.stdout, "Codex and OpenCode should now discover it from this repo via .agents/skills.");
|
|
89
|
+
return 0;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof SkillInstallError) {
|
|
92
|
+
writeLine(io.stderr, `Skill install failed: ${error.message}`);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
63
99
|
function formatStorageError(error) {
|
|
64
100
|
if (error instanceof SessionStoreError) {
|
|
65
101
|
return error.message;
|
|
@@ -471,11 +507,7 @@ async function handleWorkshopPhaseSet(io, env, positionals, deps) {
|
|
|
471
507
|
|
|
472
508
|
export async function runCli(argv, io, deps = {}) {
|
|
473
509
|
const fetchFn = deps.fetchFn ?? globalThis.fetch;
|
|
474
|
-
|
|
475
|
-
throw new Error("Fetch is required to run the harness CLI.");
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const mergedDeps = { fetchFn, sleepFn: deps.sleepFn, openUrl: deps.openUrl };
|
|
510
|
+
const mergedDeps = { fetchFn, sleepFn: deps.sleepFn, openUrl: deps.openUrl, cwd: deps.cwd };
|
|
479
511
|
const { positionals, flags } = parseArgs(argv);
|
|
480
512
|
const [scope, action, subaction] = positionals;
|
|
481
513
|
|
|
@@ -484,11 +516,25 @@ export async function runCli(argv, io, deps = {}) {
|
|
|
484
516
|
return 0;
|
|
485
517
|
}
|
|
486
518
|
|
|
519
|
+
if (flags.version === true) {
|
|
520
|
+
printVersion(io);
|
|
521
|
+
return 0;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (scope === "version") {
|
|
525
|
+
printVersion(io);
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
487
529
|
if (!scope) {
|
|
488
530
|
printUsage(io);
|
|
489
531
|
return 1;
|
|
490
532
|
}
|
|
491
533
|
|
|
534
|
+
if (typeof fetchFn !== "function") {
|
|
535
|
+
throw new Error("Fetch is required to run the harness CLI.");
|
|
536
|
+
}
|
|
537
|
+
|
|
492
538
|
if (scope === "auth" && action === "login") {
|
|
493
539
|
return handleAuthLogin(io, io.env, flags, mergedDeps);
|
|
494
540
|
}
|
|
@@ -501,6 +547,10 @@ export async function runCli(argv, io, deps = {}) {
|
|
|
501
547
|
return handleAuthStatus(io, io.env, mergedDeps);
|
|
502
548
|
}
|
|
503
549
|
|
|
550
|
+
if (scope === "skill" && action === "install") {
|
|
551
|
+
return handleSkillInstall(io, mergedDeps, flags);
|
|
552
|
+
}
|
|
553
|
+
|
|
504
554
|
if (scope === "workshop" && action === "status") {
|
|
505
555
|
return handleWorkshopStatus(io, io.env, mergedDeps);
|
|
506
556
|
}
|
package/src/session-store.js
CHANGED
|
@@ -239,23 +239,7 @@ export function getSessionStorageMode(env) {
|
|
|
239
239
|
if (requested === "file" || requested === "keychain" || requested === "credential-manager" || requested === "secret-service") {
|
|
240
240
|
return requested;
|
|
241
241
|
}
|
|
242
|
-
|
|
243
|
-
if (getDeps().platform === "darwin") {
|
|
244
|
-
return "keychain";
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (getDeps().platform === "win32") {
|
|
248
|
-
return "credential-manager";
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (getDeps().platform === "linux") {
|
|
252
|
-
return "secret-service";
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
throw new SessionStoreError(
|
|
256
|
-
"Harness CLI does not know a secure session store for this platform. Set HARNESS_SESSION_STORAGE=file only if you need an explicit insecure fallback.",
|
|
257
|
-
{ code: "unsupported_platform" },
|
|
258
|
-
);
|
|
242
|
+
return "file";
|
|
259
243
|
}
|
|
260
244
|
|
|
261
245
|
function getStorageHint(storage) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export class SkillInstallError extends Error {
|
|
5
|
+
constructor(message, options = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "SkillInstallError";
|
|
8
|
+
this.code = options.code ?? "skill_install_failed";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SKILL_NAME = "harness-lab-workshop";
|
|
13
|
+
|
|
14
|
+
async function pathExists(targetPath) {
|
|
15
|
+
try {
|
|
16
|
+
await fs.access(targetPath);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function findHarnessLabRepoRoot(startDir) {
|
|
24
|
+
let currentDir = path.resolve(startDir);
|
|
25
|
+
|
|
26
|
+
while (true) {
|
|
27
|
+
if (await pathExists(path.join(currentDir, "workshop-skill", "SKILL.md"))) {
|
|
28
|
+
return currentDir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const parentDir = path.dirname(currentDir);
|
|
32
|
+
if (parentDir === currentDir) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
currentDir = parentDir;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getInstalledSkillPath(repoRoot) {
|
|
41
|
+
return path.join(repoRoot, ".agents", "skills", SKILL_NAME);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function installWorkshopSkill(startDir, options = {}) {
|
|
45
|
+
const repoRoot = await findHarnessLabRepoRoot(startDir);
|
|
46
|
+
if (!repoRoot) {
|
|
47
|
+
throw new SkillInstallError(
|
|
48
|
+
"Harness CLI could not find `workshop-skill/SKILL.md`. Run this command inside a Harness Lab repo checkout.",
|
|
49
|
+
{ code: "repo_not_found" },
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const installPath = getInstalledSkillPath(repoRoot);
|
|
54
|
+
if ((await pathExists(installPath)) && options.force !== true) {
|
|
55
|
+
throw new SkillInstallError(
|
|
56
|
+
`Skill already installed at ${installPath}. Re-run with --force to replace it.`,
|
|
57
|
+
{ code: "already_installed" },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (options.force === true) {
|
|
62
|
+
await fs.rm(installPath, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await fs.mkdir(installPath, { recursive: true });
|
|
66
|
+
await fs.cp(path.join(repoRoot, "workshop-skill"), path.join(installPath, "workshop-skill"), { recursive: true });
|
|
67
|
+
await fs.cp(path.join(repoRoot, "content"), path.join(installPath, "content"), { recursive: true });
|
|
68
|
+
await fs.cp(path.join(repoRoot, "workshop-blueprint"), path.join(installPath, "workshop-blueprint"), { recursive: true });
|
|
69
|
+
await fs.mkdir(path.join(installPath, "docs"), { recursive: true });
|
|
70
|
+
await fs.copyFile(path.join(repoRoot, "workshop-skill", "SKILL.md"), path.join(installPath, "SKILL.md"));
|
|
71
|
+
await fs.copyFile(
|
|
72
|
+
path.join(repoRoot, "docs", "workshop-event-context-contract.md"),
|
|
73
|
+
path.join(installPath, "docs", "workshop-event-context-contract.md"),
|
|
74
|
+
);
|
|
75
|
+
await fs.copyFile(
|
|
76
|
+
path.join(repoRoot, "docs", "harness-cli-foundation.md"),
|
|
77
|
+
path.join(installPath, "docs", "harness-cli-foundation.md"),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return { repoRoot, installPath, skillName: SKILL_NAME };
|
|
81
|
+
}
|