@orchestraworks/cli 0.3.0 → 0.5.1

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.
Files changed (47) hide show
  1. package/README.md +23 -1256
  2. package/dist/cli/core/banner.d.ts +10 -0
  3. package/dist/cli/core/banner.js +63 -0
  4. package/dist/cli/core/invocation.js +1 -1
  5. package/dist/cli/core/package-root.d.ts +3 -2
  6. package/dist/cli/core/package-root.js +4 -4
  7. package/dist/cli/core/skill-pointer.d.ts +7 -2
  8. package/dist/cli/core/skill-pointer.js +13 -3
  9. package/dist/cli/factory/init.d.ts +1 -1
  10. package/dist/cli/factory/init.js +41 -13
  11. package/dist/cli/main.js +5 -3
  12. package/dist/cli/skill-install-fs.d.ts +0 -2
  13. package/dist/cli/skill-install-fs.js +0 -2
  14. package/dist/cli/skill.js +20 -7
  15. package/docs/REFERENCE.md +1278 -0
  16. package/node_modules/@orchestraworks/client/package.json +1 -1
  17. package/package.json +7 -8
  18. package/skills/orchestra/SKILL.md +151 -110
  19. package/skills/orchestra-fdk/SKILL.md +327 -120
  20. package/skills/orchestra-fdk/references/authoring.md +26 -15
  21. package/skills/orchestra-fdk/references/environment.md +39 -24
  22. package/templates/bug-fix/tsconfig.json +12 -0
  23. package/templates/engineering-team/lib/setup.sh +16 -0
  24. package/templates/engineering-team/package.json +1 -1
  25. package/templates/todo-factory/package.json +1 -1
  26. package/node_modules/@orchestraworks/workflow-render/LICENSE +0 -202
  27. package/node_modules/@orchestraworks/workflow-render/dist/constants.d.ts +0 -98
  28. package/node_modules/@orchestraworks/workflow-render/dist/context.d.ts +0 -37
  29. package/node_modules/@orchestraworks/workflow-render/dist/geometry-core.d.ts +0 -40
  30. package/node_modules/@orchestraworks/workflow-render/dist/graph-digest.d.ts +0 -4
  31. package/node_modules/@orchestraworks/workflow-render/dist/grid.d.ts +0 -33
  32. package/node_modules/@orchestraworks/workflow-render/dist/heuristic.d.ts +0 -7
  33. package/node_modules/@orchestraworks/workflow-render/dist/identity.d.ts +0 -9
  34. package/node_modules/@orchestraworks/workflow-render/dist/index.d.ts +0 -24
  35. package/node_modules/@orchestraworks/workflow-render/dist/index.js +0 -1
  36. package/node_modules/@orchestraworks/workflow-render/dist/layout.d.ts +0 -6
  37. package/node_modules/@orchestraworks/workflow-render/dist/normalize.d.ts +0 -33
  38. package/node_modules/@orchestraworks/workflow-render/dist/place.d.ts +0 -81
  39. package/node_modules/@orchestraworks/workflow-render/dist/ports.d.ts +0 -15
  40. package/node_modules/@orchestraworks/workflow-render/dist/presence.d.ts +0 -6
  41. package/node_modules/@orchestraworks/workflow-render/dist/route-cells.d.ts +0 -15
  42. package/node_modules/@orchestraworks/workflow-render/dist/sha256.d.ts +0 -3
  43. package/node_modules/@orchestraworks/workflow-render/dist/svg.d.ts +0 -7
  44. package/node_modules/@orchestraworks/workflow-render/dist/text.d.ts +0 -14
  45. package/node_modules/@orchestraworks/workflow-render/dist/types.d.ts +0 -310
  46. package/node_modules/@orchestraworks/workflow-render/dist/verify.d.ts +0 -26
  47. package/node_modules/@orchestraworks/workflow-render/package.json +0 -42
