@dujavi/ai-md 0.3.0 → 0.5.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
@@ -3,7 +3,13 @@
3
3
 
4
4
  const { spawnSync } = require("child_process");
5
5
  const path = require("path");
6
- const { resolveConfig } = require("../lib/config");
6
+ const {
7
+ resolveConfig,
8
+ applyEnvFromConfig,
9
+ writeMachineConfig,
10
+ readMachineConfig,
11
+ expandHome,
12
+ } = require("../lib/config");
7
13
  const { emit, fail } = require("../lib/output");
8
14
  const { collectStatus, statusHelp } = require("../lib/status");
9
15
  const {
@@ -14,14 +20,77 @@ const {
14
20
  ensureCursorLinks,
15
21
  ensureAgentSkillLinks,
16
22
  } = require("../lib/commands");
23
+ const { runScript, runScripts } = require("../lib/scripts");
17
24
 
18
25
  const scriptsDir = path.join(__dirname, "..", "scripts");
19
- const cfg = resolveConfig();
20
26
 
21
- process.env.AI_MD_DIR = cfg.dir;
22
- process.env.AI_MD_REMOTE = cfg.remote;
23
- process.env.CURSOR_MD_DIR = cfg.dir;
24
- process.env.CURSOR_MD_REMOTE = cfg.remote;
27
+ function applyFlag(out, flag, args) {
28
+ switch (flag) {
29
+ case "--json":
30
+ out.json = true;
31
+ return true;
32
+ case "--full":
33
+ out.full = true;
34
+ return true;
35
+ case "--force":
36
+ out.force = true;
37
+ return true;
38
+ case "--dry-run":
39
+ out.dryRun = true;
40
+ return true;
41
+ case "--fix":
42
+ out.fix = true;
43
+ out.force = true;
44
+ return true;
45
+ case "-m":
46
+ case "--message":
47
+ out.message = args.shift();
48
+ return true;
49
+ case "--repo":
50
+ out.repo = args.shift();
51
+ return true;
52
+ case "--name":
53
+ out.name = args.shift();
54
+ return true;
55
+ case "--project":
56
+ out.project = args.shift();
57
+ return true;
58
+ case "--from":
59
+ out.from = args.shift();
60
+ return true;
61
+ case "--remote":
62
+ out.remote = args.shift();
63
+ return true;
64
+ case "--dir":
65
+ out.dir = expandHome(args.shift());
66
+ return true;
67
+ case "--agents":
68
+ out.agents = String(args.shift() || "cursor")
69
+ .split(",")
70
+ .map((s) => s.trim())
71
+ .filter(Boolean);
72
+ return true;
73
+ case "--script": {
74
+ const name = args.shift();
75
+ if (!name) {
76
+ fail("--script requires a name", {
77
+ exitCode: 2,
78
+ json: out.json,
79
+ help: ["ai-md setup --script ensure-tools"],
80
+ });
81
+ process.exit(2);
82
+ }
83
+ out.scripts.push(name);
84
+ return true;
85
+ }
86
+ case "-h":
87
+ case "--help":
88
+ out.cmdHelp = true;
89
+ return true;
90
+ default:
91
+ return false;
92
+ }
93
+ }
25
94
 
26
95
  function parseArgs(argv) {
27
96
  const out = {
@@ -36,7 +105,12 @@ function parseArgs(argv) {
36
105
  name: null,
37
106
  project: null,
38
107
  from: "base",
108
+ remote: null,
109
+ dir: null,
39
110
  agents: ["cursor"],
111
+ scripts: [],
112
+ scriptName: null,
113
+ scriptArgs: [],
40
114
  rest: [],
41
115
  };
42
116
  const args = [...argv];
@@ -50,58 +124,26 @@ function parseArgs(argv) {
50
124
  return out;
51
125
  }
52
126
  if (first.startsWith("-")) {
53
- // flags before command → treat as status with flags
54
127
  out.cmd = "status";
55
128
  } else {
56
129
  out.cmd = args.shift();
57
130
  }
58
- while (args.length) {
59
- const a = args.shift();
60
- switch (a) {
61
- case "--json":
62
- out.json = true;
63
- break;
64
- case "--full":
65
- out.full = true;
66
- break;
67
- case "--force":
68
- out.force = true;
69
- break;
70
- case "--dry-run":
71
- out.dryRun = true;
72
- break;
73
- case "--fix":
74
- out.fix = true;
75
- out.force = true;
76
- break;
77
- case "-m":
78
- case "--message":
79
- out.message = args.shift();
80
- break;
81
- case "--repo":
82
- out.repo = args.shift();
83
- break;
84
- case "--name":
85
- out.name = args.shift();
86
- break;
87
- case "--project":
88
- out.project = args.shift();
89
- break;
90
- case "--from":
91
- out.from = args.shift();
92
- break;
93
- case "--agents":
94
- out.agents = String(args.shift() || "cursor")
95
- .split(",")
96
- .map((s) => s.trim())
97
- .filter(Boolean);
98
- break;
99
- case "-h":
100
- case "--help":
101
- out.cmdHelp = true;
102
- break;
103
- default:
104
- if (a.startsWith("-")) {
131
+
132
+ // `script <name> [--] [args...]` — ai-md flags only before the name
133
+ if (out.cmd === "script" || out.cmd === "run-script") {
134
+ while (args.length) {
135
+ const a = args[0];
136
+ if (a === "--") {
137
+ fail("script name required before `--`", {
138
+ exitCode: 2,
139
+ json: out.json,
140
+ help: ["ai-md script ensure-tools -- --dry-run"],
141
+ });
142
+ process.exit(2);
143
+ }
144
+ if (a.startsWith("-")) {
145
+ args.shift();
146
+ if (!applyFlag(out, a, args)) {
105
147
  fail(`unknown flag: ${a}`, {
106
148
  exitCode: 2,
107
149
  json: out.json,
@@ -109,9 +151,46 @@ function parseArgs(argv) {
109
151
  });
110
152
  process.exit(2);
111
153
  }
112
- out.rest.push(a);
113
- break;
154
+ continue;
155
+ }
156
+ out.scriptName = args.shift();
157
+ break;
158
+ }
159
+ if (!out.scriptName) {
160
+ fail("script requires a name", {
161
+ exitCode: 2,
162
+ json: out.json,
163
+ help: ["ai-md script <name> [--] [args...]"],
164
+ });
165
+ process.exit(2);
166
+ }
167
+ if (args[0] === "--") args.shift();
168
+ out.scriptArgs = args;
169
+ return out;
170
+ }
171
+
172
+ while (args.length) {
173
+ const a = args.shift();
174
+ if (a === "--") {
175
+ out.scriptArgs = args;
176
+ break;
114
177
  }
178
+ if (a === "set" || a === "show") {
179
+ out.rest.push(a);
180
+ continue;
181
+ }
182
+ if (a.startsWith("-")) {
183
+ if (!applyFlag(out, a, args)) {
184
+ fail(`unknown flag: ${a}`, {
185
+ exitCode: 2,
186
+ json: out.json,
187
+ help: ["Run `ai-md --help`"],
188
+ });
189
+ process.exit(2);
190
+ }
191
+ continue;
192
+ }
193
+ out.rest.push(a);
115
194
  }
116
195
  return out;
117
196
  }
@@ -127,44 +206,63 @@ Layout:
127
206
  ~/.ai-md/skills, rules System (global) base — linked to ~/.cursor
128
207
  ~/.ai-md/templates/<type> Project-type starters (default: base)
129
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
+
211
+ Machine config (persisted):
212
+ ~/.config/ai-md/config.json dir + remote (override with AI_MD_CONFIG)
213
+ Precedence: --flag > env > config file > defaults
214
+
215
+ Supported harnesses (--agents):
216
+ cursor ~/.cursor/skills (+ ~/.cursor/rules via install) [default]
217
+ claude ~/.claude/skills
218
+ agents ~/.agents/skills
130
219
 
131
220
  Commands:
221
+ setup First-time machine setup: save config, install, optional --script
222
+ config Show persisted config (or: config set --remote/--dir)
132
223
  status Snapshot (default when no command) [AXI]
133
224
  doctor Diagnose links/projects; --fix repairs
134
225
  install Clone remote if needed; link ~/.cursor + optional agents
135
226
  pull | push Sync private git repo
136
- ensure-tools Install/update grok + quota-axi (alias: tools)
227
+ script Run ~/.ai-md/scripts/<name> (alias: run-script)
137
228
  init-project Seed projects/<name> from templates/<from> + link .cursor/
138
229
  apply-template Merge missing files from a template into a project
139
230
  link-project Link repo .cursor/ without seeding (alias: link)
140
231
  help Show this help
141
232
 
142
233
  Options:
143
- --json JSON instead of TOON (status/doctor/init/…)
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
144
237
  --full Include paths and drift details
145
- --agents <list> Skill link targets: cursor,claude,agents (default: cursor)
238
+ --agents <list> Skill link harnesses: cursor,claude,agents (default: cursor)
239
+ --script <name> With setup/install: run private script (repeatable)
146
240
  --repo <path> App repository root
147
- --name <id> Project id under projects/ (default: basename)
241
+ --name <id> Project id under projects/
148
242
  --project <id> Target project for apply-template
149
243
  --from <id> Template under templates/ (default: base)
150
- --force Replace non-symlink .cursor / re-merge
151
- --dry-run Preview without writing
244
+ --force Replace non-symlink paths
245
+ --dry-run Preview without writing (before script name / before --)
152
246
  --fix doctor: repair symlinks
153
247
  -m, --message push commit message
248
+ -- End of ai-md options; remaining args go to private scripts
154
249
 
155
250
  Examples:
156
- ai-md
157
- ai-md status --json
158
- ai-md init-project --repo ~/presenter --from base
159
- ai-md apply-template --project presenter --from base
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
160
257
  ai-md doctor --fix --agents cursor,claude
258
+ ai-md init-project --repo ~/presenter --from base
161
259
  `);
162
260
  }
163
261
 
164
- function runBash(script, args) {
262
+ function runBash(script, args, env) {
165
263
  const result = spawnSync("bash", [path.join(scriptsDir, script), ...args], {
166
264
  stdio: "inherit",
167
- env: process.env,
265
+ env,
168
266
  });
169
267
  if (result.error) {
170
268
  fail(result.error.message, { exitCode: 1 });
@@ -173,6 +271,37 @@ function runBash(script, args) {
173
271
  process.exit(result.status === null ? 1 : result.status);
174
272
  }
175
273
 
274
+ 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);
294
+ }
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
+ }
304
+
176
305
  function main() {
177
306
  const opts = parseArgs(process.argv.slice(2));
178
307
 
@@ -181,8 +310,116 @@ function main() {
181
310
  return;
182
311
  }
183
312
 
313
+ let cfg = resolveConfig(process.env, {
314
+ dir: opts.dir || undefined,
315
+ remote: opts.remote || undefined,
316
+ });
317
+ applyEnvFromConfig(cfg);
318
+
184
319
  try {
185
320
  switch (opts.cmd) {
321
+ case "config": {
322
+ const sub = opts.rest[0] || "show";
323
+ if (sub === "set") {
324
+ if (opts.remote == null && opts.dir == null) {
325
+ fail("config set requires --remote and/or --dir", {
326
+ exitCode: 2,
327
+ json: opts.json,
328
+ help: [
329
+ "ai-md config set --remote https://github.com/<you>/.ai-md.git --dir ~/.ai-md",
330
+ ],
331
+ });
332
+ process.exit(2);
333
+ }
334
+ const saved = writeMachineConfig(
335
+ { dir: opts.dir, remote: opts.remote },
336
+ process.env,
337
+ { dryRun: opts.dryRun }
338
+ );
339
+ cfg = resolveConfig(process.env);
340
+ emit({
341
+ data: {
342
+ ...saved,
343
+ resolved: {
344
+ dir: cfg.dir,
345
+ remote: cfg.remote,
346
+ sources: cfg.sources,
347
+ },
348
+ },
349
+ 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
+ ],
354
+ });
355
+ break;
356
+ }
357
+ const stored = readMachineConfig();
358
+ cfg = resolveConfig(process.env);
359
+ emit({
360
+ data: {
361
+ path: stored.path,
362
+ stored: stored.raw,
363
+ resolved: {
364
+ dir: cfg.dir,
365
+ remote: cfg.remote,
366
+ sources: cfg.sources,
367
+ },
368
+ },
369
+ json: opts.json,
370
+ help: [
371
+ "ai-md config set --remote <url> --dir ~/.ai-md",
372
+ "Flags and env override the config file",
373
+ ],
374
+ });
375
+ break;
376
+ }
377
+ case "setup": {
378
+ const saved = writeMachineConfig(
379
+ {
380
+ dir: opts.dir || cfg.dir,
381
+ remote: opts.remote || cfg.remote,
382
+ },
383
+ process.env,
384
+ { dryRun: opts.dryRun }
385
+ );
386
+ cfg = resolveConfig(process.env, {
387
+ dir: opts.dir || undefined,
388
+ remote: opts.remote || undefined,
389
+ });
390
+ applyEnvFromConfig(cfg);
391
+ const links = opts.dryRun ? [] : runInstall(cfg, opts);
392
+ const scripts =
393
+ opts.scripts.length === 0
394
+ ? []
395
+ : runScripts(cfg, opts.scripts, opts.scriptArgs, {
396
+ dryRun: opts.dryRun,
397
+ });
398
+ const failed = scripts.find((s) => s.exitCode !== 0);
399
+ const data = collectStatus({
400
+ full: opts.full,
401
+ agents: opts.agents,
402
+ from: opts.from,
403
+ });
404
+ emit({
405
+ data: {
406
+ setup: "ok",
407
+ config: saved,
408
+ links,
409
+ scripts,
410
+ ...data,
411
+ },
412
+ 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
+ ],
419
+ });
420
+ if (failed) process.exit(failed.exitCode);
421
+ break;
422
+ }
186
423
  case "status": {
187
424
  const data = collectStatus({
188
425
  full: opts.full,
@@ -260,55 +497,65 @@ function main() {
260
497
  break;
261
498
  }
262
499
  case "install": {
263
- // clone/pull via bash, then Node links (incl. agents)
264
- const bashArgs = ["install"];
265
- if (opts.force) bashArgs.push("--force");
266
- if (opts.dryRun) bashArgs.push("--dry-run");
267
- const result = spawnSync(
268
- "bash",
269
- [path.join(scriptsDir, "sync-config.sh"), ...bashArgs],
270
- { stdio: "inherit", env: process.env }
271
- );
272
- if (result.status !== 0) {
273
- process.exit(result.status === null ? 1 : result.status);
500
+ const saved = persistIfRequested(opts);
501
+ if (saved) {
502
+ cfg = resolveConfig(process.env, {
503
+ dir: opts.dir || undefined,
504
+ remote: opts.remote || undefined,
505
+ });
506
+ applyEnvFromConfig(cfg);
274
507
  }
275
- const links = [
276
- ...ensureCursorLinks(cfg, { force: opts.force, dryRun: opts.dryRun }),
277
- ...ensureAgentSkillLinks(cfg, opts.agents, {
278
- force: opts.force,
279
- dryRun: opts.dryRun,
280
- }),
281
- ];
508
+ const links = runInstall(cfg, opts);
509
+ const scripts =
510
+ opts.scripts.length === 0
511
+ ? []
512
+ : runScripts(cfg, opts.scripts, opts.scriptArgs, {
513
+ dryRun: opts.dryRun,
514
+ });
515
+ const failed = scripts.find((s) => s.exitCode !== 0);
282
516
  const data = collectStatus({ full: opts.full, agents: opts.agents });
283
517
  emit({
284
- data: { install: "ok", links, ...data },
518
+ data: { install: "ok", config: saved, links, scripts, ...data },
285
519
  json: opts.json,
286
520
  help: [
287
- "Run `ai-md ensure-tools` to install grok + quota-axi",
521
+ "Run `ai-md script <name>` for private machine scripts",
288
522
  "Run `ai-md init-project --repo <path>` for a new app",
289
523
  ],
290
524
  });
525
+ if (failed) process.exit(failed.exitCode);
291
526
  break;
292
527
  }
293
528
  case "pull":
294
- runBash("sync-config.sh", [
295
- "pull",
296
- ...(opts.dryRun ? ["--dry-run"] : []),
297
- ]);
529
+ runBash(
530
+ "sync-config.sh",
531
+ ["pull", ...(opts.dryRun ? ["--dry-run"] : [])],
532
+ process.env
533
+ );
298
534
  break;
299
535
  case "push": {
300
536
  const args = ["push"];
301
537
  if (opts.message) args.push("-m", opts.message);
302
538
  if (opts.dryRun) args.push("--dry-run");
303
- runBash("sync-config.sh", args);
539
+ runBash("sync-config.sh", args, process.env);
304
540
  break;
305
541
  }
306
- case "ensure-tools":
307
- case "tools":
308
- runBash("ensure-agent-tools.sh", [
309
- ...(opts.dryRun ? ["--dry-run"] : []),
310
- ]);
542
+ case "script":
543
+ case "run-script": {
544
+ const result = runScript(cfg, opts.scriptName, opts.scriptArgs, {
545
+ dryRun: opts.dryRun,
546
+ });
547
+ emit({
548
+ data: { scripts: [result] },
549
+ 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
+ ],
555
+ });
556
+ process.exit(result.exitCode);
311
557
  break;
558
+ }
312
559
  default:
313
560
  fail(`unknown command: ${opts.cmd}`, {
314
561
  exitCode: 2,
@@ -319,7 +566,7 @@ function main() {
319
566
  }
320
567
  } catch (err) {
321
568
  fail(err.message || String(err), {
322
- exitCode: err.code === "EINVAL" ? 2 : 1,
569
+ exitCode: err.code === "EINVAL" || err.code === "ENOENT" ? 2 : 1,
323
570
  json: opts.json,
324
571
  help: ["Run `ai-md --help`"],
325
572
  });