@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/lib/link.js ADDED
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const crypto = require("crypto");
6
+ const {
7
+ defaultLinkMode,
8
+ isWsl,
9
+ looksLikeWindowsPath,
10
+ pathsEqual,
11
+ normalizePath,
12
+ } = require("./config-paths");
13
+
14
+ function ensureDir(p) {
15
+ fs.mkdirSync(p, { recursive: true });
16
+ }
17
+
18
+ function linkState(linkPath, expected) {
19
+ let stat;
20
+ try {
21
+ stat = fs.lstatSync(linkPath);
22
+ } catch {
23
+ return { path: linkPath, state: "missing", target: null, expected };
24
+ }
25
+ if (stat.isSymbolicLink()) {
26
+ const target = fs.readlinkSync(linkPath);
27
+ const absTarget = path.isAbsolute(target)
28
+ ? target
29
+ : path.resolve(path.dirname(linkPath), target);
30
+ if (pathsEqual(absTarget, expected) || target === expected) {
31
+ return { path: linkPath, state: "ok", target, expected, mode: "symlink" };
32
+ }
33
+ return {
34
+ path: linkPath,
35
+ state: "wrong_target",
36
+ target,
37
+ expected,
38
+ mode: "symlink",
39
+ };
40
+ }
41
+ if (stat.isDirectory()) {
42
+ return {
43
+ path: linkPath,
44
+ state: "directory",
45
+ target: null,
46
+ expected,
47
+ mode: "copy_or_real",
48
+ };
49
+ }
50
+ return { path: linkPath, state: "not_symlink", target: null, expected };
51
+ }
52
+
53
+ function trySymlink(target, link, type) {
54
+ fs.symlinkSync(target, link, type);
55
+ }
56
+
57
+ /**
58
+ * Platform-aware directory link: symlink | junction | copy
59
+ */
60
+ function linkPath(
61
+ target,
62
+ link,
63
+ { force = false, dryRun = false, linkMode = null } = {}
64
+ ) {
65
+ const absTarget = path.resolve(target);
66
+ ensureDir(path.dirname(link));
67
+ ensureDir(absTarget);
68
+
69
+ const mode = linkMode || defaultLinkMode();
70
+ const state = linkState(link, absTarget);
71
+
72
+ if (mode !== "copy" && state.state === "ok") {
73
+ return { path: link, action: "ok", target: absTarget, mode };
74
+ }
75
+
76
+ if (state.state === "directory" && mode !== "copy" && !force) {
77
+ const err = new Error(
78
+ `${link} exists as a real directory (use --force to replace, or --link-mode copy)`
79
+ );
80
+ err.code = "EEXIST";
81
+ throw err;
82
+ }
83
+
84
+ if (state.state === "not_symlink" && !force && mode !== "copy") {
85
+ const err = new Error(
86
+ `${link} exists and is not a symlink (use --force to replace)`
87
+ );
88
+ err.code = "EEXIST";
89
+ throw err;
90
+ }
91
+
92
+ if (dryRun) {
93
+ return {
94
+ path: link,
95
+ action: state.state === "missing" ? "would_link" : "would_repair",
96
+ target: absTarget,
97
+ mode,
98
+ };
99
+ }
100
+
101
+ if (mode === "copy") {
102
+ return syncCopyTree(absTarget, link, { force });
103
+ }
104
+
105
+ if (fs.existsSync(link) || state.state !== "missing") {
106
+ try {
107
+ fs.rmSync(link, { recursive: true, force: true });
108
+ } catch {
109
+ /* ignore */
110
+ }
111
+ }
112
+
113
+ const attempts =
114
+ process.platform === "win32"
115
+ ? mode === "junction"
116
+ ? ["junction", "dir"]
117
+ : [mode === "symlink" ? "dir" : mode, "junction"]
118
+ : ["dir"];
119
+
120
+ let lastErr = null;
121
+ for (const t of attempts) {
122
+ try {
123
+ trySymlink(absTarget, link, t);
124
+ return {
125
+ path: link,
126
+ action: state.state === "missing" ? "linked" : "repaired",
127
+ target: absTarget,
128
+ mode: t === "junction" ? "junction" : "symlink",
129
+ };
130
+ } catch (e) {
131
+ lastErr = e;
132
+ }
133
+ }
134
+
135
+ // Fallback to copy
136
+ const copied = syncCopyTree(absTarget, link, { force: true });
137
+ return {
138
+ ...copied,
139
+ warning: `symlink failed (${lastErr && lastErr.message}); used copy`,
140
+ mode: "copy",
141
+ };
142
+ }
143
+
144
+ function syncCopyTree(src, dest, { force = false } = {}) {
145
+ ensureDir(dest);
146
+ // Remove dest contents that aren't in src
147
+ if (fs.existsSync(dest)) {
148
+ for (const name of fs.readdirSync(dest)) {
149
+ if (name === ".ai-md-copy-marker") continue;
150
+ const s = path.join(src, name);
151
+ const d = path.join(dest, name);
152
+ if (!fs.existsSync(s)) {
153
+ fs.rmSync(d, { recursive: true, force: true });
154
+ }
155
+ }
156
+ }
157
+ fs.cpSync(src, dest, { recursive: true, force: true });
158
+ fs.writeFileSync(
159
+ path.join(dest, ".ai-md-copy-marker"),
160
+ JSON.stringify({ src: normalizePath(src), at: new Date().toISOString() }) +
161
+ "\n"
162
+ );
163
+ return { path: dest, action: "copied", target: src, mode: "copy" };
164
+ }
165
+
166
+ function wslWarnings(cfg, harnessDefs) {
167
+ const warnings = [];
168
+ if (!isWsl()) return warnings;
169
+ warnings.push(
170
+ "Running under WSL: run ai-md in the same environment as the IDE/CLI that consumes links (Windows Cursor needs Windows ai-md)."
171
+ );
172
+ for (const h of harnessDefs || []) {
173
+ for (const p of [h.skills, h.rules]) {
174
+ if (p && looksLikeWindowsPath(p)) {
175
+ warnings.push(
176
+ `Harness ${h.id} path looks Windows-native under WSL: ${p}`
177
+ );
178
+ }
179
+ }
180
+ }
181
+ return warnings;
182
+ }
183
+
184
+ function hashFile(filePath) {
185
+ const h = crypto.createHash("sha256");
186
+ h.update(fs.readFileSync(filePath));
187
+ return h.digest("hex");
188
+ }
189
+
190
+ function walkFiles(root, base = root, out = []) {
191
+ if (!fs.existsSync(root)) return out;
192
+ for (const ent of fs.readdirSync(root, { withFileTypes: true })) {
193
+ if (ent.name === ".build-manifest.json" || ent.name === ".ai-md-copy-marker") {
194
+ continue;
195
+ }
196
+ if (ent.name.startsWith(".") && ent.isDirectory()) continue;
197
+ const full = path.join(root, ent.name);
198
+ const rel = path.relative(base, full).split(path.sep).join("/");
199
+ if (ent.isDirectory()) walkFiles(full, base, out);
200
+ else if (ent.isFile()) out.push({ rel, full });
201
+ }
202
+ return out;
203
+ }
204
+
205
+ function fingerprintTree(root) {
206
+ const files = walkFiles(root);
207
+ const map = {};
208
+ for (const f of files) {
209
+ map[f.rel] = hashFile(f.full);
210
+ }
211
+ return map;
212
+ }
213
+
214
+ module.exports = {
215
+ ensureDir,
216
+ linkPath,
217
+ linkState,
218
+ syncCopyTree,
219
+ wslWarnings,
220
+ fingerprintTree,
221
+ walkFiles,
222
+ hashFile,
223
+ defaultLinkMode,
224
+ };
package/lib/rescue.js ADDED
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { distRoot, distDirty, agentRoots } = require("./build");
6
+ const { selectHarnesses, ensureAgentSourceDirs } = require("./harnesses");
7
+ const { ensureDir, walkFiles, hashFile } = require("./link");
8
+ const { readManifest } = require("./build");
9
+
10
+ /**
11
+ * Promote dirty dist files into agents/<id>/ (not shared/).
12
+ */
13
+ function runRescue(cfg, opts = {}) {
14
+ const { agents, dryRun = false, forceLink = false } = opts;
15
+ const { selected, skipped, warnings } = selectHarnesses(cfg, agents, {
16
+ forceLink: true,
17
+ includeUninstalled: true,
18
+ });
19
+
20
+ const actions = [];
21
+ const byDist = new Map();
22
+ for (const h of selected) {
23
+ if (!byDist.has(h.distId)) byDist.set(h.distId, h);
24
+ }
25
+
26
+ for (const h of byDist.values()) {
27
+ const distDir = distRoot(cfg, h.distId);
28
+ const dirty = distDirty(distDir);
29
+ if (!dirty.dirty) {
30
+ actions.push({ id: h.id, distId: h.distId, action: "clean" });
31
+ continue;
32
+ }
33
+ ensureAgentSourceDirs(cfg, h.distId);
34
+ const agent = agentRoots(cfg, h.distId);
35
+ const manifest = readManifest(distDir) || { files: {} };
36
+
37
+ for (const rel of dirty.files) {
38
+ const src = path.join(distDir, rel);
39
+ if (!fs.existsSync(src)) continue;
40
+ let dest;
41
+ if (rel.startsWith("rules/")) {
42
+ dest = path.join(agent.rules, path.basename(rel));
43
+ } else if (rel.startsWith("skills/")) {
44
+ // skills/name/...
45
+ const parts = rel.split("/");
46
+ const skillName = parts[1];
47
+ if (!skillName) continue;
48
+ if (parts.length === 2 || fs.statSync(src).isDirectory()) {
49
+ dest = path.join(agent.skills, skillName);
50
+ if (!dryRun) {
51
+ if (fs.statSync(path.join(distDir, "skills", skillName)).isDirectory()) {
52
+ fs.cpSync(path.join(distDir, "skills", skillName), dest, {
53
+ recursive: true,
54
+ force: true,
55
+ });
56
+ }
57
+ }
58
+ actions.push({
59
+ id: h.id,
60
+ action: dryRun ? "would_rescue_skill" : "rescued_skill",
61
+ from: rel,
62
+ to: dest,
63
+ });
64
+ continue;
65
+ }
66
+ dest = path.join(agent.skills, parts.slice(1).join("/"));
67
+ } else {
68
+ continue;
69
+ }
70
+ if (dryRun) {
71
+ actions.push({
72
+ id: h.id,
73
+ action: "would_rescue",
74
+ from: rel,
75
+ to: dest,
76
+ });
77
+ } else {
78
+ ensureDir(path.dirname(dest));
79
+ if (fs.statSync(src).isFile()) {
80
+ fs.copyFileSync(src, dest);
81
+ }
82
+ actions.push({
83
+ id: h.id,
84
+ action: "rescued",
85
+ from: rel,
86
+ to: dest,
87
+ });
88
+ }
89
+ }
90
+ }
91
+
92
+ return { actions, skipped, warnings };
93
+ }
94
+
95
+ module.exports = { runRescue };
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { execFileSync } = require("child_process");
6
+ const { ensureDir } = require("./link");
7
+
8
+ const SKELETON_VERSION = 1;
9
+
10
+ function packageRoot() {
11
+ return path.join(__dirname, "..");
12
+ }
13
+
14
+ function skeletonRoot() {
15
+ return path.join(packageRoot(), "skeleton");
16
+ }
17
+
18
+ function isEmptyDir(dir) {
19
+ if (!fs.existsSync(dir)) return true;
20
+ const ents = fs.readdirSync(dir).filter((n) => n !== "." && n !== "..");
21
+ return ents.length === 0;
22
+ }
23
+
24
+ function copySkeletonFile(src, dest, { force = false } = {}) {
25
+ if (fs.existsSync(dest) && !force) {
26
+ return { action: "keep", path: dest };
27
+ }
28
+ ensureDir(path.dirname(dest));
29
+ fs.cpSync(src, dest, { recursive: true, force: true });
30
+ return { action: force ? "replaced" : "added", path: dest };
31
+ }
32
+
33
+ function walkSkeleton(dir, base = dir, out = []) {
34
+ if (!fs.existsSync(dir)) return out;
35
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
36
+ const full = path.join(dir, ent.name);
37
+ const rel = path.relative(base, full);
38
+ if (ent.isDirectory()) walkSkeleton(full, base, out);
39
+ else out.push({ rel, full });
40
+ }
41
+ return out;
42
+ }
43
+
44
+ /**
45
+ * Seed missing skeleton files into cfg.dir (never overwrite unless force).
46
+ */
47
+ function seedSkeleton(cfg, { force = false, dryRun = false } = {}) {
48
+ const srcRoot = skeletonRoot();
49
+ if (!fs.existsSync(srcRoot)) {
50
+ const err = new Error(`skeleton missing in package: ${srcRoot}`);
51
+ err.code = "ENOENT";
52
+ throw err;
53
+ }
54
+ const actions = [];
55
+ for (const f of walkSkeleton(srcRoot)) {
56
+ const dest = path.join(cfg.dir, f.rel);
57
+ if (dryRun) {
58
+ actions.push({
59
+ action: fs.existsSync(dest) && !force ? "would_keep" : "would_add",
60
+ path: dest,
61
+ });
62
+ continue;
63
+ }
64
+ actions.push(copySkeletonFile(f.full, dest, { force }));
65
+ }
66
+ // ensure agent dir structure
67
+ for (const id of ["cursor", "claude", "agents", "gemini", "opencode"]) {
68
+ const r = path.join(cfg.dir, "agents", id, "rules");
69
+ const s = path.join(cfg.dir, "agents", id, "skills");
70
+ if (!dryRun) {
71
+ ensureDir(r);
72
+ ensureDir(s);
73
+ }
74
+ }
75
+ const verPath = path.join(cfg.dir, ".ai-md-skeleton-version");
76
+ if (!dryRun) {
77
+ fs.writeFileSync(verPath, String(SKELETON_VERSION) + "\n");
78
+ }
79
+ actions.push({
80
+ action: dryRun ? "would_write_version" : "version",
81
+ path: verPath,
82
+ version: SKELETON_VERSION,
83
+ });
84
+ return { actions, skeletonVersion: SKELETON_VERSION };
85
+ }
86
+
87
+ function initRepo(cfg, { noGit = false, force = false, dryRun = false } = {}) {
88
+ const dir = cfg.dir;
89
+ if (fs.existsSync(dir) && !isEmptyDir(dir) && !force) {
90
+ const err = new Error(
91
+ `${dir} is not empty. Use ai-md seed-skeleton to add missing files, or ai-md init --force`
92
+ );
93
+ err.code = "EEXIST";
94
+ throw err;
95
+ }
96
+ if (dryRun) {
97
+ return { action: "would_init", dir };
98
+ }
99
+ ensureDir(dir);
100
+ if (force && fs.existsSync(dir)) {
101
+ // only remove if force — dangerous; only clear if user asked
102
+ // We do not wipe; seed with force overwrite of skeleton files only
103
+ }
104
+ const seeded = seedSkeleton(cfg, { force: true, dryRun: false });
105
+ if (!noGit && !fs.existsSync(path.join(dir, ".git"))) {
106
+ execFileSync("git", ["init"], { cwd: dir, stdio: "pipe" });
107
+ }
108
+ return { action: "initialized", dir, ...seeded };
109
+ }
110
+
111
+ module.exports = {
112
+ SKELETON_VERSION,
113
+ skeletonRoot,
114
+ seedSkeleton,
115
+ initRepo,
116
+ isEmptyDir,
117
+ };
package/lib/status.js CHANGED
@@ -12,9 +12,16 @@ const {
12
12
  countSkills,
13
13
  listSkillNames,
14
14
  listRuleNames,
15
- symlinkState,
16
- agentSkillTargets,
17
15
  } = require("./config");
