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