auriga-cli 1.15.2 → 1.17.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.
@@ -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>;