@@ -0,0 +1,10 @@
1
+ export interface BannerTerminal {
2
+ readonly isTTY?: boolean;
3
+ readonly columns?: number;
4
+ }
5
+ /**
6
+ * The banner and a blank line, or the empty string. `NO_COLOR` follows no-color.org: any non-empty value
7
+ * turns it off. `COLORTERM` of `truecolor` or `24bit` selects 24-bit color. A terminal narrower than the
8
+ * mark and the wordmark gets the mark with the plain word beside its centre.
9
+ */
10
+ export declare function helpBanner(terminal: BannerTerminal, env: NodeJS.ProcessEnv): string;
@@ -0,0 +1,63 @@
1
+ // The Orchestra mark at the top of bare `orchestra` and `orchestra --help`, for a person at a terminal.
2
+ // A coding agent usually reads piped output, so the banner prints only to a terminal with color allowed:
3
+ // piped output stays plain text with no escape codes.
4
+ /**
5
+ * The console's 48-dot mark (`console/src/components/BrandMark.tsx`), its circles rasterized onto a
6
+ * 42-by-40 dot grid and written as braille, eight dots to a character.
7
+ */
8
+ const MARK = [
9
+ "⠀⠀⠀⠀⢀⣤⡀⠀⠀⣴⣶⡄⣼⣿⡆⠀⠀⠀⠀⠀⠀",
10
+ "⠀⠀⠀⠀⠹⠿⠧⣿⡆⠈⠉⣶⡌⢹⣷⡄⢀⣀⠀⠀⠀",
11
+ "⢠⣶⣄⣿⡷⢰⣶⠀⠀⠿⠀⢈⡀⢀⣭⠀⢿⡿⠂⠀⠀",
12
+ "⠘⠛⢋⣤⡀⢠⡄⠀⠶⠀⠀⠈⠁⠈⠉⠀⣿⠆⣾⣿⡇",
13
+ "⢰⣿⡯⠙⣁⠀⠀⣤⠀⠀⠉⠀⠙⠀⠰⠆⠀⢴⣿⡍⠀",
14
+ "⠀⠉⣤⣌⠛⠀⣤⠀⠀⠒⠀⣀⠀⢴⠄⠐⠿⠂⣩⣄⠀",
15
+ "⠀⣤⣿⠋⣤⡄⢀⡀⠀⠦⠀⢀⠀⢀⣤⠀⣶⣮⠿⠿⠀",
16
+ "⠀⠻⠟⢁⣭⡅⠈⡋⠀⣀⡀⠛⢀⣤⡁⣠⣍⠁⠀⠀⠀",
17
+ "⠀⠀⠀⠘⣻⣧⡸⠿⣂⡙⢣⣤⡄⣙⡁⠻⠟⠀⠀⠀⠀",
18
+ "⠀⠀⠀⠀⠻⠿⠇⠸⣿⠗⠈⠛⠹⣿⡿⠀⠀⠀⠀⠀⠀",
19
+ ];
20
+ /** "ORCHESTRA" in the figlet ANSI Shadow face. */
21
+ const WORDMARK = [
22
+ " ██████╗ ██████╗ ██████╗██╗ ██╗███████╗███████╗████████╗██████╗ █████╗ ",
23
+ "██╔═══██╗██╔══██╗██╔════╝██║ ██║██╔════╝██╔════╝╚══██╔══╝██╔══██╗██╔══██╗",
24
+ "██║ ██║██████╔╝██║ ███████║█████╗ ███████╗ ██║ ██████╔╝███████║",
25
+ "██║ ██║██╔══██╗██║ ██╔══██║██╔══╝ ╚════██║ ██║ ██╔══██╗██╔══██║",
26
+ "╚██████╔╝██║ ██║╚██████╗██║ ██║███████╗███████║ ██║ ██║ ██║██║ ██║",
27
+ " ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝",
28
+ ];
29
+ const MARK_WIDTH = MARK[0].length;
30
+ const GAP = " ";
31
+ /** The mark and the wordmark side by side. */
32
+ const BANNER_WIDTH = MARK_WIDTH + GAP.length + WORDMARK[0].length;
33
+ /** The first mark row the wordmark sits on, which centres it on the mark. */
34
+ const WORDMARK_TOP = Math.floor((MARK.length - WORDMARK.length) / 2);
35
+ const WORD = "Orchestra";
36
+ /** The console's light-theme accent, `consolePalette.json` `light.accent`, as 24-bit RGB. */
37
+ const ACCENT_TRUE_COLOR = "\u001b[38;2;66;99;235m";
38
+ /** The nearest color in the 256-color table, for a terminal that does not announce 24-bit color. */
39
+ const ACCENT_256 = "\u001b[38;5;63m";
40
+ const BOLD = "\u001b[1m";
41
+ const RESET = "\u001b[0m";
42
+ /**
43
+ * The banner and a blank line, or the empty string. `NO_COLOR` follows no-color.org: any non-empty value
44
+ * turns it off. `COLORTERM` of `truecolor` or `24bit` selects 24-bit color. A terminal narrower than the
45
+ * mark and the wordmark gets the mark with the plain word beside its centre.
46
+ */
47
+ export function helpBanner(terminal, env) {
48
+ if (terminal.isTTY !== true || (env.NO_COLOR ?? "") !== "") {
49
+ return "";
50
+ }
51
+ const accent = /^(truecolor|24bit)$/i.test(env.COLORTERM ?? "") ? ACCENT_TRUE_COLOR : ACCENT_256;
52
+ const wide = (terminal.columns ?? BANNER_WIDTH) >= BANNER_WIDTH;
53
+ const lines = MARK.map((row, index) => {
54
+ if (wide) {
55
+ const word = WORDMARK[index - WORDMARK_TOP];
56
+ return word === undefined ? `${accent}${row}${RESET}` : `${accent}${row}${GAP}${word.trimEnd()}${RESET}`;
57
+ }
58
+ return index === Math.floor(MARK.length / 2)
59
+ ? `${accent}${row}${RESET}${GAP}${BOLD}${WORD}${RESET}`
60
+ : `${accent}${row}${RESET}`;
61
+ });
62
+ return `${lines.join("\n")}\n\n`;
63
+ }
@@ -14,7 +14,7 @@ import { errorCode } from "./errors.js";
14
14
  export function addAuthOptions(command) {
15
15
  return command
16
16
  .option("--profile <name>", "named auth profile to read (default profile if omitted)")
17
- .option("--base-url <url>", "platform base URL (overrides PLATFORM_BASE_URL and the stored profile)")
17
+ .option("--base-url <url>", "platform base URL (overrides ORCHESTRA_BASE_URL, PLATFORM_BASE_URL and the stored profile)")
18
18
  .option("--org <id>", "target organization id (overrides ORCHESTRA_ORGANIZATION_ID and the stored profile)");
19
19
  }
