@akinet/akidevrule 3.0.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +835 -0
  2. package/LICENSE +21 -0
  3. package/README.md +356 -0
  4. package/claude/CLAUDE.md +40 -0
  5. package/claude/agents/aki-challenger.md +38 -0
  6. package/claude/agents/aki-conduct.md +54 -0
  7. package/claude/agents/aki-hands.md +59 -0
  8. package/claude/agents/aki-judge.md +37 -0
  9. package/claude/agents/aki-maker.md +36 -0
  10. package/claude/fragments/settings.akidoc.fragment.json +15 -0
  11. package/claude/hooks/aki-update-check.mjs +160 -0
  12. package/claude/hooks/aki_version_check.mjs +83 -0
  13. package/docs/ref/macos-codesign-tcc.md +59 -0
  14. package/install.mjs +1067 -0
  15. package/install.ps1 +11 -0
  16. package/install.sh +12 -0
  17. package/package.json +52 -0
  18. package/payload/GEMINI.md +147 -0
  19. package/payload/METHOD-audit-flow.md +147 -0
  20. package/payload/METHOD-audit-subtraction.md +67 -0
  21. package/payload/METHOD-audit-zero-trust.md +49 -0
  22. package/payload/METHOD-deep-think.md +172 -0
  23. package/payload/METHOD-proportionality.md +62 -0
  24. package/payload/METHOD-ux-psych.md +60 -0
  25. package/payload/RULE-agent-behavior.md +138 -0
  26. package/payload/RULE-biz.md +51 -0
  27. package/payload/RULE-coding.md +130 -0
  28. package/payload/RULE-content-write.md +54 -0
  29. package/payload/RULE-db-design.md +26 -0
  30. package/payload/RULE-docs.md +144 -0
  31. package/payload/RULE-pattern-core.md +80 -0
  32. package/payload/RULE-release.md +215 -0
  33. package/payload/RULE-seo.md +173 -0
  34. package/payload/RULE-stack-akiNuxtCf.md +179 -0
  35. package/payload/RULE-stack-tauri.md +59 -0
  36. package/payload/RULE-ui-pattern.md +167 -0
  37. package/payload/index.md +91 -0
  38. package/skills/aki-article-writer/SKILL.md +50 -0
  39. package/skills/aki-article-writer/references/article-workflow.md +377 -0
  40. package/skills/akidevsync-notes/SKILL.md +48 -0
  41. package/skills/akidevsync-notes/scripts/notes_cli.py +212 -0
  42. package/skills/akiflow/SKILL.md +221 -0
  43. package/skills/akiflow/references/harness-facts.md +215 -0
  44. package/skills/akiflow/scripts/council-cost.sh +4 -0
  45. package/skills/akiflow/scripts/council-open.sh +4 -0
  46. package/skills/akiflow/scripts/council-read.sh +4 -0
  47. package/skills/akiflow/scripts/council-verify.sh +4 -0
  48. package/skills/akiflow/scripts/council_cost.py +149 -0
  49. package/skills/akiflow/scripts/council_open.py +323 -0
  50. package/skills/akiflow/scripts/council_read.py +148 -0
  51. package/skills/akiflow/scripts/council_verify.py +315 -0
  52. package/skills/akiflow/scripts/scythe.py +307 -0
  53. package/skills/akiflow/scripts/scythe.sh +4 -0
  54. package/skills/akigitcommit/SKILL.md +85 -0
  55. package/skills/akihelp/SKILL.md +47 -0
  56. package/skills/akihtmlreport/SKILL.md +59 -0
  57. package/skills/akilint/SKILL.md +29 -0
  58. package/skills/akirule/SKILL.md +155 -0
  59. package/skills/akiship/SKILL.md +55 -0
  60. package/skills/akithink/SKILL.md +59 -0
