auriga-cli 1.17.0 → 1.18.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 +2 -0
- package/README.zh-CN.md +2 -0
- package/dist/api-types.d.ts +25 -1
- package/dist/catalog.json +9 -1
- package/dist/cli.js +6 -1
- package/dist/scan-catalog.js +85 -31
- package/dist/server.js +50 -4
- package/dist/state.d.ts +43 -6
- package/dist/state.js +447 -164
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,7 +113,9 @@ Installs selected skills via `npx skills add`, targeting both Claude Code and Co
|
|
|
113
113
|
| claude-code-agent | [Ben2pc/g-claude-code-plugins](https://github.com/Ben2pc/g-claude-code-plugins) | Delegate coding, review, diagnosis, and planning to standalone Claude Code sessions |
|
|
114
114
|
| code-simplification | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | Refactor for clarity without changing behavior — trim accumulated complexity |
|
|
115
115
|
| codex-agent | [Ben2pc/g-claude-code-plugins](https://github.com/Ben2pc/g-claude-code-plugins) | Delegate to Codex sessions for cross-model coverage |
|
|
116
|
+
| deprecation-and-migration | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | Sunset, replace, or migrate legacy code — deprecation discipline |
|
|
116
117
|
| design-taste-frontend | [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill) | Senior UI/UX engineer with metric-based design rules and strict component architecture |
|
|
118
|
+
| documentation-and-adrs | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | Record architectural decisions and the *why* — context for future engineers / agents |
|
|
117
119
|
| frontend-design | [anthropics/skills](https://github.com/anthropics/skills) | Distinctive, production-grade frontend UI generation that avoids generic AI aesthetics |
|
|
118
120
|
| make-interfaces-feel-better | [jakubkrehel/make-interfaces-feel-better](https://github.com/jakubkrehel/make-interfaces-feel-better) | Polish principles — animations, surfaces, typography, performance |
|
|
119
121
|
|
package/README.zh-CN.md
CHANGED
|
@@ -113,7 +113,9 @@ npx auriga-cli
|
|
|
113
113
|
| claude-code-agent | [Ben2pc/g-claude-code-plugins](https://github.com/Ben2pc/g-claude-code-plugins) | 通过 Claude Code Agent SDK 把任务委派给独立 Claude Code 会话 |
|
|
114
114
|
| code-simplification | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | 不改变行为前提下重构代码以提升可读性 —— 清掉累积的不必要复杂度 |
|
|
115
115
|
| codex-agent | [Ben2pc/g-claude-code-plugins](https://github.com/Ben2pc/g-claude-code-plugins) | 委派给 Codex 会话,做跨模型覆盖 |
|
|
116
|
+
| deprecation-and-migration | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | 废弃与迁移流程 —— 安全地下线、替换或迁移遗留代码 |
|
|
116
117
|
| design-taste-frontend | [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill) | 高阶 UI/UX 工程师 —— 度量化设计规则与严格的组件架构约束 |
|
|
118
|
+
| documentation-and-adrs | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | 记录架构决策与"为什么" —— 为后来的工程师和 Agent 沉淀上下文 |
|
|
117
119
|
| frontend-design | [anthropics/skills](https://github.com/anthropics/skills) | 生成有辨识度、production 级的前端界面,避开常见 AI 同质化美学 |
|
|
118
120
|
| make-interfaces-feel-better | [jakubkrehel/make-interfaces-feel-better](https://github.com/jakubkrehel/make-interfaces-feel-better) | 界面打磨原则 —— 动画、表面、排版、性能 |
|
|
119
121
|
|
package/dist/api-types.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export type ItemStatus = "installed" | "update-available" | "not-installed";
|
|
2
|
+
/**
|
|
3
|
+
* Per-category scan scope. Each category (workflow / skills / plugins / hooks)
|
|
4
|
+
* can be independently scanned in either user scope (~/.claude/, ~/.codex/)
|
|
5
|
+
* or project scope (<proj>/.claude/). The Web UI's per-column scope picker
|
|
6
|
+
* carries these through the `/api/state` query so the scanner reads the
|
|
7
|
+
* right truth source per category. Codex plugins are user-scope only by
|
|
8
|
+
* design and ignore this field.
|
|
9
|
+
*/
|
|
10
|
+
export type ScanScope = "user" | "project";
|
|
2
11
|
export interface StateReport {
|
|
3
12
|
/** Absolute path to the project the server was launched against. Surfaced
|
|
4
13
|
* in the UI's top bar so users know where Apply will write project-scope
|
|
@@ -16,6 +25,13 @@ export interface WorkflowState {
|
|
|
16
25
|
status: ItemStatus;
|
|
17
26
|
currentVersion?: string;
|
|
18
27
|
expectedVersion: string;
|
|
28
|
+
/** Which scope the scanner read to produce this row. Reflects the scope
|
|
29
|
+
* scanned, not where the file was found — e.g. when scope=user and
|
|
30
|
+
* ~/.claude/CLAUDE.md is absent, observedScope is still "user". The
|
|
31
|
+
* scanner ALWAYS sets this field at runtime; it is typed optional only
|
|
32
|
+
* so the mergePluginsById regression helper (which carries over from the
|
|
33
|
+
* pre-rewrite suite without an explicit scope) continues to compile. */
|
|
34
|
+
observedScope?: ScanScope;
|
|
19
35
|
}
|
|
20
36
|
export interface SkillState {
|
|
21
37
|
name: string;
|
|
@@ -24,6 +40,8 @@ export interface SkillState {
|
|
|
24
40
|
isWorkflow: boolean;
|
|
25
41
|
currentHash?: string;
|
|
26
42
|
expectedHash: string;
|
|
43
|
+
/** Scope the scanner read to produce this row. See WorkflowState comment. */
|
|
44
|
+
observedScope?: ScanScope;
|
|
27
45
|
}
|
|
28
46
|
export type ApplyAgent = "claude" | "codex";
|
|
29
47
|
export interface PluginState {
|
|
@@ -41,6 +59,10 @@ export interface PluginState {
|
|
|
41
59
|
currentVersion?: string;
|
|
42
60
|
expectedVersion?: string;
|
|
43
61
|
versionSource: "upstream-live" | "catalog";
|
|
62
|
+
/** Scope the scanner read to produce this row. Codex plugins are always
|
|
63
|
+
* "user" (Codex has no project-scope plugin concept). See WorkflowState
|
|
64
|
+
* comment on why this is typed optional. */
|
|
65
|
+
observedScope?: ScanScope;
|
|
44
66
|
}
|
|
45
67
|
export interface HookState {
|
|
46
68
|
name: string;
|
|
@@ -48,9 +70,11 @@ export interface HookState {
|
|
|
48
70
|
status: ItemStatus;
|
|
49
71
|
currentHash?: string;
|
|
50
72
|
expectedHash: string;
|
|
73
|
+
/** Scope the scanner read to produce this row. See WorkflowState comment. */
|
|
74
|
+
observedScope?: ScanScope;
|
|
51
75
|
}
|
|
52
76
|
export interface StateWarning {
|
|
53
|
-
code: "claude-cli-missing" | "codex-cli-missing" | "marketplace-offline";
|
|
77
|
+
code: "claude-cli-missing" | "codex-cli-missing" | "marketplace-offline" | "claude-code-not-installed" | "settings-unreadable" | "skill-malformed" | "workflow-unknown-version";
|
|
54
78
|
message: string;
|
|
55
79
|
}
|
|
56
80
|
export type ApplyCategory = "workflow" | "skill" | "recommended-skill" | "plugin" | "hook";
|
package/dist/catalog.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-05-
|
|
2
|
+
"generatedAt": "2026-05-12T13:41:34.719Z",
|
|
3
3
|
"workflowSkills": [
|
|
4
4
|
{
|
|
5
5
|
"name": "brainstorming",
|
|
@@ -51,10 +51,18 @@
|
|
|
51
51
|
"name": "codex-agent",
|
|
52
52
|
"description": "Delegate coding, review, diagnosis, planning, and browser tasks to an independent Codex session via `codex exec` / resume / review."
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
"name": "deprecation-and-migration",
|
|
56
|
+
"description": "Manages deprecation and migration. Use when removing old systems, APIs, or features. Use when migrating users from one implementation to another. Use when deciding whether to maintain or sunset existing code."
|
|
57
|
+
},
|
|
54
58
|
{
|
|
55
59
|
"name": "design-taste-frontend",
|
|
56
60
|
"description": "Senior UI/UX Engineer. Architect digital interfaces overriding default LLM biases. Enforces metric-based rules, strict component architecture, CSS hardware acceleration, and balanced design engineering."
|
|
57
61
|
},
|
|
62
|
+
{
|
|
63
|
+
"name": "documentation-and-adrs",
|
|
64
|
+
"description": "Records decisions and documentation. Use when making architectural decisions, changing public APIs, shipping features, or when you need to record context that future engineers and agents will need to understand the codebase."
|
|
65
|
+
},
|
|
58
66
|
{
|
|
59
67
|
"name": "frontend-design",
|
|
60
68
|
"description": "Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics."
|
package/dist/cli.js
CHANGED
|
@@ -562,7 +562,12 @@ async function dispatchInstaller(category, packageRoot, opts) {
|
|
|
562
562
|
// ---------------------------------------------------------------------------
|
|
563
563
|
const UI_DEFAULT_PORT = 4747;
|
|
564
564
|
const UI_PORT_RANGE = 10; // 4747..4756
|
|
565
|
-
|
|
565
|
+
// 2 minutes covers Chrome's "intensive throttling" of background tabs
|
|
566
|
+
// (kicks in after ~5 min of being hidden, drops setInterval to ~1 ping/min).
|
|
567
|
+
// At 15s the browser tab being switched away for a moment would tear down
|
|
568
|
+
// the server — bad UX. Closing the browser now takes up to 2 min to release
|
|
569
|
+
// the port; users who care can ctrl+C the CLI for immediate exit.
|
|
570
|
+
const UI_HEARTBEAT_TIMEOUT_MS = 120_000;
|
|
566
571
|
async function runUi(p, version) {
|
|
567
572
|
// Lazy-load the server-side deps so the install / guide paths stay light.
|
|
568
573
|
const { randomBytes } = await import("node:crypto");
|
package/dist/scan-catalog.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
// skills-lock.json — expected SHA256 for every vendored skill
|
|
9
9
|
// .claude/plugins.json — Claude plugin entries (agent = "claude")
|
|
10
10
|
// .agents/plugins/install.json — Codex plugin entries (agent = "codex")
|
|
11
|
-
// .claude/hooks
|
|
11
|
+
// .claude/hooks/hooks.json — registers settingsEvents per hook (event /
|
|
12
|
+
// matcher / if) used by state.ts for drift
|
|
13
|
+
// detection in <scope>/.claude/settings.json
|
|
12
14
|
// CLAUDE.md — `# auriga Workflow (vX.Y.Z)` provides
|
|
13
15
|
// workflowVersion
|
|
14
16
|
//
|
|
@@ -20,6 +22,39 @@ import { createHash } from "node:crypto";
|
|
|
20
22
|
import { readFile } from "node:fs/promises";
|
|
21
23
|
import path from "node:path";
|
|
22
24
|
import { loadCatalog } from "./catalog.js";
|
|
25
|
+
async function sha256SkillMd(skillsRoot, name) {
|
|
26
|
+
try {
|
|
27
|
+
const buf = await readFile(path.join(skillsRoot, name, "SKILL.md"));
|
|
28
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Read `plugins/<name>/.claude-plugin/plugin.json` (or `.codex-plugin/plugin.json`
|
|
35
|
+
* as fallback) and return the `version` field. Returns "" when no manifest
|
|
36
|
+
* exists or the JSON is malformed — the scanner then leaves expectedVersion
|
|
37
|
+
* unset, which means external-marketplace plugins (whose source lives
|
|
38
|
+
* upstream, not in this repo) get the "trust installed" classification. */
|
|
39
|
+
async function readPluginManifestVersion(packageRoot, name) {
|
|
40
|
+
const candidates = [
|
|
41
|
+
path.join(packageRoot, "plugins", name, ".claude-plugin", "plugin.json"),
|
|
42
|
+
path.join(packageRoot, "plugins", name, ".codex-plugin", "plugin.json"),
|
|
43
|
+
];
|
|
44
|
+
for (const p of candidates) {
|
|
45
|
+
try {
|
|
46
|
+
const raw = await readFile(p, "utf8");
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
49
|
+
return parsed.version;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* try next candidate */
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
23
58
|
const WORKFLOW_VERSION_RE = /^#\s*auriga Workflow\s*\(v([\d.]+)\)/m;
|
|
24
59
|
async function tryReadFile(p) {
|
|
25
60
|
try {
|
|
@@ -29,10 +64,6 @@ async function tryReadFile(p) {
|
|
|
29
64
|
return null;
|
|
30
65
|
}
|
|
31
66
|
}
|
|
32
|
-
async function sha256File(p) {
|
|
33
|
-
const bytes = await readFile(p);
|
|
34
|
-
return createHash("sha256").update(bytes).digest("hex");
|
|
35
|
-
}
|
|
36
67
|
export async function buildScanCatalog(packageRoot) {
|
|
37
68
|
const dist = loadCatalog(packageRoot);
|
|
38
69
|
// Workflow version: parse from auriga-cli's own CLAUDE.md template.
|
|
@@ -41,22 +72,18 @@ export async function buildScanCatalog(packageRoot) {
|
|
|
41
72
|
const claudeMd = await tryReadFile(path.join(packageRoot, "CLAUDE.md"));
|
|
42
73
|
const m = claudeMd ? WORKFLOW_VERSION_RE.exec(claudeMd) : null;
|
|
43
74
|
const workflowVersion = m ? m[1] : "";
|
|
44
|
-
// Skills + recommended:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
catch {
|
|
52
|
-
// corrupted lock → no expectations; user state still classifies safely
|
|
53
|
-
}
|
|
54
|
-
}
|
|
75
|
+
// Skills + recommended: sha256 of each shipped SKILL.md. This is the same
|
|
76
|
+
// hash the scanner computes for `<scope>/.claude/skills/<name>/SKILL.md`
|
|
77
|
+
// at scan time, so a match means "user's installed copy is identical to
|
|
78
|
+
// the version auriga-cli ships". skills-lock.json's `computedHash` field
|
|
79
|
+
// hashes the entire skill directory (every file, sorted), which doesn't
|
|
80
|
+
// line up with the scanner's per-file model — we deliberately ignore it.
|
|
81
|
+
const skillsRoot = path.join(packageRoot, ".agents", "skills");
|
|
55
82
|
const skills = {};
|
|
56
83
|
for (const entry of dist.workflowSkills) {
|
|
57
84
|
skills[entry.name] = {
|
|
58
85
|
description: entry.description,
|
|
59
|
-
expectedHash:
|
|
86
|
+
expectedHash: await sha256SkillMd(skillsRoot, entry.name),
|
|
60
87
|
isWorkflow: true,
|
|
61
88
|
};
|
|
62
89
|
}
|
|
@@ -64,7 +91,7 @@ export async function buildScanCatalog(packageRoot) {
|
|
|
64
91
|
for (const entry of dist.recommendedSkills) {
|
|
65
92
|
recommendedSkills[entry.name] = {
|
|
66
93
|
description: entry.description,
|
|
67
|
-
expectedHash:
|
|
94
|
+
expectedHash: await sha256SkillMd(skillsRoot, entry.name),
|
|
68
95
|
};
|
|
69
96
|
}
|
|
70
97
|
// Plugins: split by agent based on which config file lists them. A
|
|
@@ -111,22 +138,49 @@ export async function buildScanCatalog(packageRoot) {
|
|
|
111
138
|
agents.push("codex");
|
|
112
139
|
if (agents.length === 0)
|
|
113
140
|
agents.push("claude"); // unknown defaults to claude
|
|
114
|
-
|
|
141
|
+
// Bake expectedVersion from the owned plugin's manifest. For Claude-side
|
|
142
|
+
// plugins prefer plugins/<name>/.claude-plugin/plugin.json; fall back to
|
|
143
|
+
// .codex-plugin/plugin.json for codex-only plugins (e.g.
|
|
144
|
+
// session-instructions-loader). External-marketplace plugins (skill-creator,
|
|
145
|
+
// claude-md-management, codex) have no local manifest — they install from
|
|
146
|
+
// their upstream marketplace, so we deliberately leave expectedVersion
|
|
147
|
+
// undefined. The scanner then falls through to "trust whatever is installed",
|
|
148
|
+
// which matches what the user agreed to when they registered the upstream.
|
|
149
|
+
const expectedVersion = await readPluginManifestVersion(packageRoot, entry.name);
|
|
150
|
+
plugins[entry.name] = {
|
|
151
|
+
description: entry.description,
|
|
152
|
+
agents,
|
|
153
|
+
...(expectedVersion ? { expectedVersion } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// Hooks: the scanner reads <scope>/.claude/settings.json and matches by
|
|
157
|
+
// `_marker` (see state.ts scanHooks). Drift detection compares the
|
|
158
|
+
// registered event / matcher / if values against the hook's canonical
|
|
159
|
+
// settingsEvents[0] from .claude/hooks/hooks.json. We deliberately do NOT
|
|
160
|
+
// hash index.mjs — the user's installed index.mjs lives at <scope>/.claude/
|
|
161
|
+
// hooks/<name>/index.mjs and isn't part of the settings.json drift signal.
|
|
162
|
+
const hooksJsonPath = path.join(packageRoot, ".claude", "hooks", "hooks.json");
|
|
163
|
+
const hooksJsonRaw = await tryReadFile(hooksJsonPath);
|
|
164
|
+
const hooksJson = hooksJsonRaw ? JSON.parse(hooksJsonRaw) : {};
|
|
165
|
+
const settingsEventsByName = new Map();
|
|
166
|
+
for (const h of hooksJson.hooks ?? []) {
|
|
167
|
+
if (typeof h.name === "string" && h.settingsEvents?.length) {
|
|
168
|
+
settingsEventsByName.set(h.name, h.settingsEvents[0]);
|
|
169
|
+
}
|
|
115
170
|
}
|
|
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
171
|
const hooks = {};
|
|
120
172
|
for (const entry of dist.hooks) {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
expectedHash
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
173
|
+
const ev = settingsEventsByName.get(entry.name);
|
|
174
|
+
hooks[entry.name] = {
|
|
175
|
+
description: entry.description,
|
|
176
|
+
// Empty expectedHash flips state.ts into wildcard mode — drift judged
|
|
177
|
+
// purely from the structured expected* fields below, not from a
|
|
178
|
+
// settings-entry content hash.
|
|
179
|
+
expectedHash: "",
|
|
180
|
+
...(typeof ev?.event === "string" ? { expectedEvent: ev.event } : {}),
|
|
181
|
+
...(typeof ev?.matcher === "string" ? { expectedMatcher: ev.matcher } : {}),
|
|
182
|
+
...(typeof ev?.if === "string" ? { expectedIf: ev.if } : {}),
|
|
183
|
+
};
|
|
130
184
|
}
|
|
131
185
|
return {
|
|
132
186
|
workflowVersion,
|
package/dist/server.js
CHANGED
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Public contract is anchored in docs/architecture/web-ui.md §4 (server
|
|
9
9
|
// surface), §6 (data flow + types), §7 (errors).
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
10
11
|
import { createServer } from "node:http";
|
|
11
12
|
import { Buffer } from "node:buffer";
|
|
12
13
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
13
14
|
import { readFile } from "node:fs/promises";
|
|
14
15
|
import path from "node:path";
|
|
15
16
|
import { buildScanCatalog } from "./scan-catalog.js";
|
|
16
|
-
import { scanState } from "./state.js";
|
|
17
|
+
import { defaultExecPluginList, scanState } from "./state.js";
|
|
17
18
|
// Body parsing cap. /api/apply payloads are tiny (an array of item refs);
|
|
18
19
|
// 1 MiB is generously above the largest realistic batch and small enough that
|
|
19
20
|
// abusive clients can't pin memory.
|
|
@@ -576,7 +577,7 @@ export async function startServer(opts) {
|
|
|
576
577
|
return;
|
|
577
578
|
}
|
|
578
579
|
if (pathname === "/api/state" && method === "GET") {
|
|
579
|
-
await routeState(opts.cwd, opts.packageRoot ?? opts.cwd, res);
|
|
580
|
+
await routeState(opts.cwd, opts.packageRoot ?? opts.cwd, searchParams, res);
|
|
580
581
|
return;
|
|
581
582
|
}
|
|
582
583
|
if (pathname === "/api/apply" && method === "POST") {
|
|
@@ -705,10 +706,21 @@ export async function startServer(opts) {
|
|
|
705
706
|
// ---------------------------------------------------------------------------
|
|
706
707
|
// Route: GET /api/state
|
|
707
708
|
// ---------------------------------------------------------------------------
|
|
708
|
-
async function routeState(cwd, packageRoot, res) {
|
|
709
|
+
async function routeState(cwd, packageRoot, searchParams, res) {
|
|
709
710
|
try {
|
|
710
711
|
const catalog = await buildScanCatalog(packageRoot);
|
|
711
|
-
const
|
|
712
|
+
const scopes = parseScopesParam(searchParams);
|
|
713
|
+
// Only wire `defaultExecPluginList` if the `claude` CLI is on PATH —
|
|
714
|
+
// otherwise the scanner's degraded-path warning ("claude-cli-missing")
|
|
715
|
+
// is what we want the UI to surface. Cache the result per process so we
|
|
716
|
+
// don't re-`which` on every /api/state poll.
|
|
717
|
+
const execPluginList = isClaudeOnPath()
|
|
718
|
+
? (scope) => defaultExecPluginList(scope, cwd)
|
|
719
|
+
: undefined;
|
|
720
|
+
const report = await scanState(cwd, catalog, {
|
|
721
|
+
...(scopes ? { scopes } : {}),
|
|
722
|
+
...(execPluginList ? { execPluginList } : {}),
|
|
723
|
+
});
|
|
712
724
|
sendJson(res, 200, report);
|
|
713
725
|
}
|
|
714
726
|
catch {
|
|
@@ -717,6 +729,40 @@ async function routeState(cwd, packageRoot, res) {
|
|
|
717
729
|
sendJson(res, 500, { error: "scan-failed" });
|
|
718
730
|
}
|
|
719
731
|
}
|
|
732
|
+
let claudeOnPathCache = null;
|
|
733
|
+
function isClaudeOnPath() {
|
|
734
|
+
if (claudeOnPathCache !== null)
|
|
735
|
+
return claudeOnPathCache;
|
|
736
|
+
const res = spawnSync("which", ["claude"], { stdio: "ignore" });
|
|
737
|
+
claudeOnPathCache = res.status === 0;
|
|
738
|
+
return claudeOnPathCache;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Parses the /api/state `scopes` query into a per-category map for
|
|
742
|
+
* `scanState`. The query string shape is comma-separated `category:scope`
|
|
743
|
+
* pairs, e.g. `?scopes=workflow:user,skills:project,plugins:user`. Unknown
|
|
744
|
+
* categories and unknown scope values are dropped (rather than throwing) so
|
|
745
|
+
* the scanner falls back to install defaults on garbled input — keeps the
|
|
746
|
+
* Web UI degraded-but-functional rather than 500-ing on a stale link.
|
|
747
|
+
*
|
|
748
|
+
* Returns `null` when the param is absent so the caller can omit `opts.scopes`
|
|
749
|
+
* entirely and let `scanState` apply its built-in defaults.
|
|
750
|
+
*/
|
|
751
|
+
function parseScopesParam(searchParams) {
|
|
752
|
+
const raw = searchParams.get("scopes");
|
|
753
|
+
if (!raw)
|
|
754
|
+
return null;
|
|
755
|
+
const allowedCategories = new Set(["workflow", "skills", "plugins", "hooks"]);
|
|
756
|
+
const allowedScopes = new Set(["user", "project"]);
|
|
757
|
+
const out = {};
|
|
758
|
+
for (const pair of raw.split(",")) {
|
|
759
|
+
const [cat, scope] = pair.split(":");
|
|
760
|
+
if (allowedCategories.has(cat) && allowedScopes.has(scope)) {
|
|
761
|
+
out[cat] = scope;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
765
|
+
}
|
|
720
766
|
// ---------------------------------------------------------------------------
|
|
721
767
|
// Route: GET /api/catalog
|
|
722
768
|
// ---------------------------------------------------------------------------
|
package/dist/state.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PluginState, StateReport } from "./api-types.js";
|
|
1
|
+
import type { PluginState, ScanScope, StateReport } from "./api-types.js";
|
|
2
2
|
export interface Catalog {
|
|
3
3
|
workflowVersion: string;
|
|
4
4
|
skills: Record<string, {
|
|
@@ -18,16 +18,51 @@ export interface Catalog {
|
|
|
18
18
|
}>;
|
|
19
19
|
hooks: Record<string, {
|
|
20
20
|
description: string;
|
|
21
|
+
/** Coarse drift signal. The current production scan-catalog still
|
|
22
|
+
* populates this with sha256(index.mjs) for back-compat with the v0.x
|
|
23
|
+
* scanner that hashed the user's installed script. The new
|
|
24
|
+
* settings.json-based scanner ignores it for drift comparison unless
|
|
25
|
+
* the catalog also exposes the structured expected* fields below. */
|
|
21
26
|
expectedHash: string;
|
|
27
|
+
/** Settings.json event name (e.g. "PostToolUse", "Notification"). When
|
|
28
|
+
* set, the scanner flags drift if the on-disk settings entry registers
|
|
29
|
+
* under a different event. Optional; left undefined the scanner trusts
|
|
30
|
+
* whatever event the marker was found under. */
|
|
31
|
+
expectedEvent?: string;
|
|
32
|
+
/** Settings.json `matcher` field (e.g. "Write|Edit" for PostToolUse).
|
|
33
|
+
* Empty string means "no matcher" (e.g. Notification hooks). When set,
|
|
34
|
+
* the scanner flags drift if the on-disk value differs. */
|
|
35
|
+
expectedMatcher?: string;
|
|
36
|
+
/** Settings.json `if` field (Claude-Code-specific filter expression).
|
|
37
|
+
* Same drift semantics as expectedMatcher. */
|
|
38
|
+
expectedIf?: string;
|
|
22
39
|
}>;
|
|
23
40
|
}
|
|
24
41
|
export interface ScanOptions {
|
|
25
|
-
|
|
42
|
+
/** Run `claude plugins list` for the given scope. The scope argument is
|
|
43
|
+
* required so server.ts can pass --user / --project through to the CLI
|
|
44
|
+
* per opts.scopes.plugins. Implementations may accept a zero-arg legacy
|
|
45
|
+
* form for back-compat but MUST honor a scope argument when given. */
|
|
46
|
+
execPluginList?: (scope: ScanScope) => Promise<{
|
|
26
47
|
installed: any[];
|
|
27
48
|
available: any[];
|
|
28
49
|
}>;
|
|
29
50
|
readCodexConfig?: () => Promise<string | null>;
|
|
30
51
|
readCodexPluginsDir?: () => Promise<Map<string, string>>;
|
|
52
|
+
/** Per-category scope picker. Each field is independently routed to the
|
|
53
|
+
* right truth source. Defaults match the Web UI's per-column picker:
|
|
54
|
+
* workflow = 'project', skills = 'project',
|
|
55
|
+
* plugins = 'user', hooks = 'user'. */
|
|
56
|
+
scopes?: {
|
|
57
|
+
workflow?: ScanScope;
|
|
58
|
+
skills?: ScanScope;
|
|
59
|
+
plugins?: ScanScope;
|
|
60
|
+
hooks?: ScanScope;
|
|
61
|
+
};
|
|
62
|
+
/** Test-time HOME override. When unset the scanner reads os.homedir()
|
|
63
|
+
* (which itself consults process.env.HOME / USERPROFILE), so tests that
|
|
64
|
+
* redirect HOME via env vars also work. */
|
|
65
|
+
homeDir?: string;
|
|
31
66
|
}
|
|
32
67
|
export declare function scanState(projectRoot: string, catalog: Catalog, opts?: ScanOptions): Promise<StateReport>;
|
|
33
68
|
/**
|
|
@@ -48,10 +83,12 @@ export declare function scanState(projectRoot: string, catalog: Catalog, opts?:
|
|
|
48
83
|
* we'll need a deliberate merge policy.
|
|
49
84
|
*/
|
|
50
85
|
export declare function mergePluginsById(records: PluginState[]): PluginState[];
|
|
51
|
-
/** Default: run `claude plugins list --json`
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
86
|
+
/** Default: run `claude plugins list --json` (no scope flag — the CLI
|
|
87
|
+
* doesn't expose one) plus the `--available` variant, then filter the
|
|
88
|
+
* installed records to the requested scope (and current projectRoot for
|
|
89
|
+
* project-scope) client-side. Server.ts decides whether to pass this
|
|
90
|
+
* function based on `which claude`. */
|
|
91
|
+
export declare function defaultExecPluginList(scope?: ScanScope, projectRoot?: string): Promise<{
|
|
55
92
|
installed: any[];
|
|
56
93
|
available: any[];
|
|
57
94
|
}>;
|