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/README.md +38 -7
- package/README.zh-CN.md +39 -8
- package/dist/catalog.d.ts +12 -0
- package/dist/catalog.js +9 -0
- package/dist/catalog.json +87 -0
- package/dist/cli.d.ts +23 -1
- package/dist/cli.js +526 -46
- package/dist/guide.d.ts +12 -0
- package/dist/guide.js +120 -0
- package/dist/help.d.ts +14 -0
- package/dist/help.js +152 -0
- package/dist/hooks.d.ts +45 -1
- package/dist/hooks.js +124 -23
- package/dist/plugins.d.ts +3 -1
- package/dist/plugins.js +101 -28
- package/dist/skills.d.ts +10 -2
- package/dist/skills.js +102 -33
- package/dist/types.d.ts +8 -0
- package/dist/types.js +13 -0
- package/dist/utils.d.ts +40 -0
- package/dist/utils.js +76 -7
- package/dist/workflow.d.ts +2 -1
- package/dist/workflow.js +21 -13
- package/package.json +21 -6
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
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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 =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
2
|
-
export declare
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
49
|
+
const raw = JSON.parse(fs.readFileSync(path.join(packageRoot, "skills-lock.json"), "utf-8"));
|
|
50
|
+
validateSkillsLock(raw);
|
|
51
|
+
return raw;
|
|
27
52
|
}
|
|
28
|
-
|
|
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 =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
59
|
-
|
|
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(`${
|
|
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,
|
|
143
|
+
await installSelected(entries, false, opts);
|
|
75
144
|
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
242
|
-
|
|
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
|
};
|
package/dist/workflow.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
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 =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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.
|
|
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
|
-
"
|
|
17
|
-
"test
|
|
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",
|