@dujavi/ai-md 0.4.0 → 0.6.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/bin/ai-md.js CHANGED
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
- const { spawnSync } = require("child_process");
5
- const path = require("path");
6
4
  const {
7
5
  resolveConfig,
8
6
  applyEnvFromConfig,
@@ -17,11 +15,117 @@ const {
17
15
  applyTemplate,
18
16
  linkProject,
19
17
  runDoctor,
20
- ensureCursorLinks,
21
- ensureAgentSkillLinks,
18
+ runInstall,
19
+ runPull,
20
+ runPush,
21
+ buildAndLink,
22
+ runBuild,
23
+ runRescue,
24
+ seedSkeleton,
25
+ initRepo,
26
+ harnessList,
27
+ harnessShow,
28
+ harnessSet,
29
+ harnessUnset,
30
+ harnessEnable,
22
31
  } = require("../lib/commands");
32
+ const { runScript, runScripts } = require("../lib/scripts");
33
+ const { defaultLinkMode } = require("../lib/config-paths");
23
34
 
24
- const scriptsDir = path.join(__dirname, "..", "scripts");
35
+ function applyFlag(out, flag, args) {
36
+ switch (flag) {
37
+ case "--json":
38
+ out.json = true;
39
+ return true;
40
+ case "--full":
41
+ out.full = true;
42
+ return true;
43
+ case "--force":
44
+ out.force = true;
45
+ return true;
46
+ case "--dry-run":
47
+ out.dryRun = true;
48
+ return true;
49
+ case "--fix":
50
+ out.fix = true;
51
+ out.force = true;
52
+ return true;
53
+ case "--force-link":
54
+ out.forceLink = true;
55
+ return true;
56
+ case "--init":
57
+ out.init = true;
58
+ return true;
59
+ case "--no-git":
60
+ out.noGit = true;
61
+ return true;
62
+ case "--verbose":
63
+ out.verbose = true;
64
+ return true;
65
+ case "--paths-only":
66
+ out.pathsOnly = true;
67
+ return true;
68
+ case "-m":
69
+ case "--message":
70
+ out.message = args.shift();
71
+ return true;
72
+ case "--repo":
73
+ out.repo = args.shift();
74
+ return true;
75
+ case "--name":
76
+ out.name = args.shift();
77
+ return true;
78
+ case "--project":
79
+ out.project = args.shift();
80
+ return true;
81
+ case "--from":
82
+ out.from = args.shift();
83
+ return true;
84
+ case "--remote":
85
+ out.remote = args.shift();
86
+ return true;
87
+ case "--dir":
88
+ out.dir = expandHome(args.shift());
89
+ return true;
90
+ case "--skills":
91
+ out.skills = args.shift();
92
+ return true;
93
+ case "--rules":
94
+ out.rules = args.shift();
95
+ return true;
96
+ case "--format":
97
+ out.format = args.shift();
98
+ return true;
99
+ case "--link-mode":
100
+ out.linkMode = args.shift();
101
+ return true;
102
+ case "--agents":
103
+ out.agents = String(args.shift() || "cursor")
104
+ .split(",")
105
+ .map((s) => s.trim())
106
+ .filter(Boolean);
107
+ return true;
108
+ case "--script": {
109
+ const name = args.shift();
110
+ if (!name) {
111
+ fail("--script requires a name", {
112
+ exitCode: 2,
113
+ json: out.json,
114
+ help: ["ai-md setup --script ensure-tools"],
115
+ });
116
+ process.exit(2);
117
+ }
118
+ out.scripts.push(name);
119
+ return true;
120
+ }
121
+ case "-h":
122
+ case "--help":
123
+ out.cmdHelp = true;
124
+ return true;
125
+ default:
126
+ return false;
127
+ }
128
+ }
25
129
 
26
130
  function parseArgs(argv) {
27
131
  const out = {
@@ -31,7 +135,11 @@ function parseArgs(argv) {
31
135
  force: false,
32
136
  dryRun: false,
33
137
  fix: false,
34
- tools: false,
138
+ forceLink: false,
139
+ init: false,
140
+ noGit: false,
141
+ verbose: false,
142
+ pathsOnly: false,
35
143
  message: null,
36
144
  repo: null,
37
145
  name: null,
@@ -39,7 +147,14 @@ function parseArgs(argv) {
39
147
  from: "base",
40
148
  remote: null,
41
149
  dir: null,
42
- agents: ["cursor"],
150
+ skills: null,
151
+ rules: null,
152
+ format: null,
153
+ linkMode: null,
154
+ agents: null,
155
+ scripts: [],
156
+ scriptName: null,
157
+ scriptArgs: [],
43
158
  rest: [],
44
159
  };
45
160
  const args = [...argv];
@@ -57,176 +172,140 @@ function parseArgs(argv) {
57
172
  } else {
58
173
  out.cmd = args.shift();
59
174
  }
60
- while (args.length) {
61
- const a = args.shift();
62
- switch (a) {
63
- case "--json":
64
- out.json = true;
65
- break;
66
- case "--full":
67
- out.full = true;
68
- break;
69
- case "--force":
70
- out.force = true;
71
- break;
72
- case "--dry-run":
73
- out.dryRun = true;
74
- break;
75
- case "--fix":
76
- out.fix = true;
77
- out.force = true;
78
- break;
79
- case "--tools":
80
- out.tools = true;
81
- break;
82
- case "-m":
83
- case "--message":
84
- out.message = args.shift();
85
- break;
86
- case "--repo":
87
- out.repo = args.shift();
88
- break;
89
- case "--name":
90
- out.name = args.shift();
91
- break;
92
- case "--project":
93
- out.project = args.shift();
94
- break;
95
- case "--from":
96
- out.from = args.shift();
97
- break;
98
- case "--remote":
99
- out.remote = args.shift();
100
- break;
101
- case "--dir":
102
- out.dir = expandHome(args.shift());
103
- break;
104
- case "--agents":
105
- out.agents = String(args.shift() || "cursor")
106
- .split(",")
107
- .map((s) => s.trim())
108
- .filter(Boolean);
109
- break;
110
- case "-h":
111
- case "--help":
112
- out.cmdHelp = true;
113
- break;
114
- case "set":
115
- case "show":
116
- // subcommand for `ai-md config set|show`
117
- out.rest.push(a);
118
- break;
119
- default:
120
- if (a.startsWith("-")) {
121
- fail(`unknown flag: ${a}`, {
122
- exitCode: 2,
123
- json: out.json,
124
- help: ["Run `ai-md --help`"],
125
- });
175
+
176
+ if (out.cmd === "script" || out.cmd === "run-script") {
177
+ while (args.length) {
178
+ const a = args[0];
179
+ if (a === "--") {
180
+ fail("script name required before `--`", {
181
+ exitCode: 2,
182
+ json: out.json,
183
+ help: ["ai-md script ensure-tools -- --dry-run"],
184
+ });
185
+ process.exit(2);
186
+ }
187
+ if (a.startsWith("-")) {
188
+ args.shift();
189
+ if (!applyFlag(out, a, args)) {
190
+ fail(`unknown flag: ${a}`, { exitCode: 2, json: out.json });
126
191
  process.exit(2);
127
192
  }
128
- out.rest.push(a);
129
- break;
193
+ continue;
194
+ }
195
+ out.scriptName = args.shift();
196
+ break;
197
+ }
198
+ if (!out.scriptName) {
199
+ fail("script requires a name", { exitCode: 2, json: out.json });
200
+ process.exit(2);
201
+ }
202
+ if (args[0] === "--") args.shift();
203
+ out.scriptArgs = args;
204
+ return out;
205
+ }
206
+
207
+ while (args.length) {
208
+ const a = args.shift();
209
+ if (a === "--") {
210
+ out.scriptArgs = args;
211
+ break;
212
+ }
213
+ if (
214
+ a === "set" ||
215
+ a === "show" ||
216
+ a === "list" ||
217
+ a === "unset" ||
218
+ a === "enable" ||
219
+ a === "disable"
220
+ ) {
221
+ out.rest.push(a);
222
+ continue;
223
+ }
224
+ if (a.startsWith("-")) {
225
+ if (!applyFlag(out, a, args)) {
226
+ fail(`unknown flag: ${a}`, {
227
+ exitCode: 2,
228
+ json: out.json,
229
+ help: ["Run `ai-md --help`"],
230
+ });
231
+ process.exit(2);
232
+ }
233
+ continue;
130
234
  }
235
+ out.rest.push(a);
131
236
  }
132
237
  return out;
133
238
  }
134
239
 
135
240
  function printHelp() {
136
- process.stdout.write(`ai-md — private ~/.ai-md: system skills/rules + templates/ + projects/ (AXI-shaped)
241
+ process.stdout.write(`ai-md — private ~/.ai-md: shared + agents dist → live harnesses
137
242
 
138
243
  Usage:
139
244
  ai-md [command] [options]
140
- npx -y @dujavi/ai-md [command] [options]
141
245
 
142
- Layout:
143
- ~/.ai-md/skills, rules System (global) base — linked to ~/.cursor
144
- ~/.ai-md/templates/<type> Project-type starters (default: base)
145
- ~/.ai-md/projects/<name> Per-app overlays (repo .cursor → here)
246
+ Layout (source):
247
+ ~/.ai-md/shared/{rules,skills} Cross-harness
248
+ ~/.ai-md/agents/<id>/ Harness overlays
249
+ ~/.ai-md/dist/<id>/ Build output (gitignored)
250
+ ~/.ai-md/templates, projects, scripts
146
251
 
147
- Machine config (persisted):
148
- ~/.config/ai-md/config.json dir + remote (override with AI_MD_CONFIG)
149
- Precedence: --flag > env > config file > defaults
252
+ Unique sync targets: cursor, claude, gemini, opencode, copilot
253
+ Shared ~/.agents/skills: agents (canonical); codex is an alias
150
254
 
151
255
  Commands:
152
- setup First-time machine setup: save config, install, optional --tools
153
- config Show persisted config (or: config set --remote/--dir)
154
- status Snapshot (default when no command) [AXI]
155
- doctor Diagnose links/projects; --fix repairs
156
- install Clone remote if needed; link ~/.cursor + optional agents
157
- pull | push Sync private git repo
158
- ensure-tools Install/update grok + quota-axi (alias: tools)
159
- init-project Seed projects/<name> from templates/<from> + link .cursor/
160
- apply-template Merge missing files from a template into a project
161
- link-project Link repo .cursor/ without seeding (alias: link)
162
- help Show this help
256
+ init Scaffold skeleton into ~/.ai-md (no remote required)
257
+ seed-skeleton Add missing recommended files only
258
+ setup Persist config + install (+ optional --init / --script)
259
+ config Show config; config set --remote/--dir/--link-mode
260
+ status Snapshot (default)
261
+ doctor Diagnose; --fix rebuilds + relinks (installed AIs only)
262
+ build Merge shared+agents dist/
263
+ rescue Promote dirty dist → agents/<id>/
264
+ install | pull Git sync + build + link (skip harnesses not installed)
265
+ push Commit + push private repo
266
+ harness list | show | set | unset | enable | disable
267
+ script Run ~/.ai-md/scripts/<name>
268
+ init-project Seed projects/<name> + link .cursor/
269
+ apply-template | link-project | link
270
+ help
163
271
 
164
272
  Options:
165
- --remote <url> Private content git remote (persisted by setup/config set/install)
166
- --dir <path> Local AI_MD_DIR (default ~/.ai-md; persisted same way)
167
- --json JSON instead of TOON
168
- --full Include paths and drift details
169
- --agents <list> Skill link targets: cursor,claude,agents (default: cursor)
170
- --tools With setup: also run ensure-tools
171
- --repo <path> App repository root
172
- --name <id> Project id under projects/
173
- --project <id> Target project for apply-template
174
- --from <id> Template under templates/ (default: base)
175
- --force Replace non-symlink paths
176
- --dry-run Preview without writing
177
- --fix doctor: repair symlinks
178
- -m, --message push commit message
273
+ --agents <list> Harnesses (default: config agents or cursor)
274
+ --force-link Link even if AI does not look installed
275
+ --force Replace real dirs / discard dirty dist on build
276
+ --link-mode <m> symlink | junction | copy
277
+ --dry-run --json --full --verbose
278
+ --remote --dir --skills --rules --format
279
+ --script <name> (repeatable on setup/install)
179
280
 
180
281
  Examples:
181
- ai-md setup --remote https://github.com/<you>/.ai-md.git --tools
182
- ai-md config set --remote https://github.com/<you>/.ai-md.git --dir ~/.ai-md
183
- ai-md config
184
- ai-md install --remote https://github.com/<you>/.ai-md.git
185
- ai-md init-project --repo ~/presenter --from base
282
+ ai-md init
283
+ ai-md setup --remote https://github.com/<you>/.ai-md.git
284
+ ai-md build && ai-md doctor --fix
285
+ ai-md harness set my-tool --skills ~/.my-tool/skills --format md
286
+ ai-md rescue --agents cursor
186
287
  `);
187
288
  }
188
289
 
189
- function runBash(script, args, env) {
190
- const result = spawnSync("bash", [path.join(scriptsDir, script), ...args], {
191
- stdio: "inherit",
192
- env,
193
- });
194
- if (result.error) {
195
- fail(result.error.message, { exitCode: 1 });
196
- process.exit(1);
197
- }
198
- process.exit(result.status === null ? 1 : result.status);
290
+ function agentsOrDefault(opts, cfg) {
291
+ return opts.agents || cfg.agents || ["cursor"];
199
292
  }
200
293
 
201
294
  function persistIfRequested(opts) {
202
- if (opts.remote == null && opts.dir == null) return null;
203
- return writeMachineConfig(
204
- { dir: opts.dir, remote: opts.remote },
205
- process.env,
206
- { dryRun: opts.dryRun }
207
- );
208
- }
209
-
210
- function runInstall(cfg, opts) {
211
- const bashArgs = ["install"];
212
- if (opts.force) bashArgs.push("--force");
213
- if (opts.dryRun) bashArgs.push("--dry-run");
214
- const result = spawnSync(
215
- "bash",
216
- [path.join(scriptsDir, "sync-config.sh"), ...bashArgs],
217
- { stdio: "inherit", env: process.env }
218
- );
219
- if (result.status !== 0) {
220
- process.exit(result.status === null ? 1 : result.status);
295
+ if (
296
+ opts.remote == null &&
297
+ opts.dir == null &&
298
+ opts.linkMode == null &&
299
+ opts.agents == null
300
+ ) {
301
+ return null;
221
302
  }
222
- const links = [
223
- ...ensureCursorLinks(cfg, { force: opts.force, dryRun: opts.dryRun }),
224
- ...ensureAgentSkillLinks(cfg, opts.agents, {
225
- force: opts.force,
226
- dryRun: opts.dryRun,
227
- }),
228
- ];
229
- return links;
303
+ const patch = {};
304
+ if (opts.dir != null) patch.dir = opts.dir;
305
+ if (opts.remote != null) patch.remote = opts.remote;
306
+ if (opts.linkMode != null) patch.linkMode = opts.linkMode;
307
+ if (opts.agents != null) patch.agents = opts.agents;
308
+ return writeMachineConfig(patch, process.env, { dryRun: opts.dryRun });
230
309
  }
231
310
 
232
311
  function main() {
@@ -237,10 +316,10 @@ function main() {
237
316
  return;
238
317
  }
239
318
 
240
- // Resolve after flags so --dir/--remote apply
241
319
  let cfg = resolveConfig(process.env, {
242
320
  dir: opts.dir || undefined,
243
321
  remote: opts.remote || undefined,
322
+ linkMode: opts.linkMode || undefined,
244
323
  });
245
324
  applyEnvFromConfig(cfg);
246
325
 
@@ -249,29 +328,33 @@ function main() {
249
328
  case "config": {
250
329
  const sub = opts.rest[0] || "show";
251
330
  if (sub === "set") {
252
- if (opts.remote == null && opts.dir == null) {
253
- fail("config set requires --remote and/or --dir", {
331
+ if (
332
+ opts.remote == null &&
333
+ opts.dir == null &&
334
+ opts.linkMode == null &&
335
+ opts.agents == null
336
+ ) {
337
+ fail("config set requires --remote/--dir/--link-mode/--agents", {
254
338
  exitCode: 2,
255
339
  json: opts.json,
256
- help: [
257
- "ai-md config set --remote https://github.com/<you>/.ai-md.git --dir ~/.ai-md",
258
- ],
259
340
  });
260
341
  process.exit(2);
261
342
  }
262
- const saved = writeMachineConfig(
263
- { dir: opts.dir, remote: opts.remote },
264
- process.env,
265
- { dryRun: opts.dryRun }
266
- );
343
+ const saved = persistIfRequested(opts);
267
344
  cfg = resolveConfig(process.env);
268
345
  emit({
269
- data: { ...saved, resolved: { dir: cfg.dir, remote: cfg.remote, sources: cfg.sources } },
346
+ data: {
347
+ ...saved,
348
+ resolved: {
349
+ dir: cfg.dir,
350
+ remote: cfg.remote,
351
+ linkMode: cfg.linkMode,
352
+ agents: cfg.agents,
353
+ sources: cfg.sources,
354
+ },
355
+ },
270
356
  json: opts.json,
271
- help: [
272
- "Run `ai-md install` if ~/.ai-md is not cloned yet",
273
- "Run `ai-md setup --remote <url> --tools` for first-time machine bootstrap",
274
- ],
357
+ help: ["Run `ai-md install` or `ai-md init`"],
275
358
  });
276
359
  break;
277
360
  }
@@ -284,25 +367,188 @@ function main() {
284
367
  resolved: {
285
368
  dir: cfg.dir,
286
369
  remote: cfg.remote,
370
+ linkMode: cfg.linkMode,
371
+ agents: cfg.agents,
372
+ defaultLinkMode: defaultLinkMode(),
287
373
  sources: cfg.sources,
288
374
  },
289
375
  },
290
376
  json: opts.json,
377
+ help: ["ai-md config set --remote <url> --dir ~/.ai-md"],
378
+ });
379
+ break;
380
+ }
381
+ case "init": {
382
+ const data = initRepo(cfg, {
383
+ noGit: opts.noGit,
384
+ force: opts.force,
385
+ dryRun: opts.dryRun,
386
+ });
387
+ writeMachineConfig(
388
+ { dir: cfg.dir, agents: ["cursor"], linkMode: cfg.linkMode },
389
+ process.env,
390
+ { dryRun: opts.dryRun }
391
+ );
392
+ cfg = resolveConfig(process.env);
393
+ let bl = null;
394
+ if (!opts.dryRun) {
395
+ bl = buildAndLink(cfg, {
396
+ agents: ["cursor"],
397
+ force: true,
398
+ forceLink: opts.forceLink,
399
+ });
400
+ }
401
+ emit({
402
+ data: { init: data, buildLink: bl },
403
+ json: opts.json,
291
404
  help: [
292
- "ai-md config set --remote <url> --dir ~/.ai-md",
293
- "Flags and env override the config file",
405
+ "Edit shared/ or agents/<id>/",
406
+ "Later: ai-md config set --remote <url> && git -C ~/.ai-md push -u origin main",
294
407
  ],
295
408
  });
296
409
  break;
297
410
  }
411
+ case "seed-skeleton": {
412
+ const data = seedSkeleton(cfg, {
413
+ force: opts.force,
414
+ dryRun: opts.dryRun,
415
+ });
416
+ emit({
417
+ data,
418
+ json: opts.json,
419
+ help: ["Run `ai-md build` after seeding"],
420
+ });
421
+ break;
422
+ }
423
+ case "build": {
424
+ const data = runBuild(cfg, {
425
+ agents: agentsOrDefault(opts, cfg),
426
+ force: opts.force,
427
+ dryRun: opts.dryRun,
428
+ verbose: opts.verbose,
429
+ forceLink: true,
430
+ includeUninstalled: true,
431
+ });
432
+ emit({
433
+ data,
434
+ json: opts.json,
435
+ help: ["Run `ai-md doctor --fix` to link installed harnesses"],
436
+ });
437
+ break;
438
+ }
439
+ case "rescue": {
440
+ const data = runRescue(cfg, {
441
+ agents: agentsOrDefault(opts, cfg),
442
+ dryRun: opts.dryRun,
443
+ });
444
+ emit({
445
+ data,
446
+ json: opts.json,
447
+ help: ["Review agents/<id>/ then `ai-md build`"],
448
+ });
449
+ break;
450
+ }
451
+ case "harness": {
452
+ const sub = opts.rest[0] || "list";
453
+ const id = opts.rest[1];
454
+ if (sub === "list") {
455
+ emit({
456
+ data: { harnesses: harnessList(cfg) },
457
+ json: opts.json,
458
+ help: ["ai-md harness show cursor"],
459
+ });
460
+ break;
461
+ }
462
+ if (sub === "show") {
463
+ if (!id) {
464
+ fail("harness show requires <id>", { exitCode: 2, json: opts.json });
465
+ process.exit(2);
466
+ }
467
+ emit({
468
+ data: harnessShow(cfg, id),
469
+ json: opts.json,
470
+ help: [],
471
+ });
472
+ break;
473
+ }
474
+ if (sub === "set") {
475
+ if (!id) {
476
+ fail("harness set requires <id>", { exitCode: 2, json: opts.json });
477
+ process.exit(2);
478
+ }
479
+ const data = harnessSet(cfg, id, {
480
+ skills: opts.skills,
481
+ rules: opts.rules,
482
+ format: opts.format,
483
+ });
484
+ emit({
485
+ data,
486
+ json: opts.json,
487
+ help: [`Put overlays in agents/${id}/`, "ai-md build"],
488
+ });
489
+ break;
490
+ }
491
+ if (sub === "unset") {
492
+ if (!id) {
493
+ fail("harness unset requires <id>", { exitCode: 2, json: opts.json });
494
+ process.exit(2);
495
+ }
496
+ emit({
497
+ data: harnessUnset(cfg, id, { pathsOnly: opts.pathsOnly }),
498
+ json: opts.json,
499
+ help: [],
500
+ });
501
+ break;
502
+ }
503
+ if (sub === "enable" || sub === "disable") {
504
+ if (!id) {
505
+ fail(`harness ${sub} requires <id>`, { exitCode: 2, json: opts.json });
506
+ process.exit(2);
507
+ }
508
+ emit({
509
+ data: harnessEnable(cfg, id, sub === "enable"),
510
+ json: opts.json,
511
+ help: ["ai-md build && ai-md doctor --fix"],
512
+ });
513
+ break;
514
+ }
515
+ fail(`unknown harness subcommand: ${sub}`, { exitCode: 2, json: opts.json });
516
+ process.exit(2);
517
+ break;
518
+ }
298
519
  case "setup": {
299
- if (!opts.remote && !cfg.machineConfig?.remote && cfg.sources.remote === "default") {
300
- // Allow default for dujavi, but recommend explicit remote for others via help
520
+ if (opts.init && !opts.remote) {
521
+ const data = initRepo(cfg, {
522
+ noGit: opts.noGit,
523
+ force: opts.force,
524
+ dryRun: opts.dryRun,
525
+ });
526
+ writeMachineConfig(
527
+ { dir: cfg.dir, agents: ["cursor"], linkMode: cfg.linkMode },
528
+ process.env,
529
+ { dryRun: opts.dryRun }
530
+ );
531
+ cfg = resolveConfig(process.env);
532
+ const bl = opts.dryRun
533
+ ? null
534
+ : buildAndLink(cfg, {
535
+ agents: agentsOrDefault(opts, cfg),
536
+ force: true,
537
+ forceLink: opts.forceLink,
538
+ });
539
+ emit({
540
+ data: { setup: "init", init: data, buildLink: bl },
541
+ json: opts.json,
542
+ help: [],
543
+ });
544
+ break;
301
545
  }
302
546
  const saved = writeMachineConfig(
303
547
  {
304
548
  dir: opts.dir || cfg.dir,
305
549
  remote: opts.remote || cfg.remote,
550
+ agents: opts.agents || cfg.agents,
551
+ linkMode: opts.linkMode || cfg.linkMode,
306
552
  },
307
553
  process.env,
308
554
  { dryRun: opts.dryRun }
@@ -312,45 +558,37 @@ function main() {
312
558
  remote: opts.remote || undefined,
313
559
  });
314
560
  applyEnvFromConfig(cfg);
315
- const links = opts.dryRun
316
- ? []
317
- : runInstall(cfg, opts);
318
- let tools = null;
319
- if (opts.tools && !opts.dryRun) {
320
- const tr = spawnSync(
321
- "bash",
322
- [path.join(scriptsDir, "ensure-agent-tools.sh")],
323
- { stdio: "inherit", env: process.env }
324
- );
325
- tools = { exitCode: tr.status };
326
- }
561
+ const result = opts.dryRun
562
+ ? { build: null, link: null }
563
+ : runInstall(cfg, {
564
+ agents: agentsOrDefault(opts, cfg),
565
+ force: opts.force,
566
+ forceLink: opts.forceLink,
567
+ });
568
+ const scripts =
569
+ opts.scripts.length === 0
570
+ ? []
571
+ : runScripts(cfg, opts.scripts, opts.scriptArgs, {
572
+ dryRun: opts.dryRun,
573
+ });
574
+ const failed = scripts.find((s) => s.exitCode !== 0);
327
575
  const data = collectStatus({
328
576
  full: opts.full,
329
- agents: opts.agents,
577
+ agents: agentsOrDefault(opts, cfg),
330
578
  from: opts.from,
331
579
  });
332
580
  emit({
333
- data: {
334
- setup: "ok",
335
- config: saved,
336
- links,
337
- tools,
338
- ...data,
339
- },
581
+ data: { setup: "ok", config: saved, ...result, scripts, ...data },
340
582
  json: opts.json,
341
- help: [
342
- opts.tools
343
- ? "Run `ai-md status` to verify"
344
- : "Run `ai-md ensure-tools` (or re-run setup with --tools)",
345
- "Run `ai-md init-project --repo <path> --from base` for a new app",
346
- ],
583
+ help: statusHelp(data),
347
584
  });
585
+ if (failed) process.exit(failed.exitCode);
348
586
  break;
349
587
  }
350
588
  case "status": {
351
589
  const data = collectStatus({
352
590
  full: opts.full,
353
- agents: opts.agents,
591
+ agents: agentsOrDefault(opts, cfg),
354
592
  from: opts.from,
355
593
  });
356
594
  emit({ data, json: opts.json, help: statusHelp(data) });
@@ -361,14 +599,11 @@ function main() {
361
599
  const data = runDoctor({
362
600
  fix: opts.fix,
363
601
  force: opts.force,
364
- agents: opts.agents,
602
+ agents: agentsOrDefault(opts, cfg),
365
603
  dryRun: opts.dryRun,
604
+ forceLink: opts.forceLink,
366
605
  });
367
- emit({
368
- data,
369
- json: opts.json,
370
- help: data.help,
371
- });
606
+ emit({ data, json: opts.json, help: data.help });
372
607
  process.exitCode = data.after.problems.length > 0 ? 1 : 0;
373
608
  break;
374
609
  }
@@ -383,10 +618,7 @@ function main() {
383
618
  emit({
384
619
  data,
385
620
  json: opts.json,
386
- help: [
387
- "Customize rules/agentic-workflow.mdc for this project",
388
- 'Run `ai-md push -m "Init <project>"` after reviewing the private repo diff',
389
- ],
621
+ help: ['Run `ai-md push -m "Init <project>"` after review'],
390
622
  });
391
623
  break;
392
624
  }
@@ -396,14 +628,7 @@ function main() {
396
628
  from: opts.from,
397
629
  dryRun: opts.dryRun,
398
630
  });
399
- emit({
400
- data,
401
- json: opts.json,
402
- help: [
403
- "Review added files under ~/.ai-md/projects/<name>/",
404
- 'Run `ai-md push -m "Apply template to <project>"` when ready',
405
- ],
406
- });
631
+ emit({ data, json: opts.json, help: [] });
407
632
  break;
408
633
  }
409
634
  case "link-project":
@@ -414,13 +639,7 @@ function main() {
414
639
  force: opts.force,
415
640
  dryRun: opts.dryRun,
416
641
  });
417
- emit({
418
- data,
419
- json: opts.json,
420
- help: [
421
- "Prefer `ai-md init-project --repo <path>` to seed from template first",
422
- ],
423
- });
642
+ emit({ data, json: opts.json, help: [] });
424
643
  break;
425
644
  }
426
645
  case "install": {
@@ -432,40 +651,66 @@ function main() {
432
651
  });
433
652
  applyEnvFromConfig(cfg);
434
653
  }
435
- const links = runInstall(cfg, opts);
436
- const data = collectStatus({ full: opts.full, agents: opts.agents });
654
+ const result = runInstall(cfg, {
655
+ agents: agentsOrDefault(opts, cfg),
656
+ force: opts.force,
657
+ dryRun: opts.dryRun,
658
+ forceLink: opts.forceLink,
659
+ });
660
+ const scripts =
661
+ opts.scripts.length === 0
662
+ ? []
663
+ : runScripts(cfg, opts.scripts, opts.scriptArgs, {
664
+ dryRun: opts.dryRun,
665
+ });
666
+ const failed = scripts.find((s) => s.exitCode !== 0);
667
+ const data = collectStatus({
668
+ full: opts.full,
669
+ agents: agentsOrDefault(opts, cfg),
670
+ });
437
671
  emit({
438
- data: { install: "ok", config: saved, links, ...data },
672
+ data: { install: "ok", config: saved, ...result, scripts, ...data },
439
673
  json: opts.json,
440
- help: [
441
- "Run `ai-md ensure-tools` to install grok + quota-axi",
442
- "Run `ai-md init-project --repo <path>` for a new app",
443
- ],
674
+ help: statusHelp(data),
444
675
  });
676
+ if (failed) process.exit(failed.exitCode);
445
677
  break;
446
678
  }
447
- case "pull":
448
- runBash(
449
- "sync-config.sh",
450
- ["pull", ...(opts.dryRun ? ["--dry-run"] : [])],
451
- process.env
452
- );
679
+ case "pull": {
680
+ const result = runPull(cfg, {
681
+ agents: agentsOrDefault(opts, cfg),
682
+ force: opts.force,
683
+ dryRun: opts.dryRun,
684
+ forceLink: opts.forceLink,
685
+ });
686
+ emit({
687
+ data: { pull: "ok", ...result },
688
+ json: opts.json,
689
+ help: ["Edit shared/ or agents/<id>/; ai-md push -m \"…\""],
690
+ });
453
691
  break;
692
+ }
454
693
  case "push": {
455
- const args = ["push"];
456
- if (opts.message) args.push("-m", opts.message);
457
- if (opts.dryRun) args.push("--dry-run");
458
- runBash("sync-config.sh", args, process.env);
694
+ const data = runPush(cfg, {
695
+ message: opts.message,
696
+ dryRun: opts.dryRun,
697
+ });
698
+ emit({ data, json: opts.json, help: [] });
459
699
  break;
460
700
  }
461
- case "ensure-tools":
462
- case "tools":
463
- runBash(
464
- "ensure-agent-tools.sh",
465
- [...(opts.dryRun ? ["--dry-run"] : [])],
466
- process.env
467
- );
701
+ case "script":
702
+ case "run-script": {
703
+ const result = runScript(cfg, opts.scriptName, opts.scriptArgs, {
704
+ dryRun: opts.dryRun,
705
+ });
706
+ emit({
707
+ data: { scripts: [result] },
708
+ json: opts.json,
709
+ help: ["Scripts live in ~/.ai-md/scripts/"],
710
+ });
711
+ process.exit(result.exitCode);
468
712
  break;
713
+ }
469
714
  default:
470
715
  fail(`unknown command: ${opts.cmd}`, {
471
716
  exitCode: 2,
@@ -476,9 +721,12 @@ function main() {
476
721
  }
477
722
  } catch (err) {
478
723
  fail(err.message || String(err), {
479
- exitCode: err.code === "EINVAL" ? 2 : 1,
724
+ exitCode: err.code === "EINVAL" || err.code === "ENOENT" || err.code === "EEXIST" || err.code === "EDIRTY" ? 2 : 1,
480
725
  json: opts.json,
481
- help: ["Run `ai-md --help`"],
726
+ help:
727
+ err.code === "EDIRTY"
728
+ ? ["ai-md rescue --agents <id>", "ai-md build --force"]
729
+ : ["Run `ai-md --help`"],
482
730
  });
483
731
  process.exit(process.exitCode || 1);
484
732
  }