@moda-ai/cli 1.55.21 → 1.55.23

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.
Files changed (33) hide show
  1. package/dist/{agent-mj622k8s.js → agent-f8a1kejk.js} +8 -9
  2. package/dist/{cli-569hj0bj.js → cli-2cggp8ww.js} +1 -1
  3. package/dist/{cli-a5c9te95.js → cli-4f6jkrnm.js} +5 -5
  4. package/dist/cli-4yarp5s1.js +61 -0
  5. package/dist/{cli-4j8nffsm.js → cli-54zhv6cx.js} +57 -28
  6. package/dist/{cli-sf0g0fjg.js → cli-8dtnwp9r.js} +152 -57
  7. package/dist/{cli-9x9adrtj.js → cli-9g8vz4sv.js} +1 -1
  8. package/dist/{cli-cehskjde.js → cli-cv3jt1ms.js} +5 -7
  9. package/dist/{cli-033n2mpb.js → cli-d2zews6c.js} +6 -9
  10. package/dist/{cli-38p87ykc.js → cli-frhm2wz7.js} +1 -1
  11. package/dist/{cli-wf7gfa1e.js → cli-s6tw1fje.js} +8 -10
  12. package/dist/{cli-5ke3xe6y.js → cli-t3a6eknn.js} +26 -1
  13. package/dist/{cli-rqk6f6n6.js → cli-wkmdw40s.js} +24 -5
  14. package/dist/{cli-pc5cw7mx.js → cli-z3g07t65.js} +5 -5
  15. package/dist/cli.js +50 -35
  16. package/dist/{doctor-mx6jf435.js → doctor-snb87jvy.js} +8 -9
  17. package/dist/{fix-9fmy6p2x.js → fix-fr48gr0w.js} +6 -7
  18. package/dist/{harness-github-actions-c826bfxs.js → harness-github-actions-cfkx9ty9.js} +5 -6
  19. package/dist/{harness-xvg6ws6x.js → harness-m63b6s0k.js} +5 -6
  20. package/dist/{index-ewqbpnwz.js → index-nsaph4as.js} +58 -22
  21. package/dist/{production-intelligence-dbe9dmc5.js → production-intelligence-fwjh572m.js} +8 -7
  22. package/dist/{profile-commands-kmmd9y86.js → profile-commands-548ts402.js} +7 -48
  23. package/dist/prompt-source-mcp.js +5 -6
  24. package/dist/{prompts-1tkhrxkq.js → prompts-p66y7wep.js} +6 -7
  25. package/dist/{provision-wzttap58.js → provision-r9fhrvpw.js} +5 -4
  26. package/dist/{registry-atqyb5fa.js → registry-n7dsdxmg.js} +3 -4
  27. package/dist/{skills-bundled-r9a2vs07.js → skills-bundled-dq20spzk.js} +2 -2
  28. package/dist/skills-c17gg69y.js +18 -0
  29. package/dist/{sync-25vn78je.js → sync-bne90xvj.js} +8 -9
  30. package/package.json +1 -1
  31. package/skills/integration/index.json +2 -2
  32. package/dist/cli-7ea02wca.js +0 -30
  33. package/dist/skills-br82cpt7.js +0 -19
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  buildDoctorReport
3
- } from "./cli-pc5cw7mx.js";
4
- import"./cli-9x9adrtj.js";
5
- import"./cli-wf7gfa1e.js";
3
+ } from "./cli-z3g07t65.js";
4
+ import"./cli-9g8vz4sv.js";
5
+ import"./cli-s6tw1fje.js";
6
6
  import"./cli-mm48br00.js";
7
7
  import {
8
8
  HARNESS_REPORT_APPROVAL_PATH,
@@ -14,19 +14,18 @@ import {
14
14
  readHarnessSyncState,
15
15
  summarizeHarness,
16
16
  validateHarnessReport
17
- } from "./cli-a5c9te95.js";
17
+ } from "./cli-4f6jkrnm.js";
18
18
  import"./cli-97fnztpy.js";
19
- import"./cli-569hj0bj.js";
20
- import"./cli-cehskjde.js";
21
- import"./cli-rqk6f6n6.js";
22
- import"./cli-7ea02wca.js";
19
+ import"./cli-2cggp8ww.js";
20
+ import"./cli-cv3jt1ms.js";
21
+ import"./cli-wkmdw40s.js";
23
22
  import {
24
23
  CliInputError,
25
24
  resolveApiKey,
26
25
  resolveIngestUrl,
27
26
  resolveModaBaseUrl,
28
27
  resolveTenantId
29
- } from "./cli-5ke3xe6y.js";
28
+ } from "./cli-t3a6eknn.js";
30
29
  import"./cli-0v6na3yp.js";
