@dujavi/ai-md 0.5.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,12 +15,22 @@ 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");
23
32
  const { runScript, runScripts } = require("../lib/scripts");
24
-
25
- const scriptsDir = path.join(__dirname, "..", "scripts");
33
+ const { defaultLinkMode } = require("../lib/config-paths");
26
34
 
27
35
  function applyFlag(out, flag, args) {
28
36
  switch (flag) {
@@ -42,6 +50,21 @@ function applyFlag(out, flag, args) {
42
50
  out.fix = true;
43
51
  out.force = true;
44
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;
45
68
  case "-m":
46
69
  case "--message":
47
70
  out.message = args.shift();
@@ -64,6 +87,18 @@ function applyFlag(out, flag, args) {
64
87
  case "--dir":
65
88
  out.dir = expandHome(args.shift());
66
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;
67
102
  case "--agents":
68
103
  out.agents = String(args.shift() || "cursor")
69
104
  .split(",")
@@ -100,6 +135,11 @@ function parseArgs(argv) {
100
135
  force: false,
101
136
  dryRun: false,
102
137
  fix: false,
138
+ forceLink: false,
139
+ init: false,
140
+ noGit: false,
141
+ verbose: false,
142
+ pathsOnly: false,
103
143
  message: null,
104
144
  repo: null,
105
145
  name: null,
@@ -107,7 +147,11 @@ function parseArgs(argv) {
107
147
  from: "base",
108
148
  remote: null,
109
149
  dir: null,
110
- agents: ["cursor"],
150
+ skills: null,
151
+ rules: null,
152
+ format: null,
153
+ linkMode: null,
154
+ agents: null,
111
155
  scripts: [],
112
156
  scriptName: null,
113
157
  scriptArgs: [],
@@ -129,7 +173,6 @@ function parseArgs(argv) {
129
173
  out.cmd = args.shift();
130
174
  }
131
175
 
132
- // `script <name> [--] [args...]` — ai-md flags only before the name
133
176
  if (out.cmd === "script" || out.cmd === "run-script") {
134
177
  while (args.length) {
135
178
  const a = args[0];
@@ -144,11 +187,7 @@ function parseArgs(argv) {
144
187
  if (a.startsWith("-")) {
145
188
  args.shift();
146
189
  if (!applyFlag(out, a, args)) {
147
- fail(`unknown flag: ${a}`, {
148
- exitCode: 2,
149
- json: out.json,
150
- help: ["Run `ai-md --help`"],
151
- });
190
+ fail(`unknown flag: ${a}`, { exitCode: 2, json: out.json });
152
191
  process.exit(2);
153
192
  }
154
193
  continue;
@@ -157,11 +196,7 @@ function parseArgs(argv) {
157
196
  break;
158
197
  }
159
198
  if (!out.scriptName) {
160
- fail("script requires a name", {
161
- exitCode: 2,
162
- json: out.json,
163
- help: ["ai-md script <name> [--] [args...]"],
164
- });
199
+ fail("script requires a name", { exitCode: 2, json: out.json });
165
200
  process.exit(2);
166
201
  }
167
202
  if (args[0] === "--") args.shift();
@@ -175,7 +210,14 @@ function parseArgs(argv) {
175
210
  out.scriptArgs = args;
176
211
  break;
177
212
  }
178
- if (a === "set" || a === "show") {
213
+ if (
214
+ a === "set" ||
215
+ a === "show" ||
216
+ a === "list" ||
217
+ a === "unset" ||
218
+ a === "enable" ||
219
+ a === "disable"
220
+ ) {
179
221
  out.rest.push(a);
180
222
  continue;
181
223
  }
@@ -196,110 +238,74 @@ function parseArgs(argv) {
196
238
  }
197
239
 
198
240
  function printHelp() {
199
- 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
200
242
 
201
243
  Usage:
202
244
  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
245
 
211
- Machine config (persisted):
212
- ~/.config/ai-md/config.json dir + remote (override with AI_MD_CONFIG)
213
- Precedence: --flag > env > config file > defaults
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
214
251
 
215
- Supported harnesses (--agents):
216
- cursor ~/.cursor/skills (+ ~/.cursor/rules via install) [default]
217
- claude ~/.claude/skills
218
- agents ~/.agents/skills
252
+ Unique sync targets: cursor, claude, gemini, opencode, copilot
253
+ Shared ~/.agents/skills: agents (canonical); codex is an alias
219
254
 
220
255
  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
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
232
271
 
233
272
  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
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)
249
280
 
250
281
  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
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
259
287
  `);
260
288
  }
261
289
 
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);
290
+ function agentsOrDefault(opts, cfg) {
291
+ return opts.agents || cfg.agents || ["cursor"];
272
292
  }
273
293
 
274
294
  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);
295
+ if (
296
+ opts.remote == null &&
297
+ opts.dir == null &&
298
+ opts.linkMode == null &&
299
+ opts.agents == null
300
+ ) {
301
+ return null;
294
302
  }
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;
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 });
303
309
  }
304
310
 
305
311
  function main() {
@@ -313,6 +319,7 @@ function main() {
313
319
  let cfg = resolveConfig(process.env, {
314
320
  dir: opts.dir || undefined,
315
321
  remote: opts.remote || undefined,
322
+ linkMode: opts.linkMode || undefined,
316
323
  });
317
324
  applyEnvFromConfig(cfg);
318
325
 
@@ -321,21 +328,19 @@ function main() {
321
328
  case "config": {
322
329
  const sub = opts.rest[0] || "show";
323
330
  if (sub === "set") {
324
- if (opts.remote == null && opts.dir == null) {
325
- 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", {
326
338
  exitCode: 2,
327
339
  json: opts.json,
328
- help: [
329
- "ai-md config set --remote https://github.com/<you>/.ai-md.git --dir ~/.ai-md",
330
- ],
331
340
  });
332
341
  process.exit(2);
333
342
  }
334
- const saved = writeMachineConfig(
335
- { dir: opts.dir, remote: opts.remote },
336
- process.env,
337
- { dryRun: opts.dryRun }
338
- );
343
+ const saved = persistIfRequested(opts);
339
344
  cfg = resolveConfig(process.env);
340
345
  emit({
341
346
  data: {
@@ -343,14 +348,13 @@ function main() {
343
348
  resolved: {
344
349
  dir: cfg.dir,
345
350
  remote: cfg.remote,
351
+ linkMode: cfg.linkMode,
352
+ agents: cfg.agents,
346
353
  sources: cfg.sources,
347
354
  },
348
355
  },
349
356
  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
- ],
357
+ help: ["Run `ai-md install` or `ai-md init`"],
354
358
  });
355
359
  break;
356
360
  }
@@ -363,22 +367,188 @@ function main() {
363
367
  resolved: {
364
368
  dir: cfg.dir,
365
369
  remote: cfg.remote,
370
+ linkMode: cfg.linkMode,
371
+ agents: cfg.agents,
372
+ defaultLinkMode: defaultLinkMode(),
366
373
  sources: cfg.sources,
367
374
  },
368
375
  },
369
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,
370
404
  help: [
371
- "ai-md config set --remote <url> --dir ~/.ai-md",
372
- "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",
373
407
  ],
374
408
  });
375
409
  break;
376
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
+ 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;
545
+ }
378
546
  const saved = writeMachineConfig(
379
547
  {
380
548
  dir: opts.dir || cfg.dir,
381
549
  remote: opts.remote || cfg.remote,
550
+ agents: opts.agents || cfg.agents,
551
+ linkMode: opts.linkMode || cfg.linkMode,
382
552
  },
383
553
  process.env,
384
554
  { dryRun: opts.dryRun }
@@ -388,7 +558,13 @@ function main() {
388
558
  remote: opts.remote || undefined,
389
559
  });
390
560
  applyEnvFromConfig(cfg);
391
- const links = opts.dryRun ? [] : runInstall(cfg, opts);
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
+ });
392
568
  const scripts =
393
569
  opts.scripts.length === 0
394
570
  ? []
@@ -398,24 +574,13 @@ function main() {
398
574
  const failed = scripts.find((s) => s.exitCode !== 0);
399
575
  const data = collectStatus({
400
576
  full: opts.full,
401
- agents: opts.agents,
577
+ agents: agentsOrDefault(opts, cfg),
402
578
  from: opts.from,
403
579
  });
404
580
  emit({
405
- data: {
406
- setup: "ok",
407
- config: saved,
408
- links,
409
- scripts,
410
- ...data,
411
- },
581
+ data: { setup: "ok", config: saved, ...result, scripts, ...data },
412
582
  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
- ],
583
+ help: statusHelp(data),
419
584
  });
420
585
  if (failed) process.exit(failed.exitCode);
421
586
  break;
@@ -423,7 +588,7 @@ function main() {
423
588
  case "status": {
424
589
  const data = collectStatus({
425
590
  full: opts.full,
426
- agents: opts.agents,
591
+ agents: agentsOrDefault(opts, cfg),
427
592
  from: opts.from,
428
593
  });
429
594
  emit({ data, json: opts.json, help: statusHelp(data) });
@@ -434,14 +599,11 @@ function main() {
434
599
  const data = runDoctor({
435
600
  fix: opts.fix,
436
601
  force: opts.force,
437
- agents: opts.agents,
602
+ agents: agentsOrDefault(opts, cfg),
438
603
  dryRun: opts.dryRun,
604
+ forceLink: opts.forceLink,
439
605
  });
440
- emit({
441
- data,
442
- json: opts.json,
443
- help: data.help,
444
- });
606
+ emit({ data, json: opts.json, help: data.help });
445
607
  process.exitCode = data.after.problems.length > 0 ? 1 : 0;
446
608
  break;
447
609
  }
@@ -456,10 +618,7 @@ function main() {
456
618
  emit({
457
619
  data,
458
620
  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
- ],
621
+ help: ['Run `ai-md push -m "Init <project>"` after review'],
463
622
  });
464
623
  break;
465
624
  }
@@ -469,14 +628,7 @@ function main() {
469
628
  from: opts.from,
470
629
  dryRun: opts.dryRun,
471
630
  });
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
- });
631
+ emit({ data, json: opts.json, help: [] });
480
632
  break;
481
633
  }
482
634
  case "link-project":
@@ -487,13 +639,7 @@ function main() {
487
639
  force: opts.force,
488
640
  dryRun: opts.dryRun,
489
641
  });
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
- });
642
+ emit({ data, json: opts.json, help: [] });
497
643
  break;
498
644
  }
499
645
  case "install": {
@@ -505,7 +651,12 @@ function main() {
505
651
  });
506
652
  applyEnvFromConfig(cfg);
507
653
  }
508
- const links = runInstall(cfg, opts);
654
+ const result = runInstall(cfg, {
655
+ agents: agentsOrDefault(opts, cfg),
656
+ force: opts.force,
657
+ dryRun: opts.dryRun,
658
+ forceLink: opts.forceLink,
659
+ });
509
660
  const scripts =
510
661
  opts.scripts.length === 0
511
662
  ? []
@@ -513,30 +664,38 @@ function main() {
513
664
  dryRun: opts.dryRun,
514
665
  });
515
666
  const failed = scripts.find((s) => s.exitCode !== 0);
516
- const data = collectStatus({ full: opts.full, agents: opts.agents });
667
+ const data = collectStatus({
668
+ full: opts.full,
669
+ agents: agentsOrDefault(opts, cfg),
670
+ });
517
671
  emit({
518
- data: { install: "ok", config: saved, links, scripts, ...data },
672
+ data: { install: "ok", config: saved, ...result, scripts, ...data },
519
673
  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
- ],
674
+ help: statusHelp(data),
524
675
  });
525
676
  if (failed) process.exit(failed.exitCode);
526
677
  break;
527
678
  }
528
- case "pull":
529
- runBash(
530
- "sync-config.sh",
531
- ["pull", ...(opts.dryRun ? ["--dry-run"] : [])],
532
- process.env
533
- );
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
+ });
534
691
  break;
692
+ }
535
693
  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);
694
+ const data = runPush(cfg, {
695
+ message: opts.message,
696
+ dryRun: opts.dryRun,
697
+ });
698
+ emit({ data, json: opts.json, help: [] });
540
699
  break;
541
700
  }
542
701
  case "script":
@@ -547,11 +706,7 @@ function main() {
547
706
  emit({
548
707
  data: { scripts: [result] },
549
708
  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
- ],
709
+ help: ["Scripts live in ~/.ai-md/scripts/"],
555
710
  });
556
711
  process.exit(result.exitCode);
557
712
  break;
@@ -566,9 +721,12 @@ function main() {
566
721
  }
567
722
  } catch (err) {
568
723
  fail(err.message || String(err), {
569
- exitCode: err.code === "EINVAL" || err.code === "ENOENT" ? 2 : 1,
724
+ exitCode: err.code === "EINVAL" || err.code === "ENOENT" || err.code === "EEXIST" || err.code === "EDIRTY" ? 2 : 1,
570
725
  json: opts.json,
571
- 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`"],
572
730
  });
573
731
  process.exit(process.exitCode || 1);
574
732
  }