@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 CHANGED
@@ -1,9 +1,11 @@
1
1
  # Harness CLI
2
2
 
3
- Small facilitator-facing CLI for Harness Lab.
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 macOS Keychain, Windows Credential Manager, or Linux Secret Service by default
20
- - only uses file storage under `HARNESS_CLI_HOME` or `~/.harness` when `HARNESS_SESSION_STORAGE=file` is set explicitly
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`, or `file`)
118
+ - `HARNESS_SESSION_STORAGE` (`file`, `keychain`, `credential-manager`, or `secret-service`)
100
119
 
101
120
  ## Release Gate
102
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-lab/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Participant-facing Harness Lab CLI for facilitator auth and workshop operations",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
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
- if (typeof fetchFn !== "function") {
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
  }
@@ -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
+ }