31
30
 
32
31
  // src/agent.ts
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createCliOutput,
3
3
  outputModeFromFlags
4
- } from "./cli-5ke3xe6y.js";
4
+ } from "./cli-t3a6eknn.js";
5
5
 
6
6
  // src/command-context.ts
7
7
  function flagEnabled(flags, key) {
@@ -4,16 +4,16 @@ import {
4
4
  import {
5
5
  createCommandContext,
6
6
  stringOption
7
- } from "./cli-569hj0bj.js";
7
+ } from "./cli-2cggp8ww.js";
8
8
  import {
9
9
  assertFlags,
10
10
  assertPositionals,
11
11
  requireTenant
12
- } from "./cli-cehskjde.js";
12
+ } from "./cli-cv3jt1ms.js";
13
13
  import {
14
14
  ApiError,
15
15
  callControlAPI
16
- } from "./cli-rqk6f6n6.js";
16
+ } from "./cli-wkmdw40s.js";
17
17
  import {
18
18
  CliInputError,
19
19
  CliInputRequiredError,
@@ -39,7 +39,7 @@ import {
39
39
  resolveModaBaseUrl,
40
40
  resolveTenantId,
41
41
  shouldInventoryFile
42
- } from "./cli-5ke3xe6y.js";
42
+ } from "./cli-t3a6eknn.js";
43
43
  import {
44
44
  __commonJS,
45
45
  __require,
@@ -195871,7 +195871,7 @@ async function runHarnessCommand(context) {
195871
195871
  throw new CliInputError("Focused remote prompt analysis is not supported yet.", "Use local --analyst=claude|codex|cursor|local-scan, or drop --concern for a full remote report.");
195872
195872
  }
195873
195873
  if (context.flags["github-actions"] === "true") {
195874
- const { runGithubActionsAnalyze } = await import("./harness-github-actions-c826bfxs.js");
195874
+ const { runGithubActionsAnalyze } = await import("./harness-github-actions-cfkx9ty9.js");
195875
195875
  const result = await runGithubActionsAnalyze(rootDir, {
195876
195876
  writeReport: (report2) => {
195877
195877
  const normalized = normalizeHarnessReport(report2);
@@ -0,0 +1,61 @@
1
+ import {
2
+ apiKeyHeaders,
3
+ embeddedTenantIdFromApiKey,
4
+ recordApiRequest,
5
+ resolveApiKey,
6
+ resolveApiKeyFromActiveProfile,
7
+ resolveModaBaseUrl
8
+ } from "./cli-t3a6eknn.js";
9
+
10
+ // src/api-key-auth.ts
11
+ var PROBE_TIMEOUT_MS = 1e4;
12
+ function resolveApiKeyCredential(options = {}) {
13
+ const key = resolveApiKey(options);
14
+ if (!key)
15
+ return;
16
+ const env = options.env ?? process.env;
17
+ const source = env.MODA_API_KEY === key ? "env" : resolveApiKeyFromActiveProfile(options) === key ? "profile" : "config";
18
+ return { key, source, tenantId: embeddedTenantIdFromApiKey(key) };
19
+ }
20
+ function redactApiKey(key) {
21
+ if (key.length <= 12)
22
+ return "<redacted>";
23
+ return `${key.slice(0, 8)}...${key.slice(-4)}`;
24
+ }
25
+ async function fetchApiKeyProbe(baseUrl, headers, timeoutMs) {
26
+ const probe = (path) => {
27
+ recordApiRequest();
28
+ return fetch(`${baseUrl}/api/v1/data/${path}`, {
29
+ method: "GET",
30
+ headers,
31
+ signal: AbortSignal.timeout(timeoutMs)
32
+ });
33
+ };
34
+ const response = await probe("whoami");
35
+ if (response.status !== 404)
36
+ return response;
37
+ return probe("overview");
38
+ }
39
+ async function probeApiKey(key, options = {}) {
40
+ let response;
41
+ try {
42
+ response = await fetchApiKeyProbe(resolveModaBaseUrl(options), apiKeyHeaders(key, options), PROBE_TIMEOUT_MS);
43
+ } catch (error) {
44
+ const reason = error instanceof Error ? error.message : String(error);
45
+ return { outcome: "unreachable", reason };
46
+ }
47
+ if (response.status === 401 || response.status === 403) {
48
+ return { outcome: "rejected", status: response.status };
49
+ }
50
+ if (response.status !== 200) {
51
+ return { outcome: "unreachable", reason: `HTTP ${response.status}` };
52
+ }
53
+ try {
54
+ await response.json();
55
+ } catch {
56
+ return { outcome: "unreachable", reason: "non-JSON response from the Data API" };
57
+ }
58
+ return { outcome: "live" };
59
+ }
60
+
61
+ export { resolveApiKeyCredential, redactApiKey, fetchApiKeyProbe, probeApiKey };
@@ -1,20 +1,20 @@
1
1
  import {
2
2
  createCommandContext
3
- } from "./cli-569hj0bj.js";
3
+ } from "./cli-2cggp8ww.js";
4
4
  import {
5
5
  assertFlags,
6
6
  assertPositionals
7
- } from "./cli-cehskjde.js";
7
+ } from "./cli-cv3jt1ms.js";
8
8
  import {
9
9
  callControlAPI
10
- } from "./cli-rqk6f6n6.js";
10
+ } from "./cli-wkmdw40s.js";
11
11
  import {
12
12
  CliInputError,
13
13
  recordApiRequest,
14
14
  resolveApiKey,
15
15
  resolveIngestUrl,
16
16
  resolveTenantId
17
- } from "./cli-5ke3xe6y.js";
17
+ } from "./cli-t3a6eknn.js";
18
18
  import {
19
19
  SKILL_SOURCE_HARNESS,
20
20
  SKILL_SOURCE_INTEGRATION_BUNDLED,
@@ -23,7 +23,7 @@ import {
23
23
  pruneManagedSkillPaths,
24
24
  readSkillsManifest,
25
25
  writeSkillsManifest
26
- } from "./cli-sf0g0fjg.js";
26
+ } from "./cli-8dtnwp9r.js";
27
27
  import {
28
28
  __require
29
29
  } from "./cli-0v6na3yp.js";
@@ -80,27 +80,39 @@ function renderFixesBlock() {
80
80
  ].join(`
81
81
  `);
82
82
  }
