@coworker-jp/aidr 0.0.1
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 +55 -0
- package/bin/aidr.js +2 -0
- package/package.json +29 -0
- package/src/agents/_stub.mjs +18 -0
- package/src/agents/aider.mjs +5 -0
- package/src/agents/amazonq.mjs +5 -0
- package/src/agents/amp.mjs +5 -0
- package/src/agents/antigravity.mjs +5 -0
- package/src/agents/claude.mjs +92 -0
- package/src/agents/cline.mjs +5 -0
- package/src/agents/codex.mjs +225 -0
- package/src/agents/continue.mjs +5 -0
- package/src/agents/copilot.mjs +5 -0
- package/src/agents/crush.mjs +5 -0
- package/src/agents/cursor.mjs +109 -0
- package/src/agents/gemini.mjs +66 -0
- package/src/agents/index.mjs +36 -0
- package/src/agents/jetbrains.mjs +5 -0
- package/src/agents/kiro.mjs +79 -0
- package/src/agents/opencode.mjs +5 -0
- package/src/agents/qwen.mjs +5 -0
- package/src/agents/roo.mjs +5 -0
- package/src/agents/standalone.mjs +43 -0
- package/src/agents/trae.mjs +5 -0
- package/src/agents/windsurf.mjs +105 -0
- package/src/binary-fetcher.mjs +157 -0
- package/src/browser-extension.mjs +83 -0
- package/src/cli.mjs +494 -0
- package/src/detect.mjs +39 -0
- package/src/fs-utils.mjs +247 -0
- package/src/merge.mjs +412 -0
- package/src/scheduled.mjs +219 -0
- package/src/templates.mjs +759 -0
- package/src/toml-merge.mjs +167 -0
- package/src/verify.mjs +51 -0
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
|
|
10
|
+
const execFileP = promisify(execFile);
|
|
11
|
+
import { AGENT_ORDER, AGENT_REGISTRY, loadAgent } from "./agents/index.mjs";
|
|
12
|
+
import { verifyKey, maskKey } from "./verify.mjs";
|
|
13
|
+
import { fetchBinary, fetchOpengrep } from "./binary-fetcher.mjs";
|
|
14
|
+
import { detectInstalled } from "./detect.mjs";
|
|
15
|
+
import { installBrowserExtension } from "./browser-extension.mjs";
|
|
16
|
+
|
|
17
|
+
// Package identity — read at module load from the bundled package.json.
|
|
18
|
+
// The PROD publish workflow renames the package to `@coworker-jp/aidr`;
|
|
19
|
+
// DEV keeps `@coworker-jp/aidr-dev`. We key the default environment
|
|
20
|
+
// (verify endpoint + S3 bucket) off the package name so users don't need to
|
|
21
|
+
// remember `--env dev` when they explicitly `npx` the `-dev` package. The
|
|
22
|
+
// PROD publish also pins version to the Cargo.toml release so scanner binary
|
|
23
|
+
// URLs resolve to `v{X.Y.Z}/`; DEV uses a `-dev.<run_number>` prerelease that
|
|
24
|
+
// never matches, so DEV falls back to the latest S3 prefix.
|
|
25
|
+
const __pkgDir = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const PKG = JSON.parse(
|
|
27
|
+
await fs.readFile(path.resolve(__pkgDir, "..", "package.json"), "utf8"),
|
|
28
|
+
);
|
|
29
|
+
const IS_PROD_PACKAGE = PKG.name === "@coworker-jp/aidr";
|
|
30
|
+
const PINNED_VERSION = IS_PROD_PACKAGE ? PKG.version : undefined;
|
|
31
|
+
const DEFAULT_ENV = IS_PROD_PACKAGE ? "prod" : "dev";
|
|
32
|
+
|
|
33
|
+
const ACCESS_KEY_RE = /^ak_[A-Za-z0-9_-]{43}$/;
|
|
34
|
+
|
|
35
|
+
function validateKey(key) {
|
|
36
|
+
if (!ACCESS_KEY_RE.test(key)) {
|
|
37
|
+
throw new Error(`invalid access key format (expected ak_<43-char urlsafe base64>)`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseAgentList(val) {
|
|
42
|
+
return String(val).split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Shared settings.json paths per agent — only claude and gemini write into a
|
|
46
|
+
// config file that users may have hand-authored. Other agents' hook files are
|
|
47
|
+
// entirely owned by aidr, so no confirmation is needed for them.
|
|
48
|
+
function sharedSettingsPath(agentName, base) {
|
|
49
|
+
if (agentName === "claude") return path.join(base, ".claude", "settings.json");
|
|
50
|
+
if (agentName === "gemini") return path.join(base, ".gemini", "settings.json");
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function fileExists(p) {
|
|
55
|
+
try { await fs.access(p); return true; } catch { return false; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function promptYesNo(message) {
|
|
59
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
60
|
+
try {
|
|
61
|
+
const answer = await new Promise((resolve) => {
|
|
62
|
+
rl.question(`${message} [y/N] `, resolve);
|
|
63
|
+
});
|
|
64
|
+
return /^y(es)?$/i.test(String(answer).trim());
|
|
65
|
+
} finally {
|
|
66
|
+
rl.close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function cmdInstall(opts) {
|
|
71
|
+
// `install --uninstall` is a convenience alias so users can toggle
|
|
72
|
+
// between install/uninstall with a single memorised command. It forwards
|
|
73
|
+
// the relevant options (agent, all, scope, yes, dry-run) to the uninstall
|
|
74
|
+
// flow. --key isn't needed but commander requires it via requiredOption,
|
|
75
|
+
// so we accept and ignore it.
|
|
76
|
+
if (opts.uninstall) {
|
|
77
|
+
return cmdUninstall({
|
|
78
|
+
agent: opts.agent,
|
|
79
|
+
all: opts.all,
|
|
80
|
+
scope: opts.scope,
|
|
81
|
+
yes: opts.yes,
|
|
82
|
+
dryRun: opts.dryRun,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const home = process.env.HOME || os.homedir();
|
|
87
|
+
const scope = opts.scope || "user";
|
|
88
|
+
const base = scope === "user" ? home : process.cwd();
|
|
89
|
+
validateKey(opts.key);
|
|
90
|
+
|
|
91
|
+
if (opts.verify !== false) {
|
|
92
|
+
try {
|
|
93
|
+
const info = await verifyKey(opts.key, opts.env || DEFAULT_ENV);
|
|
94
|
+
console.log(`verified: plan=${info.plan} expires_at=${info.expires_at || "n/a"}`);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.error(`verify failed: ${e.message}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let agents;
|
|
102
|
+
if (opts.all) {
|
|
103
|
+
agents = detectInstalled(home);
|
|
104
|
+
if (agents.length === 0) {
|
|
105
|
+
console.error("no agents detected under $HOME; specify --agent explicitly");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
} else if (opts.agent) {
|
|
109
|
+
agents = parseAgentList(opts.agent);
|
|
110
|
+
} else {
|
|
111
|
+
console.error("--agent <list> or --all is required");
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const name of agents) {
|
|
116
|
+
if (!AGENT_REGISTRY[name]) {
|
|
117
|
+
console.error(`unknown agent: ${name} (see 'aidr list-agents')`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Standalone has no hook scripts and no settings.json — scheduled
|
|
123
|
+
// endpoint-info collection is the whole reason to pick it, so turn
|
|
124
|
+
// --scheduled on by default when standalone is selected. --binary-only
|
|
125
|
+
// still short-circuits later.
|
|
126
|
+
if (agents.includes("standalone")) {
|
|
127
|
+
opts.scheduled = true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --scheduled installs a cron / systemd / launchd entry that runs
|
|
131
|
+
// `ai-scanner scan endpoint-info` periodically against an agent-independent
|
|
132
|
+
// binary at ~/.aidr/bin/ai-scanner. Auto-add the standalone install so the
|
|
133
|
+
// binary fetch loop drops a copy there even when the user only listed AI
|
|
134
|
+
// agents (claude / cursor / ...) on the command line.
|
|
135
|
+
if (opts.scheduled && !agents.includes("standalone")) {
|
|
136
|
+
agents.push("standalone");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const intervalHours = parseInt(opts.interval ?? "4", 10);
|
|
140
|
+
if (opts.scheduled && (isNaN(intervalHours) || intervalHours < 1)) {
|
|
141
|
+
console.error("--interval must be an integer >= 1");
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Confirmation for existing shared settings.json (claude / gemini).
|
|
146
|
+
// --yes bypasses; --dry-run doesn't write so skip; otherwise prompt on TTY,
|
|
147
|
+
// error if non-interactive (safer than silently merging in CI).
|
|
148
|
+
if (!opts.yes && !opts.dryRun) {
|
|
149
|
+
const conflicts = (await Promise.all(
|
|
150
|
+
agents.map(async (name) => {
|
|
151
|
+
const p = sharedSettingsPath(name, base);
|
|
152
|
+
return (p && await fileExists(p)) ? p : null;
|
|
153
|
+
})
|
|
154
|
+
)).filter(Boolean);
|
|
155
|
+
if (conflicts.length > 0) {
|
|
156
|
+
const list = conflicts.map(p => ` - ${p}`).join("\n");
|
|
157
|
+
if (!process.stdin.isTTY) {
|
|
158
|
+
console.error(`Existing settings.json detected:\n${list}`);
|
|
159
|
+
console.error(`aidr will merge into these files and create timestamped backups.`);
|
|
160
|
+
console.error(`Re-run with -y/--yes to proceed non-interactively, or run in an interactive terminal.`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
console.error(`Existing settings.json detected:\n${list}`);
|
|
164
|
+
console.error(`aidr will merge ai-scanner env/hooks into each file and back up the original.`);
|
|
165
|
+
const ok = await promptYesNo("Proceed?");
|
|
166
|
+
if (!ok) {
|
|
167
|
+
console.error("aborted");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!opts.skipBinary && !opts.dryRun) {
|
|
174
|
+
const agentBinDirs = [];
|
|
175
|
+
for (const name of agents) {
|
|
176
|
+
const mod = await loadAgent(name);
|
|
177
|
+
const agentDir = mod.meta.agentDir;
|
|
178
|
+
if (!agentDir) continue;
|
|
179
|
+
agentBinDirs.push(path.join(home, agentDir, "bin"));
|
|
180
|
+
}
|
|
181
|
+
if (agentBinDirs.length === 0) {
|
|
182
|
+
console.error("no agents with binary support selected; skipping binary fetch");
|
|
183
|
+
} else {
|
|
184
|
+
try {
|
|
185
|
+
const [primaryDir, ...restDirs] = agentBinDirs;
|
|
186
|
+
const fetchOpts = {
|
|
187
|
+
env: opts.env || DEFAULT_ENV,
|
|
188
|
+
accessKey: opts.key,
|
|
189
|
+
version: PINNED_VERSION,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// ai-scanner + bundled Opengrep (LGPL-2.1). Both land in the same bin
|
|
193
|
+
// dir so ai-scanner's sibling-lookup finds opengrep without relying on
|
|
194
|
+
// PATH. Users can point AI_SCANNER_OPENGREP elsewhere to substitute
|
|
195
|
+
// their own LGPL-2.1 build.
|
|
196
|
+
const scannerRes = await fetchBinary(path.join(primaryDir, "ai-scanner"), fetchOpts);
|
|
197
|
+
console.log(`binary: ${scannerRes.path} (${scannerRes.platform}${scannerRes.verified ? ", sha256 verified" : ""})`);
|
|
198
|
+
// Opengrep is fetched unmodified from its public upstream GitHub
|
|
199
|
+
// release (LGPL-2.1); no access-key gating is needed here.
|
|
200
|
+
const opengrepRes = await fetchOpengrep(path.join(primaryDir, "opengrep"));
|
|
201
|
+
console.log(`binary: ${opengrepRes.path} (opengrep ${opengrepRes.platform}${opengrepRes.verified ? ", sha256 verified" : ""})`);
|
|
202
|
+
|
|
203
|
+
const copyOps = restDirs.flatMap(restDir =>
|
|
204
|
+
["ai-scanner", "opengrep"].map(async (file) => {
|
|
205
|
+
const src = path.join(primaryDir, file);
|
|
206
|
+
const dest = path.join(restDir, file);
|
|
207
|
+
await fs.mkdir(restDir, { recursive: true });
|
|
208
|
+
await fs.copyFile(src, dest);
|
|
209
|
+
await fs.chmod(dest, 0o755);
|
|
210
|
+
if (process.platform === "darwin") {
|
|
211
|
+
try { await execFileP("xattr", ["-d", "com.apple.quarantine", dest]); } catch {}
|
|
212
|
+
}
|
|
213
|
+
console.log(`binary: ${dest} (copied)`);
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
await Promise.all(copyOps);
|
|
217
|
+
|
|
218
|
+
console.log("Includes Opengrep (LGPL-2.1).");
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.error(`binary fetch failed: ${e.message}`);
|
|
221
|
+
if (!opts.binaryOnly) console.error("continuing with hook install (use --skip-binary to suppress)");
|
|
222
|
+
else process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (opts.binaryOnly) return;
|
|
228
|
+
|
|
229
|
+
const installResults = (await Promise.all(agents.map(async (name) => {
|
|
230
|
+
const mod = await loadAgent(name);
|
|
231
|
+
try {
|
|
232
|
+
const res = await mod.install(base, opts.key, { dryRun: opts.dryRun, force: opts.force, env: opts.env || DEFAULT_ENV, scope, home });
|
|
233
|
+
console.log(`[${name}] installed (key=${maskKey(opts.key)})`);
|
|
234
|
+
// Prefer the aggregated backupPaths list (settings + scripts); fall back
|
|
235
|
+
// to the legacy single backupPath for agents that don't report scripts yet.
|
|
236
|
+
const backups = (res && res.backupPaths && res.backupPaths.length)
|
|
237
|
+
? res.backupPaths
|
|
238
|
+
: (res && res.backupPath ? [res.backupPath] : []);
|
|
239
|
+
for (const bp of backups) {
|
|
240
|
+
console.log(`[${name}] existing file backed up to ${bp}`);
|
|
241
|
+
}
|
|
242
|
+
return { name, displayName: mod.meta.displayName || name };
|
|
243
|
+
} catch (e) {
|
|
244
|
+
console.error(`[${name}] install failed: ${e.message}`);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
}))).filter(Boolean);
|
|
248
|
+
// Standalone has no hooks to restart — exclude it from the restart notice.
|
|
249
|
+
const restartAgents = installResults
|
|
250
|
+
.filter((r) => r.name !== "standalone")
|
|
251
|
+
.map((r) => r.displayName);
|
|
252
|
+
|
|
253
|
+
if (opts.scheduled && !opts.dryRun) {
|
|
254
|
+
const { installScheduled } = await import("./scheduled.mjs");
|
|
255
|
+
// Always point the cron / systemd / launchd entry at the standalone
|
|
256
|
+
// binary (~/.aidr/bin/ai-scanner). It is agent-independent, self-updates
|
|
257
|
+
// on every endpoint-info run, and survives an individual agent's removal.
|
|
258
|
+
const standalone = await loadAgent("standalone");
|
|
259
|
+
const binaryPath = path.join(home, standalone.meta.agentDir, "bin", "ai-scanner");
|
|
260
|
+
try {
|
|
261
|
+
await installScheduled({
|
|
262
|
+
home,
|
|
263
|
+
accessKey: opts.key,
|
|
264
|
+
binaryPath,
|
|
265
|
+
dryRun: opts.dryRun,
|
|
266
|
+
interval: intervalHours,
|
|
267
|
+
});
|
|
268
|
+
} catch (e) {
|
|
269
|
+
console.error(`scheduled install failed: ${e.message}`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (opts.browserExtension && !opts.dryRun) {
|
|
275
|
+
const firstAgentName = agents[0];
|
|
276
|
+
const firstAgent = await loadAgent(firstAgentName);
|
|
277
|
+
const binaryPath = path.join(home, firstAgent.meta.agentDir, "bin", "ai-scanner");
|
|
278
|
+
try {
|
|
279
|
+
await installBrowserExtension(home, binaryPath);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
console.error(`browser extension install warning: ${e.message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (restartAgents.length > 0 && !opts.dryRun) {
|
|
286
|
+
const list = restartAgents.join(", ");
|
|
287
|
+
console.log("");
|
|
288
|
+
console.log(`Next: restart ${list} so hooks take effect.`);
|
|
289
|
+
if (process.platform === "darwin") {
|
|
290
|
+
console.log("On macOS, the first hook run may prompt: \"<your terminal> wants to control System Events\".");
|
|
291
|
+
console.log("Click Allow — the scanner uses this to collect endpoint info (login items).");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function cmdUninstall(opts) {
|
|
297
|
+
const home = process.env.HOME || os.homedir();
|
|
298
|
+
const scope = opts.scope || "user";
|
|
299
|
+
const base = scope === "user" ? home : process.cwd();
|
|
300
|
+
|
|
301
|
+
let agents;
|
|
302
|
+
if (opts.all) {
|
|
303
|
+
agents = detectInstalled(home);
|
|
304
|
+
} else if (opts.agent) {
|
|
305
|
+
agents = parseAgentList(opts.agent);
|
|
306
|
+
} else {
|
|
307
|
+
agents = detectInstalled(home);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (agents.length === 0) {
|
|
311
|
+
console.error("no agents detected; specify --agent explicitly");
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const name of agents) {
|
|
316
|
+
if (!AGENT_REGISTRY[name]) {
|
|
317
|
+
console.error(`unknown agent: ${name} (see 'aidr list-agents')`);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Phase 1: collect plans (dry-run each agent)
|
|
323
|
+
const plans = await Promise.all(agents.map(async (name) => {
|
|
324
|
+
const mod = await loadAgent(name);
|
|
325
|
+
try {
|
|
326
|
+
const plan = await mod.uninstall(base, { dryRun: true });
|
|
327
|
+
return { name, mod, plan };
|
|
328
|
+
} catch (e) {
|
|
329
|
+
console.error(`[${name}] cannot plan uninstall: ${e.message}`);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
}));
|
|
333
|
+
|
|
334
|
+
// Decide whether there's any work to confirm. Includes nested unmerge
|
|
335
|
+
// sub-actions (codex: { action: "unmerge", hooksJson, configToml }).
|
|
336
|
+
const settingsHasWork = (settings) => {
|
|
337
|
+
if (!settings || settings.action === "none") return false;
|
|
338
|
+
if (settings.action === "unmerge") {
|
|
339
|
+
return ["hooksJson", "configToml"].some((k) => {
|
|
340
|
+
const sub = settings[k];
|
|
341
|
+
return sub && sub.action !== "none" && sub.action !== "noop";
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return settings.action === "delete" || settings.action === "modify";
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const hasWork = plans.some(({ plan }) =>
|
|
348
|
+
plan && (plan.removedFiles?.length > 0 || settingsHasWork(plan.settings))
|
|
349
|
+
);
|
|
350
|
+
if (!hasWork) {
|
|
351
|
+
console.log("nothing to uninstall");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Print plan
|
|
356
|
+
console.error("aidr uninstall — the following will be removed/modified:");
|
|
357
|
+
for (const { name, plan } of plans) {
|
|
358
|
+
if (!plan) continue;
|
|
359
|
+
for (const f of plan.removedFiles || []) {
|
|
360
|
+
console.error(` [${name}] remove ${f}`);
|
|
361
|
+
}
|
|
362
|
+
if (plan.settings) {
|
|
363
|
+
if (plan.settings.action === "delete") {
|
|
364
|
+
console.error(` [${name}] delete ${plan.settings.path} (no ai-scanner entries left)`);
|
|
365
|
+
} else if (plan.settings.action === "modify") {
|
|
366
|
+
console.error(` [${name}] modify ${plan.settings.path} (strip ai-scanner env/hooks, backup created)`);
|
|
367
|
+
} else if (plan.settings.action === "unmerge") {
|
|
368
|
+
// Nested plan (codex): surface each sub-action so `-y` users can see
|
|
369
|
+
// hooks.json / config.toml will be touched.
|
|
370
|
+
for (const sub of [plan.settings.hooksJson, plan.settings.configToml]) {
|
|
371
|
+
if (!sub || sub.action === "none" || sub.action === "noop") continue;
|
|
372
|
+
if (sub.action === "delete") {
|
|
373
|
+
console.error(` [${name}] delete ${sub.path} (no ai-scanner entries left)`);
|
|
374
|
+
} else {
|
|
375
|
+
console.error(` [${name}] modify ${sub.path} (strip ai-scanner entries, backup created)`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (opts.dryRun) return;
|
|
383
|
+
|
|
384
|
+
// Phase 2: confirm
|
|
385
|
+
if (!opts.yes) {
|
|
386
|
+
if (!process.stdin.isTTY) {
|
|
387
|
+
console.error("");
|
|
388
|
+
console.error("Re-run with -y/--yes to proceed non-interactively.");
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
const ok = await promptYesNo("Proceed?");
|
|
392
|
+
if (!ok) {
|
|
393
|
+
console.error("aborted");
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Phase 3: execute
|
|
399
|
+
await Promise.all(plans.map(async ({ name, mod }) => {
|
|
400
|
+
try {
|
|
401
|
+
const res = await mod.uninstall(base, { dryRun: false });
|
|
402
|
+
console.log(`[${name}] uninstalled`);
|
|
403
|
+
if (res?.settings?.backupPath) {
|
|
404
|
+
console.log(`[${name}] existing settings backed up to ${res.settings.backupPath}`);
|
|
405
|
+
}
|
|
406
|
+
} catch (e) {
|
|
407
|
+
console.error(`[${name}] uninstall failed: ${e.message}`);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
}));
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const { uninstallScheduled } = await import("./scheduled.mjs");
|
|
414
|
+
await uninstallScheduled({ home, dryRun: opts.dryRun });
|
|
415
|
+
} catch (e) {
|
|
416
|
+
console.error(`scheduled uninstall warning: ${e.message}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function cmdDoctor() {
|
|
421
|
+
const home = process.env.HOME || os.homedir();
|
|
422
|
+
console.log(`home: ${home}`);
|
|
423
|
+
console.log(`node: ${process.version}`);
|
|
424
|
+
console.log(`platform: ${process.platform}/${process.arch}`);
|
|
425
|
+
const installed = detectInstalled(home);
|
|
426
|
+
console.log(`detected agents: ${installed.join(", ") || "(none)"}`);
|
|
427
|
+
const lines = await Promise.all(installed.map(async (name) => {
|
|
428
|
+
const mod = await loadAgent(name);
|
|
429
|
+
const agentDir = mod.meta.agentDir;
|
|
430
|
+
if (!agentDir) return null;
|
|
431
|
+
const bin = path.join(home, agentDir, "bin", "ai-scanner");
|
|
432
|
+
let present = false;
|
|
433
|
+
try {
|
|
434
|
+
await fs.access(bin, fs.constants.F_OK);
|
|
435
|
+
present = true;
|
|
436
|
+
} catch {}
|
|
437
|
+
return `binary: ${bin} ${present ? "[present]" : "[missing]"}`;
|
|
438
|
+
}));
|
|
439
|
+
for (const line of lines) {
|
|
440
|
+
if (line) console.log(line);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function cmdListAgents() {
|
|
445
|
+
for (const name of AGENT_ORDER) {
|
|
446
|
+
const mod = await loadAgent(name);
|
|
447
|
+
const d = mod.meta.displayName || name;
|
|
448
|
+
const stub = mod.meta.stub ? " (stub)" : "";
|
|
449
|
+
console.log(`${name}\t${d}${stub}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export async function run(argv) {
|
|
454
|
+
const program = new Command();
|
|
455
|
+
program
|
|
456
|
+
.name("aidr")
|
|
457
|
+
.description("AIDR setup CLI — installs ai-scanner hooks for AI coding agents")
|
|
458
|
+
.version("0.0.1");
|
|
459
|
+
|
|
460
|
+
program
|
|
461
|
+
.command("install")
|
|
462
|
+
.description("Install ai-scanner hooks for one or more agents")
|
|
463
|
+
.option("--agent <list>", "comma-separated agent names")
|
|
464
|
+
.option("--all", "install for all detected agents")
|
|
465
|
+
.requiredOption("--key <ak_xxx>", "access key (ak_<43-char>)")
|
|
466
|
+
.option("--env <env>", "target environment: dev|prod (default: auto-detected from package name — `-dev` suffix ⇒ dev, otherwise prod)")
|
|
467
|
+
.option("--dry-run", "print planned writes, do not touch disk", false)
|
|
468
|
+
.option("--force", "overwrite existing hook scripts (settings.json is always merged, never overwritten)", false)
|
|
469
|
+
.option("--binary-only", "fetch binary and exit", false)
|
|
470
|
+
.option("--no-verify", "skip /verify call")
|
|
471
|
+
.option("--skip-binary", "skip binary download", false)
|
|
472
|
+
.option("--scope <project|user>", "installation scope: user ($HOME) or project (cwd)", "user")
|
|
473
|
+
.option("-y, --yes", "proceed without confirmation when existing settings.json is detected", false)
|
|
474
|
+
.option("--uninstall", "remove all ai-scanner hooks/scripts/binaries for the selected agents (see 'uninstall' command)", false)
|
|
475
|
+
.option("--scheduled", "enable scheduled endpoint-info collection (creates a cron/systemd/launchd entry; without this flag endpoint-info is never collected)", false)
|
|
476
|
+
.option("--interval <hours>", "scheduled scan interval in hours (min 1, default 4; requires --scheduled)", "4")
|
|
477
|
+
.option("--browser-extension", "install browser extension native messaging host for Shadow IT network monitoring (default: disabled)", false)
|
|
478
|
+
.action(cmdInstall);
|
|
479
|
+
|
|
480
|
+
program
|
|
481
|
+
.command("uninstall")
|
|
482
|
+
.description("Remove ai-scanner hooks, scripts, and binaries for one or more agents")
|
|
483
|
+
.option("--agent <list>", "comma-separated agent names (default: all detected)")
|
|
484
|
+
.option("--all", "uninstall for all detected agents")
|
|
485
|
+
.option("--scope <project|user>", "scope: user ($HOME) or project (cwd)", "user")
|
|
486
|
+
.option("-y, --yes", "proceed without confirmation prompt", false)
|
|
487
|
+
.option("--dry-run", "print what would be removed without touching disk", false)
|
|
488
|
+
.action(cmdUninstall);
|
|
489
|
+
|
|
490
|
+
program.command("doctor").action(cmdDoctor);
|
|
491
|
+
program.command("list-agents").action(cmdListAgents);
|
|
492
|
+
|
|
493
|
+
await program.parseAsync(argv);
|
|
494
|
+
}
|
package/src/detect.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const ENTRIES = [
|
|
5
|
+
["claude", ".claude"],
|
|
6
|
+
["cursor", ".cursor"],
|
|
7
|
+
["windsurf", ".windsurf"],
|
|
8
|
+
["kiro", ".kiro"],
|
|
9
|
+
["gemini", ".gemini"],
|
|
10
|
+
["codex", ".codex"],
|
|
11
|
+
["cline", ".cline"],
|
|
12
|
+
["roo", ".roo"],
|
|
13
|
+
["copilot", ".github/copilot"],
|
|
14
|
+
["qwen", ".qwen"],
|
|
15
|
+
["trae", ".trae"],
|
|
16
|
+
["amazonq", ".aws/amazonq"],
|
|
17
|
+
["jetbrains", null], // IDE-managed, skip home detection
|
|
18
|
+
["opencode", ".opencode"],
|
|
19
|
+
["aider", ".aider"],
|
|
20
|
+
["amp", ".amp"],
|
|
21
|
+
["crush", ".crush"],
|
|
22
|
+
["antigravity", ".antigravity"],
|
|
23
|
+
["continue", ".continue"],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export function detectInstalled(home = process.env.HOME || "") {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const [name, rel] of ENTRIES) {
|
|
29
|
+
if (!rel) continue;
|
|
30
|
+
const p = path.join(home, rel);
|
|
31
|
+
try {
|
|
32
|
+
const st = fs.statSync(p);
|
|
33
|
+
if (st.isDirectory()) out.push(name);
|
|
34
|
+
} catch {
|
|
35
|
+
// not present
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|