package/install.mjs ADDED
@@ -0,0 +1,1067 @@
1
+ #!/usr/bin/env node
2
+ // akidevrule installer — cross-platform SSOT (pure Node). See docs/arch/rule-delivery-architecture.md.
3
+ import {
4
+ existsSync,
5
+ statSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ writeFileSync,
9
+ mkdirSync,
10
+ copyFileSync,
11
+ rmSync,
12
+ renameSync,
13
+ } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { join, dirname, basename, extname, relative, isAbsolute, sep } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { spawnSync } from "node:child_process";
18
+ import readline from "node:readline";
19
+ import {
20
+ STATE_MISSING,
21
+ STATE_CURRENT,
22
+ STATE_UPDATE,
23
+ STATE_AHEAD,
24
+ STATE_UNKNOWN,
25
+ classifyState,
26
+ fetchRemoteChangelog,
27
+ localInstallPresent,
28
+ parseChangelogVersion,
29
+ } from "./claude/hooks/aki_version_check.mjs";
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Paths
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const REPO_ROOT = dirname(fileURLToPath(import.meta.url));
36
+ const HOME = homedir();
37
+ const INSTALL_ROOT = join(HOME, ".aki", "akidevrule");
38
+
39
+ const LEGACY_INSTALL_ROOT = join(HOME, ".aki", "claudedoc");
40
+ const CLAUDE_DIR = join(HOME, ".claude");
41
+ const GEMINI_DIR = join(HOME, ".gemini");
42
+ const GEMINI_RULES_DIR = join(GEMINI_DIR, "config", "rules");
43
+ const GEMINI_SKILLS_DIR = join(GEMINI_DIR, "config", "skills");
44
+
45
+ const CODEX_SKILLS_DIR = join(HOME, ".agents", "skills");
46
+ const KIRO_SKILLS_DIR = join(HOME, ".kiro", "skills");
47
+ const GROK_SKILLS_DIR = join(HOME, ".grok", "skills");
48
+
49
+ const OLD_SKILLS = ["akidoc-rules", "akidoc-flow-audit", "akidoc-techbiz-optimizer", "akiadvise"];
50
+
51
+ const IS_WIN = process.platform === "win32";
52
+
53
+ function pad2(n) {
54
+ return String(n).padStart(2, "0");
55
+ }
56
+ function stampNow(d = new Date()) {
57
+ return `${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}${pad2(d.getHours())}${pad2(d.getMinutes())}${pad2(d.getSeconds())}`;
58
+ }
59
+ function dateTimeNow(d = new Date()) {
60
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
61
+ }
62
+ const STAMP = stampNow();
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // fs helpers
66
+ // ---------------------------------------------------------------------------
67
+
68
+ function isDir(p) {
69
+ try {
70
+ return statSync(p).isDirectory();
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+ function isFile(p) {
76
+ try {
77
+ return statSync(p).isFile();
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+ function listDir(p) {
83
+ try {
84
+ return readdirSync(p);
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+ function rmrf(p) {
90
+ rmSync(p, { recursive: true, force: true });
91
+ }
92
+ function copyTree(src, dest) {
93
+ mkdirSync(dest, { recursive: true });
94
+ for (const name of readdirSync(src)) {
95
+ const s = join(src, name);
96
+ const d = join(dest, name);
97
+ if (isDir(s)) copyTree(s, d);
98
+ else copyFileSync(s, d);
99
+ }
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // ANSI color helpers
104
+ // ---------------------------------------------------------------------------
105
+
106
+ function ansiSupported() {
107
+ if (!process.stdout.isTTY) return false;
108
+ if (IS_WIN) return process.env.TERM != null || process.env.WT_SESSION != null;
109
+ return true;
110
+ }
111
+ const USE_COLOR = ansiSupported();
112
+ function c(code, text) {
113
+ return USE_COLOR ? `\x1b[${code}m${text}\x1b[0m` : text;
114
+ }
115
+ const cyanBold = (t) => c("1;36", t);
116
+ const greenBold = (t) => c("1;32", t);
117
+ const yellowBold = (t) => c("1;33", t);
118
+ const redBold = (t) => c("1;31", t);
119
+ const blueBold = (t) => c("1;34", t);
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Backup / prune
123
+ // ---------------------------------------------------------------------------
124
+
125
+ function backup(path) {
126
+ if (!existsSync(path)) return;
127
+ const dest = join(dirname(path), `${basename(path)}.akidevrule-backup-${STAMP}`);
128
+ if (isDir(path)) copyTree(path, dest);
129
+ else copyFileSync(path, dest);
130
+ }
131
+
132
+ function pruneBackups(base) {
133
+ const parent = dirname(base);
134
+ const prefix = `${basename(base)}.akidevrule-backup-`;
135
+ const candidates = listDir(parent)
136
+ .filter((n) => n.startsWith(prefix))
137
+ .map((n) => join(parent, n))
138
+ .sort((a, b) => statSync(a).mtimeMs - statSync(b).mtimeMs);
139
+ for (const old of candidates.slice(0, -2)) {
140
+ console.log(` 🗑️ Removing old backup: ${basename(old)}`);
141
+ rmrf(old);
142
+ }
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Directory sync (rsync -a --delete, scoped to Aki-owned names)
147
+ // ---------------------------------------------------------------------------
148
+
149
+ function isBuildArtifact(name) {
150
+ const ext = extname(name);
151
+ return name === "__pycache__" || ext === ".pyc" || ext === ".pyo" || name === ".DS_Store";
152
+ }
153
+
154
+ function syncDirDelete(src, dest) {
155
+ mkdirSync(dest, { recursive: true });
156
+ const srcNames = new Set(listDir(src).filter((n) => !isBuildArtifact(n)));
157
+
158
+ // Remove anything in dest not in src (also prunes artifacts from earlier installs).
159
+ for (const child of listDir(dest)) {
160
+ if (!srcNames.has(child)) rmrf(join(dest, child));
161
+ }
162
+
163
+ // Copy everything from src to dest.
164
+ for (const child of listDir(src)) {
165
+ if (isBuildArtifact(child)) continue;
166
+ const s = join(src, child);
167
+ const d = join(dest, child);
168
+ if (isDir(s)) syncDirDelete(s, d);
169
+ else copyFileSync(s, d);
170
+ }
171
+ }
172
+
173
+ function syncAkiSkills(destRoot) {
174
+ mkdirSync(destRoot, { recursive: true });
175
+ const skillsSrc = join(REPO_ROOT, "skills");
176
+ if (!isDir(skillsSrc)) return;
177
+ for (const name of listDir(skillsSrc).sort()) {
178
+ const skillDir = join(skillsSrc, name);
179
+ if (!isDir(skillDir)) continue;
180
+ syncDirDelete(skillDir, join(destRoot, name));
181
+ }
182
+ for (const oldSkill of OLD_SKILLS) {
183
+ const oldPath = join(destRoot, oldSkill);
184
+ if (existsSync(oldPath)) rmrf(oldPath);
185
+ }
186
+ }
187
+
188
+ function syncAkiAgents() {
189
+ const agentsSrc = join(REPO_ROOT, "claude", "agents");
190
+ if (!isDir(agentsSrc)) return;
191
+ const dest = join(CLAUDE_DIR, "agents");
192
+ mkdirSync(dest, { recursive: true });
193
+ for (const name of listDir(agentsSrc).sort()) {
194
+ if (name.endsWith(".md")) copyFileSync(join(agentsSrc, name), join(dest, name));
195
+ }
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Text file writing (always LF, never CRLF)
200
+ // ---------------------------------------------------------------------------
201
+
202
+ function writeTextLf(path, content) {
203
+ mkdirSync(dirname(path), { recursive: true });
204
+ writeFileSync(path, Buffer.from(content, "utf-8"));
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Git helpers
209
+ // ---------------------------------------------------------------------------
210
+
211
+ function git(repo, args) {
212
+ try {
213
+ const r = spawnSync("git", ["-C", repo, ...args], { encoding: "utf-8", timeout: 5000 });
214
+ if (r.status === 0) return r.stdout.trim();
215
+ return "";
216
+ } catch {
217
+ return "";
218
+ }
219
+ }
220
+ function gitShortHash(repo) {
221
+ return git(repo, ["rev-parse", "--short", "HEAD"]);
222
+ }
223
+ function gitTreeClean(repo) {
224
+ try {
225
+ const r = spawnSync("git", ["-C", repo, "status", "--porcelain"], {
226
+ encoding: "utf-8",
227
+ timeout: 5000,
228
+ });
229
+ return r.status === 0 && r.stdout.trim() === "";
230
+ } catch {
231
+ return false;
232
+ }
233
+ }
234
+ function installedCommit() {
235
+ try {
236
+ for (const line of readFileSync(join(INSTALL_ROOT, ".version"), "utf-8").split("\n")) {
237
+ if (line.startsWith("commit=")) return line.slice("commit=".length).trim();
238
+ }
239
+ } catch {
240
+ /* ignore */
241
+ }
242
+ return "";
243
+ }
244
+ function gitBranch(repo) {
245
+ return git(repo, ["rev-parse", "--abbrev-ref", "HEAD"]);
246
+ }
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Antigravity rule map
250
+ // ---------------------------------------------------------------------------
251
+
252
+ // Each entry: [ruleFile, trigger, description, globs] — globs is a raw JSON array string or "".
253
+ const AG_RULE_MAP = [
254
+ ["RULE-agent-behavior.md", "always_on", "", ""],
255
+ [
256
+ "RULE-coding.md",
257
+ "model_decision",
258
+ "Coding philosophy, source-of-truth discipline, error handling and security. Load when writing, reviewing or refactoring code.",
259
+ "",
260
+ ],
261
+ [
262
+ "RULE-pattern-core.md",
263
+ "model_decision",
264
+ 'Universal design laws: single source of truth, Rule of Three, single-responsibility "and"-test, composition over inheritance, naming by role. Load on any structural or decomposition decision.',
265
+ "",
266
+ ],
267
+ [
268
+ "RULE-docs.md",
269
+ "model_decision",
270
+ "Documentation structure, plan lifecycle, doc-sync behavior and the docs-versus-code drift audit. Load when writing or reorganizing docs and plans, or when checking whether existing docs still match the code.",
271
+ "",
272
+ ],
273
+ [
274
+ "RULE-content-write.md",
275
+ "model_decision",
276
+ "UI copy, semantic stability, writing style and i18n. Load when writing user-facing text.",
277
+ "",
278
+ ],
279
+ [
280
+ "RULE-stack-akiNuxtCf.md",
281
+ "glob",
282
+ "Nuxt, Vue, Cloudflare Pages and Workers, Tailwind, i18n, state and build conventions. Load when working in a Nuxt or Cloudflare project.",
283
+ '["**/*.vue", "**/*.ts", "nuxt.config.*", "server/**/*.ts"]',
284
+ ],
285
+ [
286
+ "RULE-stack-tauri.md",
287
+ "glob",
288
+ "Tauri v2 and Rust conventions, including the never-block-the-UI rule for subprocess and network commands. Load when working in a Tauri project.",
289
+ '["src-tauri/**", "**/*.rs", "tauri.conf.json"]',
290
+ ],
291
+ [
292
+ "RULE-ui-pattern.md",
293
+ "model_decision",
294
+ "Frontend design-system layer: the subtraction pass that runs before the class-tier ladder, class taxonomy, design tokens in whichever mechanism the installed framework version uses, the aggregate style-block budget, arbitrary-value policy, variant APIs and the audit playbook. Load when building, minimizing or auditing UI components and styles.",
295
+ "",
296
+ ],
297
+ [
298
+ "RULE-seo.md",
299
+ "model_decision",
300
+ "Meta limits, schema.org, robots, sitemap, Open Graph and AI visibility. Load when working on SEO or page metadata.",
301
+ "",
302
+ ],
303
+ [
304
+ "RULE-release.md",
305
+ "model_decision",
306
+ "CHANGELOG discipline, release versus deploy boundary, severity-driven version bumps and the pre-ship gate for finished-but-unpushed work. Load when preparing a release, writing a changelog, or checking whether finished work is actually shippable.",
307
+ "",
308
+ ],
309
+ [
310
+ "RULE-db-design.md",
311
+ "model_decision",
312
+ "Immutability and event sourcing, normalization, bounded contexts, flat-query discipline. Load when designing a schema, migration or database refactor.",
313
+ "",
314
+ ],
315
+ [
316
+ "RULE-biz.md",
317
+ "model_decision",
318
+ "Positioning, audience, USP, pricing, monetization and customer-psychology messaging rules. Load on any market-facing decision or when working on docs/biz content.",
319
+ "",
320
+ ],
321
+ [
322
+ "METHOD-audit-flow.md",
323
+ "model_decision",
324
+ "Method for auditing end-to-end flow integrity. Load when guards and checks keep accumulating around a flow.",
325
+ "",
326
+ ],
327
+ [
328
+ "METHOD-audit-zero-trust.md",
329
+ "model_decision",
330
+ "Strict mechanical-first audit: scope locked by command, detectors run before any opinion, findings split into exact machine matches versus pattern-level candidates, short findings-only report. Load when the user asks for an uncompromising sweep of a project or of a change and everything it touches.",
331
+ "",
332
+ ],
333
+ [
334
+ "METHOD-deep-think.md",
335
+ "model_decision",
336
+ "Deep-think method: goal excavation, first principles, mandatory critique. Load for big, hard-to-reverse or goal-ambiguous decisions.",
337
+ "",
338
+ ],
339
+ [
340
+ "METHOD-ux-psych.md",
341
+ "model_decision",
342
+ "UX psychology audit: cognitive load, recognition, feedback, defaults, motor cost and mental-model lenses with a persona walkthrough protocol. Load when evaluating an interface or user flow through user behavior.",
343
+ "",
344
+ ],
345
+ [
346
+ "METHOD-proportionality.md",
347
+ "model_decision",
348
+ "Sizing a defense against its real threat: reach, capability, motive and blast radius measured before any guard, limit, quota or accepted risk is added, kept or removed; irreversibility outranks frequency; client-side limits are UX, never enforcement. Load whenever protection is being proposed, sized or dropped.",
349
+ "",
350
+ ],
351
+ [
352
+ "METHOD-audit-subtraction.md",
353
+ "model_decision",
354
+ "Repo-wide subtraction sweep asking what no longer needs to exist, terminating on two consecutive rounds with no new findings, with Chesterton's Fence as the brake before any removal is called certain. Load when the request is to minimize or strip an existing codebase rather than to check it is correct.",
355
+ "",
356
+ ],
357
+ ];
358
+
359
+ function agDestName(ruleFile) {
360
+ let stem = ruleFile.replace(/^(RULE|METHOD)-/, "");
361
+ stem = stem.replace(/\.md$/, "");
362
+ return `akirule-${stem.toLowerCase()}.md`;
363
+ }
364
+
365
+ function installAgRules() {
366
+ mkdirSync(GEMINI_RULES_DIR, { recursive: true });
367
+ for (const name of listDir(GEMINI_RULES_DIR)) {
368
+ if (name.startsWith("akirule-") && name.endsWith(".md")) rmrf(join(GEMINI_RULES_DIR, name));
369
+ }
370
+
371
+ let written = 0;
372
+ for (const [ruleFile, trigger, desc, globs] of AG_RULE_MAP) {
373
+ const src = join(REPO_ROOT, "payload", ruleFile);
374
+ if (!isFile(src)) {
375
+ console.log(` ⚠️ ${ruleFile} listed in AG_RULE_MAP but missing from payload/`);
376
+ continue;
377
+ }
378
+ const dest = join(GEMINI_RULES_DIR, agDestName(ruleFile));
379
+
380
+ const lines = ["---", `trigger: ${trigger}`];
381
+ if (globs) lines.push(`globs: ${globs}`);
382
+ if (trigger !== "always_on" && desc) lines.push(`description: ${JSON.stringify(desc)}`);
383
+ lines.push("---");
384
+ lines.push("");
385
+ lines.push(`<!-- Generated by akidevrule from payload/${ruleFile}. Do not edit here. -->`);
386
+ lines.push("");
387
+ lines.push(readFileSync(src, "utf-8"));
388
+
389
+ writeTextLf(dest, lines.join("\n"));
390
+ written += 1;
391
+ }
392
+ return written;
393
+ }
394
+
395
+ // ---------------------------------------------------------------------------
396
+ // settings.json merge (Claude Code)
397
+ // ---------------------------------------------------------------------------
398
+
399
+ function mergeSettings(settingsPath, installRoot, claudeDir) {
400
+ const data = JSON.parse(readFileSync(settingsPath, "utf-8"));
401
+
402
+ if (typeof data.permissions !== "object" || data.permissions === null || Array.isArray(data.permissions))
403
+ data.permissions = {};
404
+ const perms = data.permissions;
405
+ if (!Array.isArray(perms.allow)) perms.allow = [];
406
+ if (!Array.isArray(perms.additionalDirectories)) perms.additionalDirectories = [];
407
+
408
+ const readRule = `Read(//${installRoot.replace(/^\/+/, "")}/**)`;
409
+ const legacyRoot = join(HOME, ".aki", "claudedoc");
410
+ const legacyReadRule = `Read(//${legacyRoot.replace(/^\/+/, "")}/**)`;
411
+ perms.allow = perms.allow.filter((x) => x !== readRule && x !== legacyReadRule);
412
+ perms.allow.push(readRule);
413
+
414
+ const legacyBashRules = [
415
+ "Bash(python3 ~/.claude/skills/**)",
416
+ "Bash(python3 ~/.aki/akidevrule/agskills/**)",
417
+ ];
418
+ const managedBashRules = [
419
+ "Bash(python3 ~/.claude/skills/*)",
420
+ "Bash(python3 ~/.aki/akidevrule/agskills/*)",
421
+ ];
422
+ perms.allow = perms.allow.filter((x) => !legacyBashRules.includes(x));
423
+ for (const rule of managedBashRules) if (!perms.allow.includes(rule)) perms.allow.push(rule);
424
+
425
+ perms.additionalDirectories = perms.additionalDirectories.filter((d) => d !== legacyRoot);
426
+ if (!perms.additionalDirectories.includes(installRoot)) perms.additionalDirectories.push(installRoot);
427
+
428
+ if (typeof data.skillOverrides !== "object" || data.skillOverrides === null || Array.isArray(data.skillOverrides))
429
+ data.skillOverrides = {};
430
+ for (const old of ["akidoc-rules", "akidoc-flow-audit", "akidoc-techbiz-optimizer"])
431
+ delete data.skillOverrides[old];
432
+ data.skillOverrides.akirule = "on";
433
+ if (!("akihelp" in data.skillOverrides)) data.skillOverrides.akihelp = "name-only";
434
+
435
+ if (typeof data.hooks !== "object" || data.hooks === null || Array.isArray(data.hooks)) data.hooks = {};
436
+ const hooks = data.hooks;
437
+ if (!Array.isArray(hooks.SessionStart)) hooks.SessionStart = [];
438
+
439
+ const isAkiUpdate = (entry) => {
440
+ try {
441
+ return (entry.hooks || []).some((h) => (h.command || "").includes("aki-update-check"));
442
+ } catch {
443
+ return false;
444
+ }
445
+ };
446
+ hooks.SessionStart = hooks.SessionStart.filter((e) => !isAkiUpdate(e));
447
+ hooks.SessionStart.push({
448
+ matcher: "startup|resume",
449
+ hooks: [
450
+ {
451
+ type: "command",
452
+ command: `node "${join(claudeDir, "hooks", "aki-update-check.mjs")}"`,
453
+ timeout: 8,
454
+ },
455
+ ],
456
+ });
457
+
458
+ writeTextLf(settingsPath, JSON.stringify(data, null, 2) + "\n");
459
+ }
460
+
461
+ // ---------------------------------------------------------------------------
462
+ // Antigravity permissions merge
463
+ // ---------------------------------------------------------------------------
464
+
465
+ function mergeAntigravityPermissions() {
466
+ if (!isDir(GEMINI_DIR)) return;
467
+
468
+ const scriptsDir = join(REPO_ROOT, "skills", "akiflow", "scripts");
469
+ const skillScripts = listDir(scriptsDir)
470
+ .filter((n) => n.endsWith(".py"))
471
+ .map((n) => `akiflow/scripts/${n}`)
472
+ .sort();
473
+ const skillRoots = [GEMINI_SKILLS_DIR, join(CLAUDE_DIR, "skills")];
474
+ const launchers = IS_WIN ? ["py -3", "python", "python3"] : ["python3"];
475
+
476
+ const managedCommands = [];
477
+ for (const launcher of launchers) {
478
+ for (const root of skillRoots) {
479
+ for (const script of skillScripts) {
480
+ const target = join(root, script);
481
+ managedCommands.push(`command(${launcher} ${target})`);
482
+ const rel = relative(HOME, target);
483
+ if (rel && !rel.startsWith("..") && !isAbsolute(rel)) {
484
+ managedCommands.push(`command(${launcher} ~/${rel.split(sep).join("/")})`);
485
+ }
486
+ }
487
+ }
488
+ }
489
+ managedCommands.push(
490
+ `write_file(${join(HOME, ".aki", "agent-council")}/)`,
491
+ `read_file(${join(HOME, ".aki", "akidevrule")}/)`,
492
+ "write_file(~/.aki/agent-council/)",
493
+ "read_file(~/.aki/akidevrule/)"
494
+ );
495
+
496
+ const legacyCommands = [
497
+ "command(python3 ~/.gemini/config/skills/*)",
498
+ "command(python3 ~/.claude/skills/*)",
499
+ "command(python3 ~/.aki/akidevrule/agskills/*)",
500
+ ];
501
+
502
+ const targetFiles = [
503
+ join(GEMINI_DIR, "antigravity-cli", "settings.json"),
504
+ join(GEMINI_DIR, "settings.json"),
505
+ ];
506
+
507
+ for (const target of targetFiles) {
508
+ let data = {};
509
+ if (isFile(target)) {
510
+ try {
511
+ data = JSON.parse(readFileSync(target, "utf-8"));
512
+ } catch (e) {
513
+ console.log(` \u26a0\ufe0f Antigravity: ${target} is not parseable JSON (${e}) \u2014 skipped, re-run the installer to retry.`);
514
+ continue;
515
+ }
516
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
517
+ console.log(` \u26a0\ufe0f Antigravity: ${target} top-level is not an object \u2014 skipped, fix it manually.`);
518
+ continue;
519
+ }
520
+ }
521
+
522
+ if (typeof data.permissions !== "object" || data.permissions === null || Array.isArray(data.permissions))
523
+ data.permissions = {};
524
+ const perms = data.permissions;
525
+ if (!Array.isArray(perms.allow)) perms.allow = [];
526
+
527
+ let changed = false;
528
+ for (const cmd of legacyCommands) {
529
+ const i = perms.allow.indexOf(cmd);
530
+ if (i !== -1) {
531
+ perms.allow.splice(i, 1);
532
+ changed = true;
533
+ }
534
+ }
535
+ for (const cmd of managedCommands) {
536
+ if (!perms.allow.includes(cmd)) {
537
+ perms.allow.push(cmd);
538
+ changed = true;
539
+ }
540
+ }
541
+
542
+ if (changed || !isFile(target)) {
543
+ mkdirSync(dirname(target), { recursive: true });
544
+ if (isFile(target)) {
545
+ backup(target);
546
+ pruneBackups(target);
547
+ }
548
+ writeTextLf(target, JSON.stringify(data, null, 2) + "\n");
549
+ }
550
+ }
551
+ }
552
+
553
+ // ---------------------------------------------------------------------------
554
+ // Kiro permissions merge
555
+ // ---------------------------------------------------------------------------
556
+
557
+ function mergeKiroPermissions() {
558
+ const kiroDir = join(HOME, ".kiro");
559
+ if (!isDir(kiroDir)) return;
560
+
561
+ const settingsDir = join(kiroDir, "settings");
562
+ mkdirSync(settingsDir, { recursive: true });
563
+ const yamlPath = join(settingsDir, "permissions.yaml");
564
+
565
+ const managedMatches = [
566
+ "python3 ~/.kiro/skills/*",
567
+ "python3 ~/.claude/skills/*",
568
+ "python3 ~/.gemini/config/skills/*",
569
+ "python3 ~/.aki/akidevrule/agskills/*",
570
+ ];
571
+
572
+ if (!isFile(yamlPath)) {
573
+ const lines = [
574
+ "# Generated by akidevrule installer",
575
+ "rules:",
576
+ " - capability: shell",
577
+ " match:",
578
+ ];
579
+ for (const m of managedMatches) lines.push(` - "${m}"`);
580
+ lines.push(" effect: allow\n");
581
+ writeTextLf(yamlPath, lines.join("\n"));
582
+ } else {
583
+ const content = readFileSync(yamlPath, "utf-8");
584
+ const needed = managedMatches.filter((m) => !content.includes(m));
585
+ if (needed.length) {
586
+ backup(yamlPath);
587
+ pruneBackups(yamlPath);
588
+ const appendLines = ["", " - capability: shell", " match:"];
589
+ for (const m of needed) appendLines.push(` - "${m}"`);
590
+ appendLines.push(" effect: allow\n");
591
+ writeTextLf(yamlPath, content.replace(/\s+$/, "") + "\n" + appendLines.join("\n"));
592
+ }
593
+ }
594
+ }
595
+
596
+ // ---------------------------------------------------------------------------
597
+ // skills.json for Antigravity
598
+ // ---------------------------------------------------------------------------
599
+
600
+ function updateSkillsJson() {
601
+ const skillsJson = join(GEMINI_DIR, "config", "skills.json");
602
+ mkdirSync(dirname(skillsJson), { recursive: true });
603
+ let data = {};
604
+ if (existsSync(skillsJson)) {
605
+ try {
606
+ data = JSON.parse(readFileSync(skillsJson, "utf-8"));
607
+ } catch {
608
+ data = {};
609
+ }
610
+ }
611
+ if (!Array.isArray(data.entries)) data.entries = [];
612
+ const absPath = join(INSTALL_ROOT, "agskills");
613
+ const tildePath = "~/.aki/akidevrule/agskills";
614
+ for (const p of [absPath, tildePath]) {
615
+ if (!data.entries.some((e) => e && typeof e === "object" && e.path === p)) data.entries.push({ path: p });
616
+ }
617
+ writeTextLf(skillsJson, JSON.stringify(data, null, 2) + "\n");
618
+ }
619
+
620
+ // ---------------------------------------------------------------------------
621
+ // Version status (shared with the SessionStart hook)
622
+ // ---------------------------------------------------------------------------
623
+
624
+ async function getVersionStatus() {
625
+ const present = localInstallPresent(INSTALL_ROOT);
626
+ let localVersion = null;
627
+ if (present) {
628
+ try {
629
+ localVersion = parseChangelogVersion(readFileSync(join(INSTALL_ROOT, "CHANGELOG.md"), "utf-8"));
630
+ } catch {
631
+ localVersion = null;
632
+ }
633
+ }
634
+ const remoteText = await fetchRemoteChangelog();
635
+ const remoteVersion = remoteText ? parseChangelogVersion(remoteText) : null;
636
+ const state = classifyState(present, localVersion, remoteVersion);
637
+ return { state, localVersion, remoteVersion };
638
+ }
639
+
640
+ async function printVersionCheck() {
641
+ const { state, localVersion, remoteVersion } = await getVersionStatus();
642
+ console.log(cyanBold("=== akidevrule version check ==="));
643
+ let installedStr;
644
+ if (state === STATE_MISSING) installedStr = "not installed";
645
+ else if (localVersion === null) installedStr = "installed (no released version found -- Unreleased-only?)";
646
+ else installedStr = localVersion;
647
+ console.log(`Installed: ${installedStr}`);
648
+ console.log(`Latest: ${remoteVersion || "unknown (network/parse error)"}`);
649
+ const labels = {
650
+ [STATE_MISSING]: redBold("not installed"),
651
+ [STATE_CURRENT]: greenBold("up to date"),
652
+ [STATE_UPDATE]: yellowBold("update available"),
653
+ [STATE_AHEAD]: cyanBold("ahead of remote"),
654
+ [STATE_UNKNOWN]: yellowBold("unknown (network/parse error)"),
655
+ };
656
+ console.log(`Status: ${labels[state]}`);
657
+ if (state === STATE_UPDATE) console.log("Update: npx @akinet/akidevrule@latest");
658
+ else if (state === STATE_MISSING) console.log("Install: npx @akinet/akidevrule@latest");
659
+ console.log(cyanBold("================================="));
660
+ }
661
+
662
+ // ---------------------------------------------------------------------------
663
+ // inspect_status (pre-install preview)
664
+ // ---------------------------------------------------------------------------
665
+
666
+ async function inspectStatus() {
667
+ console.log(cyanBold("=== SYSTEM STATUS CHECK BEFORE INSTALL ==="));
668
+
669
+ const head = gitShortHash(REPO_ROOT);
670
+ const sameCheckout = !!head && installedCommit() === head && gitTreeClean(REPO_ROOT);
671
+ if (sameCheckout)
672
+ console.log(`\u2705 Already installed from this exact commit (${head}, clean tree) \u2014 reinstalling refreshes identical content.`);
673
+
674
+ const { state, localVersion, remoteVersion } = await getVersionStatus();
675
+ if (state === STATE_CURRENT)
676
+ console.log(`\u2139\ufe0f Installed release ${localVersion} matches remote; the overwrite below comes from this checkout, which may carry [Unreleased] work.`);
677
+ else if (state === STATE_UPDATE)
678
+ console.log(`\ud83d\udd14 A newer akidevrule is available: ${localVersion} \u2192 ${remoteVersion} (this install only refreshes local files at the current repo checkout's version).`);
679
+ else if (state === STATE_MISSING)
680
+ console.log("\ud83d\udce6 Fresh install \u2014 akidevrule is not currently installed on this machine.");
681
+ else if (state === STATE_UNKNOWN)
682
+ console.log("\u26a0\ufe0f Could not reach the remote CHANGELOG to compare versions (network/parse error) \u2014 proceeding with local install only.");
683
+ else if (state === STATE_AHEAD)
684
+ console.log(`\u2139\ufe0f Local install (${localVersion || "Unreleased-only"}) is ahead of or diverged from remote (${remoteVersion}).`);
685
+
686
+ if (isDir(INSTALL_ROOT)) console.log(`\ud83d\udce6 Payload rules: will ${yellowBold("OVERWRITE")} ${INSTALL_ROOT}`);
687
+ else console.log(`\ud83d\udce6 Payload rules: will ${greenBold("CREATE")} at ${INSTALL_ROOT}`);
688
+
689
+ const claudeMd = join(CLAUDE_DIR, "CLAUDE.md");
690
+ if (isFile(claudeMd)) console.log(`\ud83d\udcdd Global CLAUDE.md: will ${yellowBold("OVERWRITE")} ${claudeMd} (backed up)`);
691
+ else console.log(`\ud83d\udcdd Global CLAUDE.md: will ${greenBold("CREATE")} ${claudeMd}`);
692
+
693
+ const skillsSrc = join(REPO_ROOT, "skills");
694
+ if (isDir(skillsSrc)) {
695
+ for (const name of listDir(skillsSrc).sort()) {
696
+ const skillDir = join(skillsSrc, name);
697
+ if (!isDir(skillDir)) continue;
698
+ const destSkill = join(CLAUDE_DIR, "skills", name, "SKILL.md");
699
+ if (isFile(destSkill)) console.log(`\ud83d\udd27 Skill ${name}: will ${yellowBold("OVERWRITE")} ${destSkill}`);
700
+ else console.log(`\ud83d\udd27 Skill ${name}: will ${greenBold("CREATE")} ${destSkill}`);
701
+ }
702
+ }
703
+
704
+ const agentsSrc = join(REPO_ROOT, "claude", "agents");
705
+ if (isDir(agentsSrc)) {
706
+ for (const name of listDir(agentsSrc).sort()) {
707
+ if (!name.endsWith(".md")) continue;
708
+ const destAgent = join(CLAUDE_DIR, "agents", name);
709
+ if (isFile(destAgent)) console.log(`\ud83e\udde0 Agent ${name}: will ${yellowBold("OVERWRITE")} ${destAgent}`);
710
+ else console.log(`\ud83e\udde0 Agent ${name}: will ${greenBold("CREATE")} ${destAgent}`);
711
+ }
712
+ }
713
+
714
+ const oldPresent = OLD_SKILLS.filter((s) => isDir(join(CLAUDE_DIR, "skills", s)));
715
+ if (oldPresent.length) console.log(`\ud83d\uddd1\ufe0f Old skills will be REMOVED: ${redBold(oldPresent.join(" "))}`);
716
+
717
+ console.log("\u2699\ufe0f settings: checking permissions and skill overrides across platforms...");
718
+ const settingsPath = join(CLAUDE_DIR, "settings.json");
719
+ if (isFile(settingsPath)) {
720
+ try {
721
+ const data = JSON.parse(readFileSync(settingsPath, "utf-8"));
722
+ const readRule = `Read(//${INSTALL_ROOT.replace(/^\/+/, "")}/**)`;
723
+ const allow = (data.permissions && data.permissions.allow) || [];
724
+ if (allow.includes(readRule)) console.log(" \u2705 Claude Code: Read permission for payload already granted.");
725
+ else console.log(" \u26a0\ufe0f Claude Code: Read permission MISSING. Will be added automatically.");
726
+ if (allow.includes("Bash(python3 ~/.claude/skills/*)")) console.log(" \u2705 Claude Code: Skill scripts Bash execution permission already granted.");
727
+ else console.log(" \u26a0\ufe0f Claude Code: Skill scripts Bash execution permission will be added.");
728
+ const overrides = data.skillOverrides || {};
729
+ if (overrides.akirule === "on") console.log(" \u2705 Claude Code: akirule skill is already enabled (on).");
730
+ else console.log(" \u26a0\ufe0f Claude Code: Will auto-enable skill: akirule");
731
+ const stale = ["akidoc-rules", "akidoc-flow-audit", "akidoc-techbiz-optimizer"].filter((s) => s in overrides);
732
+ if (stale.length) console.log(` \ud83d\uddd1\ufe0f Claude Code: Stale skillOverrides will be REMOVED: ${stale.join(", ")}`);
733
+ } catch (e) {
734
+ console.log(` \u274c Error reading Claude settings.json: ${e}`);
735
+ }
736
+ } else {
737
+ console.log(" \u26a0\ufe0f Claude Code: No settings.json yet. Will be CREATED.");
738
+ }
739
+
740
+ if (isDir(GEMINI_DIR)) {
741
+ const agSettings = join(GEMINI_DIR, "antigravity-cli", "settings.json");
742
+ if (isFile(agSettings)) {
743
+ try {
744
+ const data = JSON.parse(readFileSync(agSettings, "utf-8"));
745
+ const allow = (data.permissions && data.permissions.allow) || [];
746
+ const probeRule = `command(python3 ${join(GEMINI_SKILLS_DIR, "akiflow", "scripts", "council_open.py")})`;
747
+ if (allow.includes(probeRule)) console.log(" \u2705 Antigravity CLI: per-script skill permissions already granted.");
748
+ else console.log(" \u26a0\ufe0f Antigravity CLI: per-script skill permissions will be added.");
749
+ } catch (e) {
750
+ console.log(` \u274c Error reading Antigravity settings: ${e}`);
751
+ }
752
+ } else {
753
+ console.log(" \u26a0\ufe0f Antigravity CLI: settings.json will be updated with skill permissions.");
754
+ }
755
+ }
756
+
757
+ console.log(cyanBold("===================================================="));
758
+ return sameCheckout;
759
+ }
760
+
761
+ // ---------------------------------------------------------------------------
762
+ // print_summary (post-install)
763
+ // ---------------------------------------------------------------------------
764
+
765
+ function printSummary() {
766
+ console.log(`\n${greenBold("=== INSTALL SUCCEEDED ===")}`);
767
+
768
+ const gitHash = gitShortHash(REPO_ROOT);
769
+ const hashSuffix = gitHash ? ` (${gitHash})` : "";
770
+ console.log(`\ud83d\udcc5 Time : ${dateTimeNow()}${hashSuffix}`);
771
+ console.log(`\ud83d\udcc2 Payload : ${INSTALL_ROOT}`);
772
+ console.log(`\ud83d\udd27 Skills : ${join(CLAUDE_DIR, "skills")}${sep}`);
773
+ console.log();
774
+
775
+ console.log(cyanBold("Rules deployed:"));
776
+ const indexPath = join(INSTALL_ROOT, "index.md");
777
+ if (isFile(indexPath)) {
778
+ const tierColors = [["Core", redBold], ["Contextual", yellowBold], ["Analytical", blueBold]];
779
+ for (const line of readFileSync(indexPath, "utf-8").split("\n")) {
780
+ const m = line.match(/^\|\s*`([^`]+)`\s*\|\s*([^|]+?)\s*\|(.+)\|/);
781
+ if (m) {
782
+ const fname = m[1];
783
+ const tier = m[2].trim();
784
+ const desc = m[3].trim();
785
+ const entry = tierColors.find(([k]) => tier.startsWith(k));
786
+ const tierStr = entry ? entry[1](tier.padEnd(12)) : tier.padEnd(12);
787
+ console.log(` ${tierStr} ${fname.padEnd(30)} ${desc}`);
788
+ }
789
+ }
790
+ }
791
+
792
+ console.log();
793
+ console.log(cyanBold("Skills deployed:"));
794
+ const claudeSkills = join(CLAUDE_DIR, "skills");
795
+ if (isDir(claudeSkills)) {
796
+ for (const name of listDir(claudeSkills).sort()) {
797
+ if (isDir(join(claudeSkills, name))) console.log(` \ud83d\udd27 ${name}`);
798
+ }
799
+ }
800
+
801
+ const agentsSrc = join(REPO_ROOT, "claude", "agents");
802
+ if (isDir(agentsSrc)) {
803
+ console.log();
804
+ console.log(cyanBold(`Agents deployed (${join(CLAUDE_DIR, "agents")} \u2014 your own agents there are untouched):`));
805
+ for (const name of listDir(agentsSrc).sort()) {
806
+ if (name.endsWith(".md")) console.log(` \ud83e\udde0 ${basename(name, ".md")}`);
807
+ }
808
+ }
809
+
810
+ console.log();
811
+ console.log(cyanBold("Other CLI skill roots synced (harmless if that CLI isn't installed):"));
812
+ console.log(` \ud83e\udd16 Codex CLI : ${CODEX_SKILLS_DIR}`);
813
+ console.log(` \ud83e\udd16 Kiro CLI : ${KIRO_SKILLS_DIR}`);
814
+ console.log(` \ud83e\udd16 Grok CLI : ${GROK_SKILLS_DIR}`);
815
+
816
+ console.log();
817
+ console.log(cyanBold("Permissions configured:"));
818
+ console.log(" \u2699\ufe0f Claude Code : Read(~/.aki/akidevrule/**), Bash(python3 ~/.claude/skills/*)");
819
+ if (isDir(GEMINI_DIR))
820
+ console.log(" \u2699\ufe0f Antigravity : per-script command() rules (akiflow scripts \u00d72 roots \u00d72 path renderings \u00d7the platform's python launchers), write_file(~/.aki/agent-council/), read_file(~/.aki/akidevrule/)");
821
+ if (isDir(join(HOME, ".kiro")))
822
+ console.log(" \u2699\ufe0f Kiro CLI : capability:shell (python3 ~/.kiro/skills/*, ~/.claude/skills/*)");
823
+
824
+ console.log();
825
+ console.log(cyanBold("Hooks deployed:"));
826
+ console.log(" \ud83d\udce2 aki-update-check (SessionStart, notify-only) \u2014 notifies when a new rule version is available");
827
+
828
+ console.log(`\n${greenBold("==============================")}`);
829
+ }
830
+
831
+ // ---------------------------------------------------------------------------
832
+ // Prompt helper
833
+ // ---------------------------------------------------------------------------
834
+
835
+ function ask(question) {
836
+ return new Promise((resolve) => {
837
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
838
+ rl.question(question, (answer) => {
839
+ rl.close();
840
+ resolve(answer);
841
+ });
842
+ });
843
+ }
844
+
845
+ // ---------------------------------------------------------------------------
846
+ // Main install logic
847
+ // ---------------------------------------------------------------------------
848
+
849
+ async function runInstall() {
850
+ // Legacy migration: ~/.aki/claudedoc -> ~/.aki/akidevrule
851
+ if (isDir(LEGACY_INSTALL_ROOT) && !existsSync(INSTALL_ROOT)) {
852
+ mkdirSync(dirname(INSTALL_ROOT), { recursive: true });
853
+ renameSync(LEGACY_INSTALL_ROOT, INSTALL_ROOT);
854
+ console.log(`\ud83d\udce6 Migrated legacy install root: ${LEGACY_INSTALL_ROOT} \u2192 ${INSTALL_ROOT}`);
855
+ }
856
+
857
+ const sameCheckout = await inspectStatus();
858
+
859
+ // Skip the prompt only for a byte-identical overwrite (same commit, clean tree).
860
+ if (process.stdin.isTTY && !sameCheckout) {
861
+ const confirm = await ask("Proceed with install/update given the changes above? (y/n): ");
862
+ if (confirm.trim().toLowerCase() !== "y") {
863
+ console.log("Install cancelled.");
864
+ process.exit(1);
865
+ }
866
+ }
867
+ console.log("Installing...");
868
+
869
+ // --- 1. Payload -> INSTALL_ROOT ---
870
+ mkdirSync(INSTALL_ROOT, { recursive: true });
871
+ mkdirSync(join(CLAUDE_DIR, "skills"), { recursive: true });
872
+
873
+ const payloadSrc = join(REPO_ROOT, "payload");
874
+ const EXCLUDED = new Set(["ref-ECC", ".DS_Store", "GEMINI.md"]);
875
+ const srcNames = new Set(listDir(payloadSrc).filter((n) => !EXCLUDED.has(n)));
876
+ const MANAGED_EXT = new Set([".md"]);
877
+ // Remove managed .md files no longer in payload/ (rsync --delete). Dotfiles are managed explicitly.
878
+ for (const child of listDir(INSTALL_ROOT)) {
879
+ if (child.startsWith(".")) continue;
880
+ if (MANAGED_EXT.has(extname(child)) && !srcNames.has(child)) rmrf(join(INSTALL_ROOT, child));
881
+ }
882
+ for (const child of listDir(payloadSrc)) {
883
+ if (EXCLUDED.has(child)) continue;
884
+ const s = join(payloadSrc, child);
885
+ const d = join(INSTALL_ROOT, child);
886
+ if (isDir(s)) syncDirDelete(s, d);
887
+ else copyFileSync(s, d);
888
+ }
889
+
890
+ const stalePayload = join(INSTALL_ROOT, "METHOD-techbiz-optimizer.md");
891
+ if (existsSync(stalePayload)) rmrf(stalePayload);
892
+
893
+ copyFileSync(join(REPO_ROOT, "CHANGELOG.md"), join(INSTALL_ROOT, "CHANGELOG.md"));
894
+
895
+ const tccSrc = join(REPO_ROOT, "docs", "ref", "macos-codesign-tcc.md");
896
+ const tccDest = join(INSTALL_ROOT, "docs", "ref", "macos-codesign-tcc.md");
897
+ rmrf(join(INSTALL_ROOT, "docs"));
898
+ mkdirSync(dirname(tccDest), { recursive: true });
899
+ copyFileSync(tccSrc, tccDest);
900
+
901
+ // Write version stamp.
902
+ const versionLines = [`installed=${dateTimeNow()}`];
903
+ const installedVersion = parseChangelogVersion(readFileSync(join(INSTALL_ROOT, "CHANGELOG.md"), "utf-8"));
904
+ if (installedVersion) versionLines.push(`version=${installedVersion}`);
905
+ const stampHash = gitShortHash(REPO_ROOT);
906
+ if (stampHash) {
907
+ versionLines.push(`commit=${stampHash}`);
908
+ const branch = gitBranch(REPO_ROOT);
909
+ if (branch) versionLines.push(`branch=${branch}`);
910
+ }
911
+ writeTextLf(join(INSTALL_ROOT, ".version"), versionLines.join("\n") + "\n");
912
+
913
+ // --- 2. Skills ---
914
+ syncAkiSkills(join(CLAUDE_DIR, "skills"));
915
+ syncAkiSkills(CODEX_SKILLS_DIR);
916
+ syncAkiSkills(KIRO_SKILLS_DIR);
917
+ syncAkiSkills(GROK_SKILLS_DIR);
918
+
919
+ // --- 3. Agents ---
920
+ syncAkiAgents();
921
+
922
+ // --- 4. Hooks + source-repo ---
923
+ const hooksDest = join(CLAUDE_DIR, "hooks");
924
+ mkdirSync(hooksDest, { recursive: true });
925
+ copyFileSync(join(REPO_ROOT, "claude", "hooks", "aki-update-check.mjs"), join(hooksDest, "aki-update-check.mjs"));
926
+ copyFileSync(join(REPO_ROOT, "claude", "hooks", "aki_version_check.mjs"), join(hooksDest, "aki_version_check.mjs"));
927
+ // Remove orphaned Python hooks left by a pre-3.0 install.
928
+ for (const legacy of ["aki-update-check.py", "aki_version_check.py"]) {
929
+ const p = join(hooksDest, legacy);
930
+ if (existsSync(p)) rmrf(p);
931
+ }
932
+ // hooks/ is shared with the user's own hooks, so only our bytecode is removed, never the whole cache.
933
+ const pyCache = join(hooksDest, "__pycache__");
934
+ if (isDir(pyCache)) {
935
+ for (const n of readdirSync(pyCache)) if (n.startsWith("aki_version_check.")) rmrf(join(pyCache, n));
936
+ if (readdirSync(pyCache).length === 0) rmrf(pyCache);
937
+ }
938
+ writeTextLf(join(INSTALL_ROOT, ".source-repo"), REPO_ROOT + "\n");
939
+
940
+ // --- 5. CLAUDE.md ---
941
+ mkdirSync(CLAUDE_DIR, { recursive: true });
942
+ backup(join(CLAUDE_DIR, "CLAUDE.md"));
943
+ console.log("\ud83e\uddf9 Pruning CLAUDE.md backups (keeping the 2 most recent):");
944
+ pruneBackups(join(CLAUDE_DIR, "CLAUDE.md"));
945
+
946
+ const claudeMdSrc = readFileSync(join(REPO_ROOT, "claude", "CLAUDE.md"), "utf-8");
947
+ const propagateCmd = `node "${join(REPO_ROOT, "install.mjs")}"`;
948
+ const ruleSourceBlock =
949
+ "\n## akidevrule \u2014 edit source, not deployed copy (ABSOLUTE)\n\n" +
950
+ `The deployed rule files at \`${INSTALL_ROOT}\` are **overwritten on every install**.\n` +
951
+ "To change any shared rule:\n" +
952
+ `1. Edit in the **source repo**: \`${join(REPO_ROOT, "payload")}/\`\n` +
953
+ `2. Run \`${propagateCmd}\` to propagate.\n\n` +
954
+ `**NEVER edit files under \`${INSTALL_ROOT}\` directly** \u2014 changes will be silently lost on the next install.\n\n` +
955
+ "@~/.claude/CLAUDE.local.md\n";
956
+ writeTextLf(join(CLAUDE_DIR, "CLAUDE.md"), claudeMdSrc + ruleSourceBlock);
957
+
958
+ // --- 6. CLAUDE.local.md (create-only) ---
959
+ const localMd = join(CLAUDE_DIR, "CLAUDE.local.md");
960
+ if (!isFile(localMd)) {
961
+ writeTextLf(
962
+ localMd,
963
+ "# Machine-local Claude instructions\n\n" +
964
+ "This file is machine-specific and never touched by akidevrule installs.\n" +
965
+ "Add any per-machine rules here (e.g. build constraints, IDE paths, remote flags).\n"
966
+ );
967
+ console.log(`\ud83d\udcdd Created ${localMd} (machine-local template)`);
968
+ }
969
+
970
+ // --- 7. GEMINI.md (only when ~/.gemini exists) ---
971
+ if (isDir(GEMINI_DIR)) {
972
+ const geminiFile = join(GEMINI_DIR, "GEMINI.md");
973
+ const geminiLocal = join(GEMINI_DIR, "GEMINI.local.md");
974
+ const geminiMarker = "[AKIRULE-AG-OVERRIDES-";
975
+
976
+ let hadUnmanaged = false;
977
+ if (isFile(geminiFile)) hadUnmanaged = !readFileSync(geminiFile, "utf-8").includes(geminiMarker);
978
+
979
+ if (!isFile(geminiLocal)) {
980
+ writeTextLf(
981
+ geminiLocal,
982
+ "# Machine-local GEMINI instructions\n\n" +
983
+ "This file is machine-specific and never touched by akidevrule installs.\n" +
984
+ "Add machine-specific paths, CLIs, and emulator commands here.\n"
985
+ );
986
+ console.log(`\ud83d\udcdd Created ${geminiLocal} (machine-local template)`);
987
+ }
988
+
989
+ backup(geminiFile);
990
+ console.log("\ud83e\uddf9 Pruning GEMINI.md backups (keeping the 2 most recent):");
991
+ pruneBackups(geminiFile);
992
+
993
+ const d = new Date();
994
+ const geminiVersion = `V${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}`;
995
+ const geminiTemplate = readFileSync(join(REPO_ROOT, "payload", "GEMINI.md"), "utf-8");
996
+ const geminiContent = geminiTemplate.split("__VERSION__").join(geminiVersion);
997
+
998
+ const geminiSourceBlock =
999
+ "\n## 15. Shared rule source \u2014 edit source, not deployed copy (ABSOLUTE)\n\n" +
1000
+ `The deployed rule corpus at \`${INSTALL_ROOT}\` is **overwritten on every install**.\n` +
1001
+ "To change any shared rule:\n" +
1002
+ `1. Edit in the **source repo**: \`${join(REPO_ROOT, "payload")}/\` (rules) or \`${join(REPO_ROOT, "claude")}/\` (runtime assets).\n` +
1003
+ `2. Read \`${join(REPO_ROOT, "CLAUDE.md")}\` first \u2014 it lists which files must be updated together.\n` +
1004
+ `3. Run \`${propagateCmd}\` to propagate.\n\n` +
1005
+ `**NEVER edit files under \`${INSTALL_ROOT}\` directly** \u2014 changes are silently lost on the next install.\n`;
1006
+
1007
+ const localContent = readFileSync(geminiLocal, "utf-8");
1008
+ const fullGemini = geminiContent + geminiSourceBlock + "\n---\n\n" + localContent;
1009
+ writeTextLf(geminiFile, fullGemini);
1010
+
1011
+ console.log(`\ud83e\udd16 Installed ${geminiFile} (marker ${geminiMarker}${geminiVersion}])`);
1012
+ if (hadUnmanaged) {
1013
+ console.log(" \u26a0\ufe0f Your previous ~/.gemini/GEMINI.md was replaced (saved as *.akidevrule-backup-*).");
1014
+ console.log(` Move any machine-local lines from that backup into ${geminiLocal}.`);
1015
+ }
1016
+
1017
+ // --- 8. Antigravity rules ---
1018
+ const agCount = installAgRules();
1019
+ console.log(`\ud83e\udded Installed ${agCount} rule(s) to ${GEMINI_RULES_DIR} (read by AG, AG IDE and AGY)`);
1020
+
1021
+ // --- 9. Antigravity skills ---
1022
+ syncAkiSkills(GEMINI_SKILLS_DIR);
1023
+ syncAkiSkills(join(INSTALL_ROOT, "agskills"));
1024
+ updateSkillsJson();
1025
+ console.log(`\ud83d\udca1 Deployed skills to ${GEMINI_SKILLS_DIR} & updated ~/.gemini/config/skills.json`);
1026
+ console.log(" \u2139\ufe0f Antigravity discovers rules and skills at startup \u2014 restart the app or start a new agy session.");
1027
+ }
1028
+
1029
+ // --- 10. settings.json ---
1030
+ const settingsPath = join(CLAUDE_DIR, "settings.json");
1031
+ if (!isFile(settingsPath)) writeTextLf(settingsPath, "{}\n");
1032
+ backup(settingsPath);
1033
+ console.log("\ud83e\uddf9 Pruning settings.json backups (keeping the 2 most recent):");
1034
+ pruneBackups(settingsPath);
1035
+ mergeSettings(settingsPath, INSTALL_ROOT, CLAUDE_DIR);
1036
+
1037
+ // --- 11. Antigravity & Kiro permissions ---
1038
+ mergeAntigravityPermissions();
1039
+ mergeKiroPermissions();
1040
+
1041
+ printSummary();
1042
+ }
1043
+
1044
+ // ---------------------------------------------------------------------------
1045
+ // Entry point
1046
+ // ---------------------------------------------------------------------------
1047
+
1048
+ async function main() {
1049
+ const argv = process.argv.slice(2);
1050
+ if (argv.includes("-h") || argv.includes("--help")) {
1051
+ console.log("akidevrule installer \u2014 deploys shared rule corpus and skills.");
1052
+ console.log("");
1053
+ console.log("Usage: node install.mjs [--check]");
1054
+ console.log(" --check Print installed vs latest akidevrule version and exit. No install, no overwrite.");
1055
+ process.exit(0);
1056
+ }
1057
+ if (argv.includes("--check")) {
1058
+ await printVersionCheck();
1059
+ process.exit(0);
1060
+ }
1061
+ await runInstall();
1062
+ }
1063
+
1064
+ main().catch((err) => {
1065
+ console.error(err && err.stack ? err.stack : String(err));
1066
+ process.exit(1);
1067
+ });