auriga-cli 1.15.1 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/README.zh-CN.md +13 -0
- package/dist/api-types.d.ts +115 -0
- package/dist/api-types.js +4 -0
- package/dist/apply-handlers.d.ts +17 -0
- package/dist/apply-handlers.js +186 -0
- package/dist/catalog.json +5 -1
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +220 -0
- package/dist/help.js +2 -0
- package/dist/hooks.d.ts +30 -0
- package/dist/hooks.js +89 -0
- package/dist/plugins.d.ts +29 -0
- package/dist/plugins.js +137 -6
- package/dist/scan-catalog.d.ts +2 -0
- package/dist/scan-catalog.js +138 -0
- package/dist/server.d.ts +71 -0
- package/dist/server.js +759 -0
- package/dist/skills.d.ts +29 -0
- package/dist/skills.js +145 -2
- package/dist/state.d.ts +63 -0
- package/dist/state.js +623 -0
- package/dist/ui-fetch.d.ts +29 -0
- package/dist/ui-fetch.js +267 -0
- package/dist/utils.d.ts +22 -0
- package/dist/utils.js +58 -1
- package/dist/workflow.d.ts +22 -0
- package/dist/workflow.js +63 -0
- package/package.json +5 -3
package/dist/ui-fetch.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Fetches the Web UI bundle for the current CLI version from GitHub Releases,
|
|
2
|
+
// verifies its SHA256, and extracts it into a per-version cache directory.
|
|
3
|
+
// Subsequent CLI invocations reuse the cache instead of re-downloading.
|
|
4
|
+
//
|
|
5
|
+
// Spec: docs/architecture/web-ui.md §4.1 (boot), §9 (release pipeline + checksum +
|
|
6
|
+
// cache policy). Tests inject a fake `fetcher` so the unit suite never
|
|
7
|
+
// touches the network.
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { mkdir, mkdtemp, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { request as httpsRequest } from "node:https";
|
|
15
|
+
import { request as httpRequest } from "node:http";
|
|
16
|
+
import { gunzipSync } from "node:zlib";
|
|
17
|
+
const RELEASE_BASE = "https://github.com/Ben2pc/auriga-cli/releases/download";
|
|
18
|
+
const BUNDLE_FILE = "ui-bundle.tar.gz";
|
|
19
|
+
const SHA_FILE = "ui-bundle.sha256";
|
|
20
|
+
/** How many cached versions to keep before LRU-evicting older ones. */
|
|
21
|
+
const CACHE_KEEP_COUNT = 3;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve / populate the per-version cache dir; return its absolute path.
|
|
24
|
+
*
|
|
25
|
+
* Algorithm:
|
|
26
|
+
* 1. If `<cacheRoot>/ui-v<version>/index.html` exists → cache hit, return.
|
|
27
|
+
* 2. Fetch tar.gz + .sha256 in parallel; verify hash.
|
|
28
|
+
* 3. Extract to a sibling tmp dir; rename atomically into place.
|
|
29
|
+
* 4. Evict older versions beyond CACHE_KEEP_COUNT.
|
|
30
|
+
*
|
|
31
|
+
* Any failure path cleans up its own scratch state and rejects with a
|
|
32
|
+
* descriptive Error so the CLI can surface "try `npx auriga-cli`" guidance.
|
|
33
|
+
*/
|
|
34
|
+
export async function ensureUiBundle(opts) {
|
|
35
|
+
const cacheRoot = opts.cacheRoot ?? defaultCacheRoot();
|
|
36
|
+
const versionDir = path.join(cacheRoot, `ui-v${opts.version}`);
|
|
37
|
+
const fetcher = opts.fetcher ?? builtinHttpsFetcher;
|
|
38
|
+
const emit = (line) => opts.onLog?.(line);
|
|
39
|
+
// 1. Cache hit.
|
|
40
|
+
if (existsSync(path.join(versionDir, "index.html"))) {
|
|
41
|
+
emit(`ui bundle cache hit: ${versionDir}`);
|
|
42
|
+
return versionDir;
|
|
43
|
+
}
|
|
44
|
+
await mkdir(cacheRoot, { recursive: true });
|
|
45
|
+
// 2. Fetch.
|
|
46
|
+
const bundleUrl = `${RELEASE_BASE}/v${opts.version}/${BUNDLE_FILE}`;
|
|
47
|
+
const shaUrl = `${RELEASE_BASE}/v${opts.version}/${SHA_FILE}`;
|
|
48
|
+
emit(`fetching ${bundleUrl}`);
|
|
49
|
+
const [bundleRes, shaRes] = await Promise.all([
|
|
50
|
+
fetcher(bundleUrl),
|
|
51
|
+
fetcher(shaUrl),
|
|
52
|
+
]);
|
|
53
|
+
if (bundleRes.status !== 200) {
|
|
54
|
+
throw new Error(`ui bundle download failed: HTTP ${bundleRes.status} ${bundleUrl}`);
|
|
55
|
+
}
|
|
56
|
+
if (shaRes.status !== 200) {
|
|
57
|
+
throw new Error(`ui bundle SHA256 download failed: HTTP ${shaRes.status} ${shaUrl}`);
|
|
58
|
+
}
|
|
59
|
+
const expectedHash = parseShaFile(shaRes.body.toString("utf8"));
|
|
60
|
+
const actualHash = createHash("sha256").update(bundleRes.body).digest("hex");
|
|
61
|
+
if (actualHash.toLowerCase() !== expectedHash.toLowerCase()) {
|
|
62
|
+
throw new Error(`ui bundle SHA256 mismatch: expected ${expectedHash}, got ${actualHash}`);
|
|
63
|
+
}
|
|
64
|
+
emit(`sha256 verified (${actualHash.slice(0, 12)}…)`);
|
|
65
|
+
// 3. Extract to a sibling scratch dir.
|
|
66
|
+
const scratch = await mkdtemp(path.join(cacheRoot, `.ui-extract-`));
|
|
67
|
+
try {
|
|
68
|
+
// Verify gzip header before paying for `tar` — gives a clearer error.
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = gunzipSync(bundleRes.body);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
throw new Error(`ui bundle gzip decode failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
75
|
+
}
|
|
76
|
+
const tarPath = path.join(scratch, "bundle.tar");
|
|
77
|
+
await writeFile(tarPath, raw);
|
|
78
|
+
await extractTar(tarPath, scratch);
|
|
79
|
+
// Sanity: extracted contents must include an `index.html` somewhere
|
|
80
|
+
// immediately under scratch. tar archives created with `tar -czf …
|
|
81
|
+
// -C dist .` place files at the root; some manual archives nest under
|
|
82
|
+
// a subdir. Handle both.
|
|
83
|
+
const indexLoc = await findIndexHtml(scratch);
|
|
84
|
+
if (!indexLoc) {
|
|
85
|
+
throw new Error("ui bundle archive missing index.html");
|
|
86
|
+
}
|
|
87
|
+
const extractRoot = path.dirname(indexLoc);
|
|
88
|
+
// 4. Atomic move into the per-version dir. If versionDir already exists
|
|
89
|
+
// (race with another invocation), prefer the existing one.
|
|
90
|
+
try {
|
|
91
|
+
await rename(extractRoot, versionDir);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (existsSync(versionDir)) {
|
|
95
|
+
// Lost the race — somebody else extracted. Use theirs.
|
|
96
|
+
emit(`cache populated concurrently; reusing ${versionDir}`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
await rm(scratch, { recursive: true, force: true });
|
|
105
|
+
// Defensive: make sure versionDir doesn't survive partial state.
|
|
106
|
+
await rm(versionDir, { recursive: true, force: true });
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
// Best-effort scratch cleanup; the rename above usually consumed it.
|
|
111
|
+
await rm(scratch, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
await evictOldCacheDirs(cacheRoot, opts.version);
|
|
114
|
+
return versionDir;
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Helpers
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
function defaultCacheRoot() {
|
|
120
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? tmpdir();
|
|
121
|
+
return path.join(home, ".cache", "auriga-cli");
|
|
122
|
+
}
|
|
123
|
+
function parseShaFile(text) {
|
|
124
|
+
// `shasum -a 256` output: `<hex> <filename>\n`. We accept either the bare
|
|
125
|
+
// 64-char hex string (e.g. produced by `openssl dgst -sha256 | head`) or
|
|
126
|
+
// the `<hex> <file>` form.
|
|
127
|
+
const trimmed = text.trim();
|
|
128
|
+
const m = /^([0-9a-fA-F]{64})\b/.exec(trimmed);
|
|
129
|
+
if (!m) {
|
|
130
|
+
throw new Error(`ui bundle SHA256 file is not parseable: ${trimmed.slice(0, 80)}`);
|
|
131
|
+
}
|
|
132
|
+
return m[1];
|
|
133
|
+
}
|
|
134
|
+
/** Spawn `tar -xf` to extract into `dest`. Throws on non-zero exit. */
|
|
135
|
+
function extractTar(tarPath, dest) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const proc = spawn("tar", ["-xf", tarPath, "-C", dest], {
|
|
138
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
139
|
+
});
|
|
140
|
+
const stderrChunks = [];
|
|
141
|
+
proc.stderr.on("data", (c) => stderrChunks.push(c));
|
|
142
|
+
proc.on("error", reject);
|
|
143
|
+
proc.on("close", (code) => {
|
|
144
|
+
if (code === 0) {
|
|
145
|
+
resolve();
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const msg = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
149
|
+
reject(new Error(`ui bundle tar extract failed (exit ${code}): ${msg || "(no stderr)"}`));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** Locate `index.html` under `root` (depth ≤ 2). Returns absolute path or null. */
|
|
155
|
+
async function findIndexHtml(root) {
|
|
156
|
+
const direct = path.join(root, "index.html");
|
|
157
|
+
if (existsSync(direct))
|
|
158
|
+
return direct;
|
|
159
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
160
|
+
for (const e of entries) {
|
|
161
|
+
if (!e.isDirectory())
|
|
162
|
+
continue;
|
|
163
|
+
const nested = path.join(root, e.name, "index.html");
|
|
164
|
+
if (existsSync(nested))
|
|
165
|
+
return nested;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
/** Overall request timeout. A hung GitHub edge would otherwise block
|
|
170
|
+
* `npx auriga-cli web-ui` before the URL line ever prints. */
|
|
171
|
+
const FETCH_TIMEOUT_MS = 30_000;
|
|
172
|
+
/** Built-in fetcher using Node's https client. Follows up to 5 redirects
|
|
173
|
+
* (GitHub Release assets bounce through a signed objects URL). Buffers the
|
|
174
|
+
* whole response — bundles are well under 5 MB — and hard-times-out after
|
|
175
|
+
* FETCH_TIMEOUT_MS so a hung peer can't deadlock the CLI. */
|
|
176
|
+
async function builtinHttpsFetcher(url) {
|
|
177
|
+
return await new Promise((resolve, reject) => {
|
|
178
|
+
let redirectsLeft = 5;
|
|
179
|
+
let settled = false;
|
|
180
|
+
const finish = (err, value) => {
|
|
181
|
+
if (settled)
|
|
182
|
+
return;
|
|
183
|
+
settled = true;
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
if (err)
|
|
186
|
+
reject(err);
|
|
187
|
+
else
|
|
188
|
+
resolve(value);
|
|
189
|
+
};
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
finish(new Error(`ui bundle fetch timeout after ${FETCH_TIMEOUT_MS}ms: ${url}`));
|
|
192
|
+
}, FETCH_TIMEOUT_MS);
|
|
193
|
+
timer.unref?.();
|
|
194
|
+
const go = (target) => {
|
|
195
|
+
const u = new URL(target);
|
|
196
|
+
const fn = u.protocol === "http:" ? httpRequest : httpsRequest;
|
|
197
|
+
const req = fn(u, { method: "GET" }, (res) => {
|
|
198
|
+
const status = res.statusCode ?? 0;
|
|
199
|
+
if ([301, 302, 303, 307, 308].includes(status) &&
|
|
200
|
+
res.headers.location &&
|
|
201
|
+
redirectsLeft > 0) {
|
|
202
|
+
redirectsLeft--;
|
|
203
|
+
res.resume();
|
|
204
|
+
const next = new URL(res.headers.location, target).toString();
|
|
205
|
+
go(next);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const chunks = [];
|
|
209
|
+
res.on("data", (c) => chunks.push(c));
|
|
210
|
+
res.on("end", () => finish(null, { status, body: Buffer.concat(chunks) }));
|
|
211
|
+
res.on("error", (err) => finish(err));
|
|
212
|
+
});
|
|
213
|
+
req.on("error", (err) => finish(err));
|
|
214
|
+
req.end();
|
|
215
|
+
};
|
|
216
|
+
go(url);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/** Keep at most CACHE_KEEP_COUNT `ui-v*` dirs (newest by mtime). The
|
|
220
|
+
* just-fetched version is always retained. */
|
|
221
|
+
async function evictOldCacheDirs(cacheRoot, currentVersion) {
|
|
222
|
+
let all;
|
|
223
|
+
try {
|
|
224
|
+
all = await readdir(cacheRoot, { withFileTypes: true });
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Cache dir was already deleted by another process — nothing to evict.
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const versionDirs = all.filter((e) => e.isDirectory() && e.name.startsWith("ui-v"));
|
|
231
|
+
if (versionDirs.length <= CACHE_KEEP_COUNT)
|
|
232
|
+
return;
|
|
233
|
+
// stat each dir individually; a permission/dangling entry must not block
|
|
234
|
+
// eviction of the rest. Entries that fail to stat are excluded from the
|
|
235
|
+
// keep set so they're considered fair game for removal.
|
|
236
|
+
const withMtime = [];
|
|
237
|
+
for (const d of versionDirs) {
|
|
238
|
+
try {
|
|
239
|
+
const s = await stat(path.join(cacheRoot, d.name));
|
|
240
|
+
withMtime.push({ name: d.name, mtime: s.mtimeMs });
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
withMtime.push({ name: d.name, mtime: 0 });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
withMtime.sort((a, b) => b.mtime - a.mtime); // newest first
|
|
247
|
+
const keep = new Set();
|
|
248
|
+
keep.add(`ui-v${currentVersion}`);
|
|
249
|
+
for (const entry of withMtime) {
|
|
250
|
+
if (keep.size >= CACHE_KEEP_COUNT)
|
|
251
|
+
break;
|
|
252
|
+
keep.add(entry.name);
|
|
253
|
+
}
|
|
254
|
+
for (const entry of withMtime) {
|
|
255
|
+
if (keep.has(entry.name))
|
|
256
|
+
continue;
|
|
257
|
+
try {
|
|
258
|
+
await rm(path.join(cacheRoot, entry.name), {
|
|
259
|
+
recursive: true,
|
|
260
|
+
force: true,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Best-effort: leave the dir; next eviction round will retry.
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -45,6 +45,14 @@ export interface InstallOpts {
|
|
|
45
45
|
/** `true` = drive via inquirer prompts (existing interactive UX);
|
|
46
46
|
* `false` = non-interactive, use only the fields above. */
|
|
47
47
|
interactive: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Optional per-line callback for installer stdout/stderr. When set,
|
|
50
|
+
* exec uses spawn under the hood and forwards each line. Used by the
|
|
51
|
+
* Web UI server's /api/apply path to stream installer output through
|
|
52
|
+
* SSE item:log events (spec §6.4). When omitted, installers fall back
|
|
53
|
+
* to inherit-style exec which writes to the parent process terminal.
|
|
54
|
+
*/
|
|
55
|
+
onLog?: (line: string, stream: "stdout" | "stderr") => void;
|
|
48
56
|
}
|
|
49
57
|
/**
|
|
50
58
|
* Whether the current process should be treated as non-interactive.
|
|
@@ -58,6 +66,20 @@ export declare function exec(cmd: string, opts?: {
|
|
|
58
66
|
cwd?: string;
|
|
59
67
|
inherit?: boolean;
|
|
60
68
|
}): string;
|
|
69
|
+
/**
|
|
70
|
+
* Async variant of `exec`: spawns the command, captures stdout/stderr
|
|
71
|
+
* line-by-line via the per-line callback, and resolves on exit code 0.
|
|
72
|
+
* Non-zero exit rejects with an Error whose `stderr` field carries the
|
|
73
|
+
* buffered stderr and whose message mirrors execSync's "Command failed:"
|
|
74
|
+
* shape — so the existing `isSkillsRemoveUnsupported` matcher still works.
|
|
75
|
+
*
|
|
76
|
+
* Used by Web UI install paths to forward installer output through SSE.
|
|
77
|
+
* Synchronous callers continue to use `exec`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function execAsync(cmd: string, opts: {
|
|
80
|
+
cwd?: string;
|
|
81
|
+
onLine?: (line: string, stream: "stdout" | "stderr") => void;
|
|
82
|
+
}): Promise<string>;
|
|
61
83
|
export interface LangOption {
|
|
62
84
|
value: string;
|
|
63
85
|
label: string;
|
package/dist/utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
1
|
+
import { execSync, spawn } from "node:child_process";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import fs from "node:fs";
|
|
@@ -43,6 +43,63 @@ export function exec(cmd, opts) {
|
|
|
43
43
|
encoding: "utf-8",
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Async variant of `exec`: spawns the command, captures stdout/stderr
|
|
48
|
+
* line-by-line via the per-line callback, and resolves on exit code 0.
|
|
49
|
+
* Non-zero exit rejects with an Error whose `stderr` field carries the
|
|
50
|
+
* buffered stderr and whose message mirrors execSync's "Command failed:"
|
|
51
|
+
* shape — so the existing `isSkillsRemoveUnsupported` matcher still works.
|
|
52
|
+
*
|
|
53
|
+
* Used by Web UI install paths to forward installer output through SSE.
|
|
54
|
+
* Synchronous callers continue to use `exec`.
|
|
55
|
+
*/
|
|
56
|
+
export function execAsync(cmd, opts) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const child = spawn(cmd, { cwd: opts.cwd, shell: true });
|
|
59
|
+
let stdout = "";
|
|
60
|
+
let stderr = "";
|
|
61
|
+
const lineBuffers = {
|
|
62
|
+
stdout: "",
|
|
63
|
+
stderr: "",
|
|
64
|
+
};
|
|
65
|
+
const drain = (stream, chunk) => {
|
|
66
|
+
const text = chunk.toString("utf-8");
|
|
67
|
+
if (stream === "stdout")
|
|
68
|
+
stdout += text;
|
|
69
|
+
else
|
|
70
|
+
stderr += text;
|
|
71
|
+
lineBuffers[stream] += text;
|
|
72
|
+
let idx;
|
|
73
|
+
while ((idx = lineBuffers[stream].indexOf("\n")) !== -1) {
|
|
74
|
+
const line = lineBuffers[stream].slice(0, idx).replace(/\r$/, "");
|
|
75
|
+
lineBuffers[stream] = lineBuffers[stream].slice(idx + 1);
|
|
76
|
+
if (opts.onLine && line.length > 0)
|
|
77
|
+
opts.onLine(line, stream);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
child.stdout?.on("data", (chunk) => drain("stdout", chunk));
|
|
81
|
+
child.stderr?.on("data", (chunk) => drain("stderr", chunk));
|
|
82
|
+
child.on("error", (err) => reject(err));
|
|
83
|
+
child.on("close", (code) => {
|
|
84
|
+
// Flush trailing partial lines (no final newline).
|
|
85
|
+
for (const s of ["stdout", "stderr"]) {
|
|
86
|
+
if (lineBuffers[s].length > 0 && opts.onLine) {
|
|
87
|
+
opts.onLine(lineBuffers[s], s);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (code === 0) {
|
|
91
|
+
resolve(stdout);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const err = new Error(`Command failed: ${cmd}`);
|
|
95
|
+
err.stderr = stderr;
|
|
96
|
+
err.stdout = stdout;
|
|
97
|
+
err.status = code;
|
|
98
|
+
reject(err);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
46
103
|
export const LANGUAGES = [
|
|
47
104
|
{ value: "en", label: "English", file: "CLAUDE.md" },
|
|
48
105
|
{ value: "zh-CN", label: "中文", file: "CLAUDE.zh-CN.md" },
|
package/dist/workflow.d.ts
CHANGED
|
@@ -1,2 +1,24 @@
|
|
|
1
1
|
import { type InstallOpts } from "./utils.js";
|
|
2
2
|
export declare function installWorkflow(packageRoot: string, opts: InstallOpts): Promise<void>;
|
|
3
|
+
/**
|
|
4
|
+
* Uninstall the workflow (CLAUDE.md + AGENTS.md) from `opts.cwd`.
|
|
5
|
+
*
|
|
6
|
+
* Safety contract:
|
|
7
|
+
* - `opts.force` MUST be true. The CLI / server caller is responsible for
|
|
8
|
+
* confirming user intent BEFORE invoking this; we refuse otherwise.
|
|
9
|
+
* - `AGENTS.md` is removed ONLY if it's a symlink (the install-time shape).
|
|
10
|
+
* A real-file AGENTS.md is left in place with a warning — the user has
|
|
11
|
+
* diverged from the install pattern and probably hand-edited it.
|
|
12
|
+
* - Missing files are a no-op: callers can re-run uninstall idempotently.
|
|
13
|
+
* - `.claude/` is not touched; skills / plugins / hooks have their own
|
|
14
|
+
* uninstall paths.
|
|
15
|
+
*
|
|
16
|
+
* `onLog`, when provided, receives one human-readable line per action so
|
|
17
|
+
* the SSE caller (server.ts) can stream progress to the browser. Internal
|
|
18
|
+
* `log.ok / warn / error` calls still go to stderr for the CLI path.
|
|
19
|
+
*/
|
|
20
|
+
export declare function uninstallWorkflow(opts: {
|
|
21
|
+
force?: boolean;
|
|
22
|
+
cwd: string;
|
|
23
|
+
onLog?: (line: string) => void;
|
|
24
|
+
}): Promise<void>;
|
package/dist/workflow.js
CHANGED
|
@@ -53,3 +53,66 @@ export async function installWorkflow(packageRoot, opts) {
|
|
|
53
53
|
fs.symlinkSync("CLAUDE.md", targetAgents);
|
|
54
54
|
log.ok("AGENTS.md -> CLAUDE.md symlink created");
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Uninstall the workflow (CLAUDE.md + AGENTS.md) from `opts.cwd`.
|
|
58
|
+
*
|
|
59
|
+
* Safety contract:
|
|
60
|
+
* - `opts.force` MUST be true. The CLI / server caller is responsible for
|
|
61
|
+
* confirming user intent BEFORE invoking this; we refuse otherwise.
|
|
62
|
+
* - `AGENTS.md` is removed ONLY if it's a symlink (the install-time shape).
|
|
63
|
+
* A real-file AGENTS.md is left in place with a warning — the user has
|
|
64
|
+
* diverged from the install pattern and probably hand-edited it.
|
|
65
|
+
* - Missing files are a no-op: callers can re-run uninstall idempotently.
|
|
66
|
+
* - `.claude/` is not touched; skills / plugins / hooks have their own
|
|
67
|
+
* uninstall paths.
|
|
68
|
+
*
|
|
69
|
+
* `onLog`, when provided, receives one human-readable line per action so
|
|
70
|
+
* the SSE caller (server.ts) can stream progress to the browser. Internal
|
|
71
|
+
* `log.ok / warn / error` calls still go to stderr for the CLI path.
|
|
72
|
+
*/
|
|
73
|
+
export async function uninstallWorkflow(opts) {
|
|
74
|
+
if (opts.force !== true) {
|
|
75
|
+
throw new Error("workflow uninstall requires force=true");
|
|
76
|
+
}
|
|
77
|
+
const resolved = path.resolve(opts.cwd);
|
|
78
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
|
|
79
|
+
throw new Error(`Not a valid directory: ${resolved}`);
|
|
80
|
+
}
|
|
81
|
+
const emit = (line) => {
|
|
82
|
+
opts.onLog?.(line);
|
|
83
|
+
};
|
|
84
|
+
const targetClaude = path.join(resolved, "CLAUDE.md");
|
|
85
|
+
const targetAgents = path.join(resolved, "AGENTS.md");
|
|
86
|
+
// CLAUDE.md — flat file. lstat to avoid following a symlink (would be
|
|
87
|
+
// unusual but we'd rather refuse to traverse than chase one out).
|
|
88
|
+
if (fs.existsSync(targetClaude)) {
|
|
89
|
+
fs.unlinkSync(targetClaude);
|
|
90
|
+
log.ok("CLAUDE.md removed");
|
|
91
|
+
emit("removed CLAUDE.md");
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
log.skip("CLAUDE.md not present");
|
|
95
|
+
emit("CLAUDE.md not present");
|
|
96
|
+
}
|
|
97
|
+
// AGENTS.md — only remove symlinks (our install shape). lstatSync
|
|
98
|
+
// refuses to follow the link so we inspect the link itself.
|
|
99
|
+
try {
|
|
100
|
+
const stat = fs.lstatSync(targetAgents);
|
|
101
|
+
if (stat.isSymbolicLink()) {
|
|
102
|
+
fs.unlinkSync(targetAgents);
|
|
103
|
+
log.ok("AGENTS.md symlink removed");
|
|
104
|
+
emit("removed AGENTS.md symlink");
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Real file (or directory) — user diverged from install. Don't
|
|
108
|
+
// silently destroy their content; warn and leave it.
|
|
109
|
+
log.warn("AGENTS.md is not a symlink; left in place");
|
|
110
|
+
emit("AGENTS.md is not a symlink; left in place");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// ENOENT — already gone, idempotent no-op.
|
|
115
|
+
log.skip("AGENTS.md not present");
|
|
116
|
+
emit("AGENTS.md not present");
|
|
117
|
+
}
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auriga-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Interactive CLI to install Claude Code harness modules (Workflow, Skills, Recommended Skills, Plugins, Hooks)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,10 +25,12 @@
|
|
|
25
25
|
"dev": "tsc --watch",
|
|
26
26
|
"start": "node dist/cli.js",
|
|
27
27
|
"pretest": "npm run build",
|
|
28
|
-
"test": "tsc -p tsconfig.test.json && DEV=1 node --test --experimental-test-module-mocks dist-test/tests/hooks.test.js dist-test/tests/skills.test.js dist-test/tests/catalog.test.js dist-test/tests/cli-parse.test.js dist-test/tests/install-nontty.test.js dist-test/tests/plugins.test.js dist-test/tests/content-fetch.test.js dist-test/tests/utils.test.js dist-test/tests/guide.test.js dist-test/tests/validators.test.js dist-test/tests/entrypoint.test.js",
|
|
29
|
-
"test:watch": "tsc -p tsconfig.test.json --watch & node --test --watch --experimental-test-module-mocks dist-test/tests/hooks.test.js dist-test/tests/skills.test.js dist-test/tests/catalog.test.js dist-test/tests/cli-parse.test.js dist-test/tests/install-nontty.test.js dist-test/tests/plugins.test.js dist-test/tests/content-fetch.test.js dist-test/tests/utils.test.js dist-test/tests/guide.test.js dist-test/tests/validators.test.js dist-test/tests/entrypoint.test.js",
|
|
28
|
+
"test": "tsc -p tsconfig.test.json && DEV=1 node --test --experimental-test-module-mocks dist-test/tests/hooks.test.js dist-test/tests/hooks-uninstall.test.js dist-test/tests/skills.test.js dist-test/tests/skills-uninstall.test.js dist-test/tests/catalog.test.js dist-test/tests/cli-parse.test.js dist-test/tests/install-nontty.test.js dist-test/tests/plugins.test.js dist-test/tests/plugins-uninstall.test.js dist-test/tests/content-fetch.test.js dist-test/tests/utils.test.js dist-test/tests/guide.test.js dist-test/tests/validators.test.js dist-test/tests/entrypoint.test.js dist-test/tests/state.test.js dist-test/tests/server.test.js dist-test/tests/server-auth.test.js dist-test/tests/server-apply.test.js dist-test/tests/apply-handlers.test.js dist-test/tests/ui-fetch.test.js dist-test/tests/workflow-uninstall.test.js",
|
|
29
|
+
"test:watch": "tsc -p tsconfig.test.json --watch & node --test --watch --experimental-test-module-mocks dist-test/tests/hooks.test.js dist-test/tests/hooks-uninstall.test.js dist-test/tests/skills.test.js dist-test/tests/skills-uninstall.test.js dist-test/tests/catalog.test.js dist-test/tests/cli-parse.test.js dist-test/tests/install-nontty.test.js dist-test/tests/plugins.test.js dist-test/tests/plugins-uninstall.test.js dist-test/tests/content-fetch.test.js dist-test/tests/utils.test.js dist-test/tests/guide.test.js dist-test/tests/validators.test.js dist-test/tests/entrypoint.test.js dist-test/tests/state.test.js dist-test/tests/server.test.js dist-test/tests/server-auth.test.js dist-test/tests/server-apply.test.js dist-test/tests/apply-handlers.test.js dist-test/tests/ui-fetch.test.js dist-test/tests/workflow-uninstall.test.js",
|
|
30
30
|
"pretest:e2e": "npm run build",
|
|
31
31
|
"test:e2e": "tsc -p tsconfig.test.json && node --test dist-test/tests/e2e-install.test.js",
|
|
32
|
+
"pretest:web-ui-e2e": "npm run build && npm --prefix ui ci && npm --prefix ui run build",
|
|
33
|
+
"test:web-ui-e2e": "tsc -p tsconfig.test.json && node --test dist-test/tests/web-ui-e2e.test.js",
|
|
32
34
|
"test:session-instructions-loader": "node tests/session-instructions-loader.test.mjs",
|
|
33
35
|
"test:git-guards": "node tests/commit-reminder.test.mjs && node tests/pr-create-guard.test.mjs && node tests/pr-ready-guard.test.mjs"
|
|
34
36
|
},
|