@dujavi/ai-md 0.2.0 → 0.4.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/README.md +35 -43
- package/bin/ai-md.js +218 -56
- package/lib/commands.js +17 -13
- package/lib/config.js +141 -7
- package/lib/status.js +65 -50
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,69 +1,61 @@
|
|
|
1
1
|
# ai-md
|
|
2
2
|
|
|
3
|
-
Public **AXI-shaped** CLI for a **private** personal AI config directory (`~/.ai-md`)
|
|
3
|
+
Public **AXI-shaped** CLI for a **private** personal AI config directory (`~/.ai-md`).
|
|
4
4
|
|
|
5
|
-
| Layer |
|
|
6
|
-
|
|
7
|
-
| **
|
|
8
|
-
| **
|
|
5
|
+
| Layer | Path | Purpose |
|
|
6
|
+
|-------|------|---------|
|
|
7
|
+
| **System** | `skills/`, `rules/` | Global base — linked to `~/.cursor/{skills,rules}` |
|
|
8
|
+
| **Templates** | `templates/<type>/` | Project-type starters (`base`, later `forms`, …) |
|
|
9
|
+
| **Projects** | `projects/<name>/` | Per-app overlays (repo `.cursor` → here) |
|
|
9
10
|
|
|
10
|
-
No personal
|
|
11
|
+
No personal content ships in this package. Reads default to TOON + `help[]` (`--json` available).
|
|
11
12
|
|
|
12
|
-
##
|
|
13
|
+
## New machine
|
|
13
14
|
|
|
14
15
|
```bash
|
|
15
16
|
npm i -g @dujavi/ai-md
|
|
16
|
-
|
|
17
|
+
ai-md setup --remote https://github.com/<you>/.ai-md.git --tools
|
|
18
|
+
# persists ~/.config/ai-md/config.json then clones + links (+ optional grok/quota-axi)
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
Or step by step:
|
|
20
22
|
|
|
21
23
|
```bash
|
|
22
|
-
|
|
24
|
+
ai-md config set --remote https://github.com/<you>/.ai-md.git --dir ~/.ai-md
|
|
23
25
|
ai-md install
|
|
24
26
|
ai-md ensure-tools
|
|
25
27
|
```
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
Precedence: `--remote`/`--dir` flags > `AI_MD_*` env > `~/.config/ai-md/config.json` > defaults.
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
ai-md install --agents cursor,claude,agents
|
|
31
|
-
```
|
|
31
|
+
## Layout idea
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
```text
|
|
34
|
+
~/.ai-md/
|
|
35
|
+
skills/ # system — all agents see these
|
|
36
|
+
rules/
|
|
37
|
+
templates/
|
|
38
|
+
base/ # default project starter (customizable stubs only)
|
|
39
|
+
# forms/ … # other use cases
|
|
40
|
+
projects/
|
|
41
|
+
presenter/
|
|
42
|
+
sendfolio/
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
Put shared agentic skills/rules in **system** folders. Put only type-specific or per-project starters under **templates/**.
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
# Seed from projects/template + link repo/.cursor + gitignore
|
|
47
|
-
ai-md init-project --repo ~/my-app
|
|
47
|
+
## Commands
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
ai-md
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
ai-md
|
|
49
|
+
```bash
|
|
50
|
+
ai-md # status
|
|
51
|
+
ai-md init-project --repo ~/app --from base
|
|
52
|
+
ai-md apply-template --project app --from base
|
|
53
|
+
ai-md doctor --fix --agents cursor,claude
|
|
54
|
+
ai-md pull | push -m "why"
|
|
55
|
+
ai-md ensure-tools
|
|
54
56
|
```
|
|
55
57
|
|
|
56
|
-
## AXI-shaped reads
|
|
57
|
-
|
|
58
|
-
| Flag | Effect |
|
|
59
|
-
|------|--------|
|
|
60
|
-
| (default) | TOON on stdout + `help[]` |
|
|
61
|
-
| `--json` | Normalized JSON |
|
|
62
|
-
| `--full` | Paths + template drift details |
|
|
63
|
-
|
|
64
|
-
Mutations (`push`, `ensure-tools`) stay human/git-oriented.
|
|
65
|
-
|
|
66
58
|
## Environment
|
|
67
59
|
|
|
68
|
-
- `AI_MD_DIR`
|
|
69
|
-
- `
|
|
60
|
+
- `AI_MD_DIR` / `AI_MD_REMOTE` — override for one shot (also set by the CLI after reading config)
|
|
61
|
+
- `AI_MD_CONFIG` — path to machine config JSON (default `~/.config/ai-md/config.json`)
|
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 {
|
|
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 {
|
|
@@ -16,12 +22,6 @@ const {
|
|
|
16
22
|
} = require("../lib/commands");
|
|
17
23
|
|
|
18
24
|
const scriptsDir = path.join(__dirname, "..", "scripts");
|
|
19
|
-
const cfg = resolveConfig();
|
|
20
|
-
|
|
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;
|
|
25
25
|
|
|
26
26
|
function parseArgs(argv) {
|
|
27
27
|
const out = {
|
|
@@ -31,11 +31,14 @@ function parseArgs(argv) {
|
|
|
31
31
|
force: false,
|
|
32
32
|
dryRun: false,
|
|
33
33
|
fix: false,
|
|
34
|
+
tools: false,
|
|
34
35
|
message: null,
|
|
35
36
|
repo: null,
|
|
36
37
|
name: null,
|
|
37
38
|
project: null,
|
|
38
|
-
from: "
|
|
39
|
+
from: "base",
|
|
40
|
+
remote: null,
|
|
41
|
+
dir: null,
|
|
39
42
|
agents: ["cursor"],
|
|
40
43
|
rest: [],
|
|
41
44
|
};
|
|
@@ -50,7 +53,6 @@ function parseArgs(argv) {
|
|
|
50
53
|
return out;
|
|
51
54
|
}
|
|
52
55
|
if (first.startsWith("-")) {
|
|
53
|
-
// flags before command → treat as status with flags
|
|
54
56
|
out.cmd = "status";
|
|
55
57
|
} else {
|
|
56
58
|
out.cmd = args.shift();
|
|
@@ -74,6 +76,9 @@ function parseArgs(argv) {
|
|
|
74
76
|
out.fix = true;
|
|
75
77
|
out.force = true;
|
|
76
78
|
break;
|
|
79
|
+
case "--tools":
|
|
80
|
+
out.tools = true;
|
|
81
|
+
break;
|
|
77
82
|
case "-m":
|
|
78
83
|
case "--message":
|
|
79
84
|
out.message = args.shift();
|
|
@@ -90,6 +95,12 @@ function parseArgs(argv) {
|
|
|
90
95
|
case "--from":
|
|
91
96
|
out.from = args.shift();
|
|
92
97
|
break;
|
|
98
|
+
case "--remote":
|
|
99
|
+
out.remote = args.shift();
|
|
100
|
+
break;
|
|
101
|
+
case "--dir":
|
|
102
|
+
out.dir = expandHome(args.shift());
|
|
103
|
+
break;
|
|
93
104
|
case "--agents":
|
|
94
105
|
out.agents = String(args.shift() || "cursor")
|
|
95
106
|
.split(",")
|
|
@@ -100,6 +111,11 @@ function parseArgs(argv) {
|
|
|
100
111
|
case "--help":
|
|
101
112
|
out.cmdHelp = true;
|
|
102
113
|
break;
|
|
114
|
+
case "set":
|
|
115
|
+
case "show":
|
|
116
|
+
// subcommand for `ai-md config set|show`
|
|
117
|
+
out.rest.push(a);
|
|
118
|
+
break;
|
|
103
119
|
default:
|
|
104
120
|
if (a.startsWith("-")) {
|
|
105
121
|
fail(`unknown flag: ${a}`, {
|
|
@@ -117,54 +133,63 @@ function parseArgs(argv) {
|
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
function printHelp() {
|
|
120
|
-
process.stdout.write(`ai-md — private ~/.ai-md skills/rules +
|
|
136
|
+
process.stdout.write(`ai-md — private ~/.ai-md: system skills/rules + templates/ + projects/ (AXI-shaped)
|
|
121
137
|
|
|
122
138
|
Usage:
|
|
123
139
|
ai-md [command] [options]
|
|
124
140
|
npx -y @dujavi/ai-md [command] [options]
|
|
125
141
|
|
|
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)
|
|
146
|
+
|
|
147
|
+
Machine config (persisted):
|
|
148
|
+
~/.config/ai-md/config.json dir + remote (override with AI_MD_CONFIG)
|
|
149
|
+
Precedence: --flag > env > config file > defaults
|
|
150
|
+
|
|
126
151
|
Commands:
|
|
152
|
+
setup First-time machine setup: save config, install, optional --tools
|
|
153
|
+
config Show persisted config (or: config set --remote/--dir)
|
|
127
154
|
status Snapshot (default when no command) [AXI]
|
|
128
155
|
doctor Diagnose links/projects; --fix repairs
|
|
129
156
|
install Clone remote if needed; link ~/.cursor + optional agents
|
|
130
157
|
pull | push Sync private git repo
|
|
131
158
|
ensure-tools Install/update grok + quota-axi (alias: tools)
|
|
132
|
-
init-project Seed projects/<name> from
|
|
133
|
-
apply-template Merge missing template
|
|
159
|
+
init-project Seed projects/<name> from templates/<from> + link .cursor/
|
|
160
|
+
apply-template Merge missing files from a template into a project
|
|
134
161
|
link-project Link repo .cursor/ without seeding (alias: link)
|
|
135
162
|
help Show this help
|
|
136
163
|
|
|
137
164
|
Options:
|
|
138
|
-
--
|
|
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
|
|
139
168
|
--full Include paths and drift details
|
|
140
169
|
--agents <list> Skill link targets: cursor,claude,agents (default: cursor)
|
|
170
|
+
--tools With setup: also run ensure-tools
|
|
141
171
|
--repo <path> App repository root
|
|
142
|
-
--name <id> Project id under projects/
|
|
172
|
+
--name <id> Project id under projects/
|
|
143
173
|
--project <id> Target project for apply-template
|
|
144
|
-
--from <id> Template
|
|
145
|
-
--force Replace non-symlink
|
|
174
|
+
--from <id> Template under templates/ (default: base)
|
|
175
|
+
--force Replace non-symlink paths
|
|
146
176
|
--dry-run Preview without writing
|
|
147
177
|
--fix doctor: repair symlinks
|
|
148
178
|
-m, --message push commit message
|
|
149
179
|
|
|
150
|
-
Environment:
|
|
151
|
-
AI_MD_DIR Private config dir (default: ~/.ai-md)
|
|
152
|
-
AI_MD_REMOTE Clone URL (default: https://github.com/dujavi/.ai-md.git)
|
|
153
|
-
|
|
154
180
|
Examples:
|
|
155
|
-
ai-md
|
|
156
|
-
ai-md
|
|
157
|
-
ai-md
|
|
158
|
-
ai-md
|
|
159
|
-
ai-md
|
|
160
|
-
ai-md push -m "Add rule"
|
|
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
|
|
161
186
|
`);
|
|
162
187
|
}
|
|
163
188
|
|
|
164
|
-
function runBash(script, args) {
|
|
189
|
+
function runBash(script, args, env) {
|
|
165
190
|
const result = spawnSync("bash", [path.join(scriptsDir, script), ...args], {
|
|
166
191
|
stdio: "inherit",
|
|
167
|
-
env
|
|
192
|
+
env,
|
|
168
193
|
});
|
|
169
194
|
if (result.error) {
|
|
170
195
|
fail(result.error.message, { exitCode: 1 });
|
|
@@ -173,6 +198,37 @@ function runBash(script, args) {
|
|
|
173
198
|
process.exit(result.status === null ? 1 : result.status);
|
|
174
199
|
}
|
|
175
200
|
|
|
201
|
+
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);
|
|
221
|
+
}
|
|
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;
|
|
230
|
+
}
|
|
231
|
+
|
|
176
232
|
function main() {
|
|
177
233
|
const opts = parseArgs(process.argv.slice(2));
|
|
178
234
|
|
|
@@ -181,10 +237,122 @@ function main() {
|
|
|
181
237
|
return;
|
|
182
238
|
}
|
|
183
239
|
|
|
240
|
+
// Resolve after flags so --dir/--remote apply
|
|
241
|
+
let cfg = resolveConfig(process.env, {
|
|
242
|
+
dir: opts.dir || undefined,
|
|
243
|
+
remote: opts.remote || undefined,
|
|
244
|
+
});
|
|
245
|
+
applyEnvFromConfig(cfg);
|
|
246
|
+
|
|
184
247
|
try {
|
|
185
248
|
switch (opts.cmd) {
|
|
249
|
+
case "config": {
|
|
250
|
+
const sub = opts.rest[0] || "show";
|
|
251
|
+
if (sub === "set") {
|
|
252
|
+
if (opts.remote == null && opts.dir == null) {
|
|
253
|
+
fail("config set requires --remote and/or --dir", {
|
|
254
|
+
exitCode: 2,
|
|
255
|
+
json: opts.json,
|
|
256
|
+
help: [
|
|
257
|
+
"ai-md config set --remote https://github.com/<you>/.ai-md.git --dir ~/.ai-md",
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
process.exit(2);
|
|
261
|
+
}
|
|
262
|
+
const saved = writeMachineConfig(
|
|
263
|
+
{ dir: opts.dir, remote: opts.remote },
|
|
264
|
+
process.env,
|
|
265
|
+
{ dryRun: opts.dryRun }
|
|
266
|
+
);
|
|
267
|
+
cfg = resolveConfig(process.env);
|
|
268
|
+
emit({
|
|
269
|
+
data: { ...saved, resolved: { dir: cfg.dir, remote: cfg.remote, sources: cfg.sources } },
|
|
270
|
+
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
|
+
],
|
|
275
|
+
});
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
const stored = readMachineConfig();
|
|
279
|
+
cfg = resolveConfig(process.env);
|
|
280
|
+
emit({
|
|
281
|
+
data: {
|
|
282
|
+
path: stored.path,
|
|
283
|
+
stored: stored.raw,
|
|
284
|
+
resolved: {
|
|
285
|
+
dir: cfg.dir,
|
|
286
|
+
remote: cfg.remote,
|
|
287
|
+
sources: cfg.sources,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
json: opts.json,
|
|
291
|
+
help: [
|
|
292
|
+
"ai-md config set --remote <url> --dir ~/.ai-md",
|
|
293
|
+
"Flags and env override the config file",
|
|
294
|
+
],
|
|
295
|
+
});
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
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
|
|
301
|
+
}
|
|
302
|
+
const saved = writeMachineConfig(
|
|
303
|
+
{
|
|
304
|
+
dir: opts.dir || cfg.dir,
|
|
305
|
+
remote: opts.remote || cfg.remote,
|
|
306
|
+
},
|
|
307
|
+
process.env,
|
|
308
|
+
{ dryRun: opts.dryRun }
|
|
309
|
+
);
|
|
310
|
+
cfg = resolveConfig(process.env, {
|
|
311
|
+
dir: opts.dir || undefined,
|
|
312
|
+
remote: opts.remote || undefined,
|
|
313
|
+
});
|
|
314
|
+
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
|
+
}
|
|
327
|
+
const data = collectStatus({
|
|
328
|
+
full: opts.full,
|
|
329
|
+
agents: opts.agents,
|
|
330
|
+
from: opts.from,
|
|
331
|
+
});
|
|
332
|
+
emit({
|
|
333
|
+
data: {
|
|
334
|
+
setup: "ok",
|
|
335
|
+
config: saved,
|
|
336
|
+
links,
|
|
337
|
+
tools,
|
|
338
|
+
...data,
|
|
339
|
+
},
|
|
340
|
+
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
|
+
],
|
|
347
|
+
});
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
186
350
|
case "status": {
|
|
187
|
-
const data = collectStatus({
|
|
351
|
+
const data = collectStatus({
|
|
352
|
+
full: opts.full,
|
|
353
|
+
agents: opts.agents,
|
|
354
|
+
from: opts.from,
|
|
355
|
+
});
|
|
188
356
|
emit({ data, json: opts.json, help: statusHelp(data) });
|
|
189
357
|
process.exitCode = data.counts.problems > 0 ? 1 : 0;
|
|
190
358
|
break;
|
|
@@ -225,6 +393,7 @@ function main() {
|
|
|
225
393
|
case "apply-template": {
|
|
226
394
|
const data = applyTemplate({
|
|
227
395
|
project: opts.project || opts.name,
|
|
396
|
+
from: opts.from,
|
|
228
397
|
dryRun: opts.dryRun,
|
|
229
398
|
});
|
|
230
399
|
emit({
|
|
@@ -255,28 +424,18 @@ function main() {
|
|
|
255
424
|
break;
|
|
256
425
|
}
|
|
257
426
|
case "install": {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
{ stdio: "inherit", env: process.env }
|
|
266
|
-
);
|
|
267
|
-
if (result.status !== 0) {
|
|
268
|
-
process.exit(result.status === null ? 1 : result.status);
|
|
427
|
+
const saved = persistIfRequested(opts);
|
|
428
|
+
if (saved) {
|
|
429
|
+
cfg = resolveConfig(process.env, {
|
|
430
|
+
dir: opts.dir || undefined,
|
|
431
|
+
remote: opts.remote || undefined,
|
|
432
|
+
});
|
|
433
|
+
applyEnvFromConfig(cfg);
|
|
269
434
|
}
|
|
270
|
-
const links =
|
|
271
|
-
...ensureCursorLinks(cfg, { force: opts.force, dryRun: opts.dryRun }),
|
|
272
|
-
...ensureAgentSkillLinks(cfg, opts.agents, {
|
|
273
|
-
force: opts.force,
|
|
274
|
-
dryRun: opts.dryRun,
|
|
275
|
-
}),
|
|
276
|
-
];
|
|
435
|
+
const links = runInstall(cfg, opts);
|
|
277
436
|
const data = collectStatus({ full: opts.full, agents: opts.agents });
|
|
278
437
|
emit({
|
|
279
|
-
data: { install: "ok", links, ...data },
|
|
438
|
+
data: { install: "ok", config: saved, links, ...data },
|
|
280
439
|
json: opts.json,
|
|
281
440
|
help: [
|
|
282
441
|
"Run `ai-md ensure-tools` to install grok + quota-axi",
|
|
@@ -286,23 +445,26 @@ function main() {
|
|
|
286
445
|
break;
|
|
287
446
|
}
|
|
288
447
|
case "pull":
|
|
289
|
-
runBash(
|
|
290
|
-
"
|
|
291
|
-
...(opts.dryRun ? ["--dry-run"] : []),
|
|
292
|
-
|
|
448
|
+
runBash(
|
|
449
|
+
"sync-config.sh",
|
|
450
|
+
["pull", ...(opts.dryRun ? ["--dry-run"] : [])],
|
|
451
|
+
process.env
|
|
452
|
+
);
|
|
293
453
|
break;
|
|
294
454
|
case "push": {
|
|
295
455
|
const args = ["push"];
|
|
296
456
|
if (opts.message) args.push("-m", opts.message);
|
|
297
457
|
if (opts.dryRun) args.push("--dry-run");
|
|
298
|
-
runBash("sync-config.sh", args);
|
|
458
|
+
runBash("sync-config.sh", args, process.env);
|
|
299
459
|
break;
|
|
300
460
|
}
|
|
301
461
|
case "ensure-tools":
|
|
302
462
|
case "tools":
|
|
303
|
-
runBash(
|
|
304
|
-
|
|
305
|
-
|
|
463
|
+
runBash(
|
|
464
|
+
"ensure-agent-tools.sh",
|
|
465
|
+
[...(opts.dryRun ? ["--dry-run"] : [])],
|
|
466
|
+
process.env
|
|
467
|
+
);
|
|
306
468
|
break;
|
|
307
469
|
default:
|
|
308
470
|
fail(`unknown command: ${opts.cmd}`, {
|
package/lib/commands.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const {
|
|
6
6
|
resolveConfig,
|
|
7
|
+
templatePath,
|
|
7
8
|
symlinkState,
|
|
8
9
|
agentSkillTargets,
|
|
9
10
|
} = require("./config");
|
|
@@ -86,7 +87,7 @@ function copyDir(src, dest, { dryRun = false } = {}) {
|
|
|
86
87
|
function initProject({
|
|
87
88
|
repo,
|
|
88
89
|
name,
|
|
89
|
-
from = "
|
|
90
|
+
from = "base",
|
|
90
91
|
force = false,
|
|
91
92
|
dryRun = false,
|
|
92
93
|
} = {}) {
|
|
@@ -103,11 +104,13 @@ function initProject({
|
|
|
103
104
|
}
|
|
104
105
|
const absRepo = fs.realpathSync(repo);
|
|
105
106
|
const projectName = name || path.basename(absRepo);
|
|
106
|
-
const
|
|
107
|
+
const tmplDir = templatePath(cfg, from);
|
|
107
108
|
const projectDir = path.join(cfg.projectsDir, projectName);
|
|
108
109
|
|
|
109
|
-
if (!fs.existsSync(
|
|
110
|
-
const err = new Error(
|
|
110
|
+
if (!fs.existsSync(tmplDir)) {
|
|
111
|
+
const err = new Error(
|
|
112
|
+
`template not found: ${tmplDir} (available under ${cfg.templatesDir}/)`
|
|
113
|
+
);
|
|
111
114
|
err.code = "ENOENT";
|
|
112
115
|
throw err;
|
|
113
116
|
}
|
|
@@ -121,11 +124,10 @@ function initProject({
|
|
|
121
124
|
if (hasContent && !force) {
|
|
122
125
|
actions.push({ action: "exists", path: projectDir });
|
|
123
126
|
} else {
|
|
124
|
-
|
|
125
|
-
actions.push(...mergeTemplate(templateDir, projectDir, { dryRun }));
|
|
127
|
+
actions.push(...mergeTemplate(tmplDir, projectDir, { dryRun }));
|
|
126
128
|
}
|
|
127
129
|
} else {
|
|
128
|
-
actions.push(copyDir(
|
|
130
|
+
actions.push(copyDir(tmplDir, projectDir, { dryRun }));
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
const link = path.join(absRepo, ".cursor");
|
|
@@ -137,6 +139,7 @@ function initProject({
|
|
|
137
139
|
projectDir,
|
|
138
140
|
repo: absRepo,
|
|
139
141
|
from,
|
|
142
|
+
templateDir: tmplDir,
|
|
140
143
|
actions,
|
|
141
144
|
};
|
|
142
145
|
}
|
|
@@ -172,7 +175,7 @@ function mergeTemplate(templateDir, projectDir, { dryRun = false } = {}) {
|
|
|
172
175
|
return actions;
|
|
173
176
|
}
|
|
174
177
|
|
|
175
|
-
function applyTemplate({ project, dryRun = false } = {}) {
|
|
178
|
+
function applyTemplate({ project, from = "base", dryRun = false } = {}) {
|
|
176
179
|
const cfg = resolveConfig();
|
|
177
180
|
if (!project) {
|
|
178
181
|
const err = new Error("missing --project <name>");
|
|
@@ -185,13 +188,14 @@ function applyTemplate({ project, dryRun = false } = {}) {
|
|
|
185
188
|
err.code = "ENOENT";
|
|
186
189
|
throw err;
|
|
187
190
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
err
|
|
191
|
+
const tmplDir = templatePath(cfg, from);
|
|
192
|
+
if (!fs.existsSync(tmplDir)) {
|
|
193
|
+
const err = new Error(`template not found: ${tmplDir}`);
|
|
194
|
+
err.code = "ENOENT";
|
|
191
195
|
throw err;
|
|
192
196
|
}
|
|
193
|
-
const actions = mergeTemplate(
|
|
194
|
-
return { project, projectDir, actions };
|
|
197
|
+
const actions = mergeTemplate(tmplDir, projectDir, { dryRun });
|
|
198
|
+
return { project, projectDir, from, templateDir: tmplDir, actions };
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
function linkProject({ repo, name, force = false, dryRun = false } = {}) {
|
package/lib/config.js
CHANGED
|
@@ -4,27 +4,140 @@ const fs = require("fs");
|
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const DEFAULT_REMOTE = "https://github.com/dujavi/.ai-md.git";
|
|
8
|
+
|
|
9
|
+
function expandHome(p) {
|
|
10
|
+
if (!p) return p;
|
|
11
|
+
if (p === "~") return os.homedir();
|
|
12
|
+
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
|
|
13
|
+
return p;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Machine-local config (not inside the private content repo). */
|
|
17
|
+
function machineConfigPath(env = process.env) {
|
|
18
|
+
if (env.AI_MD_CONFIG) return expandHome(env.AI_MD_CONFIG);
|
|
19
|
+
const xdg = env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
20
|
+
return path.join(xdg, "ai-md", "config.json");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readMachineConfig(env = process.env) {
|
|
24
|
+
const configPath = machineConfigPath(env);
|
|
25
|
+
try {
|
|
26
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
27
|
+
const data = JSON.parse(raw);
|
|
28
|
+
return {
|
|
29
|
+
path: configPath,
|
|
30
|
+
dir: data.dir ? expandHome(String(data.dir)) : null,
|
|
31
|
+
remote: data.remote ? String(data.remote) : null,
|
|
32
|
+
raw: data,
|
|
33
|
+
};
|
|
34
|
+
} catch {
|
|
35
|
+
return { path: configPath, dir: null, remote: null, raw: null };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeMachineConfig({ dir, remote }, env = process.env, { dryRun = false } = {}) {
|
|
40
|
+
const configPath = machineConfigPath(env);
|
|
41
|
+
const existing = readMachineConfig(env).raw || {};
|
|
42
|
+
const next = {
|
|
43
|
+
...existing,
|
|
44
|
+
...(dir != null ? { dir: expandHome(dir) } : {}),
|
|
45
|
+
...(remote != null ? { remote: String(remote) } : {}),
|
|
46
|
+
updatedAt: new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
if (!next.dir && !next.remote) {
|
|
49
|
+
const err = new Error("nothing to write: provide --dir and/or --remote");
|
|
50
|
+
err.code = "EINVAL";
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
if (dryRun) {
|
|
54
|
+
return { action: "would_write", path: configPath, config: next };
|
|
55
|
+
}
|
|
56
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
57
|
+
fs.writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, {
|
|
58
|
+
mode: 0o600,
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
fs.chmodSync(configPath, 0o600);
|
|
62
|
+
} catch {
|
|
63
|
+
/* best effort */
|
|
64
|
+
}
|
|
65
|
+
return { action: "wrote", path: configPath, config: next };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Precedence: explicit opts > env > machine config file > defaults.
|
|
70
|
+
* @param {{ dir?: string, remote?: string }} [opts]
|
|
71
|
+
*/
|
|
72
|
+
function resolveConfig(env = process.env, opts = {}) {
|
|
8
73
|
const home = os.homedir();
|
|
9
|
-
const
|
|
10
|
-
|
|
74
|
+
const stored = readMachineConfig(env);
|
|
75
|
+
const defaultDir = path.join(home, ".ai-md");
|
|
76
|
+
|
|
77
|
+
const envDir = env.AI_MD_DIR || env.CURSOR_MD_DIR || null;
|
|
78
|
+
const envRemote = env.AI_MD_REMOTE || env.CURSOR_MD_REMOTE || null;
|
|
79
|
+
|
|
80
|
+
const dir = expandHome(
|
|
81
|
+
opts.dir || envDir || stored.dir || defaultDir
|
|
82
|
+
);
|
|
11
83
|
const remote =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
84
|
+
opts.remote || envRemote || stored.remote || DEFAULT_REMOTE;
|
|
85
|
+
|
|
86
|
+
const sources = {
|
|
87
|
+
dir: opts.dir
|
|
88
|
+
? "flag"
|
|
89
|
+
: envDir && !(stored.dir && expandHome(envDir) === stored.dir)
|
|
90
|
+
? "env"
|
|
91
|
+
: stored.dir
|
|
92
|
+
? "config"
|
|
93
|
+
: "default",
|
|
94
|
+
remote: opts.remote
|
|
95
|
+
? "flag"
|
|
96
|
+
: envRemote && !(stored.remote && envRemote === stored.remote)
|
|
97
|
+
? "env"
|
|
98
|
+
: stored.remote
|
|
99
|
+
? "config"
|
|
100
|
+
: "default",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const templatesDir = path.join(dir, "templates");
|
|
15
104
|
return {
|
|
16
105
|
home,
|
|
17
106
|
dir,
|
|
18
107
|
remote,
|
|
108
|
+
sources,
|
|
109
|
+
machineConfigPath: stored.path,
|
|
110
|
+
machineConfig: stored.raw,
|
|
19
111
|
cursorSkills: path.join(home, ".cursor", "skills"),
|
|
20
112
|
cursorRules: path.join(home, ".cursor", "rules"),
|
|
21
113
|
projectsDir: path.join(dir, "projects"),
|
|
22
|
-
|
|
114
|
+
templatesDir,
|
|
115
|
+
templateDir: path.join(templatesDir, "base"),
|
|
23
116
|
skillsDir: path.join(dir, "skills"),
|
|
24
117
|
rulesDir: path.join(dir, "rules"),
|
|
25
118
|
};
|
|
26
119
|
}
|
|
27
120
|
|
|
121
|
+
function applyEnvFromConfig(cfg) {
|
|
122
|
+
process.env.AI_MD_DIR = cfg.dir;
|
|
123
|
+
process.env.AI_MD_REMOTE = cfg.remote;
|
|
124
|
+
process.env.CURSOR_MD_DIR = cfg.dir;
|
|
125
|
+
process.env.CURSOR_MD_REMOTE = cfg.remote;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function templatePath(cfg, name = "base") {
|
|
129
|
+
return path.join(cfg.templatesDir, name);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function listTemplates(cfg) {
|
|
133
|
+
if (!fs.existsSync(cfg.templatesDir)) return [];
|
|
134
|
+
return fs
|
|
135
|
+
.readdirSync(cfg.templatesDir, { withFileTypes: true })
|
|
136
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith("."))
|
|
137
|
+
.map((d) => d.name)
|
|
138
|
+
.sort();
|
|
139
|
+
}
|
|
140
|
+
|
|
28
141
|
function countRules(rulesDir) {
|
|
29
142
|
if (!fs.existsSync(rulesDir)) return 0;
|
|
30
143
|
return fs
|
|
@@ -88,8 +201,29 @@ function agentSkillTargets(home, agents) {
|
|
|
88
201
|
.map((name) => ({ name, path: map[name] }));
|
|
89
202
|
}
|
|
90
203
|
|
|
204
|
+
function migrateLegacyTemplate(cfg, { dryRun = false } = {}) {
|
|
205
|
+
const legacy = path.join(cfg.projectsDir, "template");
|
|
206
|
+
const dest = templatePath(cfg, "base");
|
|
207
|
+
if (!fs.existsSync(legacy) || fs.existsSync(dest)) {
|
|
208
|
+
return { action: "skip" };
|
|
209
|
+
}
|
|
210
|
+
if (dryRun) return { action: "would_migrate", from: legacy, to: dest };
|
|
211
|
+
fs.mkdirSync(cfg.templatesDir, { recursive: true });
|
|
212
|
+
fs.renameSync(legacy, dest);
|
|
213
|
+
return { action: "migrated", from: legacy, to: dest };
|
|
214
|
+
}
|
|
215
|
+
|
|
91
216
|
module.exports = {
|
|
217
|
+
DEFAULT_REMOTE,
|
|
218
|
+
machineConfigPath,
|
|
219
|
+
readMachineConfig,
|
|
220
|
+
writeMachineConfig,
|
|
92
221
|
resolveConfig,
|
|
222
|
+
applyEnvFromConfig,
|
|
223
|
+
expandHome,
|
|
224
|
+
templatePath,
|
|
225
|
+
listTemplates,
|
|
226
|
+
migrateLegacyTemplate,
|
|
93
227
|
countRules,
|
|
94
228
|
countSkills,
|
|
95
229
|
listSkillNames,
|
package/lib/status.js
CHANGED
|
@@ -5,6 +5,9 @@ const path = require("path");
|
|
|
5
5
|
const { execFileSync } = require("child_process");
|
|
6
6
|
const {
|
|
7
7
|
resolveConfig,
|
|
8
|
+
templatePath,
|
|
9
|
+
listTemplates,
|
|
10
|
+
migrateLegacyTemplate,
|
|
8
11
|
countRules,
|
|
9
12
|
countSkills,
|
|
10
13
|
listSkillNames,
|
|
@@ -19,17 +22,17 @@ function git(repo, args) {
|
|
|
19
22
|
encoding: "utf8",
|
|
20
23
|
stdio: ["ignore", "pipe", "pipe"],
|
|
21
24
|
}).trim();
|
|
22
|
-
} catch
|
|
25
|
+
} catch {
|
|
23
26
|
return null;
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
function templateDrift(projectDir,
|
|
28
|
-
if (!fs.existsSync(
|
|
30
|
+
function templateDrift(projectDir, tmplDir) {
|
|
31
|
+
if (!fs.existsSync(tmplDir)) {
|
|
29
32
|
return { missingRules: [], missingSkills: [], extraRules: [], extraSkills: [] };
|
|
30
33
|
}
|
|
31
|
-
const tRules = new Set(listRuleNames(path.join(
|
|
32
|
-
const tSkills = new Set(listSkillNames(path.join(
|
|
34
|
+
const tRules = new Set(listRuleNames(path.join(tmplDir, "rules")));
|
|
35
|
+
const tSkills = new Set(listSkillNames(path.join(tmplDir, "skills")));
|
|
33
36
|
const pRules = new Set(listRuleNames(path.join(projectDir, "rules")));
|
|
34
37
|
const pSkills = new Set(listSkillNames(path.join(projectDir, "skills")));
|
|
35
38
|
return {
|
|
@@ -40,44 +43,38 @@ function templateDrift(projectDir, templateDir) {
|
|
|
40
43
|
};
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
function
|
|
46
|
+
function collectTemplates(cfg, { full = false } = {}) {
|
|
47
|
+
return listTemplates(cfg).map((name) => {
|
|
48
|
+
const dir = templatePath(cfg, name);
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
rules: countRules(path.join(dir, "rules")),
|
|
52
|
+
skills: countSkills(path.join(dir, "skills")),
|
|
53
|
+
...(full ? { path: dir } : {}),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function collectProjects(cfg, { full = false, from = "base" } = {}) {
|
|
44
59
|
const projects = [];
|
|
60
|
+
const tmplDir = templatePath(cfg, from);
|
|
45
61
|
if (!fs.existsSync(cfg.projectsDir)) return projects;
|
|
46
62
|
for (const name of fs.readdirSync(cfg.projectsDir).sort()) {
|
|
47
63
|
if (name.startsWith(".")) continue;
|
|
64
|
+
// legacy leftover
|
|
65
|
+
if (name === "template") continue;
|
|
48
66
|
const projectDir = path.join(cfg.projectsDir, name);
|
|
49
67
|
if (!fs.statSync(projectDir).isDirectory()) continue;
|
|
50
|
-
|
|
51
|
-
projects.push({
|
|
52
|
-
name,
|
|
53
|
-
kind: "template",
|
|
54
|
-
rules: countRules(path.join(projectDir, "rules")),
|
|
55
|
-
skills: countSkills(path.join(projectDir, "skills")),
|
|
56
|
-
missingRules: 0,
|
|
57
|
-
missingSkills: 0,
|
|
58
|
-
...(full
|
|
59
|
-
? {
|
|
60
|
-
path: projectDir,
|
|
61
|
-
drift: templateDrift(projectDir, cfg.templateDir),
|
|
62
|
-
}
|
|
63
|
-
: {}),
|
|
64
|
-
});
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
const drift = templateDrift(projectDir, cfg.templateDir);
|
|
68
|
+
const drift = templateDrift(projectDir, tmplDir);
|
|
68
69
|
projects.push({
|
|
69
70
|
name,
|
|
70
71
|
kind: "project",
|
|
72
|
+
from,
|
|
71
73
|
rules: countRules(path.join(projectDir, "rules")),
|
|
72
74
|
skills: countSkills(path.join(projectDir, "skills")),
|
|
73
75
|
missingRules: drift.missingRules.length,
|
|
74
76
|
missingSkills: drift.missingSkills.length,
|
|
75
|
-
...(full
|
|
76
|
-
? {
|
|
77
|
-
path: projectDir,
|
|
78
|
-
drift,
|
|
79
|
-
}
|
|
80
|
-
: {}),
|
|
77
|
+
...(full ? { path: projectDir, drift } : {}),
|
|
81
78
|
});
|
|
82
79
|
}
|
|
83
80
|
return projects;
|
|
@@ -87,6 +84,9 @@ function collectStatus(opts = {}) {
|
|
|
87
84
|
const cfg = resolveConfig();
|
|
88
85
|
const full = Boolean(opts.full);
|
|
89
86
|
const agents = opts.agents || ["cursor"];
|
|
87
|
+
const from = opts.from || "base";
|
|
88
|
+
|
|
89
|
+
const migration = migrateLegacyTemplate(cfg);
|
|
90
90
|
|
|
91
91
|
const exists = fs.existsSync(cfg.dir);
|
|
92
92
|
const isGit = exists && fs.existsSync(path.join(cfg.dir, ".git"));
|
|
@@ -103,9 +103,9 @@ function collectStatus(opts = {}) {
|
|
|
103
103
|
...symlinkState(t.path, cfg.skillsDir),
|
|
104
104
|
}));
|
|
105
105
|
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const drifting =
|
|
106
|
+
const templates = collectTemplates(cfg, { full });
|
|
107
|
+
const projects = collectProjects(cfg, { full, from });
|
|
108
|
+
const drifting = projects.filter(
|
|
109
109
|
(p) => p.missingRules > 0 || p.missingSkills > 0
|
|
110
110
|
);
|
|
111
111
|
|
|
@@ -127,18 +127,31 @@ function collectStatus(opts = {}) {
|
|
|
127
127
|
for (const l of links) {
|
|
128
128
|
if (l.state !== "ok") problems.push(`cursor_${path.basename(l.path)}_${l.state}`);
|
|
129
129
|
}
|
|
130
|
+
if (!fs.existsSync(templatePath(cfg, "base")) && templates.length === 0) {
|
|
131
|
+
problems.push("templates_missing");
|
|
132
|
+
}
|
|
130
133
|
|
|
131
134
|
return {
|
|
132
135
|
generatedAt: new Date().toISOString(),
|
|
133
136
|
dir: cfg.dir,
|
|
134
137
|
remote: remote || cfg.remote,
|
|
138
|
+
sources: cfg.sources,
|
|
139
|
+
machineConfigPath: cfg.machineConfigPath,
|
|
140
|
+
machineConfig: cfg.machineConfig,
|
|
135
141
|
branch,
|
|
136
142
|
dirty,
|
|
137
143
|
statusLine: aheadBehind,
|
|
144
|
+
layout: {
|
|
145
|
+
system: { rules: cfg.rulesDir, skills: cfg.skillsDir },
|
|
146
|
+
templates: cfg.templatesDir,
|
|
147
|
+
projects: cfg.projectsDir,
|
|
148
|
+
},
|
|
149
|
+
migration,
|
|
138
150
|
counts: {
|
|
139
151
|
rules: countRules(cfg.rulesDir),
|
|
140
152
|
skills: countSkills(cfg.skillsDir),
|
|
141
|
-
|
|
153
|
+
templates: templates.length,
|
|
154
|
+
projects: projects.length,
|
|
142
155
|
drifting: drifting.length,
|
|
143
156
|
problems: problems.length,
|
|
144
157
|
},
|
|
@@ -153,18 +166,9 @@ function collectStatus(opts = {}) {
|
|
|
153
166
|
state: l.state,
|
|
154
167
|
...(full ? { target: l.target, expected: l.expected } : {}),
|
|
155
168
|
})),
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
: {
|
|
160
|
-
name: p.name,
|
|
161
|
-
kind: p.kind,
|
|
162
|
-
rules: p.rules,
|
|
163
|
-
skills: p.skills,
|
|
164
|
-
missingRules: p.missingRules,
|
|
165
|
-
missingSkills: p.missingSkills,
|
|
166
|
-
}
|
|
167
|
-
),
|
|
169
|
+
templates,
|
|
170
|
+
projects,
|
|
171
|
+
driftFrom: from,
|
|
168
172
|
problems,
|
|
169
173
|
};
|
|
170
174
|
}
|
|
@@ -172,22 +176,32 @@ function collectStatus(opts = {}) {
|
|
|
172
176
|
function statusHelp(data) {
|
|
173
177
|
const help = [];
|
|
174
178
|
if (data.problems.includes("ai_md_missing") || data.problems.includes("ai_md_not_git")) {
|
|
175
|
-
help.push(
|
|
179
|
+
help.push(
|
|
180
|
+
"Run `ai-md setup --remote <git-url>` (or `ai-md install --remote <git-url>`) to clone and link"
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (!data.machineConfig) {
|
|
184
|
+
help.push(
|
|
185
|
+
"Persist remote/dir with `ai-md setup --remote <url>` or `ai-md config set --remote <url> --dir ~/.ai-md`"
|
|
186
|
+
);
|
|
176
187
|
}
|
|
177
188
|
if (data.links.some((l) => l.state !== "ok")) {
|
|
178
189
|
help.push("Run `ai-md doctor --fix` to repair ~/.cursor skills/rules symlinks");
|
|
179
190
|
}
|
|
191
|
+
if (data.problems.includes("templates_missing")) {
|
|
192
|
+
help.push("Create ~/.ai-md/templates/base with project starters (system skills/rules stay in skills/ and rules/)");
|
|
193
|
+
}
|
|
180
194
|
if (data.counts.drifting > 0) {
|
|
181
195
|
help.push(
|
|
182
|
-
|
|
196
|
+
`Run \`ai-md apply-template --project <name> --from ${data.driftFrom || "base"}\` to merge missing template files`
|
|
183
197
|
);
|
|
184
198
|
}
|
|
185
199
|
if (data.dirty) {
|
|
186
200
|
help.push('Run `ai-md push -m "<why>"` to commit and push private config');
|
|
187
201
|
} else {
|
|
188
|
-
help.push("
|
|
202
|
+
help.push("Edit system skills/rules under ~/.ai-md/{skills,rules}; project overlays under projects/");
|
|
189
203
|
}
|
|
190
|
-
help.push("Run `ai-md init-project --repo <path
|
|
204
|
+
help.push("Run `ai-md init-project --repo <path> --from base` to seed a project");
|
|
191
205
|
help.push("Run `ai-md status --json` for machine-readable output");
|
|
192
206
|
return help;
|
|
193
207
|
}
|
|
@@ -195,6 +209,7 @@ function statusHelp(data) {
|
|
|
195
209
|
module.exports = {
|
|
196
210
|
collectStatus,
|
|
197
211
|
collectProjects,
|
|
212
|
+
collectTemplates,
|
|
198
213
|
templateDrift,
|
|
199
214
|
statusHelp,
|
|
200
215
|
git,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dujavi/ai-md",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AXI-shaped CLI for
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "AXI-shaped CLI for ~/.ai-md with persisted dir/remote machine config, templates/, and projects/",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ai-md": "bin/ai-md.js"
|
|
7
7
|
},
|