83
+ function countOccurrences(text, needle) {
84
+ return text.split(needle).length - 1;
85
+ }
83
86
  function upsertManagedBlock(existing, block) {
84
87
  if (existing === null) {
85
- return `${block}
86
- `;
87
- }
88
- const beginIdx = existing.indexOf(FIXES_BLOCK_BEGIN);
89
- const endIdx = existing.indexOf(FIXES_BLOCK_END);
90
- if (beginIdx !== -1 && endIdx !== -1 && endIdx >= beginIdx) {
91
- const before = existing.slice(0, beginIdx);
92
- const after = existing.slice(endIdx + FIXES_BLOCK_END.length);
93
- return `${before}${block}${after}`;
88
+ return { ok: true, content: `${block}
89
+ ` };
94
90
  }
95
- const separator = existing === "" || existing.endsWith(`
91
+ const begins = countOccurrences(existing, FIXES_BLOCK_BEGIN);
92
+ const ends = countOccurrences(existing, FIXES_BLOCK_END);
93
+ if (begins === 0 && ends === 0) {
94
+ const separator = existing === "" || existing.endsWith(`
96
95
 
97
96
  `) ? "" : existing.endsWith(`
98
97
  `) ? `
99
98
  ` : `
100
99
 
101
100
  `;
102
- return `${existing}${separator}${block}
103
- `;
101
+ return { ok: true, content: `${existing}${separator}${block}
102
+ ` };
103
+ }
104
+ const beginIdx = existing.indexOf(FIXES_BLOCK_BEGIN);
105
+ const endIdx = existing.indexOf(FIXES_BLOCK_END);
106
+ if (begins === 1 && ends === 1 && endIdx > beginIdx) {
107
+ const before = existing.slice(0, beginIdx);
108
+ const after = existing.slice(endIdx + FIXES_BLOCK_END.length);
109
+ return { ok: true, content: `${before}${block}${after}` };
110
+ }
111
+ const problem = begins === 1 && ends === 1 ? `its ${FIXES_BLOCK_END} marker comes before its ${FIXES_BLOCK_BEGIN} marker` : `it has ${begins} ${FIXES_BLOCK_BEGIN} and ${ends} ${FIXES_BLOCK_END} marker(s) instead of one of each`;
112
+ return {
113
+ ok: false,
114
+ reason: `The Moda block in ${AGENTS_MD_FILENAME} was not updated because ${problem}. ` + "The file was left unchanged so no text is lost. Keep exactly one begin marker followed by one end marker " + "(or delete the stray markers), then rerun `moda skills sync`."
115
+ };
104
116
  }
105
117
  function writeFileIfChanged(filePath, content) {
106
118
  try {
@@ -108,14 +120,14 @@ function writeFileIfChanged(filePath, content) {
108
120
  if (existsSync(filePath)) {
109
121
  const current = readFileSync(filePath, "utf8");
110
122
  if (current === content)
111
- return "unchanged";
123
+ return { status: "unchanged" };
112
124
  writeFileSync(filePath, content);
113
- return "updated";
125
+ return { status: "updated" };
114
126
  }
115
127
  writeFileSync(filePath, content);
116
- return "wrote";
117
- } catch {
118
- return "failed";
128
+ return { status: "wrote" };
129
+ } catch (err) {
130
+ return { status: "failed", error: err instanceof Error ? err.message : String(err) };
119
131
  }
120
132
  }
121
133
  function upsertFixesAgentsBlock(cwd) {
@@ -124,12 +136,28 @@ function upsertFixesAgentsBlock(cwd) {
124
136
  if (existsSync(filePath)) {
125
137
  try {
126
138
  existing = readFileSync(filePath, "utf8");
127
- } catch {
128
- return { path: AGENTS_MD_FILENAME, status: "failed" };
139
+ } catch (err) {
140
+ return {
141
+ path: AGENTS_MD_FILENAME,
142
+ status: "failed",
143
+ reason: `Could not read ${AGENTS_MD_FILENAME}: ${err instanceof Error ? err.message : String(err)}`
144
+ };
129
145
  }
130
146
  }
131
147
  const next = upsertManagedBlock(existing, renderFixesBlock());
132
- return { path: AGENTS_MD_FILENAME, status: writeFileIfChanged(filePath, next) };
148
+ if (!next.ok)
149
+ return { path: AGENTS_MD_FILENAME, status: "skipped", reason: next.reason };
150
+ const written = writeFileIfChanged(filePath, next.content);
151
+ return {
152
+ path: AGENTS_MD_FILENAME,
153
+ status: written.status,
154
+ ...written.error ? { reason: `Could not write ${AGENTS_MD_FILENAME}: ${written.error}` } : {}
155
+ };
156
+ }
157
+ function agentsMdProblem(agentsMd) {
158
+ if (!agentsMd || agentsMd.status !== "skipped" && agentsMd.status !== "failed")
159
+ return;
160
+ return agentsMd.reason ?? `The Moda block in ${agentsMd.path} could not be updated.`;
133
161
  }
134
162
 
135
163
  // src/skills.ts
@@ -717,11 +745,12 @@ async function runSkillsCommand(input) {
717
745
  if (sub === "sync") {
718
746
  assertFlags("skills sync", flags, ["dry-run"]);
719
747
  const result = await runSkillSync(flags, { profile: context.profile, env: context.env });
748
+ const agentsMdWarning = agentsMdProblem(result.agentsMd);
720
749
  context.output.writeData({
721
750
  synced: result.synced,
722
751
  dryRun: result.dryRun,
723
752
  ...result.agentsMd ? { agentsMd: result.agentsMd } : {}
724
- });
753
+ }, agentsMdWarning ? { status: "degraded", warnings: [agentsMdWarning] } : {});
725
754
  return 0;
726
755
  }
727
756
  if (sub === "install") {
@@ -866,7 +895,7 @@ function sha256(input) {
866
895
  return createHash("sha256").update(input).digest("hex");
867
896
  }
868
897
  async function runSkillsInstall(context, ids) {
869
- const { installIntegrationSkill, readBundledIndex, readBundledSkillMd } = await import("./skills-bundled-r9a2vs07.js");
898
+ const { installIntegrationSkill, readBundledIndex, readBundledSkillMd } = await import("./skills-bundled-dq20spzk.js");
870
899
  const index = readBundledIndex();
871
900
  if (context.flags.list === "true" || ids.length === 0) {
872
901
  if (!index) {
@@ -947,4 +976,4 @@ async function runSkillRegistryCommand(sub, positionals, flags, context) {
947
976
  return 0;
948
977
  }
949
978
 
950
- export { pullSkills, runSkillSync, runSkillsCommand, parseScopeInt };
979
+ export { agentsMdProblem, pullSkills, runSkillSync, runSkillsCommand, parseScopeInt };
@@ -7,8 +7,8 @@ function packageAssetPath(...parts) {
7
7
 
8
8
  // src/init/rules.ts
9
9
  import { createHash } from "node:crypto";
10
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
11
- import { dirname as dirname2, join as join2 } from "node:path";
10
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
11
+ import { dirname as dirname2, join as join2, sep } from "node:path";
12
12
  import { confirm } from "@inquirer/prompts";
13
13
  var SKILLS_SOURCE_DIR = packageAssetPath("skills");
14
14
  var SKILLS = [
@@ -93,18 +93,68 @@ function skillVersion(skillContent) {
93
93
  const frontmatter = FRONTMATTER_RE.exec(skillContent)?.[1] ?? "";
94
94
  return /^version:\s*(\S+)\s*$/m.exec(frontmatter)?.[1] ?? "unknown";
95
95
  }
96
+ function normalizeEol(text) {
97
+ return text.replace(/\r\n/g, `
98
+ `);
99
+ }
100
+ function matchesRecord(existing, recorded) {
101
+ const hash = recorded?.sha256;
102
+ return typeof hash === "string" && (hash === sha256(existing) || hash === sha256(normalizeEol(existing)));
103
+ }
96
104
  function looksLikeShippedSkill(existing, skill) {
97
- return existing.startsWith(`---
105
+ const text = normalizeEol(existing);
106
+ return text.startsWith(`---
98
107
  name: ${skill.id}
99
- `) || /^# Moda CLI$/m.test(existing);
108
+ `) || /^# Moda CLI$/m.test(text);
109
+ }
110
+ function lstatOrUndefined(path) {
111
+ try {
112
+ return lstatSync(path);
113
+ } catch {
114
+ return;
115
+ }
116
+ }
117
+ function firstSymlinkedComponent(relativePath) {
118
+ let current = "";
119
+ for (const part of relativePath.split(/[\\/]+/).filter(Boolean)) {
120
+ current = current ? join2(current, part) : part;
121
+ const stat = lstatOrUndefined(join2(process.cwd(), current));
122
+ if (!stat)
123
+ return;
124
+ if (stat.isSymbolicLink())
125
+ return current;
126
+ }
127
+ return;
100
128
  }
101
129
  function unusedBackupPath(relativePath) {
102
130
  let candidate = `${relativePath}.bak`;
103
- for (let n = 1;existsSync(join2(process.cwd(), candidate)); n++) {
131
+ for (let n = 1;lstatOrUndefined(join2(process.cwd(), candidate)); n++) {
104
132
  candidate = `${relativePath}.bak.${n}`;
105
133
  }
106
134
  return candidate;
107
135
  }
136
+ function removeStaleReferences(referencesDir, source, record) {
137
+ const removed = [];
138
+ const kept = [];
139
+ const prefix = `${referencesDir}${sep}`;
140
+ for (const key of Object.keys(record)) {
141
+ if (!key.startsWith(prefix))
142
+ continue;
143
+ const name = key.slice(prefix.length);
144
+ if (source.references.has(name) || !/^[^\\/]+\.md$/.test(name))
145
+ continue;
146
+ const path = join2(process.cwd(), key);
147
+ const stat = lstatOrUndefined(path);
148
+ if (stat?.isFile() && matchesRecord(readFileSync(path, "utf8"), record[key])) {
149
+ unlinkSync(path);
150
+ removed.push(key);
151
+ } else if (stat) {
152
+ kept.push(key);
153
+ }
154
+ delete record[key];
155
+ }
156
+ return { removed, kept };
157
+ }
108
158
  function installSkillForAgent(skill, adapter, source, record) {
109
159
  const { dir, filename } = adapter.targetFor(skill.id);
110
160
  const dirPath = join2(process.cwd(), dir);
@@ -112,6 +162,10 @@ function installSkillForAgent(skill, adapter, source, record) {
112
162
  const relativePath = join2(dir, filename);
113
163
  const referencesDir = adapter.referencesDirFor(skill.id);
114
164
  const content = adapter.transform(source.content, referencesDir);
165
+ const symlinkPath = firstSymlinkedComponent(relativePath) ?? firstSymlinkedComponent(referencesDir);
166
+ if (symlinkPath) {
167
+ return { agent: adapter.name, relativePath, status: "symlink", symlinkPath };
168
+ }
115
169
  try {
116
170
  mkdirSync(dirPath, { recursive: true });
117
171
  let status = "wrote";
@@ -119,9 +173,9 @@ function installSkillForAgent(skill, adapter, source, record) {
119
173
  if (existsSync(filePath)) {
120
174
  const existing = readFileSync(filePath, "utf8");
121
175
  const recorded = record[relativePath];
122
- if (existing === content) {
176
+ if (normalizeEol(existing) === content) {
123
177
  status = "current";
124
- } else if (recorded ? recorded.sha256 === sha256(existing) : looksLikeShippedSkill(existing, skill)) {
178
+ } else if (recorded ? matchesRecord(existing, recorded) : looksLikeShippedSkill(existing, skill)) {
125
179
  if (!recorded) {
126
180
  backupPath = unusedBackupPath(relativePath);
127
181
  writeFileSync(join2(process.cwd(), backupPath), existing);
@@ -138,12 +192,18 @@ function installSkillForAgent(skill, adapter, source, record) {
138
192
  const referencesPath = join2(process.cwd(), referencesDir);
139
193
  mkdirSync(referencesPath, { recursive: true });
140
194
  const keptReferences = [];
195
+ const symlinkedReferences = [];
141
196
  for (const [name, text] of source.references) {
142
197
  const referencePath = join2(referencesPath, name);
143
198
  const referenceKey = join2(referencesDir, name);
144
- const existing = existsSync(referencePath) ? readFileSync(referencePath, "utf8") : undefined;
145
- if (existing !== text) {
146
- if (existing !== undefined && record[referenceKey]?.sha256 !== sha256(existing)) {
199
+ const stat = lstatOrUndefined(referencePath);
200
+ if (stat?.isSymbolicLink()) {
201
+ symlinkedReferences.push(referenceKey);
202
+ continue;
203
+ }
204
+ const existing = stat ? readFileSync(referencePath, "utf8") : undefined;
205
+ if (existing === undefined || normalizeEol(existing) !== text) {
206
+ if (existing !== undefined && !matchesRecord(existing, record[referenceKey])) {
147
207
  keptReferences.push(referenceKey);
148
208
  continue;
149
209
  }
@@ -151,12 +211,16 @@ function installSkillForAgent(skill, adapter, source, record) {
151
211
  }
152
212
  record[referenceKey] = { version, sha256: sha256(text) };
153
213
  }
214
+ const stale = removeStaleReferences(referencesDir, source, record);
154
215
  return {
155
216
  agent: adapter.name,
156
217
  relativePath,
157
218
  status,
158
219
  ...backupPath ? { backupPath } : {},
159
- ...keptReferences.length > 0 ? { keptReferences } : {}
220
+ ...keptReferences.length > 0 ? { keptReferences } : {},
221
+ ...symlinkedReferences.length > 0 ? { symlinkedReferences } : {},
222
+ ...stale.removed.length > 0 ? { removedReferences: stale.removed } : {},
223
+ ...stale.kept.length > 0 ? { keptStaleReferences: stale.kept } : {}
160
224
  };
161
225
  } catch (err) {
162
226
  return {
@@ -167,30 +231,59 @@ function installSkillForAgent(skill, adapter, source, record) {
167
231
  };
168
232
  }
169
233
  }
234
+ function emptyResult(status, agents) {
235
+ return {
236
+ status,
237
+ agents,
238
+ outcomes: [],
239
+ counts: { written: 0, updated: 0, current: 0, kept: 0, skipped: 0, failed: 0 },
240
+ warnings: [],
241
+ notes: []
242
+ };
243
+ }
244
+ function summarizeInstall(result) {
245
+ const { written, updated, current, kept, skipped, failed } = result.counts;
246
+ return [
247
+ written && `${written} written`,
248
+ updated && `${updated} updated`,
249
+ current && `${current} already current`,
250
+ kept && `${kept} kept (edited locally)`,
251
+ skipped && `${skipped} skipped (symlink)`,
252
+ failed && `${failed} failed`
253
+ ].filter(Boolean).join(", ");
254
+ }
170
255
  async function installRules(detectedAgents, options = {}) {
171
256
  const adapters = detectedAgents.map((a) => ADAPTERS[a]).filter((a) => a !== undefined);
172
257
  if (adapters.length === 0) {
173
258
  if (!options.silent)
174
259
  console.log(" No supported coding agents detected");
175
- return;
260
+ return emptyResult("skipped", []);
176
261
  }
177
- const agentList = adapters.map((a) => a.name).join(", ");
262
+ const agentNames = adapters.map((a) => a.name);
178
263
  const skillList = SKILLS.map((s) => `
179
264
  • ${s.label}`).join("");
180
265
  const shouldInstall = options.yes ? true : await confirm({
181
- message: `Install Moda skills for ${agentList}?${skillList}
266
+ message: `Install Moda skills for ${agentNames.join(", ")}?${skillList}
182
267
  Continue?`,
183
268
  default: true
184
269
  });