16
+ const { linkState } = require("./link");
17
+ const { distRoot, distDirty, sharedRoots } = require("./build");
18
+ const {
19
+ selectHarnesses,
20
+ resolveHarness,
21
+ isHarnessInstalled,
22
+ } = require("./harnesses");
23
+ const { isWsl } = require("./config-paths");
24
+ const { SKELETON_VERSION } = require("./skeleton");
18
25
 
19
26
  function git(repo, args) {
20
27
  try {
@@ -60,9 +67,7 @@ function collectProjects(cfg, { full = false, from = "base" } = {}) {
60
67
  const tmplDir = templatePath(cfg, from);
61
68
  if (!fs.existsSync(cfg.projectsDir)) return projects;
62
69
  for (const name of fs.readdirSync(cfg.projectsDir).sort()) {
63
- if (name.startsWith(".")) continue;
64
- // legacy leftover
65
- if (name === "template") continue;
70
+ if (name.startsWith(".") || name === "template") continue;
66
71
  const projectDir = path.join(cfg.projectsDir, name);
67
72
  if (!fs.statSync(projectDir).isDirectory()) continue;
68
73
  const drift = templateDrift(projectDir, tmplDir);
@@ -83,25 +88,50 @@ function collectProjects(cfg, { full = false, from = "base" } = {}) {
83
88
  function collectStatus(opts = {}) {
84
89
  const cfg = resolveConfig();
85
90
  const full = Boolean(opts.full);
86
- const agents = opts.agents || ["cursor"];
91
+ const agents = opts.agents || cfg.agents || ["cursor"];
87
92
  const from = opts.from || "base";
88
93
 
89
94
  const migration = migrateLegacyTemplate(cfg);
90
-
91
95
  const exists = fs.existsSync(cfg.dir);
92
96
  const isGit = exists && fs.existsSync(path.join(cfg.dir, ".git"));
97
+ const shared = sharedRoots(cfg);
93
98
 
94
- const links = [
95
- symlinkState(cfg.cursorSkills, cfg.skillsDir),
96
- symlinkState(cfg.cursorRules, cfg.rulesDir),
97
- ];
99
+ const { selected, skipped, warnings } = selectHarnesses(cfg, agents, {
100
+ forceLink: false,
101
+ includeUninstalled: true,
102
+ });
98
103
 
99
- const agentLinks = agentSkillTargets(cfg.home, agents)
100
- .filter((t) => t.name !== "cursor")
101
- .map((t) => ({
102
- agent: t.name,
103
- ...symlinkState(t.path, cfg.skillsDir),
104
- }));
104
+ const links = [];
105
+ const harnesses = [];
106
+ for (const h of selected) {
107
+ const installed = isHarnessInstalled(h);
108
+ const dist = distRoot(cfg, h.distId);
109
+ const dirty = distDirty(dist);
110
+ const entry = {
111
+ id: h.id,
112
+ distId: h.distId,
113
+ installed,
114
+ aliasOf: h.aliasOf,
115
+ format: h.format,
116
+ distDirty: dirty.dirty,
117
+ ...(full && dirty.dirty ? { dirtyFiles: dirty.files } : {}),
118
+ };
119
+ if (installed) {
120
+ if (h.skills) {
121
+ const st = linkState(h.skills, path.join(dist, "skills"));
122
+ links.push({ harness: h.id, kind: "skills", ...st });
123
+ entry.skillsLink = st.state;
124
+ }
125
+ if (h.rules && h.format !== "skills-only") {
126
+ const st = linkState(h.rules, path.join(dist, "rules"));
127
+ links.push({ harness: h.id, kind: "rules", ...st });
128
+ entry.rulesLink = st.state;
129
+ }
130
+ } else {
131
+ entry.skillsLink = "skipped_not_installed";
132
+ }
133
+ harnesses.push(entry);
134
+ }
105
135
 
106
136
  const templates = collectTemplates(cfg, { full });
107
137
  const projects = collectProjects(cfg, { full, from });
@@ -124,44 +154,67 @@ function collectStatus(opts = {}) {
124
154
  const problems = [];
125
155
  if (!exists) problems.push("ai_md_missing");
126
156
  else if (!isGit) problems.push("ai_md_not_git");
157
+ if (fs.existsSync(path.join(cfg.dir, "rules")) || fs.existsSync(path.join(cfg.dir, "skills"))) {
158
+ problems.push("legacy_flat_layout");
159
+ }
127
160
  for (const l of links) {
128
- if (l.state !== "ok") problems.push(`cursor_${path.basename(l.path)}_${l.state}`);
161
+ if (l.state !== "ok" && l.state !== "directory") {
162
+ problems.push(`${l.harness}_${l.kind}_${l.state}`);
163
+ }
164
+ }
165
+ for (const h of harnesses) {
166
+ if (h.installed && h.distDirty) problems.push(`dist_dirty_${h.distId}`);
129
167
  }
130
168
  if (!fs.existsSync(templatePath(cfg, "base")) && templates.length === 0) {
131
169
  problems.push("templates_missing");
132
170
  }
133
171
 
172
+ let skeletonVersion = null;
173
+ try {
174
+ skeletonVersion = Number(
175
+ fs.readFileSync(path.join(cfg.dir, ".ai-md-skeleton-version"), "utf8").trim()
176
+ );
177
+ } catch {
178
+ /* none */
179
+ }
180
+ if (skeletonVersion != null && skeletonVersion < SKELETON_VERSION) {
181
+ problems.push("skeleton_outdated");
182
+ }
183
+
134
184
  return {
135
185
  generatedAt: new Date().toISOString(),
136
186
  dir: cfg.dir,
137
187
  remote: remote || cfg.remote,
138
188
  sources: cfg.sources,
189
+ linkMode: cfg.linkMode,
190
+ wsl: isWsl(),
139
191
  machineConfigPath: cfg.machineConfigPath,
140
192
  machineConfig: cfg.machineConfig,
141
193
  branch,
142
194
  dirty,
143
195
  statusLine: aheadBehind,
144
196
  layout: {
145
- system: { rules: cfg.rulesDir, skills: cfg.skillsDir },
197
+ shared: { rules: shared.rules, skills: shared.skills },
198
+ agents: cfg.agentsDir,
199
+ dist: cfg.distDir,
146
200
  templates: cfg.templatesDir,
147
201
  projects: cfg.projectsDir,
148
202
  },
149
203
  migration,
150
204
  counts: {
151
- rules: countRules(cfg.rulesDir),
152
- skills: countSkills(cfg.skillsDir),
205
+ rules: countRules(shared.rules),
206
+ skills: countSkills(shared.skills),
153
207
  templates: templates.length,
154
208
  projects: projects.length,
155
209
  drifting: drifting.length,
156
210
  problems: problems.length,
157
211
  },
212
+ harnesses,
213
+ skipped,
214
+ warnings,
158
215
  links: links.map((l) => ({
159
- path: l.path,
160
- state: l.state,
161
- ...(full ? { target: l.target, expected: l.expected } : {}),
162
- })),
163
- agentLinks: agentLinks.map((l) => ({
164
- agent: l.agent,
216
+ harness: l.harness,
217
+ kind: l.kind,
165
218
  path: l.path,
166
219
  state: l.state,
167
220
  ...(full ? { target: l.target, expected: l.expected } : {}),
@@ -169,6 +222,8 @@ function collectStatus(opts = {}) {
169
222
  templates,
170
223
  projects,
171
224
  driftFrom: from,
225
+ skeletonVersion,
226
+ packageSkeletonVersion: SKELETON_VERSION,
172
227
  problems,
173
228
  };
174
229
  }
@@ -177,7 +232,12 @@ function statusHelp(data) {
177
232
  const help = [];
178
233
  if (data.problems.includes("ai_md_missing") || data.problems.includes("ai_md_not_git")) {
179
234
  help.push(
180
- "Run `ai-md setup --remote <git-url>` (or `ai-md install --remote <git-url>`) to clone and link"
235
+ "Run `ai-md init` (no remote) or `ai-md setup --remote <git-url>`"
236
+ );
237
+ }
238
+ if (data.problems.includes("legacy_flat_layout")) {
239
+ help.push(
240
+ "Migrate flat rules/skills into shared/ (and agents/<id>/); see package README"
181
241
  );
182
242
  }
183
243
  if (!data.machineConfig) {
@@ -185,23 +245,38 @@ function statusHelp(data) {
185
245
  "Persist remote/dir with `ai-md setup --remote <url>` or `ai-md config set --remote <url> --dir ~/.ai-md`"
186
246
  );
187
247
  }
188
- if (data.links.some((l) => l.state !== "ok")) {
189
- help.push("Run `ai-md doctor --fix` to repair ~/.cursor skills/rules symlinks");
248
+ if (data.links.some((l) => l.state !== "ok" && l.state !== "directory")) {
249
+ help.push("Run `ai-md doctor --fix` to rebuild dist and repair links");
250
+ }
251
+ if (data.problems.some((p) => p.startsWith("dist_dirty_"))) {
252
+ help.push("Run `ai-md rescue --agents <id>` or `ai-md build --force`");
253
+ }
254
+ if (data.problems.includes("skeleton_outdated")) {
255
+ help.push("Run `ai-md seed-skeleton` to add new recommended files");
256
+ }
257
+ if (data.wsl) {
258
+ help.push(
259
+ "WSL detected: use Windows ai-md for Windows Cursor (homes differ)"
260
+ );
261
+ }
262
+ if (data.skipped && data.skipped.some((s) => s.reason === "not_installed")) {
263
+ help.push(
264
+ "Some harnesses skipped (AI not installed). Install the tool or use --force-link"
265
+ );
190
266
  }
191
267
  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/)");
268
+ help.push("Run `ai-md seed-skeleton` or add templates/base");
193
269
  }
194
270
  if (data.counts.drifting > 0) {
195
271
  help.push(
196
- `Run \`ai-md apply-template --project <name> --from ${data.driftFrom || "base"}\` to merge missing template files`
272
+ `Run \`ai-md apply-template --project <name> --from ${data.driftFrom || "base"}\``
197
273
  );
198
274
  }
199
275
  if (data.dirty) {
200
- help.push('Run `ai-md push -m "<why>"` to commit and push private config');
276
+ help.push('Run `ai-md push -m "<why>"` to commit and push');
201
277
  } else {
202
- help.push("Edit system skills/rules under ~/.ai-md/{skills,rules}; project overlays under projects/");
278
+ help.push("Edit shared/ or agents/<id>/ only; then `ai-md build`");
203
279
  }
204
- help.push("Run `ai-md init-project --repo <path> --from base` to seed a project");
205
280
  help.push("Run `ai-md status --json` for machine-readable output");
206
281
  return help;
207
282
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dujavi/ai-md",
3
- "version": "0.5.0",
4
- "description": "AXI-shaped CLI for a private ~/.ai-md: system skills/rules, templates, projects, and private script runner for agent harnesses (Cursor, Claude, agents)",
3
+ "version": "0.6.0",
4
+ "description": "AXI-shaped CLI for private ~/.ai-md: shared+agents build to dist, multi-harness link (Cursor, Claude, agents, …)",
5
5
  "bin": {
6
6
  "ai-md": "bin/ai-md.js"
7
7
  },
@@ -9,6 +9,7 @@
9
9
  "bin/",
10
10
  "lib/",
11
11
  "scripts/",
12
+ "skeleton/",
12
13
  "docs/",
13
14
  "README.md",
14
15
  "LICENSE"
@@ -17,7 +18,7 @@
17
18
  "node": ">=18"
18
19
  },
19
20
  "scripts": {
20
- "test": "node bin/ai-md.js --help && node bin/ai-md.js status --json >/dev/null"
21
+ "test": "node bin/ai-md.js --help && node bin/ai-md.js harness list --json >/dev/null && node bin/ai-md.js status --json >/dev/null"
21
22
  },
22
23
  "dependencies": {
23
24
  "@toon-format/toon": "^2.3.1"