20
20
  /**
@@ -1,11 +1,12 @@
1
+ import { type KitSkill } from "../skill-install-fs.js";
1
2
  /**
2
3
  * The package root this process runs from. Under `npx orchestra` outside an installed folder this is npm's
3
4
  * disposable cache, which `skills install` refuses to link into.
4
5
  */
5
6
  export declare function runningPackageRoot(): string;
6
7
  /**
7
- * The bundled authoring skill's path, or `null` when this install does not carry it. The exists-guard is the
8
+ * A bundled skill's path, or `null` when this install does not carry it. The exists-guard is the
8
9
  * contract: callers print this as a pointer to follow, and pointing a reader at a path with nothing behind
9
10
  * it is a worse repair than printing none. `packageRoot` is the test seam.
10
11
  */
11
- export declare function resolveBundledSkillPath(packageRoot?: string): string | null;
12
+ export declare function resolveBundledSkillPath(packageRoot?: string, skill?: KitSkill): string | null;
@@ -6,7 +6,7 @@
6
6
  import { existsSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
- import { SKILL_DIR_IN_PACKAGE, SKILL_FILE_NAME } from "../skill-install-fs.js";
9
+ import { SKILL_FILE_NAME, skillPackageDir } from "../skill-install-fs.js";
10
10
  /**
11
11
  * The package root this process runs from. Under `npx orchestra` outside an installed folder this is npm's
12
12
  * disposable cache, which `skills install` refuses to link into.
@@ -15,11 +15,11 @@ export function runningPackageRoot() {
15
15
  return fileURLToPath(new URL("../../..", import.meta.url));
16
16
  }
17
17
  /**
18
- * The bundled authoring skill's path, or `null` when this install does not carry it. The exists-guard is the
18
+ * A bundled skill's path, or `null` when this install does not carry it. The exists-guard is the
19
19
  * contract: callers print this as a pointer to follow, and pointing a reader at a path with nothing behind
20
20
  * it is a worse repair than printing none. `packageRoot` is the test seam.
21
21
  */
22
- export function resolveBundledSkillPath(packageRoot = runningPackageRoot()) {
23
- const skillPath = join(packageRoot, SKILL_DIR_IN_PACKAGE, SKILL_FILE_NAME);
22
+ export function resolveBundledSkillPath(packageRoot = runningPackageRoot(), skill = "orchestra") {
23
+ const skillPath = join(packageRoot, skillPackageDir(skill), SKILL_FILE_NAME);
24
24
  return existsSync(skillPath) ? skillPath : null;
25
25
  }
@@ -5,10 +5,15 @@ export interface SkillPointer {
5
5
  readonly fields: {
6
6
  readonly skillPath: string;
7
7
  };
8
+ /**
9
+ * The lines `orchestra` and `orchestra --help` end with. A coding agent often runs the help right after the
10
+ * install, and npm hides install-script output, so this tail is where the agent learns which skill to read.
11
+ */
12
+ readonly helpEpilogue: readonly string[];
8
13
  }
9
14
  /**
10
15
  * `null` when this install carries no bundled manual, in which case every surface prints nothing at all.
11
- * Omission is the contract, not a degraded mode. `skillPath` is the test seam.
16
+ * Omission is the contract, not a degraded mode. `skillPath` and `setupSkillPath` are the test seams.
12
17
  */
13
- export declare function bundledSkillPointer(skillPath?: string | null): SkillPointer | null;
18
+ export declare function bundledSkillPointer(skillPath?: string | null, setupSkillPath?: string | null): SkillPointer | null;
14
19
  export declare function emitSkillPointer(io: CliIo, pointer?: SkillPointer | null): void;
@@ -1,13 +1,23 @@
1
1
  import { resolveBundledSkillPath } from "./package-root.js";
2
2
  /**
3
3
  * `null` when this install carries no bundled manual, in which case every surface prints nothing at all.
4
- * Omission is the contract, not a degraded mode. `skillPath` is the test seam.
4
+ * Omission is the contract, not a degraded mode. `skillPath` and `setupSkillPath` are the test seams.
5
5
  */
6
- export function bundledSkillPointer(skillPath = resolveBundledSkillPath()) {
6
+ export function bundledSkillPointer(skillPath = resolveBundledSkillPath(), setupSkillPath = resolveBundledSkillPath(undefined, "orchestra-fdk")) {
7
7
  if (skillPath === null) {
8
8
  return null;
9
9
  }
10
- return { line: `Learn orchestra: read ${skillPath}`, fields: { skillPath } };
10
+ return {
11
+ line: `Learn orchestra: read ${skillPath}`,
12
+ fields: { skillPath },
13
+ helpEpilogue: [
14
+ "Coding agents: read both skills before you start.",
15
+ ` To learn how to use the orchestra CLI, read ${skillPath}`,
16
+ ...(setupSkillPath === null ? [] : [` To set up a factory, read ${setupSkillPath}`]),
17
+ " To link both skills into your harness, run:",
18
+ " orchestra skills install --harness claude|codex --scope project",
19
+ ],
20
+ };
11
21
  }
12
22
  export function emitSkillPointer(io, pointer = bundledSkillPointer()) {
13
23
  if (pointer !== null) {
@@ -13,7 +13,7 @@ type ModelKeyChoice = Exclude<TemplateChoice, "credits">;
13
13
  /** What `--model-key` takes. */
14
14
  export declare const MODEL_KEY_CHOICES: readonly ModelKeyChoice[];
15
15
  /** Every file the starter writes besides package.json, in the order it prints them. */
16
- export declare const STARTER_FILES: readonly ["fdk.json", "main.ts", "image/Dockerfile", "image/setup.sh", "agents/bugfix.md", "skills/codebase/SKILL.md"];
16
+ export declare const STARTER_FILES: readonly ["fdk.json", "tsconfig.json", "main.ts", "image/Dockerfile", "image/setup.sh", "agents/bugfix.md", "skills/codebase/SKILL.md"];
17
17
  /** The starter plain `orchestra fdk init` writes, and the only one `--repository` and `--model-key` fill in. */
18
18
  export declare const DEFAULT_TEMPLATE = "bug-fix";
19
19
  /** The starters `--template` offers by name. Each is a folder under templates/ that brings its own package.json. */
@@ -13,6 +13,7 @@ export const MODEL_KEY_CHOICES = TEMPLATE_CHOICES.filter((choice) => choice !==
13
13
  /** Every file the starter writes besides package.json, in the order it prints them. */
14
14
  export const STARTER_FILES = [
15
15
  "fdk.json",
16
+ "tsconfig.json",
16
17
  "main.ts",
17
18
  "image/Dockerfile",
18
19
  "image/setup.sh",
@@ -27,8 +28,12 @@ const TEMPLATES_DIR = "templates";
27
28
  const TEMPLATE_DIR = join(TEMPLATES_DIR, DEFAULT_TEMPLATE);
28
29
  /** The one template file `init` fills in; every other file is copied as it is. */
29
30
  const FILLED_FILE = "main.ts";
31
+ /** What runs main.ts and checks its types; each is added at the range the kit itself is built with. */
32
+ const STARTER_DEV_PACKAGES = ["tsx", "typescript", "@types/node"];
33
+ /** The script the starter adds when package.json has none of that name. */
34
+ const TYPECHECK_SCRIPT = { name: "typecheck", command: "tsc -p tsconfig.json" };
30
35
  /** Any other package in package.json means the folder is a codebase rather than a new folder beside one. */
31
- const PACKAGES_ALLOWED = new Set([KIT_PACKAGE_NAME, "tsx"]);
36
+ const PACKAGES_ALLOWED = new Set([KIT_PACKAGE_NAME, ...STARTER_DEV_PACKAGES]);
32
37
  const DEPENDENCY_FIELDS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
33
38
  /** Leaves room for the longest construct path the template declares, so every derived slug stays within 60. */
34
39
  const FACTORY_NAME_MAX = 30;
@@ -90,15 +95,24 @@ export async function initCommand(options, io = defaultIo) {
90
95
  };
91
96
  });
92
97
  const record = manifest.kind === "object" ? manifest.record : {};
93
- const addsTsx = !listsPackage(record, "tsx");
94
- const devDependencies = { ...dependencyMap(record, "devDependencies"), tsx: kitTsxRange(packageRoot) };
95
- const merged = { ...record, type: "module", ...(addsTsx ? { devDependencies } : {}) };
98
+ const added = STARTER_DEV_PACKAGES.filter((name) => !listsPackage(record, name));
99
+ const ranges = kitDevRanges(packageRoot, added);
100
+ const devDependencies = { ...dependencyMap(record, "devDependencies"), ...ranges };
101
+ // A `scripts` that is not an object is left as it is rather than replaced.
102
+ const addsTypecheck = record.scripts === undefined || (isRecord(record.scripts) && record.scripts[TYPECHECK_SCRIPT.name] === undefined);
103
+ const scripts = dependencyMap(record, "scripts");
104
+ const merged = {
105
+ ...record,
106
+ type: "module",
107
+ ...(addsTypecheck ? { scripts: { ...scripts, [TYPECHECK_SCRIPT.name]: TYPECHECK_SCRIPT.command } } : {}),
108
+ ...(added.length > 0 ? { devDependencies } : {}),
109
+ };
96
110
  writeFileSync(manifestPath, `${JSON.stringify(merged, null, 2)}\n`);
97
111
  for (const { path, text } of contents) {
98
112
  mkdirSync(dirname(join(dir, path)), { recursive: true });
99
113
  writeFileSync(join(dir, path), text);
100
114
  }
101
- printNextSteps(io, { repository: repository.full, factoryName, choice, dir, addsTsx });
115
+ printNextSteps(io, { repository: repository.full, factoryName, choice, dir, added, addsTypecheck });
102
116
  }
103
117
  /** The starters this kit carries: one folder each under templates/. */
104
118
  export function availableTemplates(packageRoot = runningPackageRoot()) {
@@ -325,14 +339,19 @@ function dependencyMap(record, field) {
325
339
  const value = record[field];
326
340
  return isRecord(value) ? value : {};
327
341
  }
328
- /** The tsx range the kit itself is tested with, so the starter runs on the tsx the kit was proven on. */
329
- function kitTsxRange(packageRoot) {
342
+ /** The ranges the kit itself is built and tested with, so the starter runs and type-checks on what the kit was proven on. */
343
+ function kitDevRanges(packageRoot, names) {
330
344
  const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
331
- const range = isRecord(manifest) ? dependencyMap(manifest, "devDependencies").tsx : undefined;
332
- if (typeof range !== "string") {
333
- throw new Error("this orchestra install names no tsx version to add. Reinstall the kit, then run orchestra fdk init again.");
345
+ const kitDev = isRecord(manifest) ? dependencyMap(manifest, "devDependencies") : {};
346
+ const ranges = {};
347
+ for (const name of names) {
348
+ const range = kitDev[name];
349
+ if (typeof range !== "string") {
350
+ throw new Error(`this orchestra install names no ${name} version to add. Reinstall the kit, then run orchestra fdk init again.`);
351
+ }
352
+ ranges[name] = range;
334
353
  }
335
- return range;
354
+ return ranges;
336
355
  }
337
356
  function isRecord(value) {
338
357
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -341,12 +360,17 @@ function printNextSteps(io, written) {
341
360
  const inputs = [...SHELL_INPUTS, ...(written.choice === "credits" ? [] : [MODEL_KEY_INPUTS[written.choice]])];
342
361
  const width = Math.max(...inputs.map(([name]) => name.length)) + 2;
343
362
  io.out(`Wrote a bug-fix factory named '${written.factoryName}' for ${written.repository} into ${written.dir}:`);
344
- io.out(` package.json (set "type": "module"${written.addsTsx ? ", added tsx to devDependencies" : ""})`);
363
+ const changes = [
364
+ `set "type": "module"`,
365
+ ...(written.addsTypecheck ? [`added the ${TYPECHECK_SCRIPT.name} script`] : []),
366
+ ...(written.added.length > 0 ? [`added ${listed(written.added)} to devDependencies`] : []),
367
+ ];
368
+ io.out(` package.json (${changes.join(", ")})`);
345
369
  for (const path of STARTER_FILES)
346
370
  io.out(` ${path}`);
347
371
  io.out("");
348
372
  io.out("Next, in this folder:");
349
- io.out(" 1. npm install");
373
+ io.out(" 1. npm install, then npm run typecheck");
350
374
  io.out(" 2. In the shell that runs orchestra, set these values. The platform's team hands them over until image");
351
375
  io.out(" hosting exists:");
352
376
  for (const [name, what] of inputs)
@@ -356,3 +380,7 @@ function printNextSteps(io, written) {
356
380
  io.out(" 3. npx orchestra fdk diff");
357
381
  io.out(" 4. npx orchestra fdk apply, then open the GitHub App install link it prints. It waits for the install.");
358
382
  }
383
+ /** `a`, `a and b`, or `a, b and c`. */
384
+ function listed(names) {
385
+ return names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
386
+ }
package/dist/cli/main.js CHANGED
@@ -37,6 +37,7 @@ import { DEFAULT_TEMPLATE, initCommand, MODEL_KEY_CHOICES, OFFERED_TEMPLATES } f
37
37
  import { grantCommand } from "./api/grant.js";
38
38
  import { addAuthOptions } from "./core/invocation.js";
39
39
  import { errorDocument, reportError, settleExitCode } from "./core/errors.js";
40
+ import { helpBanner } from "./core/banner.js";
40
41
  import { bundledSkillPointer, emitSkillPointer } from "./core/skill-pointer.js";
41
42
  import { outputModeFromArgv } from "./core/output-mode.js";
42
43
  import { defaultIo, writeLines } from "./core/output.js";
@@ -170,7 +171,7 @@ LEARN MORE
170
171
  * entirely. `pointer` is the test seam.
171
172
  */
172
173
  export function topLevelHelpText(pointer = bundledSkillPointer()) {
173
- return pointer === null ? TOP_LEVEL_HELP : `${TOP_LEVEL_HELP}\n${pointer.line}\n`;
174
+ return pointer === null ? TOP_LEVEL_HELP : `${TOP_LEVEL_HELP}\n${pointer.helpEpilogue.join("\n")}\n`;
174
175
  }
175
176
  const REQUIRE_APPROVAL_HELP = requireApprovalHelp(IMPERATIVE_APPROVAL_DEFAULT);
176
177
  /**
@@ -294,9 +295,10 @@ export function buildProgram(handlers = defaultHandlers, pointer = bundledSkillP
294
295
  // Deliberately an own-property assignment and NOT `configureHelp`: commander copies `_helpConfiguration`
295
296
  // into every subcommand created afterwards, so configuring the formatter here would make every
296
297
  // `--help` print the top-level text too. The text is computed once and the closure returns that same
297
- // string every time, so `--help` printed twice in one process cannot disagree with itself.
298
+ // string every time, so `--help` printed twice in one process cannot disagree with itself. The banner
299
+ // above it depends on the stream the help goes to: stderr for a bare `orchestra`, stdout for `--help`.
298
300
  const helpText = topLevelHelpText(pointer);
299
- program.helpInformation = () => helpText;
301
+ program.helpInformation = (context) => `${helpBanner(context?.error === true ? process.stderr : process.stdout, process.env)}${helpText}`;
300
302
  // Registered on the program, which is where commander looks: it walks a command's ancestors when an action
301
303
  // fires, so one hook here covers the whole tree. It runs after parsing, so `--help` and a usage error —
302
304
  // both of which end during the parse — never reach it.
@@ -10,8 +10,6 @@ export declare const KIT_INSTALL_SPEC = "@orchestraworks/cli";
10
10
  export declare const KIT_SKILLS: readonly ["orchestra", "orchestra-fdk"];
11
11
  export type KitSkill = (typeof KIT_SKILLS)[number];
12
12
  export declare function skillPackageDir(skill: KitSkill): string;
13
- /** The reference skill's directory, which the CLI's own pointer names. */
14
- export declare const SKILL_DIR_IN_PACKAGE: string;
15
13
  export declare const SKILL_FILE_NAME = "SKILL.md";
16
14
  /** "other" is a printed-path branch, never a link target. */
17
15
  export type LinkableHarness = "claude" | "codex";
@@ -16,8 +16,6 @@ export const KIT_SKILLS = ["orchestra", "orchestra-fdk"];
16
16
  export function skillPackageDir(skill) {
17
17
  return join("skills", skill);
18
18
  }
19
- /** The reference skill's directory, which the CLI's own pointer names. */
20
- export const SKILL_DIR_IN_PACKAGE = skillPackageDir("orchestra");
21
19
  export const SKILL_FILE_NAME = "SKILL.md";
22
20
  /**
23
21
  * Identical under a project root and under `$HOME`. The ONE home for these strings: the link math, the menu
package/dist/cli/skill.js CHANGED
@@ -21,8 +21,17 @@ const LINKABLE_HARNESSES = ["claude", "codex"];
21
21
  const HARNESS_LABEL = { claude: "Claude Code", codex: "Codex" };
22
22
  const HARNESS_VALUES = ["claude", "codex", "both", "other"];
23
23
  const SCOPE_VALUES = ["user", "project"];
24
- /** How a person keeps the linked skills current: the global install is updated in place. */
25
- const UPDATE_COMMAND = `npm update -g ${KIT_INSTALL_SPEC}`;
24
+ /**
25
+ * How a person keeps the linked skills current: the install the links point into is updated in place, so the
26
+ * command matches where the running package sits. A checkout outside both reads as global, the common case.
27
+ */
28
+ function updateCommand(running) {
29
+ return running.kind === "project" ? `npm update ${KIT_INSTALL_SPEC}` : `npm update -g ${KIT_INSTALL_SPEC}`;
30
+ }
31
+ /** The folder whose `node_modules` holds a project install: `<folder>/node_modules/@orchestraworks/cli`. */
32
+ function installFolder(packageRoot) {
33
+ return dirname(dirname(dirname(packageRoot)));
34
+ }
26
35
  export async function skillInstallCommand(options, deps = {}) {
27
36
  const io = deps.io ?? defaultIo;
28
37
  const env = deps.env ?? process.env;
@@ -90,14 +99,18 @@ export async function skillInstallCommand(options, deps = {}) {
90
99
  return;
91
100
  }
92
101
  io.out("");
93
- printSkillFiles(io, running.packageRoot);
102
+ printSkillFiles(io, running);
94
103
  }
95
104
  /** The step after the links: a harness loads a new skill only at its next start, so read them now. */
96
- function printSkillFiles(io, packageRoot) {
105
+ function printSkillFiles(io, running) {
97
106
  io.out("Read each skill now; your harness loads new skills only at its next start:");
98
107
  for (const skill of KIT_SKILLS)
99
- io.out(` ${skillFileIn(packageRoot, skill)}`);
100
- io.out(`\`${UPDATE_COMMAND}\` keeps them current.`);
108
+ io.out(` ${skillFileIn(running.packageRoot, skill)}`);
109
+ io.out("The kit may be installed globally or in the factory folder.");
110
+ io.out(running.kind === "project"
111
+ ? `This one is installed in ${installFolder(running.packageRoot)}, so \`${updateCommand(running)}\`, run in ` +
112
+ `that folder, keeps them current.`
113
+ : `This one is installed globally, so \`${updateCommand(running)}\` keeps them current.`);
101
114
  }
102
115
  function parseChoice(raw, allowed, flag) {
103
116
  if (raw === undefined) {
@@ -194,7 +207,7 @@ function printPointingInstructions(io, running, anchors, fs) {
194
207
  for (const file of skillFiles)
195
208
  io.out(` ${file}`);
196
209
  io.out("");
197
- io.out(`Point your harness at each directory (a link keeps it current across '${UPDATE_COMMAND}'):`);
210
+ io.out(`Point your harness at each directory (a link keeps it current across '${updateCommand(running)}'):`);
198
211
  for (const harness of LINKABLE_HARNESSES) {
199
212
  // The project leg prints the RELATIVE target the command itself would write, so a hand-made link
200
213
  // travels with the repo exactly like an installed one. Nothing recomputes after this line — the