agents-inc 0.164.0 → 0.166.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.
Files changed (191) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +8 -9
  3. package/dist/{chunk-X4SSJZLY.js → chunk-25MPBIK3.js} +12 -261
  4. package/dist/chunk-25MPBIK3.js.map +1 -0
  5. package/dist/{chunk-LT5GXEB2.js → chunk-2VUYFTUB.js} +8 -7
  6. package/dist/chunk-2VUYFTUB.js.map +1 -0
  7. package/dist/chunk-4AAOOAIN.js +49 -0
  8. package/dist/chunk-4AAOOAIN.js.map +1 -0
  9. package/dist/{chunk-ARYNOWKF.js → chunk-4NDBUJHK.js} +8 -8
  10. package/dist/{chunk-S63LXWMC.js → chunk-6L75SQZU.js} +2 -2
  11. package/dist/{chunk-7REKOTKG.js → chunk-6LF4CQBY.js} +3 -3
  12. package/dist/{chunk-K5ITEA3I.js → chunk-7JNJ52CD.js} +34 -17
  13. package/dist/chunk-7JNJ52CD.js.map +1 -0
  14. package/dist/{chunk-XPDVAJDH.js → chunk-A4KDXNOT.js} +3519 -1568
  15. package/dist/chunk-A4KDXNOT.js.map +1 -0
  16. package/dist/{chunk-XHPWTL4I.js → chunk-DAIEQJIF.js} +3 -3
  17. package/dist/{chunk-UITEKY2R.js → chunk-ENGYJCTF.js} +28 -4
  18. package/dist/chunk-ENGYJCTF.js.map +1 -0
  19. package/dist/{chunk-G2JJ5BO5.js → chunk-G4JK7WVO.js} +336 -140
  20. package/dist/chunk-G4JK7WVO.js.map +1 -0
  21. package/dist/{chunk-X37UYIE3.js → chunk-G6UREQ4V.js} +436 -739
  22. package/dist/chunk-G6UREQ4V.js.map +1 -0
  23. package/dist/chunk-GE3WKJ6Q.js +375 -0
  24. package/dist/chunk-GE3WKJ6Q.js.map +1 -0
  25. package/dist/{chunk-RGYDFBCT.js → chunk-HHIYT3PI.js} +5 -5
  26. package/dist/{chunk-2CQNPLVF.js → chunk-HWEKHVLE.js} +63 -42
  27. package/dist/chunk-HWEKHVLE.js.map +1 -0
  28. package/dist/{chunk-XVAQG2CV.js → chunk-JOEA52NR.js} +10 -8
  29. package/dist/chunk-JOEA52NR.js.map +1 -0
  30. package/dist/{chunk-3WVH2CTK.js → chunk-JQRJQ4SS.js} +15 -8
  31. package/dist/chunk-JQRJQ4SS.js.map +1 -0
  32. package/dist/{chunk-DSL66I4N.js → chunk-LFWLEDUG.js} +6 -6
  33. package/dist/{chunk-B3Y3QMWI.js → chunk-MAWNVT2C.js} +4 -4
  34. package/dist/{chunk-Q4IRRVXK.js → chunk-MPTNC32T.js} +3 -3
  35. package/dist/chunk-NBXXFIIW.js +73 -0
  36. package/dist/chunk-NBXXFIIW.js.map +1 -0
  37. package/dist/{chunk-B6FSLXYN.js → chunk-PMJWD27C.js} +3 -3
  38. package/dist/{chunk-XATLUKXK.js → chunk-PNB3524H.js} +6 -6
  39. package/dist/chunk-PXWG7KFO.js +39 -0
  40. package/dist/chunk-PXWG7KFO.js.map +1 -0
  41. package/dist/{chunk-4VL3CEOS.js → chunk-QHDPLLK6.js} +3 -3
  42. package/dist/{chunk-AOTCIUGX.js → chunk-RAHSOJQ2.js} +2 -2
  43. package/dist/{chunk-PHCPUQVR.js → chunk-RZDXDMT6.js} +2 -2
  44. package/dist/{chunk-6MV4JOCL.js → chunk-SAYZWCH6.js} +2 -2
  45. package/dist/{chunk-URXIQE7I.js → chunk-SCOQ34YM.js} +2 -2
  46. package/dist/{chunk-NQXO4LR3.js → chunk-SXVHSOTH.js} +16 -11
  47. package/dist/chunk-SXVHSOTH.js.map +1 -0
  48. package/dist/{chunk-QZX7SYKU.js → chunk-T6UWJAB3.js} +2 -2
  49. package/dist/{chunk-ZVXGW2IC.js → chunk-TODCQFRB.js} +4 -6
  50. package/dist/chunk-TODCQFRB.js.map +1 -0
  51. package/dist/{chunk-YRS3QZP2.js → chunk-TTY6DPJN.js} +3 -3
  52. package/dist/{chunk-QGUP3QJF.js → chunk-TZ2YJJKK.js} +22 -18
  53. package/dist/{chunk-QGUP3QJF.js.map → chunk-TZ2YJJKK.js.map} +1 -1
  54. package/dist/chunk-V3I7VQ7N.js +356 -0
  55. package/dist/chunk-V3I7VQ7N.js.map +1 -0
  56. package/dist/{chunk-AVK2HEKB.js → chunk-V5WXIM2F.js} +2 -6
  57. package/dist/{chunk-AVK2HEKB.js.map → chunk-V5WXIM2F.js.map} +1 -1
  58. package/dist/{chunk-NKGYKOB7.js → chunk-VAPTVR3J.js} +7 -8
  59. package/dist/chunk-VAPTVR3J.js.map +1 -0
  60. package/dist/chunk-VRXX4D6K.js +31 -0
  61. package/dist/chunk-VRXX4D6K.js.map +1 -0
  62. package/dist/{chunk-ABAZMMG4.js → chunk-WVUDLKIO.js} +2 -2
  63. package/dist/{chunk-ECURHW3T.js → chunk-WWIFNPJG.js} +13 -12
  64. package/dist/chunk-WWIFNPJG.js.map +1 -0
  65. package/dist/{chunk-J27W6TXS.js → chunk-XQHZPINL.js} +8 -4
  66. package/dist/chunk-XQHZPINL.js.map +1 -0
  67. package/dist/{chunk-V5GLL2JB.js → chunk-XZEVWPX6.js} +7 -7
  68. package/dist/chunk-Y3VIRVVE.js +932 -0
  69. package/dist/chunk-Y3VIRVVE.js.map +1 -0
  70. package/dist/{chunk-NTF4UXGK.js → chunk-Y47EV74W.js} +73 -30
  71. package/dist/chunk-Y47EV74W.js.map +1 -0
  72. package/dist/chunk-ZR4YPKXQ.js +159 -0
  73. package/dist/chunk-ZR4YPKXQ.js.map +1 -0
  74. package/dist/chunk-ZXKBERQU.js +250 -0
  75. package/dist/chunk-ZXKBERQU.js.map +1 -0
  76. package/dist/commands/build/marketplace.js +17 -17
  77. package/dist/commands/build/marketplace.js.map +1 -1
  78. package/dist/commands/build/plugins.js +11 -9
  79. package/dist/commands/build/plugins.js.map +1 -1
  80. package/dist/commands/compile.js +105 -27
  81. package/dist/commands/compile.js.map +1 -1
  82. package/dist/commands/doctor.js +309 -171
  83. package/dist/commands/doctor.js.map +1 -1
  84. package/dist/commands/edit.js +503 -314
  85. package/dist/commands/edit.js.map +1 -1
  86. package/dist/commands/eject.js +40 -23
  87. package/dist/commands/eject.js.map +1 -1
  88. package/dist/commands/init.js +44 -40
  89. package/dist/commands/list.js +44 -10
  90. package/dist/commands/list.js.map +1 -1
  91. package/dist/commands/new/marketplace.js +13 -10
  92. package/dist/commands/new/marketplace.js.map +1 -1
  93. package/dist/commands/search.js +12 -10
  94. package/dist/commands/search.js.map +1 -1
  95. package/dist/commands/share.js +102 -15
  96. package/dist/commands/share.js.map +1 -1
  97. package/dist/commands/uninstall.js +231 -103
  98. package/dist/commands/uninstall.js.map +1 -1
  99. package/dist/commands/update.js +49 -20
  100. package/dist/commands/update.js.map +1 -1
  101. package/dist/components/common/prompt-confirm.js +1 -1
  102. package/dist/components/common/removal-plan-confirm.js +2 -2
  103. package/dist/components/common/select-list.js +3 -3
  104. package/dist/components/common/spinner.js +6 -3
  105. package/dist/components/wizard/category-grid.js +5 -5
  106. package/dist/components/wizard/checkbox-grid.js +4 -4
  107. package/dist/components/wizard/domain-selection.js +9 -9
  108. package/dist/components/wizard/run-wizard-session.js +29 -29
  109. package/dist/components/wizard/scroll-affordance.js +2 -2
  110. package/dist/components/wizard/skill-agent-summary.js +5 -5
  111. package/dist/components/wizard/source-grid.js +6 -6
  112. package/dist/components/wizard/stack-selection.js +8 -8
  113. package/dist/components/wizard/step-agents.js +8 -8
  114. package/dist/components/wizard/step-build.js +8 -8
  115. package/dist/components/wizard/step-confirm.js +9 -9
  116. package/dist/components/wizard/step-sources.js +9 -9
  117. package/dist/components/wizard/step-stack.js +9 -9
  118. package/dist/components/wizard/summary-panel.js +8 -8
  119. package/dist/components/wizard/toast.js +2 -2
  120. package/dist/components/wizard/wizard-layout.js +12 -12
  121. package/dist/components/wizard/wizard-tabs.js +2 -2
  122. package/dist/components/wizard/wizard.js +27 -27
  123. package/dist/hooks/init.js +69 -43
  124. package/dist/hooks/init.js.map +1 -1
  125. package/dist/load-agent-defs-3C27ENC7.js +14 -0
  126. package/dist/recompile-project-agents-DGNBCU3N.js +18 -0
  127. package/dist/{source-loader-GRBVNRAW.js → source-loader-7KZ3MD5O.js} +6 -4
  128. package/dist/src/agents/_templates/agent-body.liquid +55 -0
  129. package/dist/src/agents/_templates/agent.liquid +1 -55
  130. package/dist/src/agents/meta/agent-summoner/output.md +1 -1
  131. package/dist/src/agents/meta/agent-summoner/playbook.md +15 -7
  132. package/dist/stores/wizard-store.js +4 -4
  133. package/package.json +10 -9
  134. package/src/agents/_templates/agent-body.liquid +55 -0
  135. package/src/agents/_templates/agent.liquid +1 -55
  136. package/src/agents/meta/agent-summoner/output.md +1 -1
  137. package/src/agents/meta/agent-summoner/playbook.md +15 -7
  138. package/src/schemas/project-config.schema.json +1 -1
  139. package/src/schemas/project-source-config.schema.json +1 -1
  140. package/dist/chunk-2CQNPLVF.js.map +0 -1
  141. package/dist/chunk-3WVH2CTK.js.map +0 -1
  142. package/dist/chunk-7ANJQLPJ.js +0 -35
  143. package/dist/chunk-7ANJQLPJ.js.map +0 -1
  144. package/dist/chunk-ECURHW3T.js.map +0 -1
  145. package/dist/chunk-FLW25ORT.js +0 -130
  146. package/dist/chunk-FLW25ORT.js.map +0 -1
  147. package/dist/chunk-G2JJ5BO5.js.map +0 -1
  148. package/dist/chunk-J27W6TXS.js.map +0 -1
  149. package/dist/chunk-JCPANDNR.js +0 -35
  150. package/dist/chunk-JCPANDNR.js.map +0 -1
  151. package/dist/chunk-JIERYE6B.js +0 -480
  152. package/dist/chunk-JIERYE6B.js.map +0 -1
  153. package/dist/chunk-JU4ZGJ7A.js +0 -668
  154. package/dist/chunk-JU4ZGJ7A.js.map +0 -1
  155. package/dist/chunk-K5ITEA3I.js.map +0 -1
  156. package/dist/chunk-LT5GXEB2.js.map +0 -1
  157. package/dist/chunk-NKGYKOB7.js.map +0 -1
  158. package/dist/chunk-NQXO4LR3.js.map +0 -1
  159. package/dist/chunk-NTF4UXGK.js.map +0 -1
  160. package/dist/chunk-UITEKY2R.js.map +0 -1
  161. package/dist/chunk-X37UYIE3.js.map +0 -1
  162. package/dist/chunk-X4SSJZLY.js.map +0 -1
  163. package/dist/chunk-XPDVAJDH.js.map +0 -1
  164. package/dist/chunk-XVAQG2CV.js.map +0 -1
  165. package/dist/chunk-ZBPEFQ4G.js +0 -12
  166. package/dist/chunk-ZBPEFQ4G.js.map +0 -1
  167. package/dist/chunk-ZVXGW2IC.js.map +0 -1
  168. package/dist/load-agent-defs-7IM3GBEQ.js +0 -13
  169. package/dist/recompile-project-agents-VR4NLGOU.js +0 -17
  170. /package/dist/{chunk-ARYNOWKF.js.map → chunk-4NDBUJHK.js.map} +0 -0
  171. /package/dist/{chunk-S63LXWMC.js.map → chunk-6L75SQZU.js.map} +0 -0
  172. /package/dist/{chunk-7REKOTKG.js.map → chunk-6LF4CQBY.js.map} +0 -0
  173. /package/dist/{chunk-XHPWTL4I.js.map → chunk-DAIEQJIF.js.map} +0 -0
  174. /package/dist/{chunk-RGYDFBCT.js.map → chunk-HHIYT3PI.js.map} +0 -0
  175. /package/dist/{chunk-DSL66I4N.js.map → chunk-LFWLEDUG.js.map} +0 -0
  176. /package/dist/{chunk-B3Y3QMWI.js.map → chunk-MAWNVT2C.js.map} +0 -0
  177. /package/dist/{chunk-Q4IRRVXK.js.map → chunk-MPTNC32T.js.map} +0 -0
  178. /package/dist/{chunk-B6FSLXYN.js.map → chunk-PMJWD27C.js.map} +0 -0
  179. /package/dist/{chunk-XATLUKXK.js.map → chunk-PNB3524H.js.map} +0 -0
  180. /package/dist/{chunk-4VL3CEOS.js.map → chunk-QHDPLLK6.js.map} +0 -0
  181. /package/dist/{chunk-AOTCIUGX.js.map → chunk-RAHSOJQ2.js.map} +0 -0
  182. /package/dist/{chunk-PHCPUQVR.js.map → chunk-RZDXDMT6.js.map} +0 -0
  183. /package/dist/{chunk-6MV4JOCL.js.map → chunk-SAYZWCH6.js.map} +0 -0
  184. /package/dist/{chunk-URXIQE7I.js.map → chunk-SCOQ34YM.js.map} +0 -0
  185. /package/dist/{chunk-QZX7SYKU.js.map → chunk-T6UWJAB3.js.map} +0 -0
  186. /package/dist/{chunk-YRS3QZP2.js.map → chunk-TTY6DPJN.js.map} +0 -0
  187. /package/dist/{chunk-ABAZMMG4.js.map → chunk-WVUDLKIO.js.map} +0 -0
  188. /package/dist/{chunk-V5GLL2JB.js.map → chunk-XZEVWPX6.js.map} +0 -0
  189. /package/dist/{load-agent-defs-7IM3GBEQ.js.map → load-agent-defs-3C27ENC7.js.map} +0 -0
  190. /package/dist/{recompile-project-agents-VR4NLGOU.js.map → recompile-project-agents-DGNBCU3N.js.map} +0 -0
  191. /package/dist/{source-loader-GRBVNRAW.js.map → source-loader-7KZ3MD5O.js.map} +0 -0
