auriga-cli 1.7.0 → 1.9.2

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/dist/plugins.js CHANGED
@@ -2,6 +2,49 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { checkbox, select } from "@inquirer/prompts";
4
4
  import { exec, log, withEsc } from "./utils.js";
5
+ // Plugin names, marketplace names/sources, and plugin-package names all
6
+ // end up in `claude plugins ...` shell commands via string interpolation.
7
+ // .claude/plugins.json is fetched from raw GitHub at runtime, so every
8
+ // value must pass a conservative whitelist before composing the command.
9
+ // Without this a compromised plugins.json would execute arbitrary
10
+ // commands via shell metachar injection.
11
+ const PLUGIN_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
12
+ const PLUGIN_SOURCE_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$/;
13
+ const MARKETPLACE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
14
+ const PLUGIN_PACKAGE_RE = /^[A-Za-z0-9][A-Za-z0-9._@/-]{0,255}$/;
15
+ export function validatePluginsConfig(raw) {
16
+ if (!raw || typeof raw !== "object") {
17
+ throw new Error("plugins.json: root must be an object");
18
+ }
19
+ const cfg = raw;
20
+ if (!Array.isArray(cfg.plugins)) {
21
+ throw new Error("plugins.json: .plugins must be an array");
22
+ }
23
+ cfg.plugins.forEach((p, i) => {
24
+ if (!p || typeof p !== "object") {
25
+ throw new Error(`plugins.json: plugins[${i}] must be an object`);
26
+ }
27
+ const plugin = p;
28
+ if (typeof plugin.name !== "string" || !PLUGIN_NAME_RE.test(plugin.name)) {
29
+ throw new Error(`plugins.json: plugins[${i}].name ${JSON.stringify(plugin.name)} does not match ${PLUGIN_NAME_RE}`);
30
+ }
31
+ if (typeof plugin.package !== "string" || !PLUGIN_PACKAGE_RE.test(plugin.package)) {
32
+ throw new Error(`plugins.json: plugins[${i}].package ${JSON.stringify(plugin.package)} does not match ${PLUGIN_PACKAGE_RE}`);
33
+ }
34
+ if (plugin.marketplace !== undefined) {
35
+ if (!plugin.marketplace || typeof plugin.marketplace !== "object") {
36
+ throw new Error(`plugins.json: plugins[${i}].marketplace must be an object`);
37
+ }
38
+ const mp = plugin.marketplace;
39
+ if (typeof mp.name !== "string" || !MARKETPLACE_NAME_RE.test(mp.name)) {
40
+ throw new Error(`plugins.json: plugins[${i}].marketplace.name ${JSON.stringify(mp.name)} does not match ${MARKETPLACE_NAME_RE}`);
41
+ }
42
+ if (typeof mp.source !== "string" || !PLUGIN_SOURCE_RE.test(mp.source)) {
43
+ throw new Error(`plugins.json: plugins[${i}].marketplace.source ${JSON.stringify(mp.source)} does not match ${PLUGIN_SOURCE_RE}`);
44
+ }
45
+ }
46
+ });
47
+ }
5
48
  function getInstalledPlugins() {
6
49
  try {
7
50
  const output = exec("claude plugins list --json");
@@ -22,6 +65,17 @@ function getInstalledPlugins() {
22
65
  return new Map();
23
66
  }
24
67
  }
68
+ /**
69
+ * Non-interactive selection resolver for plugins. Mirrors the skills
70
+ * resolveSelected: `undefined` / `["*"]` = full set; explicit names =
71
+ * filter. CLI parser validates names up-front.
72
+ */
73
+ function resolvePluginSelection(all, selected) {
74
+ if (!selected || (selected.length === 1 && selected[0] === "*"))
75
+ return all;
76
+ const wanted = new Set(selected);
77
+ return all.filter((p) => wanted.has(p.name));
78
+ }
25
79
  function getInstalledMarketplaces() {
26
80
  try {
27
81
  const output = exec("claude plugins marketplace list");
@@ -35,45 +89,58 @@ function getInstalledMarketplaces() {
35
89
  return new Set();
36
90
  }
37
91
  }
38
- export async function installPlugins(packageRoot) {
39
- // Check claude CLI availability
40
- try {
41
- exec("which claude");
42
- }
43
- catch {
44
- log.error("'claude' CLI not found. Please install Claude Code first.");
45
- return;
92
+ export async function installPlugins(packageRoot, opts) {
93
+ // Non-interactive path already ran `precheckExternal(["plugins"])` in
94
+ // cli.ts's runAll / runSingle before dispatching here, so rechecking
95
+ // `which claude` would be a redundant subprocess on every install.
96
+ // The interactive TTY menu doesn't have that precheck, so still
97
+ // validate there — and fail soft (log-and-return) to match the menu's
98
+ // continue-on-failure ergonomics.
99
+ if (opts.interactive) {
100
+ try {
101
+ exec("which claude");
102
+ }
103
+ catch {
104
+ log.error("'claude' CLI not found. Please install Claude Code first.");
105
+ return;
106
+ }
46
107
  }
47
108
  const configPath = path.join(packageRoot, ".claude", "plugins.json");
48
109
  if (!fs.existsSync(configPath)) {
49
110
  log.warn("No .claude/plugins.json found");
50
111
  return;
51
112
  }
52
- const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
113
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
114
+ validatePluginsConfig(raw);
115
+ const config = raw;
53
116
  if (config.plugins.length === 0) {
54
117
  log.warn("No plugins defined in plugins.json");
55
118
  return;
56
119
  }
57
- const scope = await withEsc(select({
58
- message: "Plugins installation scope:",
59
- choices: [
60
- { name: "User (user-level)", value: "user" },
61
- { name: "Project (current project)", value: "project" },
62
- ],
63
- }));
120
+ const scope = opts.interactive
121
+ ? await withEsc(select({
122
+ message: "Plugins installation scope:",
123
+ choices: [
124
+ { name: "User (user-level)", value: "user" },
125
+ { name: "Project (current project)", value: "project" },
126
+ ],
127
+ }))
128
+ : opts.scope ?? "project";
64
129
  const installed = getInstalledPlugins();
65
- const selected = await withEsc(checkbox({
66
- message: "Select plugins to install:",
67
- choices: config.plugins.map((p) => {
68
- const scopes = installed.get(p.package);
69
- const suffix = scopes ? ` (installed: ${scopes.join(", ")})` : "";
70
- return {
71
- name: `${p.name} — ${p.description}${suffix}`,
72
- value: p,
73
- checked: !scopes || !(scopes.includes("user") && scopes.includes("project")),
74
- };
75
- }),
76
- }));
130
+ const selected = opts.interactive
131
+ ? await withEsc(checkbox({
132
+ message: "Select plugins to install:",
133
+ choices: config.plugins.map((p) => {
134
+ const scopes = installed.get(p.package);
135
+ const suffix = scopes ? ` (installed: ${scopes.join(", ")})` : "";
136
+ return {
137
+ name: `${p.name} — ${p.description}${suffix}`,
138
+ value: p,
139
+ checked: !scopes || !(scopes.includes("user") && scopes.includes("project")),
140
+ };
141
+ }),
142
+ }))
143
+ : resolvePluginSelection(config.plugins, opts.selected);
77
144
  if (selected.length === 0) {
78
145
  log.skip("No plugins selected");
79
146
  return;
@@ -86,6 +153,7 @@ export async function installPlugins(packageRoot) {
86
153
  marketplacesToAdd.set(plugin.marketplace.name, plugin.marketplace.source);
87
154
  }
88
155
  }
156
+ const failures = [];
89
157
  for (const [name, source] of marketplacesToAdd) {
90
158
  console.log(`\nAdding marketplace: ${name}...`);
91
159
  try {
@@ -94,6 +162,7 @@ export async function installPlugins(packageRoot) {
94
162
  }
95
163
  catch {
96
164
  log.error(`Failed to add marketplace: ${name}`);
165
+ failures.push(`marketplace ${name}`);
97
166
  }
98
167
  }
99
168
  // Install plugins
@@ -107,6 +176,10 @@ export async function installPlugins(packageRoot) {
107
176
  }
108
177
  catch {
109
178
  log.error(`Failed to install: ${plugin.name}`);
179
+ failures.push(plugin.name);
110
180
  }
111
181
  }
182
+ if (failures.length > 0 && !opts.interactive) {
183
+ throw new Error(`${failures.length} plugin operation(s) failed: ${failures.join(", ")}`);
184
+ }
112
185
  }
package/dist/skills.d.ts CHANGED
@@ -1,2 +1,10 @@
1
- export declare function installSkills(packageRoot: string): Promise<void>;
2
- export declare function installRecommendedSkills(packageRoot: string): Promise<void>;
1
+ import type { InstallOpts, SkillsLock } from "./utils.js";
2
+ export declare const WORKFLOW_SKILLS: string[];
3
+ export declare function validateSkillsLock(raw: unknown): asserts raw is SkillsLock;
4
+ export declare function planSkillInstallCommands(selected: string[], lock: SkillsLock["skills"], globalFlag: string): {
5
+ source: string;
6
+ skills: string[];
7
+ command: string;
8
+ }[];
9
+ export declare function installSkills(packageRoot: string, opts: InstallOpts): Promise<void>;
10
+ export declare function installRecommendedSkills(packageRoot: string, opts: InstallOpts): Promise<void>;
package/dist/skills.js CHANGED
@@ -5,8 +5,7 @@ import { exec, log, withEsc } from "./utils.js";
5
5
  // Curated default-on set: skills that the workflow in the root CLAUDE.md
6
6
  // directly references. Anything else in skills-lock.json is surfaced via
7
7
  // installRecommendedSkills as an opt-in utility.
8
- const WORKFLOW_SKILLS = [
9
- "auriga-go",
8
+ export const WORKFLOW_SKILLS = [
10
9
  "brainstorming",
11
10
  "deep-review",
12
11
  "parallel-implementation",
@@ -18,58 +17,128 @@ const WORKFLOW_SKILLS = [
18
17
  "ui-ux-pro-max",
19
18
  "verification-before-completion",
20
19
  ];
21
- const RECOMMENDED_DESCRIPTIONS = {
22
- "claude-code-agent": "Delegate tasks to another Claude Code CLI instance",
23
- "codex-agent": "Delegate tasks to Codex CLI",
24
- };
20
+ // Skill names and npm-style sources are interpolated into the shell
21
+ // command we hand to `exec()`. The lock file is fetched from raw GitHub
22
+ // at runtime, so every value must pass a conservative whitelist before
23
+ // we compose the command. Without this a compromised skills-lock.json
24
+ // would execute arbitrary commands via shell metachar injection.
25
+ const SKILL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
26
+ const SKILL_SOURCE_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$/;
27
+ export function validateSkillsLock(raw) {
28
+ if (!raw || typeof raw !== "object") {
29
+ throw new Error("skills-lock.json: root must be an object");
30
+ }
31
+ const lock = raw;
32
+ if (!lock.skills || typeof lock.skills !== "object") {
33
+ throw new Error("skills-lock.json: .skills must be an object");
34
+ }
35
+ for (const [name, entry] of Object.entries(lock.skills)) {
36
+ if (!SKILL_NAME_RE.test(name)) {
37
+ throw new Error(`skills-lock.json: skill name ${JSON.stringify(name)} does not match ${SKILL_NAME_RE}`);
38
+ }
39
+ if (!entry || typeof entry !== "object") {
40
+ throw new Error(`skills-lock.json: .skills[${name}] must be an object`);
41
+ }
42
+ const src = entry.source;
43
+ if (typeof src !== "string" || !SKILL_SOURCE_RE.test(src)) {
44
+ throw new Error(`skills-lock.json: .skills[${name}].source ${JSON.stringify(src)} does not match ${SKILL_SOURCE_RE}`);
45
+ }
46
+ }
47
+ }
25
48
  function loadLock(packageRoot) {
26
- return JSON.parse(fs.readFileSync(path.join(packageRoot, "skills-lock.json"), "utf-8"));
49
+ const raw = JSON.parse(fs.readFileSync(path.join(packageRoot, "skills-lock.json"), "utf-8"));
50
+ validateSkillsLock(raw);
51
+ return raw;
27
52
  }
28
- async function installSelected(entries, defaultChecked, descriptionMap) {
53
+ // Deterministic: selection order is preserved; the first occurrence of
54
+ // each source fixes its position in the returned array.
55
+ export function planSkillInstallCommands(selected, lock, globalFlag) {
56
+ const bySource = new Map();
57
+ for (const name of selected) {
58
+ const entry = lock[name];
59
+ if (!entry)
60
+ continue;
61
+ const bucket = bySource.get(entry.source);
62
+ if (bucket)
63
+ bucket.push(name);
64
+ else
65
+ bySource.set(entry.source, [name]);
66
+ }
67
+ return [...bySource].map(([source, skills]) => ({
68
+ source,
69
+ skills,
70
+ command: `npx -y skills add ${source}${globalFlag} --skill ${skills.join(" ")} --agent claude-code codex --yes`,
71
+ }));
72
+ }
73
+ async function installSelected(entries, defaultChecked, opts) {
29
74
  if (entries.length === 0) {
30
75
  log.warn("No skills found");
31
76
  return;
32
77
  }
33
- const scope = await withEsc(select({
34
- message: "Skills installation scope:",
35
- choices: [
36
- { name: "Project (current directory)", value: "project" },
37
- { name: "Global (user-level)", value: "global" },
38
- ],
39
- }));
40
- const selected = await withEsc(checkbox({
41
- message: "Select skills to install:",
42
- choices: entries.map(([name, entry]) => {
43
- const desc = descriptionMap?.[name];
44
- const label = desc ? `${name} — ${desc}` : `${name} (${entry.source})`;
45
- return { name: label, value: name, checked: defaultChecked };
46
- }),
47
- }));
78
+ const scope = opts.interactive
79
+ ? await withEsc(select({
80
+ message: "Skills installation scope:",
81
+ choices: [
82
+ { name: "Project (current directory)", value: "project" },
83
+ { name: "Global (user-level)", value: "global" },
84
+ ],
85
+ }))
86
+ : opts.scope === "user" ? "global" : "project";
87
+ const availableNames = entries.map(([name]) => name);
88
+ const selected = opts.interactive
89
+ ? await withEsc(checkbox({
90
+ message: "Select skills to install:",
91
+ choices: entries.map(([name, entry]) => ({
92
+ name: `${name} (${entry.source})`,
93
+ value: name,
94
+ checked: defaultChecked,
95
+ })),
96
+ }))
97
+ : resolveSelected(opts.selected, availableNames);
48
98
  if (selected.length === 0) {
49
99
  log.skip("No skills selected");
50
100
  return;
51
101
  }
52
102
  const globalFlag = scope === "global" ? " -g" : "";
53
103
  const lock = Object.fromEntries(entries);
54
- for (const name of selected) {
55
- const entry = lock[name];
56
- console.log(`\nInstalling ${name}...`);
104
+ const batches = planSkillInstallCommands(selected, lock, globalFlag);
105
+ const failures = [];
106
+ for (const batch of batches) {
107
+ console.log(`\nInstalling ${batch.skills.join(", ")} from ${batch.source}...`);
57
108
  try {
58
- exec(`npx skills add ${entry.source}${globalFlag} --skill ${name} --agent claude-code codex --yes`, { inherit: true });
59
- log.ok(`${name}: installed`);
109
+ exec(batch.command, { inherit: true });
110
+ for (const name of batch.skills)
111
+ log.ok(`${name}: installed`);
60
112
  }
61
113
  catch {
62
- log.error(`${name}: failed to install`);
114
+ log.error(`${batch.source}: failed to install (${batch.skills.join(", ")})`);
115
+ failures.push(batch.source);
63
116
  }
64
117
  }
118
+ if (failures.length > 0 && !opts.interactive) {
119
+ throw new Error(`${failures.length} skill batch(es) failed: ${failures.join(", ")}`);
120
+ }
121
+ }
122
+ /**
123
+ * Resolves the non-interactive `opts.selected` filter against the set
124
+ * of names available in the current category. Semantics match spec
125
+ * §3.2: `undefined` = all; `["*"]` = all; any other list = that list.
126
+ * The CLI parser is responsible for rejecting unknown names up-front
127
+ * (so installers can trust the list).
128
+ */
129
+ function resolveSelected(selected, available) {
130
+ if (!selected || (selected.length === 1 && selected[0] === "*")) {
131
+ return available;
132
+ }
133
+ return selected;
65
134
  }
66
- export async function installSkills(packageRoot) {
135
+ export async function installSkills(packageRoot, opts) {
67
136
  const lock = loadLock(packageRoot);
68
137
  const entries = Object.entries(lock.skills).filter(([name]) => WORKFLOW_SKILLS.includes(name));
69
- await installSelected(entries, true);
138
+ await installSelected(entries, true, opts);
70
139
  }
71
- export async function installRecommendedSkills(packageRoot) {
140
+ export async function installRecommendedSkills(packageRoot, opts) {
72
141
  const lock = loadLock(packageRoot);
73
142
  const entries = Object.entries(lock.skills).filter(([name]) => !WORKFLOW_SKILLS.includes(name));
74
- await installSelected(entries, false, RECOMMENDED_DESCRIPTIONS);
143
+ await installSelected(entries, false, opts);
75
144
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared leaf types. Put things here when multiple modules need the
3
+ * same nominal type and a natural "owner" doesn't exist — avoids
4
+ * forcing leaf renderers (help.ts, guide.ts) to depend on the CLI
5
+ * entrypoint just to pull one union.
6
+ */
7
+ export type CategoryName = "workflow" | "skills" | "recommended" | "plugins" | "hooks";
8
+ export declare const CATEGORY_NAMES: readonly CategoryName[];
package/dist/types.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared leaf types. Put things here when multiple modules need the
3
+ * same nominal type and a natural "owner" doesn't exist — avoids
4
+ * forcing leaf renderers (help.ts, guide.ts) to depend on the CLI
5
+ * entrypoint just to pull one union.
6
+ */
7
+ export const CATEGORY_NAMES = [
8
+ "workflow",
9
+ "skills",
10
+ "recommended",
11
+ "plugins",
12
+ "hooks",
13
+ ];
package/dist/utils.d.ts CHANGED
@@ -19,6 +19,39 @@ export interface PluginDef {
19
19
  export interface PluginsConfig {
20
20
  plugins: PluginDef[];
21
21
  }
22
+ /**
23
+ * Shared install function argument shape. Each installer consumes the
24
+ * subset of fields meaningful to its category; irrelevant fields are
25
+ * ignored (e.g. `lang` / `cwd` only apply to workflow).
26
+ *
27
+ * `interactive` is required (no default) to force callers to be
28
+ * explicit — silently falling back to prompts in a piped Agent session
29
+ * was the original bug this spec closes.
30
+ */
31
+ export interface InstallOpts {
32
+ /** workflow only — language code from `LANGUAGES`. */
33
+ lang?: string;
34
+ /** workflow only — install target directory (absolute or cwd-relative). */
35
+ cwd?: string;
36
+ /** skills / recommended / plugins / hooks — `"user"` means install globally. */
37
+ scope?: "project" | "user";
38
+ /**
39
+ * sub-item filter. `undefined` = full set of this category.
40
+ * Names are validated against the catalog by the CLI layer; installers
41
+ * take the list as authoritative.
42
+ */
43
+ selected?: string[];
44
+ /** `true` = drive via inquirer prompts (existing interactive UX);
45
+ * `false` = non-interactive, use only the fields above. */
46
+ interactive: boolean;
47
+ }
48
+ /**
49
+ * Whether the current process should be treated as non-interactive.
50
+ * Used by the top-level CLI dispatcher to pick the interactive vs
51
+ * non-interactive code path when only the verb was supplied with no
52
+ * positional types / flags.
53
+ */
54
+ export declare function isNonInteractive(): boolean;
22
55
  export declare function getPackageRoot(): string;
23
56
  export declare function exec(cmd: string, opts?: {
24
57
  cwd?: string;
@@ -30,6 +63,13 @@ export interface LangOption {
30
63
  file: string;
31
64
  }
32
65
  export declare const LANGUAGES: LangOption[];
66
+ /**
67
+ * Reads `version` from the packaged manifest. Throws when the package
68
+ * root / manifest is unreadable — callers that need a fallback should
69
+ * wrap in try/catch and pick their own default (see
70
+ * `resolveContentRef` for an example).
71
+ */
72
+ export declare function readPackageVersion(): string;
33
73
  export declare function fetchContentRoot(): Promise<string>;
34
74
  export declare function fetchExtraContent(tmpDir: string, file: string): Promise<void>;
35
75
  export declare function fetchExtraContentBinary(tmpDir: string, file: string): Promise<void>;
package/dist/utils.js CHANGED
@@ -3,11 +3,36 @@ import { fileURLToPath } from "node:url";
3
3
  import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
+ /**
7
+ * Whether the current process should be treated as non-interactive.
8
+ * Used by the top-level CLI dispatcher to pick the interactive vs
9
+ * non-interactive code path when only the verb was supplied with no
10
+ * positional types / flags.
11
+ */
12
+ export function isNonInteractive() {
13
+ return !process.stdin.isTTY;
14
+ }
6
15
  // --- Package root ---
16
+ // Walks up from the current module file until it finds the auriga-cli
17
+ // package.json. Handles both `dist/utils.js` (production) and
18
+ // `dist-test/src/utils.js` (test compile output) uniformly — a plain
19
+ // `path.resolve(..., "..")` works for the first but not the second.
7
20
  export function getPackageRoot() {
8
21
  const __filename = fileURLToPath(import.meta.url);
9
- // dist/utils.js -> package root
10
- return path.resolve(path.dirname(__filename), "..");
22
+ let dir = path.dirname(__filename);
23
+ while (dir !== path.dirname(dir)) {
24
+ const pkgPath = path.join(dir, "package.json");
25
+ if (fs.existsSync(pkgPath)) {
26
+ try {
27
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
28
+ if (pkg.name === "auriga-cli")
29
+ return dir;
30
+ }
31
+ catch { /* malformed parent package.json — keep walking */ }
32
+ }
33
+ dir = path.dirname(dir);
34
+ }
35
+ return process.cwd();
11
36
  }
12
37
  // --- Exec ---
13
38
  export function exec(cmd, opts) {
@@ -23,7 +48,45 @@ export const LANGUAGES = [
23
48
  ];
24
49
  // --- Remote content ---
25
50
  const REPO = "Ben2pc/auriga-cli";
26
- const BRANCH = "main";
51
+ /**
52
+ * Reads `version` from the packaged manifest. Throws when the package
53
+ * root / manifest is unreadable — callers that need a fallback should
54
+ * wrap in try/catch and pick their own default (see
55
+ * `resolveContentRef` for an example).
56
+ */
57
+ export function readPackageVersion() {
58
+ const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), "package.json"), "utf-8"));
59
+ return pkg.version;
60
+ }
61
+ /**
62
+ * Git ref to fetch content from. Defaults to the tag matching the
63
+ * published CLI version (`v<package.version>`) so a pinned npm install
64
+ * never drifts against `main`. Overridable via `AURIGA_CONTENT_REF`
65
+ * (CI / debugging) and auto-falls-back to `main` for the legacy
66
+ * behavior when `AURIGA_CONTENT_REF=main` or when the package version
67
+ * can't be read.
68
+ *
69
+ * Release discipline: cut the git tag `v<version>` BEFORE `npm
70
+ * publish`. Publishing without tagging would leave `fetchContentRoot`
71
+ * hitting a 404 for the first minutes until the tag exists.
72
+ */
73
+ function resolveContentRef() {
74
+ const override = process.env.AURIGA_CONTENT_REF;
75
+ if (override && override.length > 0)
76
+ return override;
77
+ try {
78
+ const version = readPackageVersion();
79
+ if (typeof version === "string" && /^\d+\.\d+\.\d+/.test(version)) {
80
+ return `v${version}`;
81
+ }
82
+ }
83
+ catch {
84
+ // Fall through to main; getPackageRoot can legitimately fail in
85
+ // bizarre installs (broken tarball), and a live-main fetch is
86
+ // strictly better than a hard crash on `--help`.
87
+ }
88
+ return "main";
89
+ }
27
90
  const CONTENT_FILES = [
28
91
  "CLAUDE.md",
29
92
  "skills-lock.json",
@@ -31,14 +94,16 @@ const CONTENT_FILES = [
31
94
  ".claude/hooks/hooks.json",
32
95
  ];
33
96
  async function fetchFile(file) {
34
- const url = `https://raw.githubusercontent.com/${REPO}/${BRANCH}/${file}`;
97
+ const ref = resolveContentRef();
98
+ const url = `https://raw.githubusercontent.com/${REPO}/${ref}/${file}`;
35
99
  const res = await fetch(url);
36
100
  if (!res.ok)
37
101
  throw new Error(`Failed to fetch ${url}: ${res.status}`);
38
102
  return res.text();
39
103
  }
40
104
  async function fetchFileBinary(file) {
41
- const url = `https://raw.githubusercontent.com/${REPO}/${BRANCH}/${file}`;
105
+ const ref = resolveContentRef();
106
+ const url = `https://raw.githubusercontent.com/${REPO}/${ref}/${file}`;
42
107
  const res = await fetch(url);
43
108
  if (!res.ok)
44
109
  throw new Error(`Failed to fetch ${url}: ${res.status}`);
@@ -238,7 +303,11 @@ export function printBanner(version) {
238
303
  // --- Log ---
239
304
  export const log = {
240
305
  ok: (msg) => console.log(`${green}\u2713${reset} ${msg}`),
241
- warn: (msg) => console.log(`${yellow}\u26a0${reset} ${msg}`),
242
- error: (msg) => console.log(`${red}\u2717${reset} ${msg}`),
306
+ // warn / error go to stderr so shell redirection (and non-interactive
307
+ // agents) can separate diagnostics from normal CLI output. Earlier
308
+ // both wrote to stdout via console.log, which collapsed the two
309
+ // streams and forced callers to re-parse mixed output.
310
+ warn: (msg) => console.error(`${yellow}\u26a0${reset} ${msg}`),
311
+ error: (msg) => console.error(`${red}\u2717${reset} ${msg}`),
243
312
  skip: (msg) => console.log(`${dim} skip: ${msg}${reset}`),
244
313
  };
@@ -1 +1,2 @@
1
- export declare function installWorkflow(packageRoot: string): Promise<void>;
1
+ import { type InstallOpts } from "./utils.js";
2
+ export declare function installWorkflow(packageRoot: string, opts: InstallOpts): Promise<void>;
package/dist/workflow.js CHANGED
@@ -1,21 +1,29 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { input, select } from "@inquirer/prompts";
4
- import { LANGUAGES, fetchExtraContent, log, withEsc } from "./utils.js";
5
- export async function installWorkflow(packageRoot) {
6
- const lang = await withEsc(select({
7
- message: "CLAUDE.md language:",
8
- choices: LANGUAGES.map((l) => ({ name: l.label, value: l.value })),
9
- default: "en",
10
- }));
11
- const targetDir = await withEsc(input({
12
- message: "Workflow install target directory:",
13
- default: process.cwd(),
14
- }));
4
+ import { LANGUAGES, fetchExtraContent, log, withEsc, } from "./utils.js";
5
+ export async function installWorkflow(packageRoot, opts) {
6
+ const lang = opts.interactive
7
+ ? await withEsc(select({
8
+ message: "CLAUDE.md language:",
9
+ choices: LANGUAGES.map((l) => ({ name: l.label, value: l.value })),
10
+ default: "en",
11
+ }))
12
+ : (opts.lang ?? "en");
13
+ const targetDir = opts.interactive
14
+ ? await withEsc(input({
15
+ message: "Workflow install target directory:",
16
+ default: process.cwd(),
17
+ }))
18
+ : (opts.cwd ?? process.cwd());
15
19
  const resolved = path.resolve(targetDir);
16
20
  if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
17
- log.error(`Not a valid directory: ${resolved}`);
18
- return;
21
+ const msg = `Not a valid directory: ${resolved}`;
22
+ if (opts.interactive) {
23
+ log.error(msg);
24
+ return;
25
+ }
26
+ throw new Error(msg);
19
27
  }
20
28
  const langOpt = LANGUAGES.find((l) => l.value === lang);
21
29
  // Lazy fetch: only download non-default language file when needed
package/package.json CHANGED
@@ -1,26 +1,41 @@
1
1
  {
2
2
  "name": "auriga-cli",
3
- "version": "1.7.0",
3
+ "version": "1.9.2",
4
4
  "description": "Interactive CLI to install Claude Code harness modules (Workflow, Skills, Recommended Skills, Plugins, Hooks)",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Ben2pc/auriga-cli.git"
9
+ },
10
+ "homepage": "https://github.com/Ben2pc/auriga-cli#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/Ben2pc/auriga-cli/issues"
13
+ },
5
14
  "type": "module",
6
15
  "bin": {
7
16
  "auriga-cli": "dist/cli.js"
8
17
  },
9
18
  "files": [
10
- "dist"
19
+ "dist/*.js",
20
+ "dist/*.d.ts",
21
+ "dist/catalog.json"
11
22
  ],
12
23
  "scripts": {
13
- "build": "tsc",
24
+ "build": "tsc && node dist/build/generate-catalog.js",
14
25
  "dev": "tsc --watch",
15
26
  "start": "node dist/cli.js",
16
- "test": "tsc -p tsconfig.test.json && DEV=1 node --test dist-test/tests/hooks.test.js",
17
- "test:watch": "tsc -p tsconfig.test.json --watch & node --test --watch dist-test/tests/hooks.test.js"
27
+ "pretest": "npm run build",
28
+ "test": "tsc -p tsconfig.test.json && DEV=1 node --test --experimental-test-module-mocks dist-test/tests/hooks.test.js dist-test/tests/skills.test.js dist-test/tests/catalog.test.js dist-test/tests/cli-parse.test.js dist-test/tests/install-nontty.test.js dist-test/tests/guide.test.js dist-test/tests/validators.test.js dist-test/tests/entrypoint.test.js",
29
+ "test:watch": "tsc -p tsconfig.test.json --watch & node --test --watch --experimental-test-module-mocks dist-test/tests/hooks.test.js dist-test/tests/skills.test.js dist-test/tests/catalog.test.js dist-test/tests/cli-parse.test.js dist-test/tests/install-nontty.test.js dist-test/tests/guide.test.js dist-test/tests/validators.test.js dist-test/tests/entrypoint.test.js",
30
+ "pretest:e2e": "npm run build",
31
+ "test:e2e": "tsc -p tsconfig.test.json && node --test dist-test/tests/e2e-install.test.js"
18
32
  },
19
33
  "engines": {
20
34
  "node": ">=18"
21
35
  },
22
36
  "dependencies": {
23
- "@inquirer/prompts": "^8.0.0"
37
+ "@inquirer/prompts": "^8.0.0",
38
+ "gray-matter": "^4.0.3"
24
39
  },
25
40
  "devDependencies": {
26
41
  "@types/node": "^22.0.0",