185
270
  if (!shouldInstall) {
186
271
  if (!options.silent)
187
272
  console.log(" Skipped");
188
- return;
273
+ return emptyResult("skipped", agentNames);
189
274
  }
190
- let wrote = 0;
191
- let skipped = 0;
192
- let failed = 0;
193
- let updated = 0;
275
+ const result = emptyResult("ok", agentNames);
276
+ const { counts, warnings, notes } = result;
277
+ const warn = (message) => {
278
+ warnings.push(message);
279
+ if (!options.silent)
280
+ console.error(` ${message}`);
281
+ };
282
+ const note = (message) => {
283
+ notes.push(message);
284
+ if (!options.silent)
285
+ console.log(` ${message}`);
286
+ };
194
287
  const record = readInstallRecord();
195
288
  const recordBefore = JSON.stringify(record);
196
289
  for (const skill of SKILLS) {
@@ -198,45 +291,54 @@ async function installRules(detectedAgents, options = {}) {
198
291
  try {
199
292
  source = loadSkillSource(skill.id);
200
293
  } catch (err) {
201
- const message = err instanceof Error ? err.message : String(err);
202
- console.error(` Warning: ${message}`);
203
- failed += adapters.length;
294
+ warn(`Warning: ${err instanceof Error ? err.message : String(err)}`);
295
+ counts.failed += adapters.length;
204
296
  continue;
205
297
  }
206
298
  for (const adapter of adapters) {
207
299
  const outcome = installSkillForAgent(skill, adapter, source, record);
208
- if (!options.silent) {
209
- for (const kept of outcome.keptReferences ?? []) {
210
- console.log(` ${outcome.agent}: ${kept} was edited locally — skipping (delete it to reinstall)`);
211
- }
300
+ result.outcomes.push(outcome);
301
+ const agent = outcome.agent;
302
+ for (const kept of outcome.keptReferences ?? []) {
303
+ note(`${agent}: ${kept} was edited locally — skipping (delete it to reinstall)`);
304
+ }
305
+ for (const linked of outcome.symlinkedReferences ?? []) {
306
+ warn(`${agent}: ${linked} is a symlink — skipping so the file it points to is not overwritten`);
307
+ }
308
+ for (const removed of outcome.removedReferences ?? []) {
309
+ note(`${agent}: removed ${removed} (no longer part of the skill)`);
310
+ }
311
+ for (const kept of outcome.keptStaleReferences ?? []) {
312
+ note(`${agent}: ${kept} is no longer part of the skill but was edited locally — leaving it in place`);
212
313
  }
213
314
  switch (outcome.status) {
214
315
  case "wrote":
215
316
  if (!options.silent)
216
- console.log(` ${outcome.agent}: wrote ${outcome.relativePath}`);
217
- wrote++;
317
+ console.log(` ${agent}: wrote ${outcome.relativePath}`);
318
+ counts.written++;
218
319
  break;
219
320
  case "updated":
220
- if (!options.silent) {
221
- const backup = outcome.backupPath ? ` (previous copy saved to ${outcome.backupPath})` : "";
222
- console.log(` ${outcome.agent}: updated ${outcome.relativePath} to the current skill${backup}`);
321
+ if (outcome.backupPath) {
322
+ warn(`${agent}: replaced ${outcome.relativePath} with the current skill (previous copy saved to ${outcome.backupPath})`);
323
+ } else if (!options.silent) {
324
+ console.log(` ${agent}: updated ${outcome.relativePath} to the current skill`);
223
325
  }
224
- updated++;
326
+ counts.updated++;
225
327
  break;
226
328
  case "current":
227
- skipped++;
329
+ counts.current++;
228
330
  break;
229
331
  case "exists":
230
- if (!options.silent) {
231
- console.log(` ${outcome.agent}: ${outcome.relativePath} was edited locally — skipping (delete it to reinstall)`);
232
- }
233
- skipped++;
332
+ note(`${agent}: ${outcome.relativePath} was edited locally — skipping (delete it to reinstall)`);
333
+ counts.kept++;
334
+ break;
335
+ case "symlink":
336
+ warn(`${agent}: skipped ${outcome.relativePath} because ${outcome.symlinkPath} is a symlink ` + "(install never writes through a link, which could overwrite files outside this project)");
337
+ counts.skipped++;
234
338
  break;
235
339
  case "failed":
236
- if (!options.silent) {
237
- console.error(` ${outcome.agent}: failed to write ${outcome.relativePath} — ${outcome.error}`);
238
- }
239
- failed++;
340
+ warn(`${agent}: failed to write ${outcome.relativePath} — ${outcome.error}`);
341
+ counts.failed++;
240
342
  break;
241
343
  }
242
344
  }
@@ -245,21 +347,14 @@ async function installRules(detectedAgents, options = {}) {
245
347
  try {
246
348
  writeInstallRecord(record);
247
349
  } catch (err) {
248
- if (!options.silent) {
249
- console.error(` Could not save ${INSTALL_RECORD_PATH} — ${err instanceof Error ? err.message : String(err)}. ` + "A later moda init may overwrite local edits to these skill files.");
250
- }
350
+ warn(`Could not save ${INSTALL_RECORD_PATH} — ${err instanceof Error ? err.message : String(err)}. ` + "A later moda init may overwrite local edits to these skill files.");
251
351
  }
252
352
  }
253
- const summary = [
254
- wrote && `${wrote} written`,
255
- updated && `${updated} updated`,
256
- skipped && `${skipped} already present`,
257
- failed && `${failed} failed`
258
- ].filter(Boolean).join(", ");
259
- if (summary) {
260
- if (!options.silent)
261
- console.log(` Done — ${summary}`);
262
- }
353
+ result.status = counts.failed > 0 ? "failed" : warnings.length > 0 ? "warning" : "ok";
354
+ const summary = summarizeInstall(result);
355
+ if (summary && !options.silent)
356
+ console.log(` Done — ${summary}`);
357
+ return result;
263
358
  }
264
359
 
265
360
  // src/init/mcp.ts
@@ -293,7 +388,7 @@ import {
293
388
  readdirSync as readdirSync2,
294
389
  readFileSync as readFileSync2,
295
390
  rmdirSync,
296
- unlinkSync,
391
+ unlinkSync as unlinkSync2,
297
392
  writeFileSync as writeFileSync2
298
393
  } from "node:fs";
299
394
  import { dirname as dirname3, isAbsolute, join as join4, normalize, relative, resolve } from "node:path";
@@ -376,7 +471,7 @@ function pruneManagedSkillPaths(cwd, managedPaths, log) {
376
471
  if (!absolutePath || !existsSync3(absolutePath))
377
472
  continue;
378
473
  try {
379
- unlinkSync(absolutePath);
474
+ unlinkSync2(absolutePath);
380
475
  pruned++;
381
476
  log(` pruned ${safePath}`);
382
477
  pruneEmptyClaudeSkillDir(cwd, safePath);
@@ -505,4 +600,4 @@ function yamlQuote(value) {
505
600
  return JSON.stringify(value);
506
601
  }
507
602
 
508
- export { packageAssetPath, claudeSkillToCursorRule, installRules, detectAgents, SKILL_SOURCE_HARNESS, SKILL_SOURCE_INTEGRATION_BUNDLED, readSkillsManifest, writeSkillsManifest, upsertSkillManifestRecord, pruneManagedSkillPaths };
603
+ export { packageAssetPath, claudeSkillToCursorRule, summarizeInstall, installRules, detectAgents, SKILL_SOURCE_HARNESS, SKILL_SOURCE_INTEGRATION_BUNDLED, readSkillsManifest, writeSkillsManifest, upsertSkillManifestRecord, pruneManagedSkillPaths };
@@ -8,7 +8,7 @@ import {
8
8
  saveProfileConfig,
9
9
  secretFilePathFromRef,
10
10
  writeSecretJsonFile
11
- } from "./cli-5ke3xe6y.js";
11
+ } from "./cli-t3a6eknn.js";
12
12
  import {
13
13
  __require
14
14
  } from "./cli-0v6na3yp.js";