@harness-lab/cli 0.1.2 → 0.1.4

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`
@@ -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,22 @@ 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
+ After install, the CLI prints the first recommended agent commands, starting with `/workshop reference`.
70
+
52
71
  Default device/browser login:
53
72
 
54
73
  ```bash
@@ -81,6 +100,7 @@ Workshop commands:
81
100
 
82
101
  ```bash
83
102
  harness auth status
103
+ harness skill install
84
104
  harness workshop status
85
105
  harness workshop phase set rotation
86
106
  harness workshop archive --notes "Manual archive"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-lab/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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,42 @@ 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
+ writeLine(io.stdout, "Next steps:");
90
+ writeLine(io.stdout, " 1. Open Codex or OpenCode in this repo.");
91
+ writeLine(io.stdout, " 2. Run `/workshop reference` for the command overview.");
92
+ writeLine(io.stdout, " 3. Run `/workshop setup` if your environment is not ready yet.");
93
+ writeLine(io.stdout, " 4. Run `/workshop` to get the current phase or fallback guidance.");
94
+ return 0;
95
+ } catch (error) {
96
+ if (error instanceof SkillInstallError) {
97
+ writeLine(io.stderr, `Skill install failed: ${error.message}`);
98
+ return 1;
99
+ }
100
+ throw error;
101
+ }
102
+ }
103
+
63
104
  function formatStorageError(error) {
64
105
  if (error instanceof SessionStoreError) {
65
106
  return error.message;
@@ -471,11 +512,7 @@ async function handleWorkshopPhaseSet(io, env, positionals, deps) {
471
512
 
472
513
  export async function runCli(argv, io, deps = {}) {
473
514
  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 };
515
+ const mergedDeps = { fetchFn, sleepFn: deps.sleepFn, openUrl: deps.openUrl, cwd: deps.cwd };
479
516
  const { positionals, flags } = parseArgs(argv);
480
517
  const [scope, action, subaction] = positionals;
481
518
 
@@ -484,11 +521,25 @@ export async function runCli(argv, io, deps = {}) {
484
521
  return 0;
485
522
  }
486
523
 
524
+ if (flags.version === true) {
525
+ printVersion(io);
526
+ return 0;
527
+ }
528
+
529
+ if (scope === "version") {
530
+ printVersion(io);
531
+ return 0;
532
+ }
533
+
487
534
  if (!scope) {
488
535
  printUsage(io);
489
536
  return 1;
490
537
  }
491
538
 
539
+ if (typeof fetchFn !== "function") {
540
+ throw new Error("Fetch is required to run the harness CLI.");
541
+ }
542
+
492
543
  if (scope === "auth" && action === "login") {
493
544
  return handleAuthLogin(io, io.env, flags, mergedDeps);
494
545
  }
@@ -501,6 +552,10 @@ export async function runCli(argv, io, deps = {}) {
501
552
  return handleAuthStatus(io, io.env, mergedDeps);
502
553
  }
503
554
 
555
+ if (scope === "skill" && action === "install") {
556
+ return handleSkillInstall(io, mergedDeps, flags);
557
+ }
558
+
504
559
  if (scope === "workshop" && action === "status") {
505
560
  return handleWorkshopStatus(io, io.env, mergedDeps);
506
561
  }
@@ -0,0 +1,82 @@
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.rm(path.join(installPath, "workshop-skill", "SKILL.md"), { force: true });
70
+ await fs.mkdir(path.join(installPath, "docs"), { recursive: true });
71
+ await fs.copyFile(path.join(repoRoot, "workshop-skill", "SKILL.md"), path.join(installPath, "SKILL.md"));
72
+ await fs.copyFile(
73
+ path.join(repoRoot, "docs", "workshop-event-context-contract.md"),
74
+ path.join(installPath, "docs", "workshop-event-context-contract.md"),
75
+ );
76
+ await fs.copyFile(
77
+ path.join(repoRoot, "docs", "harness-cli-foundation.md"),
78
+ path.join(installPath, "docs", "harness-cli-foundation.md"),
79
+ );
80
+
81
+ return { repoRoot, installPath, skillName: SKILL_NAME };
82
+ }