auriga-cli 1.15.1 → 1.16.0
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 +13 -0
- package/README.zh-CN.md +13 -0
- package/dist/api-types.d.ts +115 -0
- package/dist/api-types.js +4 -0
- package/dist/apply-handlers.d.ts +17 -0
- package/dist/apply-handlers.js +186 -0
- package/dist/catalog.json +5 -1
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +220 -0
- package/dist/help.js +2 -0
- package/dist/hooks.d.ts +30 -0
- package/dist/hooks.js +89 -0
- package/dist/plugins.d.ts +29 -0
- package/dist/plugins.js +137 -6
- package/dist/scan-catalog.d.ts +2 -0
- package/dist/scan-catalog.js +138 -0
- package/dist/server.d.ts +71 -0
- package/dist/server.js +759 -0
- package/dist/skills.d.ts +29 -0
- package/dist/skills.js +145 -2
- package/dist/state.d.ts +63 -0
- package/dist/state.js +623 -0
- package/dist/ui-fetch.d.ts +29 -0
- package/dist/ui-fetch.js +267 -0
- package/dist/utils.d.ts +22 -0
- package/dist/utils.js +58 -1
- package/dist/workflow.d.ts +22 -0
- package/dist/workflow.js +63 -0
- package/package.json +5 -3
package/dist/help.js
CHANGED
|
@@ -15,6 +15,8 @@ USAGE
|
|
|
15
15
|
(excludes recommended — install separately)
|
|
16
16
|
npx auriga-cli install <type> [type-specific flags] single category
|
|
17
17
|
npx auriga-cli install <type> --help per-category help + catalog subset
|
|
18
|
+
npx auriga-cli web-ui [--port <n>] [--ui-dir <path>] [--no-open]
|
|
19
|
+
open the local Web UI (spec §4)
|
|
18
20
|
npx auriga-cli --help
|
|
19
21
|
|
|
20
22
|
For non-interactive (Agent) use, prepend npx's own -y flag:
|
package/dist/hooks.d.ts
CHANGED
|
@@ -203,4 +203,34 @@ export declare function resolveHookSelection(compatible: HookDef[], selected: st
|
|
|
203
203
|
*/
|
|
204
204
|
export declare function findIncompatibleExplicit(all: HookDef[], compatible: HookDef[], selected: string[]): string[];
|
|
205
205
|
export declare function installHooks(packageRoot: string, opts: InstallOpts): Promise<void>;
|
|
206
|
+
/**
|
|
207
|
+
* Uninstall a single hook. Defaults to project scope; explicit
|
|
208
|
+
* `scope:"user"` cleans `~/.claude/...` instead.
|
|
209
|
+
*
|
|
210
|
+
* Project scope (default):
|
|
211
|
+
* - rm `<cwd>/.claude/hooks/<name>/` directory.
|
|
212
|
+
* - Strip the hook's marker from `<cwd>/.claude/settings.json` AND
|
|
213
|
+
* `<cwd>/.claude/settings.local.json` (project + project-local share
|
|
214
|
+
* the on-disk hook dir, so cleaning both settings files keeps users
|
|
215
|
+
* who switched scopes from accumulating dangling registrations).
|
|
216
|
+
*
|
|
217
|
+
* User scope:
|
|
218
|
+
* - rm `~/.claude/hooks/<name>/` directory.
|
|
219
|
+
* - Strip the hook's marker from `~/.claude/settings.json`.
|
|
220
|
+
* - Project files are NOT touched.
|
|
221
|
+
*
|
|
222
|
+
* Marker discovery: tries the live registry at `<cwd>` (or the npx
|
|
223
|
+
* package root if that fails) so we use the same marker the install path
|
|
224
|
+
* stamped in. If the registry can't resolve the hook (renamed / removed
|
|
225
|
+
* upstream), we fall back to a `auriga:<name>` convention — every shipped
|
|
226
|
+
* hook to date follows it, so the fallback is reliable for the common
|
|
227
|
+
* case.
|
|
228
|
+
*
|
|
229
|
+
* Idempotent: missing hook dir / missing settings / absent marker → no-op.
|
|
230
|
+
*/
|
|
231
|
+
export declare function uninstallHook(name: string, opts: {
|
|
232
|
+
cwd: string;
|
|
233
|
+
scope?: "project" | "user";
|
|
234
|
+
onLog?: (line: string) => void;
|
|
235
|
+
}): Promise<void>;
|
|
206
236
|
export {};
|
package/dist/hooks.js
CHANGED
|
@@ -868,3 +868,92 @@ export async function installHooks(packageRoot, opts) {
|
|
|
868
868
|
throw new Error(`${failures.length} hook(s) failed to install: ${failures.join(", ")}`);
|
|
869
869
|
}
|
|
870
870
|
}
|
|
871
|
+
// --- Uninstall ----------------------------------------------------------------
|
|
872
|
+
const HOOK_NAME_RE_STRICT = /^[a-z][a-z0-9-]*$/;
|
|
873
|
+
/**
|
|
874
|
+
* Uninstall a single hook. Defaults to project scope; explicit
|
|
875
|
+
* `scope:"user"` cleans `~/.claude/...` instead.
|
|
876
|
+
*
|
|
877
|
+
* Project scope (default):
|
|
878
|
+
* - rm `<cwd>/.claude/hooks/<name>/` directory.
|
|
879
|
+
* - Strip the hook's marker from `<cwd>/.claude/settings.json` AND
|
|
880
|
+
* `<cwd>/.claude/settings.local.json` (project + project-local share
|
|
881
|
+
* the on-disk hook dir, so cleaning both settings files keeps users
|
|
882
|
+
* who switched scopes from accumulating dangling registrations).
|
|
883
|
+
*
|
|
884
|
+
* User scope:
|
|
885
|
+
* - rm `~/.claude/hooks/<name>/` directory.
|
|
886
|
+
* - Strip the hook's marker from `~/.claude/settings.json`.
|
|
887
|
+
* - Project files are NOT touched.
|
|
888
|
+
*
|
|
889
|
+
* Marker discovery: tries the live registry at `<cwd>` (or the npx
|
|
890
|
+
* package root if that fails) so we use the same marker the install path
|
|
891
|
+
* stamped in. If the registry can't resolve the hook (renamed / removed
|
|
892
|
+
* upstream), we fall back to a `auriga:<name>` convention — every shipped
|
|
893
|
+
* hook to date follows it, so the fallback is reliable for the common
|
|
894
|
+
* case.
|
|
895
|
+
*
|
|
896
|
+
* Idempotent: missing hook dir / missing settings / absent marker → no-op.
|
|
897
|
+
*/
|
|
898
|
+
export async function uninstallHook(name, opts) {
|
|
899
|
+
if (!HOOK_NAME_RE_STRICT.test(name)) {
|
|
900
|
+
throw new Error(`uninstallHook: invalid hook name ${JSON.stringify(name)}`);
|
|
901
|
+
}
|
|
902
|
+
const cwd = path.resolve(opts.cwd);
|
|
903
|
+
const scope = opts.scope ?? "project";
|
|
904
|
+
const emit = (line) => { opts.onLog?.(line); };
|
|
905
|
+
// Look up marker from the registry. If the registry is absent or the
|
|
906
|
+
// hook isn't listed, fall back to `auriga:<name>` (the shipped naming
|
|
907
|
+
// convention for every hook in this repo).
|
|
908
|
+
let marker = `auriga:${name}`;
|
|
909
|
+
try {
|
|
910
|
+
const cfg = loadHooksConfig(cwd);
|
|
911
|
+
const def = cfg.hooks.find((h) => h.name === name);
|
|
912
|
+
if (def)
|
|
913
|
+
marker = def.marker;
|
|
914
|
+
}
|
|
915
|
+
catch {
|
|
916
|
+
// No registry at cwd — fine, fall back to the convention.
|
|
917
|
+
}
|
|
918
|
+
// Build a minimal HookDef stub for cleanHookFromScope. It only reads
|
|
919
|
+
// `marker` and `name` (via resolveScope), both of which we have.
|
|
920
|
+
const stub = {
|
|
921
|
+
name,
|
|
922
|
+
description: "",
|
|
923
|
+
runtimePlatforms: [],
|
|
924
|
+
settingsEvents: [],
|
|
925
|
+
command: 'node "$HOOK_DIR/index.mjs"',
|
|
926
|
+
files: [],
|
|
927
|
+
marker,
|
|
928
|
+
};
|
|
929
|
+
// resolveScope picks the settings/hooks paths based on scope.
|
|
930
|
+
// Project scope covers both project + project-local settings (shared
|
|
931
|
+
// on-disk hook dir); user scope covers only ~/.claude/settings.json.
|
|
932
|
+
const cleanScopes = scope === "user" ? ["user"] : ["project", "project-local"];
|
|
933
|
+
let totalRemoved = 0;
|
|
934
|
+
for (const s of cleanScopes) {
|
|
935
|
+
const r = cleanHookFromScope(stub, s, cwd);
|
|
936
|
+
if (r.removed > 0) {
|
|
937
|
+
totalRemoved += r.removed;
|
|
938
|
+
log.ok(`${name}: removed ${r.removed} entr${r.removed === 1 ? "y" : "ies"} from ${r.settingsPath}`);
|
|
939
|
+
emit(`removed ${r.removed} entr${r.removed === 1 ? "y" : "ies"} from ${r.settingsPath}`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (totalRemoved === 0) {
|
|
943
|
+
log.skip(`${name}: no settings entries found`);
|
|
944
|
+
emit(`${name}: no settings entries found`);
|
|
945
|
+
}
|
|
946
|
+
// Hook directory: user → ~/.claude/hooks/<name>; project → <cwd>/.claude/hooks/<name>.
|
|
947
|
+
const hookDir = scope === "user"
|
|
948
|
+
? path.join(os.homedir(), ".claude", "hooks", name)
|
|
949
|
+
: path.join(cwd, ".claude", "hooks", name);
|
|
950
|
+
if (fs.existsSync(hookDir)) {
|
|
951
|
+
fs.rmSync(hookDir, { recursive: true, force: true });
|
|
952
|
+
log.ok(`${name}: directory removed`);
|
|
953
|
+
emit(`removed ${hookDir}`);
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
log.skip(`${name}: directory not present`);
|
|
957
|
+
emit(`${name}: directory not present`);
|
|
958
|
+
}
|
|
959
|
+
}
|
package/dist/plugins.d.ts
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
1
|
import type { InstallOpts, PluginsConfig } from "./utils.js";
|
|
2
2
|
export declare function validatePluginsConfig(raw: unknown): asserts raw is PluginsConfig;
|
|
3
3
|
export declare function installPlugins(packageRoot: string, opts: InstallOpts): Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* Uninstall a single plugin.
|
|
6
|
+
*
|
|
7
|
+
* Claude side: shells out to `claude plugins uninstall <id>` (the
|
|
8
|
+
* canonical CLI path). Errors are propagated — the CLI sometimes
|
|
9
|
+
* surfaces nuanced failure modes (marketplace gone, network) that
|
|
10
|
+
* the caller needs to see verbatim.
|
|
11
|
+
*
|
|
12
|
+
* Codex side: no `codex plugin uninstall` exists today (spec §10.4
|
|
13
|
+
* flagged this as v0.1 needs-confirm). We mimic the install path
|
|
14
|
+
* in reverse:
|
|
15
|
+
* 1. Read + parse `~/.codex/config.toml`, delete `[plugins."<id>"]`,
|
|
16
|
+
* atomic write back. Throws on parse error (don't half-corrupt).
|
|
17
|
+
* 2. rm `~/.codex/plugins/cache/<marketplace>/<plugin>/` directory.
|
|
18
|
+
* Both steps are idempotent — missing config / missing cache dir is
|
|
19
|
+
* a no-op (the user may have manually cleaned half of the install).
|
|
20
|
+
*
|
|
21
|
+
* Caveat: we deliberately do NOT remove the marketplace itself. A
|
|
22
|
+
* single marketplace may host multiple plugins; tearing it down
|
|
23
|
+
* because one plugin left would break others. The user can
|
|
24
|
+
* `codex plugin marketplace remove` separately when they want.
|
|
25
|
+
*
|
|
26
|
+
* Validation happens before any I/O — a malformed id throws cleanly with
|
|
27
|
+
* no side effects, so retries are safe.
|
|
28
|
+
*/
|
|
29
|
+
export declare function uninstallPlugin(id: string, agent: "claude" | "codex", opts: {
|
|
30
|
+
cwd: string;
|
|
31
|
+
onLog?: (line: string) => void;
|
|
32
|
+
}): Promise<void>;
|
package/dist/plugins.js
CHANGED
|
@@ -5,7 +5,7 @@ import { checkbox, select } from "@inquirer/prompts";
|
|
|
5
5
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
6
6
|
import { codexManifestPath, validateCodexInstallConfig, validateCodexMarketplace, } from "./codex-plugin-config.js";
|
|
7
7
|
import { validateMarketplaceField } from "./marketplace.js";
|
|
8
|
-
import { atomicWriteFile, exec, fetchExtraContent, log, withEsc } from "./utils.js";
|
|
8
|
+
import { atomicWriteFile, exec, execAsync, fetchExtraContent, log, withEsc } from "./utils.js";
|
|
9
9
|
// Plugin names and plugin-package names end up in `claude plugins ...`
|
|
10
10
|
// shell commands via string interpolation. .claude/plugins.json is
|
|
11
11
|
// fetched from raw GitHub at runtime, so every value must pass a
|
|
@@ -531,7 +531,14 @@ export async function installPlugins(packageRoot, opts) {
|
|
|
531
531
|
for (const [name, source] of marketplacesToAdd) {
|
|
532
532
|
console.log(`\nAdding marketplace: ${name}...`);
|
|
533
533
|
try {
|
|
534
|
-
|
|
534
|
+
const cmd = `claude plugins marketplace add ${source}`;
|
|
535
|
+
if (opts.onLog) {
|
|
536
|
+
opts.onLog(`▸ ${cmd}`, "stdout");
|
|
537
|
+
await execAsync(cmd, { onLine: opts.onLog });
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
exec(cmd, { inherit: true });
|
|
541
|
+
}
|
|
535
542
|
log.ok(`Marketplace ${name} added`);
|
|
536
543
|
}
|
|
537
544
|
catch {
|
|
@@ -542,7 +549,14 @@ export async function installPlugins(packageRoot, opts) {
|
|
|
542
549
|
for (const name of marketplacesToUpdate) {
|
|
543
550
|
console.log(`\nUpdating marketplace: ${name}...`);
|
|
544
551
|
try {
|
|
545
|
-
|
|
552
|
+
const cmd = `claude plugins marketplace update ${name}`;
|
|
553
|
+
if (opts.onLog) {
|
|
554
|
+
opts.onLog(`▸ ${cmd}`, "stdout");
|
|
555
|
+
await execAsync(cmd, { onLine: opts.onLog });
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
exec(cmd, { inherit: true });
|
|
559
|
+
}
|
|
546
560
|
log.ok(`Marketplace ${name} updated`);
|
|
547
561
|
}
|
|
548
562
|
catch (e) {
|
|
@@ -557,9 +571,14 @@ export async function installPlugins(packageRoot, opts) {
|
|
|
557
571
|
for (const plugin of selected) {
|
|
558
572
|
console.log(`\nInstalling ${plugin.name}...`);
|
|
559
573
|
try {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
574
|
+
const cmd = `claude plugins install ${plugin.package} --scope ${scope}`;
|
|
575
|
+
if (opts.onLog) {
|
|
576
|
+
opts.onLog(`▸ ${cmd}`, "stdout");
|
|
577
|
+
await execAsync(cmd, { onLine: opts.onLog });
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
exec(cmd, { inherit: true });
|
|
581
|
+
}
|
|
563
582
|
log.ok(`${plugin.name} installed`);
|
|
564
583
|
}
|
|
565
584
|
catch {
|
|
@@ -584,3 +603,115 @@ export async function installPlugins(packageRoot, opts) {
|
|
|
584
603
|
}
|
|
585
604
|
}
|
|
586
605
|
}
|
|
606
|
+
// --- Uninstall ----------------------------------------------------------------
|
|
607
|
+
// Plugin id format: `<plugin>@<marketplace>` (matches the Codex config.toml
|
|
608
|
+
// key shape and Claude Code's `claude plugins install ...` argument).
|
|
609
|
+
// Tightened with the same name regex used everywhere else in this file
|
|
610
|
+
// (PLUGIN_NAME_RE on the plugin side, MARKETPLACE_NAME_RE on the
|
|
611
|
+
// marketplace side, both anchored). `name` is interpolated into a shell
|
|
612
|
+
// command (Claude path) and used as a filesystem segment (Codex path);
|
|
613
|
+
// rejecting unsafe shapes here closes both attack surfaces in one place.
|
|
614
|
+
const PLUGIN_ID_RE = /^([A-Za-z0-9][A-Za-z0-9._-]{0,127})@([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/;
|
|
615
|
+
function parsePluginId(id) {
|
|
616
|
+
const m = PLUGIN_ID_RE.exec(id);
|
|
617
|
+
if (!m) {
|
|
618
|
+
throw new Error(`uninstallPlugin: invalid plugin id ${JSON.stringify(id)}; expected <plugin>@<marketplace>`);
|
|
619
|
+
}
|
|
620
|
+
return { plugin: m[1], marketplace: m[2] };
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Remove `[plugins."<id>"]` from a parsed Codex config TOML tree.
|
|
624
|
+
* Returns true if anything was removed. Idempotent: missing key → false.
|
|
625
|
+
*
|
|
626
|
+
* Pure function operating on the parsed tree — no I/O. Lets the test
|
|
627
|
+
* harness assert tree shape without touching disk + lets the I/O wrapper
|
|
628
|
+
* skip the atomic write when nothing changed.
|
|
629
|
+
*/
|
|
630
|
+
function removeCodexPluginFromConfig(parsed, pluginId) {
|
|
631
|
+
const plugins = parsed.plugins;
|
|
632
|
+
if (!isTomlTable(plugins))
|
|
633
|
+
return false;
|
|
634
|
+
if (!(pluginId in plugins))
|
|
635
|
+
return false;
|
|
636
|
+
delete plugins[pluginId];
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Uninstall a single plugin.
|
|
641
|
+
*
|
|
642
|
+
* Claude side: shells out to `claude plugins uninstall <id>` (the
|
|
643
|
+
* canonical CLI path). Errors are propagated — the CLI sometimes
|
|
644
|
+
* surfaces nuanced failure modes (marketplace gone, network) that
|
|
645
|
+
* the caller needs to see verbatim.
|
|
646
|
+
*
|
|
647
|
+
* Codex side: no `codex plugin uninstall` exists today (spec §10.4
|
|
648
|
+
* flagged this as v0.1 needs-confirm). We mimic the install path
|
|
649
|
+
* in reverse:
|
|
650
|
+
* 1. Read + parse `~/.codex/config.toml`, delete `[plugins."<id>"]`,
|
|
651
|
+
* atomic write back. Throws on parse error (don't half-corrupt).
|
|
652
|
+
* 2. rm `~/.codex/plugins/cache/<marketplace>/<plugin>/` directory.
|
|
653
|
+
* Both steps are idempotent — missing config / missing cache dir is
|
|
654
|
+
* a no-op (the user may have manually cleaned half of the install).
|
|
655
|
+
*
|
|
656
|
+
* Caveat: we deliberately do NOT remove the marketplace itself. A
|
|
657
|
+
* single marketplace may host multiple plugins; tearing it down
|
|
658
|
+
* because one plugin left would break others. The user can
|
|
659
|
+
* `codex plugin marketplace remove` separately when they want.
|
|
660
|
+
*
|
|
661
|
+
* Validation happens before any I/O — a malformed id throws cleanly with
|
|
662
|
+
* no side effects, so retries are safe.
|
|
663
|
+
*/
|
|
664
|
+
export async function uninstallPlugin(id, agent, opts) {
|
|
665
|
+
const { plugin, marketplace } = parsePluginId(id);
|
|
666
|
+
const emit = (line) => { opts.onLog?.(line); };
|
|
667
|
+
if (agent === "claude") {
|
|
668
|
+
// Note: scope is intentionally NOT specified. `claude plugins
|
|
669
|
+
// uninstall <id>` operates against whatever scope the plugin is
|
|
670
|
+
// installed in (user / project) — letting the CLI find it is
|
|
671
|
+
// more robust than guessing wrong and silently no-op'ing.
|
|
672
|
+
exec(`claude plugins uninstall ${id}`, { cwd: opts.cwd, inherit: true });
|
|
673
|
+
log.ok(`${id} uninstalled from Claude Code`);
|
|
674
|
+
emit(`uninstalled ${id} from Claude Code`);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
// Codex path.
|
|
678
|
+
const home = codexHome();
|
|
679
|
+
const configPath = path.join(home, "config.toml");
|
|
680
|
+
if (fs.existsSync(configPath)) {
|
|
681
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
682
|
+
// Parse-then-mutate: any parse failure aborts BEFORE we touch the
|
|
683
|
+
// filesystem (cache dir removal also gets skipped) so a damaged
|
|
684
|
+
// config doesn't end up half-uninstalled. The test "config.toml
|
|
685
|
+
// damaged → throw before mutation" locks this in.
|
|
686
|
+
const parsed = parseCodexConfigToml(content, configPath);
|
|
687
|
+
const removed = removeCodexPluginFromConfig(parsed, id);
|
|
688
|
+
if (removed) {
|
|
689
|
+
const next = stringifyToml(parsed);
|
|
690
|
+
atomicWriteFile(configPath, next.endsWith("\n") ? next : `${next}\n`);
|
|
691
|
+
log.ok(`${id} disabled in Codex config.toml`);
|
|
692
|
+
emit(`removed ${id} from Codex config.toml`);
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
log.skip(`${id} not present in Codex config.toml`);
|
|
696
|
+
emit(`${id} not present in Codex config.toml`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
log.skip(`Codex config.toml not present`);
|
|
701
|
+
emit(`Codex config.toml not present`);
|
|
702
|
+
}
|
|
703
|
+
// Cache dir: ~/.codex/plugins/cache/<marketplace>/<plugin>/
|
|
704
|
+
// PLUGIN_ID_RE constrains both segments to a safe charset, so the
|
|
705
|
+
// path can't escape via injection. rmSync with recursive+force is
|
|
706
|
+
// the standard rm-rf idiom; missing dir is a no-op.
|
|
707
|
+
const cacheDir = path.join(home, "plugins", "cache", marketplace, plugin);
|
|
708
|
+
if (fs.existsSync(cacheDir)) {
|
|
709
|
+
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
710
|
+
log.ok(`${id} cache directory removed`);
|
|
711
|
+
emit(`removed Codex cache directory for ${id}`);
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
log.skip(`${id} cache directory not present`);
|
|
715
|
+
emit(`Codex cache directory for ${id} not present`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Build the scan-time Catalog (the shape src/state.ts consumes) from
|
|
2
|
+
// auriga-cli's installed package state. This bridges the build-time
|
|
3
|
+
// `dist/catalog.json` (which carries names + descriptions for the menu)
|
|
4
|
+
// and the runtime scanner's need for expected hashes + versions.
|
|
5
|
+
//
|
|
6
|
+
// Inputs (all under packageRoot):
|
|
7
|
+
// dist/catalog.json — names + descriptions for 5 categories
|
|
8
|
+
// skills-lock.json — expected SHA256 for every vendored skill
|
|
9
|
+
// .claude/plugins.json — Claude plugin entries (agent = "claude")
|
|
10
|
+
// .agents/plugins/install.json — Codex plugin entries (agent = "codex")
|
|
11
|
+
// .claude/hooks/<name>/index.mjs — runtime SHA256 = expected hash
|
|
12
|
+
// CLAUDE.md — `# auriga Workflow (vX.Y.Z)` provides
|
|
13
|
+
// workflowVersion
|
|
14
|
+
//
|
|
15
|
+
// Anything missing is treated as "no expectation" (empty hash / version)
|
|
16
|
+
// rather than throwing; scanState will still produce a structurally valid
|
|
17
|
+
// StateReport — items just classify as not-installed or installed
|
|
18
|
+
// depending on whether the user-side data exists.
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import { readFile } from "node:fs/promises";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { loadCatalog } from "./catalog.js";
|
|
23
|
+
const WORKFLOW_VERSION_RE = /^#\s*auriga Workflow\s*\(v([\d.]+)\)/m;
|
|
24
|
+
async function tryReadFile(p) {
|
|
25
|
+
try {
|
|
26
|
+
return await readFile(p, "utf8");
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function sha256File(p) {
|
|
33
|
+
const bytes = await readFile(p);
|
|
34
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
35
|
+
}
|
|
36
|
+
export async function buildScanCatalog(packageRoot) {
|
|
37
|
+
const dist = loadCatalog(packageRoot);
|
|
38
|
+
// Workflow version: parse from auriga-cli's own CLAUDE.md template.
|
|
39
|
+
// If missing, leave as empty string so workflow always classifies as
|
|
40
|
+
// not-installed (no expectation set).
|
|
41
|
+
const claudeMd = await tryReadFile(path.join(packageRoot, "CLAUDE.md"));
|
|
42
|
+
const m = claudeMd ? WORKFLOW_VERSION_RE.exec(claudeMd) : null;
|
|
43
|
+
const workflowVersion = m ? m[1] : "";
|
|
44
|
+
// Skills + recommended: hashes from skills-lock.json.
|
|
45
|
+
let lock = {};
|
|
46
|
+
const lockText = await tryReadFile(path.join(packageRoot, "skills-lock.json"));
|
|
47
|
+
if (lockText) {
|
|
48
|
+
try {
|
|
49
|
+
lock = JSON.parse(lockText);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// corrupted lock → no expectations; user state still classifies safely
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const skills = {};
|
|
56
|
+
for (const entry of dist.workflowSkills) {
|
|
57
|
+
skills[entry.name] = {
|
|
58
|
+
description: entry.description,
|
|
59
|
+
expectedHash: lock.skills?.[entry.name]?.computedHash ?? "",
|
|
60
|
+
isWorkflow: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const recommendedSkills = {};
|
|
64
|
+
for (const entry of dist.recommendedSkills) {
|
|
65
|
+
recommendedSkills[entry.name] = {
|
|
66
|
+
description: entry.description,
|
|
67
|
+
expectedHash: lock.skills?.[entry.name]?.computedHash ?? "",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// Plugins: split by agent based on which config file lists them. A
|
|
71
|
+
// plugin can appear in both registries (cross-agent plugins like
|
|
72
|
+
// auriga-go); we represent it once per agent.
|
|
73
|
+
const plugins = {};
|
|
74
|
+
const claudePluginsText = await tryReadFile(path.join(packageRoot, ".claude", "plugins.json"));
|
|
75
|
+
const claudeNames = new Set();
|
|
76
|
+
if (claudePluginsText) {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(claudePluginsText);
|
|
79
|
+
for (const p of parsed.plugins ?? []) {
|
|
80
|
+
if (p.name)
|
|
81
|
+
claudeNames.add(p.name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
/* ignore */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const codexInstallText = await tryReadFile(path.join(packageRoot, ".agents", "plugins", "install.json"));
|
|
89
|
+
const codexNames = new Set();
|
|
90
|
+
if (codexInstallText) {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(codexInstallText);
|
|
93
|
+
for (const p of parsed.plugins ?? []) {
|
|
94
|
+
if (p.name)
|
|
95
|
+
codexNames.add(p.name);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
/* ignore */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const entry of dist.plugins) {
|
|
103
|
+
// Collect every agent that registers this plugin. A plugin can ship in
|
|
104
|
+
// both registries (cross-agent plugins like auriga-go); we emit it as
|
|
105
|
+
// a single multi-agent record so the UI shows one row + BOTH badge and
|
|
106
|
+
// Apply installs to each side.
|
|
107
|
+
const agents = [];
|
|
108
|
+
if (claudeNames.has(entry.name))
|
|
109
|
+
agents.push("claude");
|
|
110
|
+
if (codexNames.has(entry.name))
|
|
111
|
+
agents.push("codex");
|
|
112
|
+
if (agents.length === 0)
|
|
113
|
+
agents.push("claude"); // unknown defaults to claude
|
|
114
|
+
plugins[entry.name] = { description: entry.description, agents };
|
|
115
|
+
}
|
|
116
|
+
// Hooks: runtime SHA256 of each hook's index.mjs serves as the expected
|
|
117
|
+
// hash. If the file can't be read, leave the expectation empty so the
|
|
118
|
+
// hook classifies as not-installed.
|
|
119
|
+
const hooks = {};
|
|
120
|
+
for (const entry of dist.hooks) {
|
|
121
|
+
const hookEntry = path.join(packageRoot, ".claude", "hooks", entry.name, "index.mjs");
|
|
122
|
+
let expectedHash = "";
|
|
123
|
+
try {
|
|
124
|
+
expectedHash = await sha256File(hookEntry);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
/* missing or unreadable hook payload; leave hash empty */
|
|
128
|
+
}
|
|
129
|
+
hooks[entry.name] = { description: entry.description, expectedHash };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
workflowVersion,
|
|
133
|
+
skills,
|
|
134
|
+
recommendedSkills,
|
|
135
|
+
plugins,
|
|
136
|
+
hooks,
|
|
137
|
+
};
|
|
138
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type LogLevel = "info" | "warn" | "error";
|
|
2
|
+
export interface ApplyHandlerOptions {
|
|
3
|
+
onLog: (line: string, level: LogLevel) => void;
|
|
4
|
+
/** Installer scope from the ApplyItemRef. Forwarded as-is — handlers
|
|
5
|
+
* translate into the per-installer flag (`--scope project|user`). The
|
|
6
|
+
* workflow handler ignores it (workflow has no scope concept). */
|
|
7
|
+
scope?: "project" | "user";
|
|
8
|
+
/** Workflow CLAUDE.md language variant. Only meaningful for the workflow
|
|
9
|
+
* handler; other handlers ignore it. Omitted = "en". */
|
|
10
|
+
lang?: "en" | "zh-CN";
|
|
11
|
+
}
|
|
12
|
+
export type ApplyHandler = (action: ApplyAction, name: string, opts: ApplyHandlerOptions) => Promise<void>;
|
|
13
|
+
export interface ApplyHandlers {
|
|
14
|
+
workflow: ApplyHandler;
|
|
15
|
+
skill: ApplyHandler;
|
|
16
|
+
"recommended-skill": ApplyHandler;
|
|
17
|
+
plugin: ApplyHandler;
|
|
18
|
+
hook: ApplyHandler;
|
|
19
|
+
}
|
|
20
|
+
export interface ApplyCatalog {
|
|
21
|
+
workflow: Set<string>;
|
|
22
|
+
skill: Set<string>;
|
|
23
|
+
"recommended-skill": Set<string>;
|
|
24
|
+
plugin: Set<string>;
|
|
25
|
+
hook: Set<string>;
|
|
26
|
+
}
|
|
27
|
+
export interface StartServerOptions {
|
|
28
|
+
port?: number;
|
|
29
|
+
token: string;
|
|
30
|
+
cwd: string;
|
|
31
|
+
/** Where auriga-cli itself lives — source of dist/catalog.json,
|
|
32
|
+
* skills-lock.json, hook payloads, etc. Defaults to cwd, which is
|
|
33
|
+
* correct when running tests from the auriga-cli checkout. CLI mode
|
|
34
|
+
* must pass getPackageRoot() so the server uses the installed package
|
|
35
|
+
* rather than the user's project. */
|
|
36
|
+
packageRoot?: string;
|
|
37
|
+
/** Idle-shutdown timeout in ms. The browser POSTs /api/ping every 5s;
|
|
38
|
+
* if no ping arrives for this duration, the server shuts down
|
|
39
|
+
* gracefully (closing-browser-closes-server UX). `0` disables the
|
|
40
|
+
* heartbeat (used by tests so a single suite doesn't time-bomb). */
|
|
41
|
+
heartbeatTimeoutMs?: number;
|
|
42
|
+
/** Apply handlers per category. When omitted, /api/apply falls back to
|
|
43
|
+
* built-in installers wired by the CLI. Tests inject mocks to make apply
|
|
44
|
+
* behavior deterministic without touching real installers. */
|
|
45
|
+
applyHandlers?: ApplyHandlers;
|
|
46
|
+
/** Per-category name whitelist. When set, /api/apply rejects (400) any
|
|
47
|
+
* item whose name is not present in the matching category's Set. When
|
|
48
|
+
* omitted, name membership is not enforced (CLI builds a default
|
|
49
|
+
* catalog at boot time). */
|
|
50
|
+
applyCatalog?: ApplyCatalog;
|
|
51
|
+
/** Directory whose contents are served for non-/api paths (the extracted
|
|
52
|
+
* UI bundle). When undefined, every static path returns 404 — useful in
|
|
53
|
+
* tests and the M1 server smoke checks. */
|
|
54
|
+
uiDir?: string;
|
|
55
|
+
/** Max time to wait for an in-flight job during graceful shutdown before
|
|
56
|
+
* force-closing sockets (spec §4.3 / §6.6). Defaults to 30000 ms in
|
|
57
|
+
* production; tests override to a small value (e.g. 200 ms) so they
|
|
58
|
+
* don't time-bomb. */
|
|
59
|
+
shutdownGraceMs?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface RunningServer {
|
|
62
|
+
port: number;
|
|
63
|
+
/** Explicit shutdown. Idempotent. */
|
|
64
|
+
close(): Promise<void>;
|
|
65
|
+
/** Resolves when the server has fully stopped — either via close() or the
|
|
66
|
+
* heartbeat-driven shutdown. CLI callers await this to block their event
|
|
67
|
+
* loop until "browser was closed" actually fires. */
|
|
68
|
+
closed: Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
import type { ApplyAction } from "./api-types.js";
|
|
71
|
+
export declare function startServer(opts: StartServerOptions): Promise<RunningServer>;
|