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/README.md
CHANGED
|
@@ -50,6 +50,18 @@ npx -y auriga-cli --help # full catalog + flags
|
|
|
50
50
|
|
|
51
51
|
Exit codes: `0` success, `1` fatal (precheck / parse / fetch), `2` partial success — `stderr` lists per-category `[OK]/[FAIL]` and a `Retry:` hint. After install, reload the Claude Code or Codex session so the new `CLAUDE.md` / skills / plugins / hook registrations are picked up.
|
|
52
52
|
|
|
53
|
+
### Web UI (opt-in)
|
|
54
|
+
|
|
55
|
+
For a browser-based view of what's installed and one-click apply, run:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx auriga-cli web-ui
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This boots a local server on `127.0.0.1`, opens your default browser, and serves a dashboard that scans the current project, shows each module's status (installed / update-available / not-installed), and applies install / update / uninstall in a queue with live SSE progress. The server shuts down on its own ~15 s after the browser closes.
|
|
62
|
+
|
|
63
|
+
The UI is opt-in — `npx auriga-cli` still launches the TTY menu below.
|
|
64
|
+
|
|
53
65
|
### Interactive menu
|
|
54
66
|
|
|
55
67
|
```bash
|
|
@@ -99,6 +111,7 @@ Installs selected skills via `npx skills add`, targeting both Claude Code and Co
|
|
|
99
111
|
| Skill | Source | Description |
|
|
100
112
|
|---|---|---|
|
|
101
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
|
+
| code-simplification | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | Refactor for clarity without changing behavior — trim accumulated complexity |
|
|
102
115
|
| codex-agent | [Ben2pc/g-claude-code-plugins](https://github.com/Ben2pc/g-claude-code-plugins) | Delegate to Codex sessions for cross-model coverage |
|
|
103
116
|
| 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 |
|
|
104
117
|
| frontend-design | [anthropics/skills](https://github.com/anthropics/skills) | Distinctive, production-grade frontend UI generation that avoids generic AI aesthetics |
|
package/README.zh-CN.md
CHANGED
|
@@ -50,6 +50,18 @@ npx -y auriga-cli --help # 完整 catalog + flag 说明
|
|
|
50
50
|
|
|
51
51
|
退出码:`0` 成功;`1` 致命错误(前置检查 / 解析 / 拉取失败);`2` 部分成功——`stderr` 会列出逐类 `[OK]/[FAIL]` 和 `Retry:` 提示。装完后请重启 Claude Code 或 Codex 会话,让新的 `CLAUDE.md` / skills / plugins / hook 注册生效。
|
|
52
52
|
|
|
53
|
+
### Web UI(可选)
|
|
54
|
+
|
|
55
|
+
如果想在浏览器里看到“已装 / 可更新 / 未装”全景并一键 apply,跑:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx auriga-cli web-ui
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
它会在 `127.0.0.1` 起一个本地 server、自动开浏览器,扫描当前项目并展示 5 个分类的状态。勾选要安装/更新/卸载的项目后点 Apply,SSE 实时回传执行进度。关浏览器后约 15 秒 server 自动退出。
|
|
62
|
+
|
|
63
|
+
Web UI 是显式入口;`npx auriga-cli` 仍然走下面的 TTY 菜单。
|
|
64
|
+
|
|
53
65
|
### 交互式菜单
|
|
54
66
|
|
|
55
67
|
```bash
|
|
@@ -99,6 +111,7 @@ npx auriga-cli
|
|
|
99
111
|
| Skill | 来源 | 说明 |
|
|
100
112
|
|---|---|---|
|
|
101
113
|
| claude-code-agent | [Ben2pc/g-claude-code-plugins](https://github.com/Ben2pc/g-claude-code-plugins) | 通过 Claude Code Agent SDK 把任务委派给独立 Claude Code 会话 |
|
|
114
|
+
| code-simplification | [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | 不改变行为前提下重构代码以提升可读性 —— 清掉累积的不必要复杂度 |
|
|
102
115
|
| codex-agent | [Ben2pc/g-claude-code-plugins](https://github.com/Ben2pc/g-claude-code-plugins) | 委派给 Codex 会话,做跨模型覆盖 |
|
|
103
116
|
| design-taste-frontend | [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill) | 高阶 UI/UX 工程师 —— 度量化设计规则与严格的组件架构约束 |
|
|
104
117
|
| frontend-design | [anthropics/skills](https://github.com/anthropics/skills) | 生成有辨识度、production 级的前端界面,避开常见 AI 同质化美学 |
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export type ItemStatus = "installed" | "update-available" | "not-installed";
|
|
2
|
+
export interface StateReport {
|
|
3
|
+
/** Absolute path to the project the server was launched against. Surfaced
|
|
4
|
+
* in the UI's top bar so users know where Apply will write project-scope
|
|
5
|
+
* changes. The path may be redacted (e.g. `~/...` home expansion) but the
|
|
6
|
+
* contract is "human-readable identifier for the current cwd". */
|
|
7
|
+
cwd: string;
|
|
8
|
+
workflow: WorkflowState;
|
|
9
|
+
skills: SkillState[];
|
|
10
|
+
recommendedSkills: SkillState[];
|
|
11
|
+
plugins: PluginState[];
|
|
12
|
+
hooks: HookState[];
|
|
13
|
+
warnings: StateWarning[];
|
|
14
|
+
}
|
|
15
|
+
export interface WorkflowState {
|
|
16
|
+
status: ItemStatus;
|
|
17
|
+
currentVersion?: string;
|
|
18
|
+
expectedVersion: string;
|
|
19
|
+
}
|
|
20
|
+
export interface SkillState {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
status: ItemStatus;
|
|
24
|
+
isWorkflow: boolean;
|
|
25
|
+
currentHash?: string;
|
|
26
|
+
expectedHash: string;
|
|
27
|
+
}
|
|
28
|
+
export type ApplyAgent = "claude" | "codex";
|
|
29
|
+
export interface PluginState {
|
|
30
|
+
id: string;
|
|
31
|
+
description: string;
|
|
32
|
+
status: ItemStatus;
|
|
33
|
+
/** Which Agent runtimes this plugin can install into. Most plugins target
|
|
34
|
+
* a single agent; dual-Agent plugins (e.g. auriga-go) have both. When
|
|
35
|
+
* `agents.length === 2` the UI shows a BOTH badge and Apply installs to
|
|
36
|
+
* each agent in turn. Status is aggregated across all targeted agents:
|
|
37
|
+
* `installed` ⇔ all agents installed; `not-installed` ⇔ all not-installed;
|
|
38
|
+
* any partial state (one side installed, other not) → `update-available`
|
|
39
|
+
* so a single Apply backfills the missing side. */
|
|
40
|
+
agents: ApplyAgent[];
|
|
41
|
+
currentVersion?: string;
|
|
42
|
+
expectedVersion?: string;
|
|
43
|
+
versionSource: "upstream-live" | "catalog";
|
|
44
|
+
}
|
|
45
|
+
export interface HookState {
|
|
46
|
+
name: string;
|
|
47
|
+
description: string;
|
|
48
|
+
status: ItemStatus;
|
|
49
|
+
currentHash?: string;
|
|
50
|
+
expectedHash: string;
|
|
51
|
+
}
|
|
52
|
+
export interface StateWarning {
|
|
53
|
+
code: "claude-cli-missing" | "codex-cli-missing" | "marketplace-offline";
|
|
54
|
+
message: string;
|
|
55
|
+
}
|
|
56
|
+
export type ApplyCategory = "workflow" | "skill" | "recommended-skill" | "plugin" | "hook";
|
|
57
|
+
export type ApplyAction = "install" | "update" | "uninstall";
|
|
58
|
+
/**
|
|
59
|
+
* Installer scope. Carried per-item so the Web UI can mix scopes within a
|
|
60
|
+
* single apply batch.
|
|
61
|
+
*
|
|
62
|
+
* - workflow: no scope; field MUST be omitted.
|
|
63
|
+
* - skill / recommended-skill / plugin: "project" | "user". Default project.
|
|
64
|
+
* - hook: "project" | "user" for v0.1 (project-local deferred to v0.2).
|
|
65
|
+
*/
|
|
66
|
+
export type ApplyScope = "project" | "user";
|
|
67
|
+
/**
|
|
68
|
+
* Workflow CLAUDE.md language variant.
|
|
69
|
+
*
|
|
70
|
+
* - "en": English CLAUDE.md (the default).
|
|
71
|
+
* - "zh-CN": Simplified Chinese CLAUDE.md (the localized variant).
|
|
72
|
+
*
|
|
73
|
+
* Only meaningful for `category === "workflow"`; rejected for other
|
|
74
|
+
* categories so the API surface stays explicit.
|
|
75
|
+
*/
|
|
76
|
+
export type ApplyLang = "en" | "zh-CN";
|
|
77
|
+
export interface ApplyItemRef {
|
|
78
|
+
category: ApplyCategory;
|
|
79
|
+
name: string;
|
|
80
|
+
action: ApplyAction;
|
|
81
|
+
/** Installer scope. Omitted = "project" (back-compat default). The server
|
|
82
|
+
* rejects this field for category="workflow" because workflow has no
|
|
83
|
+
* scope concept (it's a single file at the project root). */
|
|
84
|
+
scope?: ApplyScope;
|
|
85
|
+
/** Workflow CLAUDE.md language variant. Omitted = "en" (back-compat
|
|
86
|
+
* default). The server rejects this field for any non-workflow
|
|
87
|
+
* category. */
|
|
88
|
+
lang?: ApplyLang;
|
|
89
|
+
}
|
|
90
|
+
export interface ApplyRequest {
|
|
91
|
+
items: ApplyItemRef[];
|
|
92
|
+
}
|
|
93
|
+
export interface ApplyResponse {
|
|
94
|
+
jobId: string;
|
|
95
|
+
}
|
|
96
|
+
export type ProgressEvent = {
|
|
97
|
+
type: "item:start";
|
|
98
|
+
index: number;
|
|
99
|
+
total: number;
|
|
100
|
+
item: ApplyItemRef;
|
|
101
|
+
} | {
|
|
102
|
+
type: "item:log";
|
|
103
|
+
index: number;
|
|
104
|
+
line: string;
|
|
105
|
+
level: "info" | "warn" | "error";
|
|
106
|
+
} | {
|
|
107
|
+
type: "item:done";
|
|
108
|
+
index: number;
|
|
109
|
+
success: boolean;
|
|
110
|
+
error?: string;
|
|
111
|
+
} | {
|
|
112
|
+
type: "all-done";
|
|
113
|
+
success: boolean;
|
|
114
|
+
failedCount: number;
|
|
115
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ApplyLang } from "./api-types.js";
|
|
2
|
+
import type { ApplyHandlers } from "./server.js";
|
|
3
|
+
export interface ApplyHandlerContext {
|
|
4
|
+
/** Where auriga-cli lives — source of skills-lock.json + .claude config. */
|
|
5
|
+
packageRoot: string;
|
|
6
|
+
/** Target project directory. Installers write here. */
|
|
7
|
+
cwd: string;
|
|
8
|
+
/** Resolves plugin name → the list of agents this plugin can install
|
|
9
|
+
* into. Built at boot from the same scan catalog the /api/state route
|
|
10
|
+
* uses. dual-Agent plugins (e.g. `auriga-go`) yield `["claude","codex"]`
|
|
11
|
+
* and the handler iterates the list, installing to each agent in turn.
|
|
12
|
+
* Names not in the map default to `["claude"]` (existing CLI default). */
|
|
13
|
+
pluginAgentsByName: Map<string, ("claude" | "codex")[]>;
|
|
14
|
+
/** Workflow language for install. Defaults to "en". */
|
|
15
|
+
workflowLang?: ApplyLang;
|
|
16
|
+
}
|
|
17
|
+
export declare function buildDefaultApplyHandlers(ctx: ApplyHandlerContext): ApplyHandlers;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Builds the default ApplyHandlers map that wires the Web UI's /api/apply
|
|
2
|
+
// route to the real installers in workflow.ts / skills.ts / plugins.ts /
|
|
3
|
+
// hooks.ts. Tests inject their own mock handlers; CLI mode (the `web-ui`
|
|
4
|
+
// subcommand) calls `buildDefaultApplyHandlers` at boot.
|
|
5
|
+
//
|
|
6
|
+
// Per-item dispatch is layered on top of the existing bulk installers via
|
|
7
|
+
// `opts.selected = [name]`. The recipe per category:
|
|
8
|
+
//
|
|
9
|
+
// workflow: installWorkflow(packageRoot, …) ─┐
|
|
10
|
+
// workflow: uninstallWorkflow({force:true,…})│ no name needed
|
|
11
|
+
// skill: installSkills(…, selected:[name])│
|
|
12
|
+
// skill: uninstallSkill(name, …) │
|
|
13
|
+
// recommended-skill: installRecommendedSkills(…) │
|
|
14
|
+
// recommended-skill: uninstallSkill(name, …) │ same store
|
|
15
|
+
// plugin: installPlugins(…, agent, sel:[]) │ per-name
|
|
16
|
+
// plugin: uninstallPlugin(name, agent, …) │
|
|
17
|
+
// hook: installHook(hookDef, "project",…)│ needs HookDef
|
|
18
|
+
// hook: uninstallHook(name, …) │
|
|
19
|
+
//
|
|
20
|
+
// Spec: docs/architecture/web-ui.md §6.4 (apply execution model).
|
|
21
|
+
import { installHook, loadHooksConfig, uninstallHook } from "./hooks.js";
|
|
22
|
+
import { installPlugins, uninstallPlugin, } from "./plugins.js";
|
|
23
|
+
import { installRecommendedSkills, installSkills, uninstallSkill, } from "./skills.js";
|
|
24
|
+
import { installWorkflow, uninstallWorkflow } from "./workflow.js";
|
|
25
|
+
const ALL_ACTIONS = new Set([
|
|
26
|
+
"install",
|
|
27
|
+
"update",
|
|
28
|
+
"uninstall",
|
|
29
|
+
]);
|
|
30
|
+
function assertAction(action) {
|
|
31
|
+
// Defense in depth — the server validates the shape already, but the
|
|
32
|
+
// adapter must not silently fall through if a new action is added later.
|
|
33
|
+
if (!ALL_ACTIONS.has(action)) {
|
|
34
|
+
throw new Error(`unsupported action: ${action}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function buildDefaultApplyHandlers(ctx) {
|
|
38
|
+
const { packageRoot, cwd, pluginAgentsByName } = ctx;
|
|
39
|
+
const lang = ctx.workflowLang ?? "en";
|
|
40
|
+
const workflow = async (action, _name, { onLog, lang: requestedLang }) => {
|
|
41
|
+
assertAction(action);
|
|
42
|
+
// Per-item lang overrides the ctx default (the UI now drives this via
|
|
43
|
+
// the Workflow column's EN/ZH-CN picker). Falls back to ctx lang for
|
|
44
|
+
// CLI-mode callers that don't pass it.
|
|
45
|
+
const installLang = requestedLang ?? lang;
|
|
46
|
+
if (action === "install" || action === "update") {
|
|
47
|
+
await installWorkflow(packageRoot, {
|
|
48
|
+
interactive: false,
|
|
49
|
+
cwd,
|
|
50
|
+
lang: installLang,
|
|
51
|
+
});
|
|
52
|
+
onLog(`workflow installed (${installLang})`, "info");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// uninstall
|
|
56
|
+
await uninstallWorkflow({
|
|
57
|
+
force: true,
|
|
58
|
+
cwd,
|
|
59
|
+
onLog: (line) => onLog(line, "info"),
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
// Adapter from InstallOpts.onLog (stdout|stderr) → handler's onLog
|
|
63
|
+
// (info|warn|error). stderr lines surface as warnings; the SSE consumer
|
|
64
|
+
// can decide whether to render them differently.
|
|
65
|
+
const streamLog = (onLog) => (line, stream) => {
|
|
66
|
+
onLog(line, stream === "stderr" ? "warn" : "info");
|
|
67
|
+
};
|
|
68
|
+
const skill = async (action, name, { onLog, scope }) => {
|
|
69
|
+
assertAction(action);
|
|
70
|
+
const installScope = scope ?? "project";
|
|
71
|
+
if (action === "install" || action === "update") {
|
|
72
|
+
await installSkills(packageRoot, {
|
|
73
|
+
interactive: false,
|
|
74
|
+
cwd,
|
|
75
|
+
selected: [name],
|
|
76
|
+
scope: installScope,
|
|
77
|
+
onLog: streamLog(onLog),
|
|
78
|
+
});
|
|
79
|
+
onLog(`skill ${name} installed (${installScope})`, "info");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
await uninstallSkill(name, {
|
|
83
|
+
cwd,
|
|
84
|
+
scope: installScope,
|
|
85
|
+
onLog: (line) => onLog(line, "info"),
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const recommendedSkill = async (action, name, { onLog, scope }) => {
|
|
89
|
+
assertAction(action);
|
|
90
|
+
const installScope = scope ?? "project";
|
|
91
|
+
if (action === "install" || action === "update") {
|
|
92
|
+
await installRecommendedSkills(packageRoot, {
|
|
93
|
+
interactive: false,
|
|
94
|
+
cwd,
|
|
95
|
+
selected: [name],
|
|
96
|
+
scope: installScope,
|
|
97
|
+
onLog: streamLog(onLog),
|
|
98
|
+
});
|
|
99
|
+
onLog(`recommended skill ${name} installed (${installScope})`, "info");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Uninstall path is the same regardless of workflow vs recommended —
|
|
103
|
+
// both live in skills-lock.json + .claude/skills/<name>.
|
|
104
|
+
await uninstallSkill(name, {
|
|
105
|
+
cwd,
|
|
106
|
+
scope: installScope,
|
|
107
|
+
onLog: (line) => onLog(line, "info"),
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
const plugin = async (action, name, { onLog, scope }) => {
|
|
111
|
+
assertAction(action);
|
|
112
|
+
const agents = pluginAgentsByName.get(name) ?? ["claude"];
|
|
113
|
+
const installScope = scope ?? "project";
|
|
114
|
+
// dual-Agent plugins (length === 2) install into each agent in turn.
|
|
115
|
+
// Per-agent try/catch: if Claude install fails, still try Codex so
|
|
116
|
+
// partial coverage is visible to the user. Failures are aggregated
|
|
117
|
+
// and thrown at the end → SSE marks the item failed but onLog
|
|
118
|
+
// shows both agent outcomes.
|
|
119
|
+
const failures = [];
|
|
120
|
+
for (const agent of agents) {
|
|
121
|
+
try {
|
|
122
|
+
if (action === "install" || action === "update") {
|
|
123
|
+
await installPlugins(packageRoot, {
|
|
124
|
+
interactive: false,
|
|
125
|
+
cwd,
|
|
126
|
+
agent,
|
|
127
|
+
selected: [name],
|
|
128
|
+
scope: installScope,
|
|
129
|
+
onLog: streamLog(onLog),
|
|
130
|
+
});
|
|
131
|
+
onLog(`plugin ${name} installed (${agent}, ${installScope})`, "info");
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
await uninstallPlugin(name, agent, {
|
|
135
|
+
cwd,
|
|
136
|
+
onLog: (line) => onLog(`[${agent}] ${line}`, "info"),
|
|
137
|
+
});
|
|
138
|
+
onLog(`plugin ${name} uninstalled (${agent})`, "info");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
143
|
+
failures.push({ agent, error });
|
|
144
|
+
onLog(`plugin ${name} ${action} failed (${agent}): ${error.message}`, "error");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (failures.length > 0) {
|
|
148
|
+
const summary = failures
|
|
149
|
+
.map((f) => `${f.agent}: ${f.error.message}`)
|
|
150
|
+
.join("; ");
|
|
151
|
+
throw new Error(`plugin ${name} ${action} failed for ${failures.length} agent(s) — ${summary}`);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const hook = async (action, name, { onLog, scope }) => {
|
|
155
|
+
assertAction(action);
|
|
156
|
+
const installScope = scope ?? "project";
|
|
157
|
+
if (action === "uninstall") {
|
|
158
|
+
await uninstallHook(name, {
|
|
159
|
+
cwd,
|
|
160
|
+
scope: installScope,
|
|
161
|
+
onLog: (line) => onLog(line, "info"),
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// install + update both run installHook with the requested scope. The
|
|
166
|
+
// hook installer is idempotent — re-running == "update". Look up the
|
|
167
|
+
// HookDef in the bundled registry; unknown name → loud throw so the
|
|
168
|
+
// SSE caller surfaces it as item:done success=false.
|
|
169
|
+
const config = loadHooksConfig(packageRoot);
|
|
170
|
+
const hookDef = config.hooks.find((h) => h.name === name);
|
|
171
|
+
if (!hookDef) {
|
|
172
|
+
throw new Error(`hook not found in registry: ${name}`);
|
|
173
|
+
}
|
|
174
|
+
// installHook takes a wider Scope union ("project"|"user"|"project-local");
|
|
175
|
+
// v0.1 only exposes "project"|"user" via the API. Cast is safe.
|
|
176
|
+
await installHook(hookDef, scope ?? "project", cwd, packageRoot);
|
|
177
|
+
onLog(`hook ${name} installed (${scope ?? "project"})`, "info");
|
|
178
|
+
};
|
|
179
|
+
return {
|
|
180
|
+
workflow,
|
|
181
|
+
skill,
|
|
182
|
+
"recommended-skill": recommendedSkill,
|
|
183
|
+
plugin,
|
|
184
|
+
hook,
|
|
185
|
+
};
|
|
186
|
+
}
|
package/dist/catalog.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-05-
|
|
2
|
+
"generatedAt": "2026-05-12T10:18:53.130Z",
|
|
3
3
|
"workflowSkills": [
|
|
4
4
|
{
|
|
5
5
|
"name": "brainstorming",
|
|
@@ -43,6 +43,10 @@
|
|
|
43
43
|
"name": "claude-code-agent",
|
|
44
44
|
"description": "Delegate coding, review, diagnosis, planning, and structured-output tasks to an independent Claude Code session via `claude -p` (Agent SDK)."
|
|
45
45
|
},
|
|
46
|
+
{
|
|
47
|
+
"name": "code-simplification",
|
|
48
|
+
"description": "Simplifies code for clarity. Use when refactoring code for clarity without changing behavior. Use when code works but is harder to read, maintain, or extend than it should be. Use when reviewing code that has accumulated unnecessary complexity."
|
|
49
|
+
},
|
|
46
50
|
{
|
|
47
51
|
"name": "codex-agent",
|
|
48
52
|
"description": "Delegate coding, review, diagnosis, planning, and browser tasks to an independent Codex session via `codex exec` / resume / review."
|
package/dist/cli.d.ts
CHANGED
|
@@ -11,6 +11,16 @@ export interface InstallParsed {
|
|
|
11
11
|
scope?: "project" | "user";
|
|
12
12
|
agent?: PluginAgent;
|
|
13
13
|
}
|
|
14
|
+
export interface UiParsed {
|
|
15
|
+
/** Override the default port (4747). When set, the fallback range is
|
|
16
|
+
* also skipped — we either succeed on this port or fail. */
|
|
17
|
+
port?: number;
|
|
18
|
+
/** Override ui-fetch; serve from this local directory instead. Useful
|
|
19
|
+
* for development against `ui/dist` without a release tag. */
|
|
20
|
+
uiDir?: string;
|
|
21
|
+
/** Skip opening the browser. Prints the URL only. */
|
|
22
|
+
noOpen?: boolean;
|
|
23
|
+
}
|
|
14
24
|
export type ParsedArgs = {
|
|
15
25
|
command: "help";
|
|
16
26
|
helpType?: CategoryName;
|
|
@@ -21,6 +31,9 @@ export type ParsedArgs = {
|
|
|
21
31
|
} | {
|
|
22
32
|
command: "install";
|
|
23
33
|
install: InstallParsed;
|
|
34
|
+
} | {
|
|
35
|
+
command: "web-ui";
|
|
36
|
+
ui: UiParsed;
|
|
24
37
|
};
|
|
25
38
|
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
26
39
|
export declare function main(argv: string[]): Promise<number>;
|
package/dist/cli.js
CHANGED
|
@@ -102,6 +102,16 @@ export function parseArgs(argv) {
|
|
|
102
102
|
}
|
|
103
103
|
return { command: "guide" };
|
|
104
104
|
}
|
|
105
|
+
if (head === "web-ui") {
|
|
106
|
+
try {
|
|
107
|
+
return { command: "web-ui", ui: parseUi(argv.slice(1)) };
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
if (e instanceof PerTypeHelpRequest)
|
|
111
|
+
return { command: "help" };
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
105
115
|
if (head !== "install") {
|
|
106
116
|
parseErr(`unknown argument '${head}'. Run 'npx auriga-cli --help' for usage.`);
|
|
107
117
|
}
|
|
@@ -115,6 +125,43 @@ export function parseArgs(argv) {
|
|
|
115
125
|
throw e;
|
|
116
126
|
}
|
|
117
127
|
}
|
|
128
|
+
function parseUi(argv) {
|
|
129
|
+
const out = {};
|
|
130
|
+
let i = 0;
|
|
131
|
+
while (i < argv.length) {
|
|
132
|
+
const t = argv[i];
|
|
133
|
+
if (t === "--help" || t === "-h") {
|
|
134
|
+
// ui --help routes to the top-level help — keeps the parser narrow
|
|
135
|
+
// and avoids a per-command help renderer for one subcommand.
|
|
136
|
+
throw new PerTypeHelpRequest(undefined);
|
|
137
|
+
}
|
|
138
|
+
if (t === "--port" || t.startsWith("--port=")) {
|
|
139
|
+
const [v, adv] = readSingleValue(argv, i, "--port");
|
|
140
|
+
const n = Number.parseInt(v, 10);
|
|
141
|
+
// 0 is a deliberate "OS-assigned ephemeral port" affordance used by
|
|
142
|
+
// hermetic e2e tests (spec §8.1). Real users pass a normal port.
|
|
143
|
+
if (!Number.isFinite(n) || n < 0 || n > 65535) {
|
|
144
|
+
parseErr(`--port must be a port number in [0, 65535], got '${v}'`);
|
|
145
|
+
}
|
|
146
|
+
out.port = n;
|
|
147
|
+
i += adv;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (t === "--ui-dir" || t.startsWith("--ui-dir=")) {
|
|
151
|
+
const [v, adv] = readSingleValue(argv, i, "--ui-dir");
|
|
152
|
+
out.uiDir = v;
|
|
153
|
+
i += adv;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (t === "--no-open") {
|
|
157
|
+
out.noOpen = true;
|
|
158
|
+
i += 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
parseErr(`unknown argument '${t}' for 'web-ui'. Run 'npx auriga-cli --help' for usage.`);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
118
165
|
function parseInstall(argv) {
|
|
119
166
|
const out = { all: false };
|
|
120
167
|
let filterFlag = null;
|
|
@@ -326,6 +373,9 @@ export async function main(argv) {
|
|
|
326
373
|
process.stdout.write(renderGuide({ color, version }));
|
|
327
374
|
return 0;
|
|
328
375
|
}
|
|
376
|
+
if (parsed.command === "web-ui") {
|
|
377
|
+
return runUi(parsed.ui, version);
|
|
378
|
+
}
|
|
329
379
|
// install — catalog is required for filter validation and for the TTY
|
|
330
380
|
// menu's category descriptions; fail-fast at entry rather than produce
|
|
331
381
|
// a cryptic error mid-dispatch (spec §7 / §11 acceptance).
|
|
@@ -508,6 +558,176 @@ async function dispatchInstaller(category, packageRoot, opts) {
|
|
|
508
558
|
}
|
|
509
559
|
}
|
|
510
560
|
// ---------------------------------------------------------------------------
|
|
561
|
+
// `web-ui` subcommand — boots the local Web UI server (spec §4)
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
const UI_DEFAULT_PORT = 4747;
|
|
564
|
+
const UI_PORT_RANGE = 10; // 4747..4756
|
|
565
|
+
const UI_HEARTBEAT_TIMEOUT_MS = 15_000;
|
|
566
|
+
async function runUi(p, version) {
|
|
567
|
+
// Lazy-load the server-side deps so the install / guide paths stay light.
|
|
568
|
+
const { randomBytes } = await import("node:crypto");
|
|
569
|
+
const { startServer } = await import("./server.js");
|
|
570
|
+
const { buildDefaultApplyHandlers } = await import("./apply-handlers.js");
|
|
571
|
+
const { buildScanCatalog } = await import("./scan-catalog.js");
|
|
572
|
+
const { ensureUiBundle } = await import("./ui-fetch.js");
|
|
573
|
+
const cwd = process.cwd();
|
|
574
|
+
const packageRoot = getPackageRoot();
|
|
575
|
+
// 1. Resolve UI bundle directory.
|
|
576
|
+
let uiDir;
|
|
577
|
+
if (p.uiDir) {
|
|
578
|
+
uiDir = path.resolve(p.uiDir);
|
|
579
|
+
if (!fs.existsSync(path.join(uiDir, "index.html"))) {
|
|
580
|
+
log.error(`--ui-dir does not contain index.html: ${uiDir}`);
|
|
581
|
+
return 3;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
else if (process.env.DEV === "1") {
|
|
585
|
+
// Dev convenience: prefer the locally-built ui/dist over a network fetch.
|
|
586
|
+
const localDist = path.join(packageRoot, "ui", "dist");
|
|
587
|
+
if (fs.existsSync(path.join(localDist, "index.html"))) {
|
|
588
|
+
uiDir = localDist;
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
log.error("DEV mode: ui/dist not built. Run 'npm --prefix ui run build' or unset DEV to fetch from GitHub.");
|
|
592
|
+
return 3;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
try {
|
|
597
|
+
uiDir = await ensureUiBundle({
|
|
598
|
+
version,
|
|
599
|
+
onLog: (line) => log.ok(line),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
catch (e) {
|
|
603
|
+
log.error(`Failed to fetch UI bundle: ${e.message}\n` +
|
|
604
|
+
` Try again or pass --ui-dir <path> with a locally-built bundle.`);
|
|
605
|
+
return 3;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// 2. Build scan catalog → ApplyCatalog + pluginAgentsByName.
|
|
609
|
+
let scanCatalog;
|
|
610
|
+
try {
|
|
611
|
+
scanCatalog = await buildScanCatalog(packageRoot);
|
|
612
|
+
}
|
|
613
|
+
catch (e) {
|
|
614
|
+
log.error(`Failed to build catalog: ${e.message}`);
|
|
615
|
+
return 1;
|
|
616
|
+
}
|
|
617
|
+
const applyCatalog = {
|
|
618
|
+
// Workflow is a singleton (one CLAUDE.md per project); we pick the
|
|
619
|
+
// sentinel name "workflow" to match what the Web UI's Dashboard sends
|
|
620
|
+
// and to remain semantically self-describing. The handler ignores the
|
|
621
|
+
// name argument either way.
|
|
622
|
+
workflow: new Set(["workflow"]),
|
|
623
|
+
skill: new Set(Object.keys(scanCatalog.skills)),
|
|
624
|
+
"recommended-skill": new Set(Object.keys(scanCatalog.recommendedSkills)),
|
|
625
|
+
plugin: new Set(Object.keys(scanCatalog.plugins)),
|
|
626
|
+
hook: new Set(Object.keys(scanCatalog.hooks)),
|
|
627
|
+
};
|
|
628
|
+
const pluginAgentsByName = new Map();
|
|
629
|
+
for (const [name, def] of Object.entries(scanCatalog.plugins)) {
|
|
630
|
+
pluginAgentsByName.set(name, def.agents);
|
|
631
|
+
}
|
|
632
|
+
const applyHandlers = buildDefaultApplyHandlers({
|
|
633
|
+
packageRoot,
|
|
634
|
+
cwd,
|
|
635
|
+
pluginAgentsByName,
|
|
636
|
+
});
|
|
637
|
+
// 3. Token: 32 bytes hex per spec §4.4.
|
|
638
|
+
const token = randomBytes(32).toString("hex");
|
|
639
|
+
// 4. Bind port: try requested → otherwise 4747..4756 in sequence.
|
|
640
|
+
// Use `!== undefined` so `--port 0` (OS-ephemeral) is honored. `0` is
|
|
641
|
+
// falsy in JS; `p.port ? [p.port] : range` would silently fall back to
|
|
642
|
+
// the default range and break hermetic e2e isolation.
|
|
643
|
+
const ports = p.port !== undefined
|
|
644
|
+
? [p.port]
|
|
645
|
+
: Array.from({ length: UI_PORT_RANGE }, (_, i) => UI_DEFAULT_PORT + i);
|
|
646
|
+
let server = null;
|
|
647
|
+
let lastErr = null;
|
|
648
|
+
for (const port of ports) {
|
|
649
|
+
try {
|
|
650
|
+
server = await startServer({
|
|
651
|
+
port,
|
|
652
|
+
token,
|
|
653
|
+
cwd,
|
|
654
|
+
packageRoot,
|
|
655
|
+
heartbeatTimeoutMs: UI_HEARTBEAT_TIMEOUT_MS,
|
|
656
|
+
applyHandlers,
|
|
657
|
+
applyCatalog,
|
|
658
|
+
uiDir,
|
|
659
|
+
});
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
catch (e) {
|
|
663
|
+
lastErr = e;
|
|
664
|
+
// Only swallow address-in-use; everything else propagates.
|
|
665
|
+
if (!/EADDRINUSE|EACCES/i.test(lastErr.message)) {
|
|
666
|
+
log.error(`Failed to start server on port ${port}: ${lastErr.message}`);
|
|
667
|
+
return 1;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (!server) {
|
|
672
|
+
log.error(`All ports occupied in range (${ports[0]}..${ports[ports.length - 1]}). ` +
|
|
673
|
+
`Try '--port <n>' or 'npx auriga-cli' for the TTY menu. Last error: ${lastErr?.message ?? "unknown"}`);
|
|
674
|
+
return 2;
|
|
675
|
+
}
|
|
676
|
+
// 5. URL + browser open.
|
|
677
|
+
const url = `http://127.0.0.1:${server.port}/?token=${token}`;
|
|
678
|
+
process.stdout.write(`\n${highlight("auriga UI is live:")} ${url}\n` +
|
|
679
|
+
` (closing the browser shuts the server down after ~${Math.round(UI_HEARTBEAT_TIMEOUT_MS / 1000)}s of inactivity)\n` +
|
|
680
|
+
` Note: the URL contains a per-session token — don't paste it into chats, CI logs, or screenshots.\n\n`);
|
|
681
|
+
if (!p.noOpen) {
|
|
682
|
+
await openBrowser(url);
|
|
683
|
+
}
|
|
684
|
+
// 6. Block until the server fully stops. The heartbeat closes it after
|
|
685
|
+
// UI_HEARTBEAT_TIMEOUT_MS without a /api/ping; SIGINT triggers an
|
|
686
|
+
// explicit close().
|
|
687
|
+
const onSig = () => {
|
|
688
|
+
void server.close();
|
|
689
|
+
};
|
|
690
|
+
process.once("SIGINT", onSig);
|
|
691
|
+
process.once("SIGTERM", onSig);
|
|
692
|
+
try {
|
|
693
|
+
await server.closed;
|
|
694
|
+
}
|
|
695
|
+
finally {
|
|
696
|
+
process.off("SIGINT", onSig);
|
|
697
|
+
process.off("SIGTERM", onSig);
|
|
698
|
+
}
|
|
699
|
+
return 0;
|
|
700
|
+
}
|
|
701
|
+
/** Best-effort cross-platform browser open. Failure is non-fatal — the
|
|
702
|
+
* printed URL is still actionable. */
|
|
703
|
+
async function openBrowser(url) {
|
|
704
|
+
const opener = process.platform === "darwin"
|
|
705
|
+
? ["open", [url]]
|
|
706
|
+
: process.platform === "win32"
|
|
707
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
708
|
+
: ["xdg-open", [url]];
|
|
709
|
+
try {
|
|
710
|
+
const { spawn } = await import("node:child_process");
|
|
711
|
+
const proc = spawn(opener[0], opener[1], {
|
|
712
|
+
stdio: "ignore",
|
|
713
|
+
detached: true,
|
|
714
|
+
});
|
|
715
|
+
proc.on("error", () => {
|
|
716
|
+
/* swallow: URL was already printed */
|
|
717
|
+
});
|
|
718
|
+
proc.unref();
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
/* swallow */
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/** Bold + cyan when stdout is a TTY; otherwise plain. */
|
|
725
|
+
function highlight(text) {
|
|
726
|
+
if (!process.stdout.isTTY || process.env.NO_COLOR)
|
|
727
|
+
return text;
|
|
728
|
+
return `\x1b[1;36m${text}\x1b[0m`;
|
|
729
|
+
}
|
|
730
|
+
// ---------------------------------------------------------------------------
|
|
511
731
|
// Legacy checkbox menu — preserved for `npx auriga-cli install` in TTY
|
|
512
732
|
// and `npx auriga-cli` with no args.
|
|
513
733
|
// ---------------------------------------------------------------------------
|