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/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
- exec(`claude plugins marketplace add ${source}`, { inherit: true });
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
- exec(`claude plugins marketplace update ${name}`, { inherit: true });
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
- exec(`claude plugins install ${plugin.package} --scope ${scope}`, {
561
- inherit: true,
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,2 @@
1
+ import type { Catalog as ScanCatalog } from "./state.js";
2
+ export declare function buildScanCatalog(packageRoot: string): Promise<ScanCatalog>;
@@ -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
+ }
@@ -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>;