@@ -0,0 +1,932 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ installPluginSkills,
4
+ pluginInstallFailureError,
5
+ unbackedPluginInstallError,
6
+ unbackedPluginSkillIds
7
+ } from "./chunk-VAPTVR3J.js";
8
+ import {
9
+ clearTerminalScreen,
10
+ formatTerminalTooSmallMessage,
11
+ isTerminalLargeEnough
12
+ } from "./chunk-RZDXDMT6.js";
13
+ import {
14
+ STATUS_MESSAGES,
15
+ claimSlug,
16
+ completedWithFailures,
17
+ computeFileHash,
18
+ copy,
19
+ defaultUsageGuidance,
20
+ directoryExists,
21
+ fetchMarketplace,
22
+ findConfigLoadFailures,
23
+ findUnusableSavedSkillMetadata,
24
+ getErrorMessage,
25
+ getStackSkillIds,
26
+ glob,
27
+ hostAt,
28
+ injectForkedFromMetadata,
29
+ installationConfigsUnreadable,
30
+ isHomeDirectory,
31
+ parseFrontmatter,
32
+ pluginsInstalled,
33
+ propagatedRecompileSummary,
34
+ readFile,
35
+ readForkedFromMetadata,
36
+ readLocalSkillMetadata,
37
+ readSkillMetadata,
38
+ refuseUnofferedPlacements,
39
+ refuseUnofferedStack,
40
+ refuseUnwritableStack,
41
+ registeredProjectConfigUnreadable,
42
+ registeredProjectGone,
43
+ registeredProjectOnAnotherProvider,
44
+ remove,
45
+ resolveBranding,
46
+ resolveInstallPaths,
47
+ savedSkillMetadataUnusableError,
48
+ scopeBlockedStackAssignment,
49
+ sharedConfigProjectScopeAtHome,
50
+ skillAssignedToNoAgent,
51
+ skillMetadataUnusableDetail,
52
+ validateSkillPath,
53
+ verbose,
54
+ warn,
55
+ writeFile,
56
+ writeMetadataYaml
57
+ } from "./chunk-A4KDXNOT.js";
58
+ import {
59
+ BUILT_IN_MATRIX,
60
+ getCategoryDomain,
61
+ typedEntries,
62
+ typedKeys
63
+ } from "./chunk-V5WXIM2F.js";
64
+ import {
65
+ CLI_COLORS,
66
+ DEFAULT_BRANDING,
67
+ GITHUB_SOURCE,
68
+ LOCAL_SKILLS_PATH,
69
+ MIN_TERMINAL_SIZE,
70
+ STANDARD_FILES,
71
+ isActiveAt,
72
+ seedExternalSkillSchema
73
+ } from "./chunk-ENGYJCTF.js";
74
+
75
+ // src/cli/lib/exit-codes.ts
76
+ var EXIT_CODES = {
77
+ SUCCESS: 0,
78
+ ERROR: 1,
79
+ INVALID_ARGS: 2,
80
+ NETWORK_ERROR: 3,
81
+ CANCELLED: 4,
82
+ /**
83
+ * The command ran to the end and part of what it set out to do did not happen.
84
+ *
85
+ * Distinct from {@link EXIT_CODES.ERROR} because the two ask opposite things of whoever reads
86
+ * them: an `ERROR` aborted, so nothing landed and the run can be repeated; this one landed and
87
+ * the repeat is the wrong move — what is owed is the remedy the output names, per failure.
88
+ * Collapsing it into `ERROR` would re-make the defect it exists to close one value along, and
89
+ * a spec asserting `not.toBe(0)` cannot tell a refusal from a partial apply either.
90
+ *
91
+ * It is raised at the END of a command, never mid-flight: the work completes first, which is
92
+ * the whole difference between this and `this.error`.
93
+ */
94
+ COMPLETED_WITH_FAILURES: 5
95
+ };
96
+
97
+ // src/cli/lib/seed/external-skills.ts
98
+ import os from "os";
99
+ import path from "path";
100
+ import { mkdtemp } from "fs/promises";
101
+ import { isDeepEqual, omit } from "remeda";
102
+
103
+ // ../compile/src/carried-skill.ts
104
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
105
+ var FRONTMATTER_BLOCK = /^---\r?\n([\s\S]*?)\r?\n---/;
106
+ var NAME_FIELD = /^name:.*$/m;
107
+ function withInstalledName(manifest, { id, description }) {
108
+ const named = `name: ${id}`;
109
+ const block = FRONTMATTER_BLOCK.exec(manifest);
110
+ if (!block) {
111
+ return `---
112
+ ${named}
113
+ description: ${description}
114
+ ---
115
+
116
+ ${manifest}`;
117
+ }
118
+ const [whole, fields = ""] = block;
119
+ const renamed = NAME_FIELD.test(fields) ? fields.replace(NAME_FIELD, named) : `${named}
120
+ ${fields}`;
121
+ return `---
122
+ ${renamed}
123
+ ---${manifest.slice(whole.length)}`;
124
+ }
125
+ function carriedSkillAuthor(repo) {
126
+ const [owner = repo] = repo.split("/");
127
+ return `@${owner}`;
128
+ }
129
+ function carriedSkillMetadata({
130
+ id,
131
+ skill,
132
+ domain,
133
+ usageGuidance,
134
+ shipped
135
+ }) {
136
+ return {
137
+ author: carriedSkillAuthor(skill.repo),
138
+ usageGuidance,
139
+ ...shipped,
140
+ displayName: skill.displayName,
141
+ slug: id,
142
+ category: skill.categoryId,
143
+ domain,
144
+ cliDescription: shortLabel(skill.description),
145
+ custom: true
146
+ };
147
+ }
148
+ var CLI_DESCRIPTION_MAX_LENGTH = 60;
149
+ var ELLIPSIS = "\u2026";
150
+ function shortLabel(description) {
151
+ if (description.length <= CLI_DESCRIPTION_MAX_LENGTH) return description;
152
+ const room = description.slice(
153
+ 0,
154
+ CLI_DESCRIPTION_MAX_LENGTH - ELLIPSIS.length
155
+ );
156
+ return `${toLastWholeWord(room)}${ELLIPSIS}`;
157
+ }
158
+ function toLastWholeWord(text) {
159
+ const lastBreak = text.lastIndexOf(" ");
160
+ return (lastBreak > 0 ? text.slice(0, lastBreak) : text).trimEnd();
161
+ }
162
+
163
+ // src/cli/lib/seed/external-skills.ts
164
+ function pluginInstallError(ids) {
165
+ return [
166
+ "This configuration cannot be installed: these skills travel inside it, so no marketplace serves them and they cannot be installed as plugins:",
167
+ ...ids.map((id) => ` ${id}`),
168
+ "Re-share it with each skill above set to eject, which is the only way a carried skill installs."
169
+ ].join("\n");
170
+ }
171
+ function selectedExternals(payload) {
172
+ return typedEntries(payload.external ?? {}).flatMap(([id, skill]) => {
173
+ const entry = payload.skills[id];
174
+ return entry ? [{ id, skill, entry }] : [];
175
+ });
176
+ }
177
+ function askedForAsPlugin(externals) {
178
+ return externals.filter(({ entry }) => entry.install !== "eject").map(({ id }) => id);
179
+ }
180
+ function claimingACatalogueId(externals, matrix) {
181
+ return externals.filter(({ id }) => heldByEitherCatalogue(id, matrix)).map(({ id }) => id);
182
+ }
183
+ var CATALOGUE_SKILL_IDS = new Set(typedKeys(BUILT_IN_MATRIX.skills));
184
+ function heldByEitherCatalogue(id, matrix) {
185
+ return heldByLoadedCatalogue(matrix.skills[id]) || CATALOGUE_SKILL_IDS.has(id);
186
+ }
187
+ function heldByLoadedCatalogue(incumbent) {
188
+ return incumbent !== void 0 && incumbent.local !== true;
189
+ }
190
+ function catalogueIdCollisionError(ids) {
191
+ return [
192
+ "This configuration cannot be installed: these skills travel inside it under ids a catalogue already owns:",
193
+ ...ids.map((id) => ` ${id}`),
194
+ "A skill id is the directory the skill installs into, so each would be written over the catalogue's own copy of that skill. Re-share it with each skill above added again, which mints an id \u2014 'external-<category>-<name>' \u2014 that no catalogue owns."
195
+ ].join("\n");
196
+ }
197
+ function externalSlug(id) {
198
+ return id;
199
+ }
200
+ function repoRef(repo) {
201
+ return `${GITHUB_SOURCE.GITHUB_PREFIX}${repo}`;
202
+ }
203
+ function externalSkillDir(id, scope, projectDir) {
204
+ return path.join(resolveInstallPaths(projectDir, scope).skillsDir, id);
205
+ }
206
+ function externalCatalogueEntry(install) {
207
+ return {
208
+ id: install.id,
209
+ slug: externalSlug(install.id),
210
+ displayName: install.skill.displayName,
211
+ description: install.skill.description,
212
+ // Boundary cast held by the guard above it: the category was confirmed against the catalogue
213
+ // the payload names, and this one has just been shown to declare it too.
214
+ category: install.skill.categoryId,
215
+ author: carriedSkillAuthor(install.skill.repo),
216
+ path: `${LOCAL_SKILLS_PATH}/${install.id}/`,
217
+ conflictsWith: [],
218
+ requires: [],
219
+ alternatives: [],
220
+ discourages: [],
221
+ // Local and custom for the same reason: the bytes are on this machine and nowhere else, and
222
+ // the skill was authored outside the vocabulary this catalogue ships.
223
+ local: true,
224
+ custom: true,
225
+ localPath: install.skillDir
226
+ };
227
+ }
228
+ function seatExternalSkill({ id, skill, entry }, matrix, projectDir) {
229
+ const domain = getCategoryDomain(skill.categoryId);
230
+ if (domain === void 0) return [];
231
+ const install = {
232
+ id,
233
+ skill,
234
+ domain,
235
+ scope: entry.scope,
236
+ skillDir: externalSkillDir(id, entry.scope, projectDir)
237
+ };
238
+ matrix.skills[id] = externalCatalogueEntry(install);
239
+ claimSlug(matrix.slugMap, externalSlug(id), id);
240
+ return [install];
241
+ }
242
+ function registerExternalSkills(payload, matrix, projectDir) {
243
+ const externals = selectedExternals(payload);
244
+ const asPlugin = askedForAsPlugin(externals);
245
+ const colliding = claimingACatalogueId(externals, matrix);
246
+ if (asPlugin.length > 0) throw new Error(pluginInstallError(asPlugin));
247
+ if (colliding.length > 0) throw new Error(catalogueIdCollisionError(colliding));
248
+ return externals.flatMap((external) => seatExternalSkill(external, matrix, projectDir));
249
+ }
250
+ async function writeExternalSkills(installs) {
251
+ await refuseUncarriedDestinations(installs);
252
+ const written = await Promise.all(installs.map(writeExternalSkill));
253
+ return changedIds(written);
254
+ }
255
+ var SCRATCH_DIR_PREFIX = "agents-inc-carried-";
256
+ async function carriedSkillsThatDiffer(installs) {
257
+ if (installs.length === 0) return [];
258
+ const scratch = await mkdtemp(path.join(os.tmpdir(), SCRATCH_DIR_PREFIX));
259
+ try {
260
+ const written = await Promise.all(
261
+ installs.map(async (install) => writeExternalSkill(await scratchCopyOf(install, scratch)))
262
+ );
263
+ return changedIds(written);
264
+ } finally {
265
+ await remove(scratch);
266
+ }
267
+ }
268
+ async function scratchCopyOf(install, scratch) {
269
+ const skillDir = path.join(scratch, install.id);
270
+ if (await directoryExists(install.skillDir)) await copy(install.skillDir, skillDir);
271
+ return { ...install, skillDir };
272
+ }
273
+ function changedIds(written) {
274
+ return written.filter(({ changed }) => changed).map(({ id }) => id);
275
+ }
276
+ async function writeExternalSkill(install) {
277
+ const before = await readSkillDir(install.skillDir);
278
+ await writeSkillTree(install);
279
+ await writeSkillManifest(install);
280
+ await registerSkillOnDisk(install);
281
+ return { id: install.id, changed: !isDeepEqual(before, await readSkillDir(install.skillDir)) };
282
+ }
283
+ async function readSkillDir(skillDir) {
284
+ if (!await directoryExists(skillDir)) return null;
285
+ const files = omit(await readSkillTree(skillDir), [STANDARD_FILES.METADATA_YAML]);
286
+ return { files, metadata: undated(await readLocalSkillMetadata(skillDir)) };
287
+ }
288
+ function undated(metadata) {
289
+ if (metadata?.forkedFrom === void 0) return metadata;
290
+ return { ...metadata, forkedFrom: omit(metadata.forkedFrom, ["date"]) };
291
+ }
292
+ async function refuseUncarriedDestinations(installs) {
293
+ const judged = await Promise.all(installs.map(judgeDestination));
294
+ const blocked = judged.filter((destination) => destination !== null);
295
+ if (blocked.length > 0) throw new Error(uncarriedDestinationError(blocked));
296
+ }
297
+ async function judgeDestination(install) {
298
+ const { id, skillDir } = install;
299
+ if (!await directoryExists(skillDir)) return null;
300
+ if ((await readForkedFromMetadata(skillDir))?.path !== void 0) return null;
301
+ return { id, skillDir };
302
+ }
303
+ function uncarriedDestinationError(blocked) {
304
+ return [
305
+ "This configuration cannot be installed: these skills travel inside it under ids this installation already keeps a skill directory for, and no shared configuration put those directories there:",
306
+ ...blocked.map(({ id, skillDir }) => ` ${id} -> ${skillDir}`),
307
+ "A skill id is the directory the skill installs into, so each would be written over a skill written here by hand or ejected from a marketplace. Move or rename the directory above, or re-share the configuration with each skill added again, which mints an id \u2014 'external-<category>-<name>' \u2014 that nothing here holds."
308
+ ].join("\n");
309
+ }
310
+ async function writeSkillTree(install) {
311
+ for (const [relativePath, contents] of Object.entries(install.skill.files)) {
312
+ const filePath = path.join(install.skillDir, relativePath);
313
+ validateSkillPath(filePath, install.skillDir, relativePath);
314
+ await writeFile(filePath, contents);
315
+ }
316
+ }
317
+ async function writeSkillManifest(install) {
318
+ const manifestPath = path.join(install.skillDir, STANDARD_FILES.SKILL_MD);
319
+ await writeFile(
320
+ manifestPath,
321
+ withInstalledName(await readFile(manifestPath), {
322
+ id: install.id,
323
+ description: install.skill.description
324
+ })
325
+ );
326
+ }
327
+ async function registerSkillOnDisk(install) {
328
+ const metadataPath = path.join(install.skillDir, STANDARD_FILES.METADATA_YAML);
329
+ const shipped = await readLocalSkillMetadata(install.skillDir);
330
+ await writeMetadataYaml(
331
+ metadataPath,
332
+ carriedSkillMetadata({
333
+ id: install.id,
334
+ skill: install.skill,
335
+ domain: install.domain,
336
+ usageGuidance: defaultUsageGuidance(install.skill.categoryId),
337
+ shipped
338
+ })
339
+ );
340
+ await injectForkedFromMetadata(
341
+ install.skillDir,
342
+ install.id,
343
+ await computeFileHash(path.join(install.skillDir, STANDARD_FILES.SKILL_MD)),
344
+ { source: repoRef(install.skill.repo), path: install.skill.path }
345
+ );
346
+ }
347
+ function unnameableRepoMessage(id) {
348
+ return `${id} travels inside a shared configuration, and this installation does not record the repository its content came from`;
349
+ }
350
+ function uncarryableContentMessage(id, reason) {
351
+ return `${id} travels inside a shared configuration, and ${reason}`;
352
+ }
353
+ function repoFromRef(ref) {
354
+ if (ref === void 0 || !ref.startsWith(GITHUB_SOURCE.GITHUB_PREFIX)) return void 0;
355
+ return ref.slice(GITHUB_SOURCE.GITHUB_PREFIX.length);
356
+ }
357
+ var NO_MANIFEST_DESCRIPTION = `no description can be read from its ${STANDARD_FILES.SKILL_MD}`;
358
+ function manifestDescription(files, skillDir) {
359
+ const manifest = files[STANDARD_FILES.SKILL_MD];
360
+ if (manifest === void 0) return null;
361
+ const frontmatter = parseFrontmatter(manifest, path.join(skillDir, STANDARD_FILES.SKILL_MD));
362
+ return frontmatter === null ? null : frontmatter.description;
363
+ }
364
+ async function readSkillTree(skillDir) {
365
+ const files = await glob("**/*", skillDir, { dot: true });
366
+ const entries = await Promise.all(
367
+ files.map(async (file) => [file, await readFile(path.join(skillDir, file))])
368
+ );
369
+ return Object.fromEntries(entries);
370
+ }
371
+ async function readCarriedSkill({ id, skillDir, provenance }) {
372
+ if (provenance.path === void 0) return { carries: "nothing" };
373
+ const repo = repoFromRef(provenance.source);
374
+ if (repo === void 0) return { carries: "unshareable", line: unnameableRepoMessage(id) };
375
+ const described = await readSkillMetadata(path.join(skillDir, STANDARD_FILES.METADATA_YAML));
376
+ if (!described.usable) {
377
+ return { carries: "unshareable", line: uncarryableContentMessage(id, described.reason) };
378
+ }
379
+ const files = await readSkillTree(skillDir);
380
+ const description = manifestDescription(files, skillDir);
381
+ if (description === null) {
382
+ return { carries: "unshareable", line: uncarryableContentMessage(id, NO_MANIFEST_DESCRIPTION) };
383
+ }
384
+ const parsed = seedExternalSkillSchema.safeParse({
385
+ displayName: described.metadata.displayName,
386
+ description,
387
+ categoryId: described.metadata.category,
388
+ repo,
389
+ path: provenance.path,
390
+ files
391
+ });
392
+ if (!parsed.success) {
393
+ const reason = parsed.error.issues.map((issue) => issue.message).join("; ");
394
+ return { carries: "unshareable", line: uncarryableContentMessage(id, reason) };
395
+ }
396
+ return { carries: "content", id, skill: parsed.data };
397
+ }
398
+ async function readCarriedSkills(owned) {
399
+ const reads = await Promise.all(owned.map(readCarriedSkill));
400
+ return {
401
+ external: Object.fromEntries(
402
+ reads.flatMap((read) => read.carries === "content" ? [[read.id, read.skill]] : [])
403
+ ),
404
+ uncarryable: reads.flatMap((read) => read.carries === "unshareable" ? [read.line] : [])
405
+ };
406
+ }
407
+
408
+ // src/cli/base-command.ts
409
+ import { Command } from "@oclif/core";
410
+ import chalk from "chalk";
411
+ import { unique } from "remeda";
412
+
413
+ // src/cli/lib/operations/source/ensure-marketplace.ts
414
+ async function ensureMarketplace(sourceResult, host) {
415
+ const source = sourceResult.sourceConfig.source;
416
+ if (!sourceResult.marketplace) {
417
+ try {
418
+ const marketplaceResult = await fetchMarketplace(source);
419
+ sourceResult.marketplace = marketplaceResult.marketplace.name;
420
+ } catch (error) {
421
+ warn(`Could not resolve a marketplace from '${source}': ${getErrorMessage(error)}`);
422
+ return { marketplace: null, registered: false };
423
+ }
424
+ }
425
+ const marketplace = sourceResult.marketplace;
426
+ const exists = await host.marketplaceExists(marketplace);
427
+ if (!exists) {
428
+ const marketplaceSource = source.replace(/^github:/, "");
429
+ await host.addMarketplace(marketplaceSource);
430
+ return { marketplace, registered: true };
431
+ }
432
+ try {
433
+ await host.refreshMarketplace(marketplace);
434
+ } catch {
435
+ warn("Could not update marketplace \u2014 continuing with cached version");
436
+ }
437
+ return { marketplace, registered: false };
438
+ }
439
+
440
+ // src/cli/lib/operations/source/require-marketplace.ts
441
+ async function requireMarketplace(sourceResult, purpose, host) {
442
+ const mpResult = await ensureMarketplace(sourceResult, host);
443
+ if (!mpResult.marketplace) {
444
+ return {
445
+ ok: false,
446
+ error: `Cannot ${purpose}: marketplace could not be resolved from '${sourceResult.sourceConfig.source}'. Plugin install mode requires a marketplace \u2014 fix the marketplace or switch the affected skills to eject mode.`
447
+ };
448
+ }
449
+ return { ok: true, marketplace: mpResult.marketplace, registered: mpResult.registered };
450
+ }
451
+
452
+ // src/cli/base-command.ts
453
+ var TERMINAL_RESIZE_POLL_MS = 500;
454
+ var terminalColumns = () => process.stdout.columns ?? MIN_TERMINAL_SIZE.COLS;
455
+ var terminalRows = () => process.stdout.rows ?? MIN_TERMINAL_SIZE.ROWS;
456
+ function isTerminalBigEnough() {
457
+ return isTerminalLargeEnough(terminalColumns(), terminalRows());
458
+ }
459
+ function waitForTerminal(isSatisfied) {
460
+ return new Promise((resolve) => {
461
+ const check = () => {
462
+ if (!isSatisfied()) return;
463
+ clearInterval(interval);
464
+ process.stdout.off("resize", check);
465
+ resolve();
466
+ };
467
+ const interval = setInterval(check, TERMINAL_RESIZE_POLL_MS);
468
+ process.stdout.on("resize", check);
469
+ });
470
+ }
471
+ function findUnassignedSkills(config, selected) {
472
+ const assignedIds = new Set(config.stack ? getStackSkillIds(config.stack) : []);
473
+ const selectedIds = new Set(selected.map((skill) => skill.id));
474
+ const globalAgentNames = config.agents.filter((agent) => isActiveAt(agent, "global")).map((agent) => agent.name);
475
+ const unassignedIds = unique(
476
+ config.skills.filter((skill) => !skill.excluded && selectedIds.has(skill.id)).map((skill) => skill.id).filter((skillId) => !assignedIds.has(skillId))
477
+ );
478
+ return unassignedIds.map((skillId) => ({
479
+ skillId,
480
+ blockedBy: hasActiveProjectEntry(config.skills, skillId) ? globalAgentNames : []
481
+ }));
482
+ }
483
+ function hasActiveProjectEntry(skills, skillId) {
484
+ return skills.some((skill) => skill.id === skillId && isActiveAt(skill, "project"));
485
+ }
486
+ function projectScopedContent(result) {
487
+ return {
488
+ skillIds: result.skills.filter((skill) => isActiveAt(skill, "project")).map((skill) => skill.id),
489
+ agentNames: result.agentConfigs.filter((agent) => isActiveAt(agent, "project")).map((agent) => agent.name)
490
+ };
491
+ }
492
+ var BaseCommand = class extends Command {
493
+ /**
494
+ * Everything this run set out to do and did not, in the order it hit them.
495
+ *
496
+ * Non-empty is the whole of the exit-code decision — see {@link exitIfWorkIncomplete}. It is
497
+ * deliberately not a boolean: a non-zero exit whose output does not say what failed and what
498
+ * finishes it is worse than the silent success it replaces.
499
+ *
500
+ * Private, with {@link hasIncompleteWork} the only reading a subclass gets. A command records
501
+ * through the two methods below and asks whether anything is owed; the list itself is this
502
+ * class's, because it is this class that answers for it.
503
+ */
504
+ incompleteWork = [];
505
+ async init() {
506
+ await super.init();
507
+ await this.ensureTerminalSize();
508
+ }
509
+ /**
510
+ * Blocks the command from launching in a terminal below
511
+ * {@link MIN_TERMINAL_SIZE}. This runs once, before Ink mounts; the terminal
512
+ * shrinking mid-session is caught by the matching guard in `WizardLayout`.
513
+ */
514
+ async ensureTerminalSize() {
515
+ if (isTerminalBigEnough()) return;
516
+ this.clearTerminal();
517
+ this.log(chalk.hex(CLI_COLORS.WARNING)(formatTerminalTooSmallMessage(terminalColumns())));
518
+ await waitForTerminal(isTerminalBigEnough);
519
+ }
520
+ handleError(error) {
521
+ const message = getErrorMessage(error);
522
+ this.error(message, { exit: EXIT_CODES.ERROR });
523
+ }
524
+ /**
525
+ * The name this run prints itself under: `branding.name` where a config supplies one, and
526
+ * {@link DEFAULT_BRANDING.NAME} everywhere else.
527
+ *
528
+ * **DEGRADE is the posture, and it is chosen here for every command that prints a name.**
529
+ * `loadSourceConfig` raises for a config that exists and cannot be evaluated, and two of the
530
+ * four callers must survive precisely that state: `doctor`'s whole job is naming it, and
531
+ * `uninstall` is the only way out of it. A heading is decoration — nothing about a display name
532
+ * is worth refusing a command over, and refusing here would turn the branding wiring into a new
533
+ * way for an unreadable config to abort two commands that survive it today. `init` and `eject`
534
+ * cannot reach the catch at all: {@link ensureConfigReadable} has already refused every config
535
+ * they would read.
536
+ *
537
+ * The cause is not swallowed: it goes to `verbose`, and the command's own config reader reports
538
+ * it in full where the user needs to act on it. Reporting it twice would put a warning about
539
+ * the configuration above a heading, which is the one line that has to be the first thing on
540
+ * screen.
541
+ */
542
+ async resolveBrandingName(projectDir) {
543
+ try {
544
+ const branding = await resolveBranding(projectDir);
545
+ return branding.name;
546
+ } catch (error) {
547
+ verbose(`Could not read branding from the config: ${getErrorMessage(error)}`);
548
+ return DEFAULT_BRANDING.NAME;
549
+ }
550
+ }
551
+ logSuccess(message) {
552
+ this.log(`\u2713 ${message}`);
553
+ }
554
+ logWarning(message) {
555
+ this.warn(message);
556
+ }
557
+ logInfo(message) {
558
+ this.log(message);
559
+ }
560
+ clearTerminal() {
561
+ clearTerminalScreen();
562
+ }
563
+ /**
564
+ * Resolves the marketplace required for a plugin operation, or hard-exits with
565
+ * an actionable error when it cannot be resolved. `onRegistered` is invoked
566
+ * (when provided) if the marketplace had to be registered — init uses it to log.
567
+ *
568
+ * `host` is the caller's because a marketplace registration is handed no directory of its own:
569
+ * every caller here holds the install root the run is about, and `hostAt` turns one into the
570
+ * host whose registry the marketplace has to land in.
571
+ */
572
+ async requireMarketplaceOrExit(sourceResult, purpose, host, onRegistered) {
573
+ const required = await requireMarketplace(sourceResult, purpose, host);
574
+ if (!required.ok) {
575
+ this.error(required.error, { exit: EXIT_CODES.ERROR });
576
+ }
577
+ if (required.registered) {
578
+ onRegistered?.(required.marketplace);
579
+ }
580
+ return required.marketplace;
581
+ }
582
+ /**
583
+ * Reports every constraint a completed selection breaks — a conflicting pair, an unmet
584
+ * requirement, an exclusive category with two picks.
585
+ *
586
+ * Shared by `init` and `edit` so one selection cannot be reported two ways. Both commands ask
587
+ * the same question of the same matrix, so a roster that draws a warning through one must draw
588
+ * the same warning through the other; `init` used to compute the answer and drop it, which is
589
+ * the whole reason this lives here rather than on either command.
590
+ *
591
+ * These are advisory (`ValidationError` in types/matrix.ts): the selection is installed either
592
+ * way and neither command's exit code turns on them, so they are warnings and not errors. The
593
+ * loop is its own guard — an accepted selection carries no errors and says nothing.
594
+ */
595
+ reportValidationErrors(validation) {
596
+ for (const error of validation.errors) {
597
+ this.warn(error.message);
598
+ }
599
+ }
600
+ /**
601
+ * Installs skill plugins and narrates the operation.
602
+ *
603
+ * Shared by `init` and `edit` for the same reason as {@link reportValidationErrors}:
604
+ * both run the SAME install against the same marketplace, so a line one prints about
605
+ * it the other owes. `edit` used to run it in silence — a real `claude plugin install`
606
+ * was indistinguishable in the output from nothing having happened.
607
+ *
608
+ * The skills nothing carries are refused FIRST, by name: an install no marketplace
609
+ * could serve is knowable before the Claude CLI is asked, and the generic
610
+ * install-failure advice is unfollowable for a skill the user wrote themselves.
611
+ */
612
+ async installPluginSkillsReported(skills, marketplace, projectDir, matrix) {
613
+ const unbacked = unbackedPluginSkillIds(skills, matrix);
614
+ if (unbacked.length > 0) {
615
+ this.error(unbackedPluginInstallError(unbacked), { exit: EXIT_CODES.ERROR });
616
+ }
617
+ this.announcePluginInstall();
618
+ const result = await installPluginSkills(skills, marketplace, projectDir);
619
+ this.reportPluginInstalls(result);
620
+ return result;
621
+ }
622
+ /**
623
+ * The opening half of the narration above, exposed for the one caller that cannot
624
+ * use the whole of it: an eject→plugin migration installs each plugin and deletes
625
+ * that skill's working copy in the same step (see `executeMigration`), so the
626
+ * install it performs is not this command's to run.
627
+ */
628
+ announcePluginInstall() {
629
+ this.log(STATUS_MESSAGES.INSTALLING_PLUGINS);
630
+ }
631
+ /**
632
+ * The closing half: what was installed, what was not, and the refusal to continue
633
+ * past a failure.
634
+ *
635
+ * The hard-error is part of the report rather than each caller's own postscript
636
+ * because it is the same rule everywhere — plugin install intent is inviolable, so
637
+ * a run that could not honor it must stop BEFORE any config records a marketplace
638
+ * `origin` for a skill with no plugin registration.
639
+ */
640
+ reportPluginInstalls(result) {
641
+ for (const item of result.installed) {
642
+ this.log(` Installed ${item.ref}`);
643
+ }
644
+ for (const item of result.failed) {
645
+ this.warn(`Failed to install plugin ${item.id}: ${item.error}`);
646
+ }
647
+ if (result.failed.length > 0) {
648
+ this.error(pluginInstallFailureError(result.failed.length), { exit: EXIT_CODES.ERROR });
649
+ }
650
+ this.log(`${pluginsInstalled(result.installed.length)}
651
+ `);
652
+ }
653
+ /**
654
+ * Renders what a gated write's fan-out did to the OTHER registered projects: one line for each
655
+ * it left alone because its config.ts cannot be loaded, then the recompile it performed.
656
+ *
657
+ * `uninstall` alone calls {@link reportPropagatedRecompile} directly, since it words the projects
658
+ * whose config it could not prune in its own terms.
659
+ */
660
+ reportFanOut(report) {
661
+ for (const projectPath of report.propagated.unreadable) {
662
+ this.warn(registeredProjectConfigUnreadable(projectPath));
663
+ }
664
+ this.reportPropagatedRecompile(report);
665
+ }
666
+ /**
667
+ * One line for each registered project a fan-out left alone because nothing of this
668
+ * installation is there to rewrite: nothing is installed at its path any more, or it is another
669
+ * provider's installation. `compile` and `uninstall` print it — neither project's config can
670
+ * hold what either run changed.
671
+ */
672
+ reportProjectsOutOfReach(propagated) {
673
+ for (const projectPath of propagated.gone) {
674
+ this.warn(registeredProjectGone(projectPath));
675
+ }
676
+ for (const projectPath of propagated.notOurs) {
677
+ this.warn(registeredProjectOnAnotherProvider(projectPath));
678
+ }
679
+ }
680
+ /**
681
+ * Renders the recompile a gated write already performed on every OTHER registered
682
+ * project this run's global change was propagated into.
683
+ *
684
+ * Shared by all four commands that fan out — the count and its wording are one
685
+ * statement about one operation, and four copies of it had already drifted into two
686
+ * spellings of the same sentence.
687
+ */
688
+ reportPropagatedRecompile(report) {
689
+ if (report.propagated.updated.length === 0) return;
690
+ const { rewrittenCount, unchangedCount, failedCount, warnings } = report.recompile;
691
+ for (const warning of warnings) {
692
+ this.warn(warning);
693
+ }
694
+ this.log(propagatedRecompileSummary(rewrittenCount, unchangedCount, failedCount));
695
+ }
696
+ /**
697
+ * Names every selected skill this save left in no sub-agent's stack, and — where the
698
+ * scope rule is why — every sub-agent it was kept away from.
699
+ *
700
+ * Shared by `init` and `edit` for the reason {@link reportValidationErrors} is: both
701
+ * build the stack from the same rules over the same selection, so a skill that reaches
702
+ * nothing through one reaches nothing through the other. The rule being reported is
703
+ * CORRECT and nothing here changes it — a project-scoped skill installs, is written to
704
+ * `config.ts`, and is loaded by nobody, which every other surface reports as a clean
705
+ * install.
706
+ *
707
+ * `compile` says the same thing about the same shape one layer over, for a config
708
+ * hand-edited into it — see `Compile.warnScopeDroppedStackPairs`.
709
+ */
710
+ reportUnassignedSkills(config, selected) {
711
+ for (const { skillId, blockedBy } of findUnassignedSkills(config, selected)) {
712
+ this.warn(skillAssignedToNoAgent(skillId));
713
+ if (blockedBy.length > 0) {
714
+ this.warn(scopeBlockedStackAssignment(blockedBy, skillId));
715
+ }
716
+ }
717
+ }
718
+ /**
719
+ * Refuses to run when a config file exists but cannot be loaded.
720
+ *
721
+ * Both configs this run would read are checked, because both are read on the way through: a
722
+ * project's own `config.ts` and the global one every project write inlines. Checking up
723
+ * front is the whole point — an unreadable GLOBAL config otherwise surfaced only at the
724
+ * config write, after the wizard had copied skills and installed plugins, as a warning that
725
+ * the write never happened.
726
+ *
727
+ * Every command that reads an installation's config calls it before anything else reads one:
728
+ * the writes (`init`, `edit`, `compile`, `update`, `share`, `eject`) and the two that only
729
+ * report (`search`, `list`). `doctor` reports the same file instead, and `uninstall` is the way
730
+ * out of it, so neither does.
731
+ *
732
+ * A MISSING config is not a corrupt one and passes here untouched: it is the legitimate
733
+ * state `init` exists for, and the state `edit` reports as "no installation".
734
+ */
735
+ async ensureConfigReadable(projectDir) {
736
+ const failures = await findConfigLoadFailures(projectDir);
737
+ if (failures.length > 0) {
738
+ this.error(installationConfigsUnreadable(failures), { exit: EXIT_CODES.ERROR });
739
+ }
740
+ }
741
+ /**
742
+ * Refuses to run when a saved skill entry names a local skill that IS installed and whose
743
+ * metadata.yaml describes no skill.
744
+ *
745
+ * One layer below {@link ensureConfigReadable} and for the same reason: the config parses,
746
+ * and a file it points at does not. Both wizards resolve their saved roster against the
747
+ * loaded catalogue, and an id nothing can be loaded for reaches no screen — so without this
748
+ * the entry is dropped and reported as a skill the marketplace no longer carries, which is a
749
+ * removal the user never asked for and a sentence blaming the wrong thing.
750
+ *
751
+ * Only entries the catalogue does not carry are looked at, so a healthy install pays a
752
+ * filesystem read for nothing. An entry whose files are simply GONE is not refused — there
753
+ * is no file to repair, so it is removed with a reason that says so
754
+ * (`unresolvedSkillRemovalReasons`).
755
+ *
756
+ * Runs before the wizard mounts, which is the whole point: past that, a refusal costs the
757
+ * user the session they had already spent, and everything this protects has been rewritten.
758
+ */
759
+ async ensureSavedSkillsReadable(savedSkills, matrix, projectDir) {
760
+ const unresolved = savedSkills.filter((saved) => !saved.excluded && !matrix.skills[saved.id]).map((saved) => saved.id);
761
+ if (unresolved.length === 0) return;
762
+ const unusable = await findUnusableSavedSkillMetadata(unresolved, savedSkills, projectDir);
763
+ if (unusable.length === 0) return;
764
+ for (const entry of unusable) {
765
+ this.log(skillMetadataUnusableDetail(entry));
766
+ }
767
+ this.error(savedSkillMetadataUnusableError(unusable), { exit: EXIT_CODES.ERROR });
768
+ }
769
+ /**
770
+ * Refuses a shared configuration carrying project-scoped content at the home directory.
771
+ *
772
+ * The home directory IS the global scope, and a global installation holds only global-scoped
773
+ * content. Both scopes read one config file there, and on Claude one skills directory and one
774
+ * agents directory as well — so a project-scoped entry lands in the global config wearing a
775
+ * label that contradicts the file it is in, and on Claude `toClaudePluginScope` maps that
776
+ * declared scope onward, registering the skill against `$HOME` as a project. Nothing
777
+ * below this point reads the scope again, so whatever the boundary lets through becomes the
778
+ * truth.
779
+ *
780
+ * Shared by `init --from` and `edit --from` for the reason {@link reportValidationErrors} is
781
+ * shared: it is a fact about the payload and the directory, identical whichever command asked,
782
+ * and an invariant enforced on one producer and not the other is enforced nowhere. It matters
783
+ * more on the destructive one — `edit --from` reaches the same contradiction through the other
784
+ * door and can remove global entries on the way.
785
+ *
786
+ * The interactive producer has always answered this for itself — `isHomeDirectory(projectDir)`
787
+ * reaches the wizard session as `isGlobalRoot` and the scope toggles are inert under it — so
788
+ * this puts both `--from` paths on the rule the wizard already follows.
789
+ *
790
+ * It takes the DECODED selection, so it must run after the decode and can run no earlier: an
791
+ * all-global configuration is exactly what a global installation is for, and only the decode
792
+ * says which entries survived this catalogue and what scope each rests at. Every caller runs it
793
+ * before it reports a skip and before it writes anything, so a run about to be refused neither
794
+ * narrates nor mutates.
795
+ */
796
+ refuseProjectScopedContentAtHome(result, projectDir) {
797
+ if (!isHomeDirectory(projectDir)) return;
798
+ const { skillIds, agentNames } = projectScopedContent(result);
799
+ if (skillIds.length === 0 && agentNames.length === 0) return;
800
+ this.error(sharedConfigProjectScopeAtHome(skillIds, agentNames), { exit: EXIT_CODES.ERROR });
801
+ }
802
+ /**
803
+ * The refusals a `--from` producer can decide only once it holds what it would install — the
804
+ * selection and the skills the configuration carries — asked before it lists any of it or asks
805
+ * whether to install it.
806
+ *
807
+ * A run that lists, asks, and is told yes has promised that a yes installs what was listed (the
808
+ * u01 ruling: "On yes, it installs what is listed"), so a refusal still to come after the
809
+ * question would ask a person to confirm an install that was never going to happen. Every other
810
+ * `--from` refusal is already decided above the lists — a carried skill asked for as a plugin or
811
+ * under a catalogue's id, project content at the home directory, an installation already here —
812
+ * and these four are the ones that need the decoded selection to be asked of:
813
+ *
814
+ * - **A placement the host does not offer.** Read off the selection and the host the run
815
+ * installs through, nothing else, so nothing makes it wait for the yes.
816
+ * - **A stack the marketplace does not ship.** The stack supplies only the configuration's
817
+ * description, yet the install refuses an id it cannot resolve — after the plugins are
818
+ * installed and the skills copied. So it is asked here, through the lookup the install makes,
819
+ * of the marketplace the install reads.
820
+ * - **A carried skill over a directory no shared configuration put there** — the user's own
821
+ * skill at the id the configuration minted for it. Judged on the same set the producer writes,
822
+ * so the skills already installed globally, which it does not write, are not asked about.
823
+ * - **A stack the config writer would refuse** — two skills in a category that holds one, as a
824
+ * hand-written row or the kept skills an apply puts back can make. Asked through the writer's
825
+ * own rule, because `edit --from` writes the config after its removals have landed.
826
+ *
827
+ * Shared by `init --from` and `edit --from` for the reason {@link refuseProjectScopedContentAtHome}
828
+ * is: an invariant enforced on one producer and not the other is enforced nowhere.
829
+ */
830
+ async refuseSharedConfigBeforeAsking(selection, carried, root, sourceResult) {
831
+ this.refuseUnofferablePlacementsBeforeWriting(selection.skills, root);
832
+ try {
833
+ await refuseUnofferedStack(selection.selectedStackId, sourceResult);
834
+ await refuseUncarriedDestinations(carried);
835
+ refuseUnwritableStack(selection.assignedStack, sourceResult.matrix);
836
+ } catch (error) {
837
+ this.handleError(error);
838
+ }
839
+ }
840
+ /**
841
+ * Refuses a SELECTION asking for a mode/scope cell this host does not offer, before the run has
842
+ * touched anything.
843
+ *
844
+ * **`bindsItsOfferedPlacements` is not a pre-flight and cannot be made into one.** It refuses
845
+ * at `installPlugin`, which `installPluginSkills` calls once per skill inside a loop that
846
+ * collects per-skill failures — so a payload whose first plugin row is offerable and whose
847
+ * second is not installed a real plugin into the user's Codex registry and only then stopped,
848
+ * on a message ending "Nothing has been changed." Nothing removed it afterwards: no `config.ts`
849
+ * was written, so `uninstall` had no row naming it and `doctor` had no configuration to check
850
+ * it against — an orphan in the host's own registry that no command in this CLI can see. The
851
+ * single-row case cannot show it, because with one unofferable row the loop's first iteration
852
+ * IS the refusal. `edit --from` had no pre-flight at all until it came here, and met exactly
853
+ * that per-plugin guard after its own yes.
854
+ *
855
+ * It reads the whole selection — ejected rows included — because the roster is about CELLS
856
+ * rather than about plugins: a host that stopped offering `eject+project` would owe the same
857
+ * refusal, and a guard that only looked at plugin rows would go on writing files for it.
858
+ *
859
+ * The same function the read path calls (`refuseUnofferedPlacements`, off the same host
860
+ * roster), so `init`'s sentence and `compile`'s are one sentence rather than two that agree
861
+ * today. The per-call guard stays where it is: it is the backstop for a caller that did not
862
+ * come through here, and for the one host this release ships every cell is offered, so a
863
+ * backstop that never fires is what a deleted one would look like.
864
+ */
865
+ refuseUnofferablePlacementsBeforeWriting(skills, root) {
866
+ try {
867
+ refuseUnofferedPlacements(skills, hostAt(root));
868
+ } catch (error) {
869
+ this.error(getErrorMessage(error), { exit: EXIT_CODES.ERROR });
870
+ }
871
+ }
872
+ /** Whether this run owes an account — the one thing a command may ask about the list. */
873
+ get hasIncompleteWork() {
874
+ return this.incompleteWork.length > 0;
875
+ }
876
+ /**
877
+ * Warns about work this run attempted and did not complete, and records it for the ending.
878
+ *
879
+ * Both halves, always. The warning stays at the point of failure because that is where it is
880
+ * explicable and where the surrounding output says what was being attempted; the record is
881
+ * what decides the exit code, and a failure the user is told about while the process reports
882
+ * success is exactly the state this closes.
883
+ *
884
+ * What it must never do is abort. Every caller sits past the point where the mutation it is
885
+ * reporting on has already happened, so stopping would relabel a state already committed to
886
+ * disk rather than prevent it — and would abandon the steps after it that can still land.
887
+ *
888
+ * The `recovery` sentences live in `INCOMPLETE_WORK_RECOVERY` in `utils/messages.ts`. One
889
+ * spelling per remedy is what matters; three spellings of "run compile" read as three
890
+ * different instructions.
891
+ */
892
+ reportIncompleteWork(what, recovery) {
893
+ this.warn(what);
894
+ this.recordIncompleteWork(what, recovery);
895
+ }
896
+ /**
897
+ * The recording half alone, for a caller that has already said its piece: a recompile warns
898
+ * once per failed sub-agent with the compiler's own reason, so the ending names the roster
899
+ * those reasons belong to instead of repeating any of them.
900
+ */
901
+ recordIncompleteWork(what, recovery) {
902
+ this.incompleteWork.push({ what, recovery });
903
+ }
904
+ /**
905
+ * The command's last word: what did not happen, what finishes it, and a non-zero exit.
906
+ *
907
+ * `this.exit` raises oclif's `ExitError`, which the framework carries to the process exit
908
+ * code and prints nothing for — so the account printed here is the whole of the output, with
909
+ * none of the `Error:` framing `this.error` would add to a command that in fact completed.
910
+ *
911
+ * A command that records must call this, or it files failures into a list nothing reads —
912
+ * which is the original silence wearing the mechanism built to close it, and is what
913
+ * `failure-reporting-classification.test.ts` holds every command in `src/cli/commands/` to.
914
+ */
915
+ exitIfWorkIncomplete() {
916
+ if (!this.hasIncompleteWork) return;
917
+ this.log(`
918
+ ${chalk.hex(CLI_COLORS.WARNING)(completedWithFailures(this.incompleteWork))}
919
+ `);
920
+ this.exit(EXIT_CODES.COMPLETED_WITH_FAILURES);
921
+ }
922
+ };
923
+
924
+ export {
925
+ EXIT_CODES,
926
+ registerExternalSkills,
927
+ writeExternalSkills,
928
+ carriedSkillsThatDiffer,
929
+ readCarriedSkills,
930
+ BaseCommand
931
+ };
932
+ //# sourceMappingURL=chunk-Y3VIRVVE.js.map