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