@seanmars/tospec 0.24.0 → 0.26.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 (332) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/README.md +10 -6
  3. package/assets/dashboard/lib/derive.js +11 -5
  4. package/assets/dashboard/lib/store.js +4 -2
  5. package/assets/dashboard/views/change.js +5 -3
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +42 -7
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/commands/config.d.ts.map +1 -1
  10. package/dist/commands/config.js +213 -100
  11. package/dist/commands/config.js.map +1 -1
  12. package/dist/commands/dashboard.d.ts +9 -0
  13. package/dist/commands/dashboard.d.ts.map +1 -1
  14. package/dist/commands/dashboard.js +77 -21
  15. package/dist/commands/dashboard.js.map +1 -1
  16. package/dist/commands/decision.d.ts +38 -1
  17. package/dist/commands/decision.d.ts.map +1 -1
  18. package/dist/commands/decision.js +303 -27
  19. package/dist/commands/decision.js.map +1 -1
  20. package/dist/commands/metrics.d.ts +3 -4
  21. package/dist/commands/metrics.d.ts.map +1 -1
  22. package/dist/commands/metrics.js +3 -5
  23. package/dist/commands/metrics.js.map +1 -1
  24. package/dist/commands/shared-output.d.ts +24 -0
  25. package/dist/commands/shared-output.d.ts.map +1 -1
  26. package/dist/commands/shared-output.js +21 -2
  27. package/dist/commands/shared-output.js.map +1 -1
  28. package/dist/commands/show.d.ts.map +1 -1
  29. package/dist/commands/show.js +12 -9
  30. package/dist/commands/show.js.map +1 -1
  31. package/dist/commands/validate.d.ts +19 -0
  32. package/dist/commands/validate.d.ts.map +1 -1
  33. package/dist/commands/validate.js +243 -64
  34. package/dist/commands/validate.js.map +1 -1
  35. package/dist/commands/workflow/instructions.d.ts +7 -1
  36. package/dist/commands/workflow/instructions.d.ts.map +1 -1
  37. package/dist/commands/workflow/instructions.js +299 -81
  38. package/dist/commands/workflow/instructions.js.map +1 -1
  39. package/dist/commands/workflow/new-change.d.ts.map +1 -1
  40. package/dist/commands/workflow/new-change.js +26 -17
  41. package/dist/commands/workflow/new-change.js.map +1 -1
  42. package/dist/commands/workflow/prompt-escape.d.ts +25 -0
  43. package/dist/commands/workflow/prompt-escape.d.ts.map +1 -0
  44. package/dist/commands/workflow/prompt-escape.js +62 -0
  45. package/dist/commands/workflow/prompt-escape.js.map +1 -0
  46. package/dist/commands/workflow/schemas.d.ts.map +1 -1
  47. package/dist/commands/workflow/schemas.js +34 -16
  48. package/dist/commands/workflow/schemas.js.map +1 -1
  49. package/dist/commands/workflow/shared.d.ts +46 -10
  50. package/dist/commands/workflow/shared.d.ts.map +1 -1
  51. package/dist/commands/workflow/shared.js +66 -30
  52. package/dist/commands/workflow/shared.js.map +1 -1
  53. package/dist/commands/workflow/status.d.ts +5 -2
  54. package/dist/commands/workflow/status.d.ts.map +1 -1
  55. package/dist/commands/workflow/status.js +62 -28
  56. package/dist/commands/workflow/status.js.map +1 -1
  57. package/dist/core/archive.d.ts +38 -0
  58. package/dist/core/archive.d.ts.map +1 -1
  59. package/dist/core/archive.js +492 -75
  60. package/dist/core/archive.js.map +1 -1
  61. package/dist/core/artifact-graph/index.d.ts +2 -2
  62. package/dist/core/artifact-graph/index.d.ts.map +1 -1
  63. package/dist/core/artifact-graph/index.js +2 -2
  64. package/dist/core/artifact-graph/index.js.map +1 -1
  65. package/dist/core/artifact-graph/instruction-loader.d.ts +8 -0
  66. package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
  67. package/dist/core/artifact-graph/instruction-loader.js +58 -36
  68. package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
  69. package/dist/core/artifact-graph/resolver.d.ts +39 -12
  70. package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
  71. package/dist/core/artifact-graph/resolver.js +82 -36
  72. package/dist/core/artifact-graph/resolver.js.map +1 -1
  73. package/dist/core/artifact-graph/schema.d.ts +24 -0
  74. package/dist/core/artifact-graph/schema.d.ts.map +1 -1
  75. package/dist/core/artifact-graph/schema.js +32 -1
  76. package/dist/core/artifact-graph/schema.js.map +1 -1
  77. package/dist/core/artifact-graph/stub-detection.d.ts +10 -2
  78. package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
  79. package/dist/core/artifact-graph/stub-detection.js +18 -11
  80. package/dist/core/artifact-graph/stub-detection.js.map +1 -1
  81. package/dist/core/artifact-graph/types.d.ts +2 -2
  82. package/dist/core/artifact-graph/types.d.ts.map +1 -1
  83. package/dist/core/artifact-graph/types.js +10 -1
  84. package/dist/core/artifact-graph/types.js.map +1 -1
  85. package/dist/core/change-metadata/schema.d.ts.map +1 -1
  86. package/dist/core/change-metadata/schema.js +18 -4
  87. package/dist/core/change-metadata/schema.js.map +1 -1
  88. package/dist/core/change-presenter.d.ts.map +1 -1
  89. package/dist/core/change-presenter.js +13 -8
  90. package/dist/core/change-presenter.js.map +1 -1
  91. package/dist/core/change-status-policy.d.ts.map +1 -1
  92. package/dist/core/change-status-policy.js +3 -1
  93. package/dist/core/change-status-policy.js.map +1 -1
  94. package/dist/core/codex-metrics.d.ts +1 -1
  95. package/dist/core/codex-metrics.js +1 -1
  96. package/dist/core/config-schema.d.ts +8 -0
  97. package/dist/core/config-schema.d.ts.map +1 -1
  98. package/dist/core/config-schema.js +10 -0
  99. package/dist/core/config-schema.js.map +1 -1
  100. package/dist/core/dashboard-activity.d.ts +4 -4
  101. package/dist/core/dashboard-activity.js +4 -4
  102. package/dist/core/dashboard-data.d.ts +11 -5
  103. package/dist/core/dashboard-data.d.ts.map +1 -1
  104. package/dist/core/dashboard-data.js +54 -28
  105. package/dist/core/dashboard-data.js.map +1 -1
  106. package/dist/core/dashboard-hub.d.ts +3 -3
  107. package/dist/core/dashboard-hub.d.ts.map +1 -1
  108. package/dist/core/dashboard-hub.js +16 -4
  109. package/dist/core/dashboard-hub.js.map +1 -1
  110. package/dist/core/dashboard-routes.d.ts +2 -1
  111. package/dist/core/dashboard-routes.d.ts.map +1 -1
  112. package/dist/core/dashboard-routes.js +102 -33
  113. package/dist/core/dashboard-routes.js.map +1 -1
  114. package/dist/core/global-config.d.ts +71 -2
  115. package/dist/core/global-config.d.ts.map +1 -1
  116. package/dist/core/global-config.js +149 -4
  117. package/dist/core/global-config.js.map +1 -1
  118. package/dist/core/id.d.ts +9 -0
  119. package/dist/core/id.d.ts.map +1 -1
  120. package/dist/core/id.js +37 -0
  121. package/dist/core/id.js.map +1 -1
  122. package/dist/core/init.d.ts +9 -2
  123. package/dist/core/init.d.ts.map +1 -1
  124. package/dist/core/init.js +116 -33
  125. package/dist/core/init.js.map +1 -1
  126. package/dist/core/list.d.ts.map +1 -1
  127. package/dist/core/list.js +69 -45
  128. package/dist/core/list.js.map +1 -1
  129. package/dist/core/local-server.d.ts +9 -2
  130. package/dist/core/local-server.d.ts.map +1 -1
  131. package/dist/core/local-server.js +24 -4
  132. package/dist/core/local-server.js.map +1 -1
  133. package/dist/core/markdown-render.d.ts.map +1 -1
  134. package/dist/core/markdown-render.js +7 -2
  135. package/dist/core/markdown-render.js.map +1 -1
  136. package/dist/core/migrate.d.ts +24 -2
  137. package/dist/core/migrate.d.ts.map +1 -1
  138. package/dist/core/migrate.js +335 -59
  139. package/dist/core/migrate.js.map +1 -1
  140. package/dist/core/parsers/change-parser.d.ts +20 -1
  141. package/dist/core/parsers/change-parser.d.ts.map +1 -1
  142. package/dist/core/parsers/change-parser.js +76 -73
  143. package/dist/core/parsers/change-parser.js.map +1 -1
  144. package/dist/core/parsers/markdown-parser.d.ts +24 -0
  145. package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
  146. package/dist/core/parsers/markdown-parser.js +65 -10
  147. package/dist/core/parsers/markdown-parser.js.map +1 -1
  148. package/dist/core/parsers/requirement-blocks.d.ts +127 -6
  149. package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
  150. package/dist/core/parsers/requirement-blocks.js +349 -56
  151. package/dist/core/parsers/requirement-blocks.js.map +1 -1
  152. package/dist/core/parsers/requirement-text.d.ts +25 -1
  153. package/dist/core/parsers/requirement-text.d.ts.map +1 -1
  154. package/dist/core/parsers/requirement-text.js +62 -6
  155. package/dist/core/parsers/requirement-text.js.map +1 -1
  156. package/dist/core/parsers/spec-structure.d.ts.map +1 -1
  157. package/dist/core/parsers/spec-structure.js +36 -24
  158. package/dist/core/parsers/spec-structure.js.map +1 -1
  159. package/dist/core/project-config.d.ts +9 -2
  160. package/dist/core/project-config.d.ts.map +1 -1
  161. package/dist/core/project-config.js +30 -7
  162. package/dist/core/project-config.js.map +1 -1
  163. package/dist/core/project-layout.d.ts +26 -0
  164. package/dist/core/project-layout.d.ts.map +1 -1
  165. package/dist/core/project-layout.js +51 -1
  166. package/dist/core/project-layout.js.map +1 -1
  167. package/dist/core/schema-names.d.ts +17 -0
  168. package/dist/core/schema-names.d.ts.map +1 -1
  169. package/dist/core/schema-names.js +24 -0
  170. package/dist/core/schema-names.js.map +1 -1
  171. package/dist/core/shared/index.d.ts +1 -1
  172. package/dist/core/shared/index.d.ts.map +1 -1
  173. package/dist/core/shared/index.js +1 -1
  174. package/dist/core/shared/index.js.map +1 -1
  175. package/dist/core/shared/rules-generation.d.ts +12 -0
  176. package/dist/core/shared/rules-generation.d.ts.map +1 -1
  177. package/dist/core/shared/rules-generation.js +26 -2
  178. package/dist/core/shared/rules-generation.js.map +1 -1
  179. package/dist/core/shared/skill-generation.d.ts +7 -0
  180. package/dist/core/shared/skill-generation.d.ts.map +1 -1
  181. package/dist/core/shared/skill-generation.js +19 -0
  182. package/dist/core/shared/skill-generation.js.map +1 -1
  183. package/dist/core/shared/tool-detection.d.ts +10 -1
  184. package/dist/core/shared/tool-detection.d.ts.map +1 -1
  185. package/dist/core/shared/tool-detection.js +37 -6
  186. package/dist/core/shared/tool-detection.js.map +1 -1
  187. package/dist/core/spec-presenter.d.ts.map +1 -1
  188. package/dist/core/spec-presenter.js +23 -1
  189. package/dist/core/spec-presenter.js.map +1 -1
  190. package/dist/core/specs-apply.d.ts +98 -1
  191. package/dist/core/specs-apply.d.ts.map +1 -1
  192. package/dist/core/specs-apply.js +426 -63
  193. package/dist/core/specs-apply.js.map +1 -1
  194. package/dist/core/templates/fragments/change-diff.d.ts +37 -0
  195. package/dist/core/templates/fragments/change-diff.d.ts.map +1 -0
  196. package/dist/core/templates/fragments/change-diff.js +37 -0
  197. package/dist/core/templates/fragments/change-diff.js.map +1 -0
  198. package/dist/core/templates/fragments/sync-fail.d.ts +8 -0
  199. package/dist/core/templates/fragments/sync-fail.d.ts.map +1 -0
  200. package/dist/core/templates/fragments/sync-fail.js +8 -0
  201. package/dist/core/templates/fragments/sync-fail.js.map +1 -0
  202. package/dist/core/templates/fragments/validate.d.ts +14 -1
  203. package/dist/core/templates/fragments/validate.d.ts.map +1 -1
  204. package/dist/core/templates/fragments/validate.js +14 -1
  205. package/dist/core/templates/fragments/validate.js.map +1 -1
  206. package/dist/core/templates/fragments/verify.d.ts +2 -2
  207. package/dist/core/templates/fragments/verify.d.ts.map +1 -1
  208. package/dist/core/templates/fragments/verify.js +8 -5
  209. package/dist/core/templates/fragments/verify.js.map +1 -1
  210. package/dist/core/templates/workflows/apply.d.ts +1 -1
  211. package/dist/core/templates/workflows/apply.d.ts.map +1 -1
  212. package/dist/core/templates/workflows/apply.js +29 -10
  213. package/dist/core/templates/workflows/apply.js.map +1 -1
  214. package/dist/core/templates/workflows/archive.d.ts.map +1 -1
  215. package/dist/core/templates/workflows/archive.js +27 -9
  216. package/dist/core/templates/workflows/archive.js.map +1 -1
  217. package/dist/core/templates/workflows/decision.d.ts.map +1 -1
  218. package/dist/core/templates/workflows/decision.js +10 -4
  219. package/dist/core/templates/workflows/decision.js.map +1 -1
  220. package/dist/core/templates/workflows/explore.d.ts +2 -2
  221. package/dist/core/templates/workflows/explore.d.ts.map +1 -1
  222. package/dist/core/templates/workflows/explore.js +7 -3
  223. package/dist/core/templates/workflows/explore.js.map +1 -1
  224. package/dist/core/templates/workflows/grill.d.ts +2 -2
  225. package/dist/core/templates/workflows/grill.d.ts.map +1 -1
  226. package/dist/core/templates/workflows/grill.js +5 -4
  227. package/dist/core/templates/workflows/grill.js.map +1 -1
  228. package/dist/core/templates/workflows/issue.d.ts.map +1 -1
  229. package/dist/core/templates/workflows/issue.js +9 -3
  230. package/dist/core/templates/workflows/issue.js.map +1 -1
  231. package/dist/core/templates/workflows/propose.d.ts.map +1 -1
  232. package/dist/core/templates/workflows/propose.js +4 -1
  233. package/dist/core/templates/workflows/propose.js.map +1 -1
  234. package/dist/core/templates/workflows/sync.d.ts.map +1 -1
  235. package/dist/core/templates/workflows/sync.js +25 -11
  236. package/dist/core/templates/workflows/sync.js.map +1 -1
  237. package/dist/core/templates/workflows/update.d.ts.map +1 -1
  238. package/dist/core/templates/workflows/update.js +17 -10
  239. package/dist/core/templates/workflows/update.js.map +1 -1
  240. package/dist/core/update.d.ts +5 -0
  241. package/dist/core/update.d.ts.map +1 -1
  242. package/dist/core/update.js +66 -29
  243. package/dist/core/update.js.map +1 -1
  244. package/dist/core/validation/constants.d.ts +7 -1
  245. package/dist/core/validation/constants.d.ts.map +1 -1
  246. package/dist/core/validation/constants.js +16 -2
  247. package/dist/core/validation/constants.js.map +1 -1
  248. package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
  249. package/dist/core/validation/purpose-placeholder.js +35 -11
  250. package/dist/core/validation/purpose-placeholder.js.map +1 -1
  251. package/dist/core/validation/section-validator.d.ts +7 -0
  252. package/dist/core/validation/section-validator.d.ts.map +1 -1
  253. package/dist/core/validation/section-validator.js +22 -2
  254. package/dist/core/validation/section-validator.js.map +1 -1
  255. package/dist/core/validation/sister-changes.d.ts +93 -0
  256. package/dist/core/validation/sister-changes.d.ts.map +1 -0
  257. package/dist/core/validation/sister-changes.js +180 -0
  258. package/dist/core/validation/sister-changes.js.map +1 -0
  259. package/dist/core/validation/task-markers.d.ts +9 -0
  260. package/dist/core/validation/task-markers.d.ts.map +1 -0
  261. package/dist/core/validation/task-markers.js +71 -0
  262. package/dist/core/validation/task-markers.js.map +1 -0
  263. package/dist/core/validation/types.d.ts +58 -0
  264. package/dist/core/validation/types.d.ts.map +1 -1
  265. package/dist/core/validation/types.js +22 -0
  266. package/dist/core/validation/types.js.map +1 -1
  267. package/dist/core/validation/validator.d.ts +79 -2
  268. package/dist/core/validation/validator.d.ts.map +1 -1
  269. package/dist/core/validation/validator.js +565 -84
  270. package/dist/core/validation/validator.js.map +1 -1
  271. package/dist/utils/change-metadata.d.ts +62 -5
  272. package/dist/utils/change-metadata.d.ts.map +1 -1
  273. package/dist/utils/change-metadata.js +193 -31
  274. package/dist/utils/change-metadata.js.map +1 -1
  275. package/dist/utils/change-utils.d.ts +21 -0
  276. package/dist/utils/change-utils.d.ts.map +1 -1
  277. package/dist/utils/change-utils.js +168 -53
  278. package/dist/utils/change-utils.js.map +1 -1
  279. package/dist/utils/command-line.d.ts +11 -0
  280. package/dist/utils/command-line.d.ts.map +1 -0
  281. package/dist/utils/command-line.js +43 -0
  282. package/dist/utils/command-line.js.map +1 -0
  283. package/dist/utils/display-width.d.ts +10 -0
  284. package/dist/utils/display-width.d.ts.map +1 -0
  285. package/dist/utils/display-width.js +15 -0
  286. package/dist/utils/display-width.js.map +1 -0
  287. package/dist/utils/file-system.d.ts +16 -0
  288. package/dist/utils/file-system.d.ts.map +1 -1
  289. package/dist/utils/file-system.js +49 -2
  290. package/dist/utils/file-system.js.map +1 -1
  291. package/dist/utils/frontmatter.d.ts.map +1 -1
  292. package/dist/utils/frontmatter.js +11 -5
  293. package/dist/utils/frontmatter.js.map +1 -1
  294. package/dist/utils/interactive.d.ts.map +1 -1
  295. package/dist/utils/interactive.js +3 -1
  296. package/dist/utils/interactive.js.map +1 -1
  297. package/dist/utils/item-discovery.d.ts +3 -2
  298. package/dist/utils/item-discovery.d.ts.map +1 -1
  299. package/dist/utils/item-discovery.js +34 -16
  300. package/dist/utils/item-discovery.js.map +1 -1
  301. package/dist/utils/link.d.ts +0 -1
  302. package/dist/utils/link.d.ts.map +1 -1
  303. package/dist/utils/link.js +0 -1
  304. package/dist/utils/link.js.map +1 -1
  305. package/dist/utils/match.d.ts.map +1 -1
  306. package/dist/utils/match.js +10 -1
  307. package/dist/utils/match.js.map +1 -1
  308. package/dist/utils/option-flags.d.ts +9 -0
  309. package/dist/utils/option-flags.d.ts.map +1 -0
  310. package/dist/utils/option-flags.js +12 -0
  311. package/dist/utils/option-flags.js.map +1 -0
  312. package/dist/utils/sentence.d.ts +9 -0
  313. package/dist/utils/sentence.d.ts.map +1 -0
  314. package/dist/utils/sentence.js +12 -0
  315. package/dist/utils/sentence.js.map +1 -0
  316. package/dist/utils/spec-files.d.ts +84 -1
  317. package/dist/utils/spec-files.d.ts.map +1 -1
  318. package/dist/utils/spec-files.js +192 -3
  319. package/dist/utils/spec-files.js.map +1 -1
  320. package/dist/utils/task-progress.d.ts.map +1 -1
  321. package/dist/utils/task-progress.js +32 -9
  322. package/dist/utils/task-progress.js.map +1 -1
  323. package/dist/utils/timestamp.d.ts +27 -0
  324. package/dist/utils/timestamp.d.ts.map +1 -1
  325. package/dist/utils/timestamp.js +37 -0
  326. package/dist/utils/timestamp.js.map +1 -1
  327. package/package.json +4 -1
  328. package/schemas/decision/templates/decision.md +2 -2
  329. package/schemas/issue/schema.yaml +12 -6
  330. package/schemas/issue/templates/task.md +1 -1
  331. package/schemas/sdd/schema.yaml +21 -10
  332. package/schemas/sdd/templates/proposal.md +1 -1
@@ -2,6 +2,7 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import * as os from 'node:os';
4
4
  import { stripBom } from './parsers/requirement-text.js';
5
+ import { findInvalidConfigFields } from './config-schema.js';
5
6
  export const GLOBAL_CONFIG_DIR_NAME = 'tospec';
6
7
  export const GLOBAL_CONFIG_FILE_NAME = 'config.json';
7
8
  export const GLOBAL_DATA_DIR_NAME = 'tospec';
@@ -78,6 +79,21 @@ export function getLegacyConfigDir(options = {}) {
78
79
  export function getGlobalConfigPath() {
79
80
  return path.join(getGlobalConfigDir(), GLOBAL_CONFIG_FILE_NAME);
80
81
  }
82
+ // Once per line per process: init reads the config several times in one run,
83
+ // and the same line repeated reads as several problems. Keyed by the line, not
84
+ // the path, so a file that turns from one kind of invalid into another within
85
+ // a process still reports the new problem.
86
+ const issuedWarnings = new Set();
87
+ function warnOnce(line) {
88
+ if (issuedWarnings.has(line))
89
+ return;
90
+ issuedWarnings.add(line);
91
+ console.error(line);
92
+ }
93
+ function warnInvalidSettings(configPath, invalid) {
94
+ const details = invalid.map((entry) => entry.detail).join('; ');
95
+ warnOnce(`Warning: Invalid settings in ${configPath} (${details}), ignoring the whole file and using defaults`);
96
+ }
81
97
  function readConfigFile(configPath) {
82
98
  try {
83
99
  const content = fs.readFileSync(configPath, 'utf-8');
@@ -90,7 +106,17 @@ function readConfigFile(configPath) {
90
106
  // SyntaxError, and spreading either yields an index-keyed object that would
91
107
  // be returned as configuration.
92
108
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
93
- console.error(`Warning: ${configPath} is not a JSON object, using defaults`);
109
+ warnOnce(`Warning: ${configPath} is not a JSON object, using defaults`);
110
+ return { ...DEFAULT_CONFIG };
111
+ }
112
+ // The schema `config set` writes through. Without it a hand-edited
113
+ // `"workflows": "explore"` reached update as a string (`.filter` threw) and
114
+ // init as seven one-letter workflows. The whole file falls back, as an
115
+ // unparseable one does: dropping only the bad field would leave `custom`
116
+ // with no workflows, and the next update would remove every skill.
117
+ const invalid = findInvalidConfigFields(parsed);
118
+ if (invalid.length > 0) {
119
+ warnInvalidSettings(configPath, invalid);
94
120
  return { ...DEFAULT_CONFIG };
95
121
  }
96
122
  // Loaded values win; unknown keys from older versions pass through.
@@ -99,7 +125,7 @@ function readConfigFile(configPath) {
99
125
  catch (error) {
100
126
  // A missing file is the normal case and says nothing.
101
127
  if (error instanceof SyntaxError) {
102
- console.error(`Warning: Invalid JSON in ${configPath}, using defaults`);
128
+ warnOnce(`Warning: Invalid JSON in ${configPath}, using defaults`);
103
129
  }
104
130
  return { ...DEFAULT_CONFIG };
105
131
  }
@@ -131,11 +157,130 @@ function resolveConfigPath(configDir, legacyConfigDir) {
131
157
  export function getEffectiveConfigPath() {
132
158
  return resolveConfigPath(getGlobalConfigDir(), getLegacyConfigDir());
133
159
  }
134
- /** Merged over the defaults; a missing or invalid file yields the defaults. */
160
+ /**
161
+ * Merged over the defaults; a missing or invalid file yields the defaults, and
162
+ * so does a file with any field the schema refuses.
163
+ */
135
164
  export function getGlobalConfig() {
136
165
  return loadGlobalConfigFrom(getGlobalConfigDir(), getLegacyConfigDir());
137
166
  }
138
- export function saveGlobalConfig(config) {
167
+ /**
168
+ * The object the effective file holds, or `{}` when it is missing or
169
+ * unparseable. Without the defaults, so a caller can tell an explicit value
170
+ * from a default.
171
+ */
172
+ export function getStoredGlobalConfigFields() {
173
+ try {
174
+ const parsed = JSON.parse(stripBom(fs.readFileSync(getEffectiveConfigPath(), 'utf-8')));
175
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
176
+ return parsed;
177
+ }
178
+ }
179
+ catch {
180
+ // Missing, or unparseable: saveGlobalConfig refuses to write over the latter.
181
+ }
182
+ return {};
183
+ }
184
+ /**
185
+ * The file's settings, without the schema check reads apply: the base for a
186
+ * read-modify-write. getGlobalConfig() yields bare defaults for a file with one
187
+ * invalid field, so saving from it would erase every other setting the user
188
+ * wrote. Without the defaults too: merged in, the first `config set` of any key
189
+ * saved `profile: "core"` as well, and `config list` then called the default
190
+ * profile explicit. Reads still merge the defaults, so nothing reads the gap.
191
+ */
192
+ export function getStoredGlobalConfig() {
193
+ return getStoredGlobalConfigFields();
194
+ }
195
+ /**
196
+ * Whether the config file a read lands on exists but cannot be used: invalid
197
+ * JSON, not a JSON object, or unreadable (EACCES). A missing file is not
198
+ * unreadable — it is the normal first-run state.
199
+ */
200
+ export function isGlobalConfigUnreadable() {
201
+ const configPath = getEffectiveConfigPath();
202
+ let content;
203
+ try {
204
+ content = fs.readFileSync(configPath, 'utf-8');
205
+ }
206
+ catch (error) {
207
+ return error.code !== 'ENOENT';
208
+ }
209
+ try {
210
+ const parsed = JSON.parse(stripBom(content));
211
+ return typeof parsed !== 'object' || parsed === null || Array.isArray(parsed);
212
+ }
213
+ catch {
214
+ return true;
215
+ }
216
+ }
217
+ /** Carries the `status[]` diagnostic the CLI's failure path reads. */
218
+ export class GlobalConfigUnreadableError extends Error {
219
+ diagnostic;
220
+ constructor(configPath) {
221
+ const message = `${configPath} cannot be parsed, so tospec refuses to overwrite it: the write would replace every setting in it with defaults.`;
222
+ super(message);
223
+ this.name = 'GlobalConfigUnreadableError';
224
+ this.diagnostic = {
225
+ severity: 'error',
226
+ code: 'global_config_unreadable',
227
+ message,
228
+ fix: 'Fix the file with "tospec config edit", or replace it with defaults with "tospec config reset --all".',
229
+ };
230
+ }
231
+ }
232
+ /**
233
+ * For a command whose answer comes from the file, not only its write: `config
234
+ * unset` on a key the defaults lack writes nothing, and would report "was not
235
+ * set" about a file it could not read.
236
+ */
237
+ export function assertGlobalConfigReadable() {
238
+ if (isGlobalConfigUnreadable()) {
239
+ throw new GlobalConfigUnreadableError(getEffectiveConfigPath());
240
+ }
241
+ }
242
+ /** Carries the `status[]` diagnostic the CLI's failure path reads. */
243
+ export class GlobalConfigInvalidError extends Error {
244
+ diagnostic;
245
+ constructor(configPath, invalid) {
246
+ const details = invalid.map((entry) => entry.detail).join('; ');
247
+ const message = `${configPath} has invalid settings (${details}), so reads ignore the whole file and this write would not take effect.`;
248
+ super(message);
249
+ this.name = 'GlobalConfigInvalidError';
250
+ this.diagnostic = {
251
+ severity: 'error',
252
+ code: 'global_config_invalid',
253
+ message,
254
+ fix: `Repair it first with "tospec config unset ${invalid[0].field}" or "tospec config set ${invalid[0].field}", or fix the file with "tospec config edit".`,
255
+ };
256
+ }
257
+ }
258
+ /**
259
+ * Refuses a write that leaves every invalid field as it was: reads ignore such
260
+ * a file, so the write would report success and change nothing, which is why
261
+ * an unparseable file refuses writes too. A write to an invalid field is the
262
+ * repair and goes through even with another one left; requiring all of them at
263
+ * once would refuse `set` and `unset` alike when two are bad, each pointing at
264
+ * the other.
265
+ */
266
+ export function assertGlobalConfigWritable(touchedKeys) {
267
+ const invalid = findInvalidConfigFields(getStoredGlobalConfigFields());
268
+ if (invalid.length === 0)
269
+ return;
270
+ if (invalid.some((entry) => touchedKeys.includes(entry.field)))
271
+ return;
272
+ throw new GlobalConfigInvalidError(getEffectiveConfigPath(), invalid);
273
+ }
274
+ /**
275
+ * Refuses to write over a config file that exists but cannot be parsed. Reads
276
+ * fall back to defaults for such a file, so a read-modify-write (`config set`,
277
+ * `unset`, `profile`) would otherwise save those defaults and erase the user's
278
+ * whole file over one stray comma. `overwriteUnreadable` is for
279
+ * `config reset --all`, the one write whose purpose is to replace the file.
280
+ */
281
+ export function saveGlobalConfig(config, options = {}) {
282
+ if (!options.overwriteUnreadable)
283
+ assertGlobalConfigReadable();
139
284
  const configDir = getGlobalConfigDir();
140
285
  const configPath = getGlobalConfigPath();
141
286
  if (!fs.existsSync(configDir)) {
@@ -1 +1 @@
1
- {"version":3,"file":"global-config.js","sourceRoot":"","sources":["../../src/core/global-config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"global-config.js","sourceRoot":"","sources":["../../src/core/global-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAA2B,MAAM,oBAAoB,CAAC;AAEtF,MAAM,CAAC,MAAM,sBAAsB,GAAG,QAAQ,CAAC;AAC/C,MAAM,CAAC,MAAM,uBAAuB,GAAG,aAAa,CAAC;AACrD,MAAM,CAAC,MAAM,oBAAoB,GAAG,QAAQ,CAAC;AAW7C,MAAM,cAAc,GAAiB;IACnC,OAAO,EAAE,MAAM;CAChB,CAAC;AAYF;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAO,GAAsB,EAAE;IAChE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC;IAEnD,8DAA8D;IAC9D,MAAM,aAAa,GAAG,GAAG,CAAC,eAAe,CAAC;IAC1C,IAAI,aAAa,EAAE,CAAC;QAClB,OAAO,cAAc,CAAC,QAAQ,EAAE,aAAa,EAAE,sBAAsB,CAAC,CAAC;IACzE,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;IAChD,OAAO,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,sBAAsB,CAAC,CAAC;AAC9E,CAAC;AAED,mEAAmE;AACnE,SAAS,cAAc,CAAC,QAAyB,EAAE,GAAG,QAAkB;IACtE,OAAO,QAAQ,KAAK,OAAO;QACzB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC;QAC9B,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAO,GAAsB,EAAE;IAC9D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC;IACnD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;IAEhD,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,2BAA2B;QAC3B,MAAM,YAAY,GAAG,GAAG,CAAC,YAAY,CAAC;QACtC,IAAI,YAAY,EAAE,CAAC;YACjB,OAAO,cAAc,CAAC,QAAQ,EAAE,YAAY,EAAE,oBAAoB,CAAC,CAAC;QACtE,CAAC;QACD,gCAAgC;QAChC,OAAO,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAAC;IACrF,CAAC;IAED,8BAA8B;IAC9B,OAAO,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAAC;AACpF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAO,GAAsB,EAAE;IAChE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC;IAEnD,IAAI,GAAG,CAAC,eAAe,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChD,OAAO,kBAAkB,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;IAChD,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAC5B,OAAO,OAAO;QACZ,CAAC,CAAC,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,sBAAsB,CAAC;QAC3D,CAAC,CAAC,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,sBAAsB,CAAC,CAAC;AACtF,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,uBAAuB,CAAC,CAAC;AAClE,CAAC;AAED,6EAA6E;AAC7E,+EAA+E;AAC/E,8EAA8E;AAC9E,2CAA2C;AAC3C,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;AAEzC,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO;IACrC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC;AAED,SAAS,mBAAmB,CAAC,UAAkB,EAAE,OAA6B;IAC5E,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,QAAQ,CAAC,gCAAgC,UAAU,KAAK,OAAO,+CAA+C,CAAC,CAAC;AAClH,CAAC;AAED,SAAS,cAAc,CAAC,UAAkB;IACxC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACrD,6EAA6E;QAC7E,4EAA4E;QAC5E,6EAA6E;QAC7E,2DAA2D;QAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QAC7C,2EAA2E;QAC3E,4EAA4E;QAC5E,gCAAgC;QAChC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,QAAQ,CAAC,YAAY,UAAU,uCAAuC,CAAC,CAAC;YACxE,OAAO,EAAE,GAAG,cAAc,EAAE,CAAC;QAC/B,CAAC;QACD,mEAAmE;QACnE,4EAA4E;QAC5E,uEAAuE;QACvE,yEAAyE;QACzE,mEAAmE;QACnE,MAAM,OAAO,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;QAChD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,mBAAmB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YACzC,OAAO,EAAE,GAAG,cAAc,EAAE,CAAC;QAC/B,CAAC;QACD,oEAAoE;QACpE,OAAO,EAAE,GAAG,cAAc,EAAE,GAAG,MAAM,EAAE,CAAC;IAC1C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,sDAAsD;QACtD,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;YACjC,QAAQ,CAAC,4BAA4B,UAAU,kBAAkB,CAAC,CAAC;QACrE,CAAC;QACD,OAAO,EAAE,GAAG,cAAc,EAAE,CAAC;IAC/B,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAiB,EAAE,eAAuB;IAC7E,MAAM,UAAU,GAAG,iBAAiB,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IACjE,OAAO,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,cAAc,EAAE,CAAC;AACxF,CAAC;AAED,8DAA8D;AAC9D,SAAS,iBAAiB,CAAC,SAAiB,EAAE,eAAuB;IACnE,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,uBAAuB,CAAC,CAAC;IACjE,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9B,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,uBAAuB,CAAC,CAAC;IACvE,OAAO,UAAU,KAAK,UAAU,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;AAC1F,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO,iBAAiB,CAAC,kBAAkB,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC;AACvE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,oBAAoB,CAAC,kBAAkB,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC;AAC1E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B;IACzC,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAChC,QAAQ,CAAC,EAAE,CAAC,YAAY,CAAC,sBAAsB,EAAE,EAAE,OAAO,CAAC,CAAC,CAC7D,CAAC;QACF,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5E,OAAO,MAAiC,CAAC;QAC3C,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,8EAA8E;IAChF,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,2BAA2B,EAAE,CAAC;AACvC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB;IACtC,MAAM,UAAU,GAAG,sBAAsB,EAAE,CAAC;IAC5C,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACjD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAQ,KAA+B,CAAC,IAAI,KAAK,QAAQ,CAAC;IAC5D,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QAC7C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAChF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,OAAO,2BAA4B,SAAQ,KAAK;IAC3C,UAAU,CAAoE;IAEvF,YAAY,UAAkB;QAC5B,MAAM,OAAO,GAAG,GAAG,UAAU,kHAAkH,CAAC;QAChJ,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAC;QAC1C,IAAI,CAAC,UAAU,GAAG;YAChB,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,0BAA0B;YAChC,OAAO;YACP,GAAG,EAAE,uGAAuG;SAC7G,CAAC;IACJ,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B;IACxC,IAAI,wBAAwB,EAAE,EAAE,CAAC;QAC/B,MAAM,IAAI,2BAA2B,CAAC,sBAAsB,EAAE,CAAC,CAAC;IAClE,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACxC,UAAU,CAAoE;IAEvF,YAAY,UAAkB,EAAE,OAA6B;QAC3D,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChE,MAAM,OAAO,GAAG,GAAG,UAAU,0BAA0B,OAAO,yEAAyE,CAAC;QACxI,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;QACvC,IAAI,CAAC,UAAU,GAAG;YAChB,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,uBAAuB;YAC7B,OAAO;YACP,GAAG,EAAE,6CAA6C,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,2BAA2B,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,+CAA+C;SAC7J,CAAC;IACJ,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,0BAA0B,CAAC,WAA8B;IACvE,MAAM,OAAO,GAAG,uBAAuB,CAAC,2BAA2B,EAAE,CAAC,CAAC;IACvE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACjC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAAE,OAAO;IACvE,MAAM,IAAI,wBAAwB,CAAC,sBAAsB,EAAE,EAAE,OAAO,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAoB,EACpB,OAAO,GAAsC,EAAE;IAE/C,IAAI,CAAC,OAAO,CAAC,mBAAmB;QAAE,0BAA0B,EAAE,CAAC;IAC/D,MAAM,SAAS,GAAG,kBAAkB,EAAE,CAAC;IACvC,MAAM,UAAU,GAAG,mBAAmB,EAAE,CAAC;IAEzC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;AAChF,CAAC"}
package/dist/core/id.d.ts CHANGED
@@ -6,4 +6,13 @@ export declare const KEBAB_ID_REGEX: RegExp;
6
6
  export declare function isKebabId(value: string): boolean;
7
7
  /** Human rendering of the grammar, shared so the wording never forks. */
8
8
  export declare const KEBAB_ID_DESCRIPTION = "must be kebab-case with lowercase letters, numbers, and single hyphen separators";
9
+ export declare function isUnicodeKebabName(value: string): boolean;
10
+ export declare const UNICODE_KEBAB_NAME_DESCRIPTION = "is not kebab-case (lowercase letters, numbers, and single hyphen separators; letters with no case, such as \u4E2D\u6587, are allowed)";
11
+ /**
12
+ * Why `name` cannot exist as a directory on every OS, or undefined when it can.
13
+ * A name created on Linux or macOS that Windows refuses makes the repo fail to
14
+ * check out there, so the rule is Windows' even when the project never runs on
15
+ * it. Worded to follow the name in a sentence: `"con" is a reserved ...`.
16
+ */
17
+ export declare function portableNameFault(name: string): string | undefined;
9
18
  //# sourceMappingURL=id.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/core/id.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAgC,CAAC;AAE5D,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,qFACmD,CAAC"}
1
+ {"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/core/id.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAgC,CAAC;AAE5D,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,qFACmD,CAAC;AAWrF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,eAAO,MAAM,8BAA8B,0IACoF,CAAC;AAShI;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUlE"}
package/dist/core/id.js CHANGED
@@ -8,4 +8,41 @@ export function isKebabId(value) {
8
8
  }
9
9
  /** Human rendering of the grammar, shared so the wording never forks. */
10
10
  export const KEBAB_ID_DESCRIPTION = 'must be kebab-case with lowercase letters, numbers, and single hyphen separators';
11
+ /**
12
+ * Kebab-case for names a user writes in their own language: a capability is
13
+ * named after a functional area, and `list --specs` already lays out names like
14
+ * `中文`. Letters with no case (CJK), modifier letters (the `ー` in katakana) and
15
+ * combining marks count as lowercase; what stays out is the ASCII grammar's
16
+ * exclusions — uppercase, `_`, whitespace, doubled or edge hyphens.
17
+ */
18
+ const UNICODE_KEBAB_NAME_REGEX = /^[\p{Ll}\p{Lm}\p{Lo}\p{M}\p{Nd}]+(?:-[\p{Ll}\p{Lm}\p{Lo}\p{M}\p{Nd}]+)*$/u;
19
+ export function isUnicodeKebabName(value) {
20
+ return UNICODE_KEBAB_NAME_REGEX.test(value);
21
+ }
22
+ export const UNICODE_KEBAB_NAME_DESCRIPTION = 'is not kebab-case (lowercase letters, numbers, and single hyphen separators; letters with no case, such as 中文, are allowed)';
23
+ // Windows refuses these as a file or directory name in any letter case and with
24
+ // any extension (`con.md` is the console too). The superscript digits are in
25
+ // Microsoft's list as well.
26
+ const WINDOWS_RESERVED_NAME_REGEX = /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\..*)?$/iu;
27
+ // The characters Win32 forbids in a name, plus the control range it also forbids.
28
+ const WINDOWS_FORBIDDEN_CHAR_REGEX = /[<>:"/\\|?*\u0000-\u001f]/u;
29
+ /**
30
+ * Why `name` cannot exist as a directory on every OS, or undefined when it can.
31
+ * A name created on Linux or macOS that Windows refuses makes the repo fail to
32
+ * check out there, so the rule is Windows' even when the project never runs on
33
+ * it. Worded to follow the name in a sentence: `"con" is a reserved ...`.
34
+ */
35
+ export function portableNameFault(name) {
36
+ if (WINDOWS_RESERVED_NAME_REGEX.test(name))
37
+ return 'is a reserved device name on Windows';
38
+ const forbidden = WINDOWS_FORBIDDEN_CHAR_REGEX.exec(name);
39
+ if (forbidden) {
40
+ const shown = forbidden[0] < ' ' ? `U+${forbidden[0].charCodeAt(0).toString(16).padStart(4, '0').toUpperCase()}` : forbidden[0];
41
+ return `contains "${shown}", which Windows does not allow in a file name`;
42
+ }
43
+ // Win32 strips a trailing dot or space, so `auth.` and `auth` name one directory.
44
+ if (/[. ]$/u.test(name))
45
+ return 'ends with a dot or a space, which Windows strips from a file name';
46
+ return undefined;
47
+ }
11
48
  //# sourceMappingURL=id.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/core/id.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAE5D,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACpC,CAAC;AAED,yEAAyE;AACzE,MAAM,CAAC,MAAM,oBAAoB,GAC/B,kFAAkF,CAAC"}
1
+ {"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/core/id.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAE5D,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACpC,CAAC;AAED,yEAAyE;AACzE,MAAM,CAAC,MAAM,oBAAoB,GAC/B,kFAAkF,CAAC;AAErF;;;;;;GAMG;AACH,MAAM,wBAAwB,GAAG,2EAA2E,CAAC;AAE7G,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,OAAO,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,CAAC,MAAM,8BAA8B,GACzC,6HAA6H,CAAC;AAEhI,gFAAgF;AAChF,6EAA6E;AAC7E,4BAA4B;AAC5B,MAAM,2BAA2B,GAAG,0DAA0D,CAAC;AAC/F,kFAAkF;AAClF,MAAM,4BAA4B,GAAG,4BAA4B,CAAC;AAElE;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,sCAAsC,CAAC;IAC1F,MAAM,SAAS,GAAG,4BAA4B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1D,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAChI,OAAO,aAAa,KAAK,gDAAgD,CAAC;IAC5E,CAAC;IACD,kFAAkF;IAClF,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,mEAAmE,CAAC;IACpG,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -1,6 +1,7 @@
1
1
  /** Sets up tospec Agent Skills for Claude Code and Codex. */
2
2
  type InitCommandOptions = {
3
3
  tools?: string;
4
+ /** Accepted for compatibility and ignored: init never asks before overwriting. */
4
5
  force?: boolean;
5
6
  interactive?: boolean;
6
7
  profile?: string;
@@ -11,7 +12,6 @@ type InitCommandOptions = {
11
12
  export declare const INIT_FAILURE_PAYLOAD: Record<string, unknown>;
12
13
  export declare class InitCommand {
13
14
  private readonly toolsArg?;
14
- private readonly force;
15
15
  private readonly interactiveOption?;
16
16
  private readonly profileOverride?;
17
17
  private readonly json;
@@ -50,9 +50,16 @@ export declare class InitCommand {
50
50
  */
51
51
  private createDirectoryStructure;
52
52
  private generateSkillsAndCommands;
53
+ /**
54
+ * The canonical sweep cannot stop at .agents/: every dependent tool is a copy
55
+ * of it, so pruning only the canonical side leaves a tool this run did not
56
+ * select holding skills the profile dropped, and the two directories disagree
57
+ * until the next `update`. Prune only, never write: the selection still
58
+ * decides which tools receive content.
59
+ */
60
+ private pruneUnselectedTools;
53
61
  private createConfig;
54
62
  private displaySuccessMessage;
55
- private removeLegacyCommands;
56
63
  }
57
64
  export {};
58
65
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/core/init.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAkD7D,KAAK,kBAAkB,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAA8B,CAAC;AAExF,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;IAChC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAU;IAC7C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAU;IAE/B,YAAY,OAAO,GAAE,kBAAuB,EAM3C;IAED;;;OAGG;IACH,OAAO,CAAC,GAAG;IAIX;;;OAGG;IACH,OAAO,CAAC,IAAI;IAQZ,6EAA6E;IAC7E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA2B;IAElD,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4G/C;YAEa,QAAQ;IAMtB;;;;;;OAMG;IACH,OAAO,CAAC,wBAAwB;IAiBhC,OAAO,CAAC,sBAAsB;IAM9B,OAAO,CAAC,sBAAsB;YAYhB,gBAAgB;IAoG9B,OAAO,CAAC,eAAe;IAyDvB,OAAO,CAAC,aAAa;IAkCrB;;;;OAIG;YACW,wBAAwB;YAcxB,yBAAyB;YAkFzB,YAAY;IAgB1B,OAAO,CAAC,qBAAqB;YA+Ff,oBAAoB;CAsBnC"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/core/init.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAoD7D,KAAK,kBAAkB,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAA8B,CAAC;AAsCxF,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAU;IAC7C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAU;IAE/B,YAAY,OAAO,GAAE,kBAAuB,EAK3C;IAED;;;OAGG;IACH,OAAO,CAAC,GAAG;IAIX;;;OAGG;IACH,OAAO,CAAC,IAAI;IAQZ,6EAA6E;IAC7E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA2B;IAElD,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoJ/C;YAEa,QAAQ;IAMtB;;;;;;OAMG;IACH,OAAO,CAAC,wBAAwB;IAiBhC,OAAO,CAAC,sBAAsB;IAM9B,OAAO,CAAC,sBAAsB;YAYhB,gBAAgB;IAoG9B,OAAO,CAAC,eAAe;IAyDvB,OAAO,CAAC,aAAa;IAkCrB;;;;OAIG;YACW,wBAAwB;YAcxB,yBAAyB;IAgGvC;;;;;;OAMG;YACW,oBAAoB;YAqCpB,YAAY;IAgB1B,OAAO,CAAC,qBAAqB;CA8F9B"}
package/dist/core/init.js CHANGED
@@ -1,6 +1,5 @@
1
1
  /** Sets up tospec Agent Skills for Claude Code and Codex. */
2
2
  import path from 'path';
3
- import * as fs from 'fs';
4
3
  import { createRequire } from 'module';
5
4
  import { FileSystemUtils } from '../utils/file-system.js';
6
5
  import { ensureDirectoryAnchor, tospecLayoutDirs } from './project-layout.js';
@@ -8,10 +7,10 @@ import { AI_TOOLS, TOSPEC_DIR_NAME, fallbackToolIds, resolveToolSelection, } fro
8
7
  import { isInteractive } from '../utils/interactive.js';
9
8
  import { serializeConfig } from './config-prompts.js';
10
9
  import { copyDir } from '../utils/link.js';
11
- import { removeLegacySkillDirs, removeUnselectedSkillDirs, removeLegacyToolSkillLayout, getToolsWithSkillsDir, getToolStates, getSkillTemplates, writeSkillDir, findWorkflowReferenceGaps, describeWorkflowReferenceGaps, DASHBOARD_MOD_TOOL_ID, syncDashboardMod, } from './shared/index.js';
10
+ import { removeLegacySkillDirs, removeUnselectedSkillDirs, removeLegacyToolSkillLayout, removeLegacyCommands, getToolsWithSkillsDir, getToolStates, getConfiguredTools, getSkillTemplates, writeSkillDir, findWorkflowReferenceGaps, describeWorkflowReferenceGaps, DASHBOARD_MOD_TOOL_ID, DASHBOARD_MOD_DIR, syncDashboardMod, } from './shared/index.js';
12
11
  import { getGlobalConfig } from './global-config.js';
13
12
  import { runLegacyUserStateMigration } from './user-state-migration.js';
14
- import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
13
+ import { getProfileWorkflows } from './profiles.js';
15
14
  import { getAvailableTools } from './available-tools.js';
16
15
  import { resolveConfigFileInDir } from './project-config.js';
17
16
  import { planWorkflowRules, writeWorkflowRules } from './shared/rules-generation.js';
@@ -26,15 +25,41 @@ function agentsSkillsDirFor(projectPath) {
26
25
  }
27
26
  /** The `--json` null-shape; mirrors the success payload with `init` nulled. */
28
27
  export const INIT_FAILURE_PAYLOAD = { init: null, root: null };
28
+ /**
29
+ * A failure init can name and advise on. Carries a full `CommandDiagnostic`:
30
+ * `asStatus` passes one through by `typeof diagnostic.code === 'string'`, where
31
+ * a bare Error falls back to `init_error` with no `fix`.
32
+ */
33
+ class InitError extends Error {
34
+ diagnostic;
35
+ constructor(code, message, fix, target) {
36
+ super(message);
37
+ this.name = 'InitError';
38
+ this.diagnostic = { severity: 'error', code, message, target, fix };
39
+ }
40
+ }
41
+ /**
42
+ * A file (or anything but a directory) where the layout puts a directory makes
43
+ * `mkdir` fail with a bare `EEXIST`, which names neither the cause nor a next
44
+ * step. Checked before any prompt or write, so the run stops before asking for
45
+ * tools it cannot install. Parents precede children in the list, so a file at
46
+ * `tospec` is reported before a child path under it is ever probed.
47
+ */
48
+ async function assertLayoutPathsAreDirectories(projectPath, tospecPath) {
49
+ for (const dir of [tospecPath, ...tospecLayoutDirs(tospecPath)]) {
50
+ if ((await FileSystemUtils.fileExists(dir)) && !(await FileSystemUtils.directoryExists(dir))) {
51
+ const relative = path.relative(projectPath, dir).split(path.sep).join('/');
52
+ throw new InitError('init_layout_path_not_directory', `"${relative}" exists and is not a directory, so init cannot create the tospec layout there.`, `Move or rename "${relative}", then rerun "tospec init".`, dir);
53
+ }
54
+ }
55
+ }
29
56
  export class InitCommand {
30
57
  toolsArg;
31
- force;
32
58
  interactiveOption;
33
59
  profileOverride;
34
60
  json;
35
61
  constructor(options = {}) {
36
62
  this.toolsArg = options.tools;
37
- this.force = options.force ?? false;
38
63
  this.interactiveOption = options.interactive;
39
64
  this.profileOverride = options.profile;
40
65
  this.json = options.json ?? false;
@@ -56,7 +81,7 @@ export class InitCommand {
56
81
  if (!this.json) {
57
82
  console.log(`Warning: ${message}`);
58
83
  if (fix)
59
- console.log(fix);
84
+ console.log(`Fix: ${fix}`);
60
85
  }
61
86
  }
62
87
  /** Emitted alongside the success payload; empty means the key is omitted. */
@@ -67,6 +92,7 @@ export class InitCommand {
67
92
  const tospecPath = path.join(projectPath, tospecDir);
68
93
  // Detect without writing: even the permission probe must follow rule preflight.
69
94
  const extendMode = await FileSystemUtils.directoryExists(tospecPath);
95
+ await assertLayoutPathsAreDirectories(projectPath, tospecPath);
70
96
  const detectedTools = getAvailableTools(projectPath);
71
97
  const canPrompt = this.canPromptInteractively();
72
98
  if (canPrompt) {
@@ -112,12 +138,44 @@ export class InitCommand {
112
138
  silent: this.json,
113
139
  });
114
140
  this.displaySuccessMessage(projectPath, validatedTools, results, configStatus);
141
+ // A warning, not progress prose: the run deleted from a tool the caller did
142
+ // not select, and under `--json` the envelope's `created` / `refreshed`
143
+ // otherwise say nothing about it.
144
+ if (results.prunedTools.length > 0) {
145
+ const removed = results.prunedTools.reduce((sum, tool) => sum + tool.removed, 0);
146
+ const where = results.prunedTools
147
+ .map((tool) => `${tool.name} (${tool.skillsDir}/skills/, ${tool.removed})`)
148
+ .join(', ');
149
+ this.warn('init_removed_deselected_skills', `Removed ${removed} skill director${removed === 1 ? 'y' : 'ies'} the profile '${initProfile}' does not select from tools this run did not set up: ${where}.`, 'To keep those workflows, add them with "tospec config profile", then run "tospec update".');
150
+ }
115
151
  // After every write, so a run that dies in rule preflight never leaves
116
152
  // advice about a state it did not reach; before the JSON envelope, or the
117
153
  // warning is recorded into a document already on stdout.
118
154
  if (hasResidualCodexOutput(projectPath)) {
119
155
  this.warn('codex_residue', RESIDUAL_CODEX_NOTICE, RESIDUAL_CODEX_FIX);
120
156
  }
157
+ // A tool that failed is a partial result, not a success: the envelope stays
158
+ // complete (`failed` names it) and the exit code says part of the run did
159
+ // not land, per the batch-command contract. Printed `Failed:` alone, a CI
160
+ // run or an agent reading exit 0 never saw it.
161
+ const failures = results.failedTools.map((f) => ({
162
+ severity: 'error',
163
+ code: 'init_tool_failed',
164
+ message: `Failed to set up ${f.name}: ${f.error.message}`,
165
+ target: f.toolId,
166
+ fix: "Fix the cause, then rerun 'tospec init'.",
167
+ }));
168
+ if (failures.length > 0) {
169
+ process.exitCode = 1;
170
+ if (!this.json) {
171
+ for (const failure of failures) {
172
+ console.error(`Error: ${failure.message}`);
173
+ if (failure.fix)
174
+ console.error(`Fix: ${failure.fix}`);
175
+ }
176
+ }
177
+ }
178
+ const status = [...failures, ...this.jsonWarnings];
121
179
  if (this.json) {
122
180
  const globalCfg = getGlobalConfig();
123
181
  const activeProfile = this.profileOverride ?? globalCfg.profile ?? 'core';
@@ -128,7 +186,9 @@ export class InitCommand {
128
186
  workflows: activeWorkflows,
129
187
  created: results.createdTools.map((t) => ({ tool: t.value, dir: t.skillsDir })),
130
188
  refreshed: results.refreshedTools.map((t) => ({ tool: t.value, dir: t.skillsDir })),
131
- failed: results.failedTools.map((f) => ({ name: f.name, error: f.error.message })),
189
+ // Keyed by tool id like `created` / `refreshed`: the display name is
190
+ // prose, and an agent matching it against `--tools` ids never could.
191
+ failed: results.failedTools.map((f) => ({ tool: f.toolId, error: f.error.message })),
132
192
  // `created` only records that a tool was configured, so this is the
133
193
  // only field saying what was installed, and it must count what
134
194
  // reached a tool directory: `--tools none` writes nothing anywhere.
@@ -137,7 +197,7 @@ export class InitCommand {
137
197
  : 0,
138
198
  config: configStatus,
139
199
  },
140
- ...(this.jsonWarnings.length ? { status: this.jsonWarnings } : {}),
200
+ ...(status.length ? { status } : {}),
141
201
  }, { path: projectPath, source: 'explicit' });
142
202
  }
143
203
  }
@@ -349,6 +409,10 @@ export class InitCommand {
349
409
  const profile = this.resolveProfileOverride() ?? globalConfig.profile ?? 'core';
350
410
  const workflows = getProfileWorkflows(profile, globalConfig.workflows);
351
411
  const skillTemplates = getSkillTemplates(workflows);
412
+ // Read before any sweep: an install is recognised by the very skill dirs the
413
+ // sweeps remove, so afterwards a pruned tool would no longer count as one.
414
+ const selectedIds = new Set(tools.map((tool) => tool.value));
415
+ const unselectedInstalled = getConfiguredTools(projectPath).filter((id) => !selectedIds.has(id));
352
416
  // Canonical files, written once to .agents/. Gated on "any target at all" so
353
417
  // `--tools none` writes nothing; dependency expansion guarantees .agents is
354
418
  // present whenever anything else is.
@@ -362,9 +426,10 @@ export class InitCommand {
362
426
  // on this run writing anything. Gated, `--tools none` would leave a retired
363
427
  // skill dir loadable by every agent reading .agents/skills/, and a dropped
364
428
  // workflow would come back on the next copy from the canonical directory.
365
- await removeLegacySkillDirs(agentsSkillsDir);
366
- await removeUnselectedSkillDirs(agentsSkillsDir, workflows);
367
- removedCommandCount += await this.removeLegacyCommands(projectPath);
429
+ const removedCanonical = (await removeLegacySkillDirs(agentsSkillsDir)) +
430
+ (await removeUnselectedSkillDirs(agentsSkillsDir, workflows));
431
+ removedCommandCount += await removeLegacyCommands(projectPath);
432
+ const prunedTools = await this.pruneUnselectedTools(projectPath, unselectedInstalled, workflows, removedCanonical);
368
433
  for (const tool of tools) {
369
434
  this.say(`Setting up ${tool.name}...`);
370
435
  try {
@@ -397,7 +462,7 @@ export class InitCommand {
397
462
  }
398
463
  catch (error) {
399
464
  this.say(`Failed for ${tool.name}`);
400
- failedTools.push({ name: tool.name, error: error });
465
+ failedTools.push({ toolId: tool.value, name: tool.name, error: error });
401
466
  }
402
467
  }
403
468
  return {
@@ -405,8 +470,47 @@ export class InitCommand {
405
470
  refreshedTools,
406
471
  failedTools,
407
472
  removedCommandCount,
473
+ prunedTools,
408
474
  };
409
475
  }
476
+ /**
477
+ * The canonical sweep cannot stop at .agents/: every dependent tool is a copy
478
+ * of it, so pruning only the canonical side leaves a tool this run did not
479
+ * select holding skills the profile dropped, and the two directories disagree
480
+ * until the next `update`. Prune only, never write: the selection still
481
+ * decides which tools receive content.
482
+ */
483
+ async pruneUnselectedTools(projectPath, toolIds, workflows, removedCanonical) {
484
+ const pruned = [];
485
+ const agentsSkillsDir = path.resolve(agentsSkillsDirFor(projectPath));
486
+ for (const toolId of toolIds) {
487
+ const tool = AI_TOOLS.find((t) => t.value === toolId);
488
+ if (!tool?.skillsDir)
489
+ continue;
490
+ const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
491
+ let removed;
492
+ if (path.resolve(skillsDir) === agentsSkillsDir) {
493
+ // Already swept above; counted here so the report names it.
494
+ removed = removedCanonical;
495
+ }
496
+ else {
497
+ removed =
498
+ (await removeLegacySkillDirs(skillsDir)) +
499
+ (await removeUnselectedSkillDirs(skillsDir, workflows));
500
+ // The mod goes with the last workflow, as a selected tool's sync does;
501
+ // with workflows left it stays untouched, since refreshing it is a write.
502
+ if (toolId === DASHBOARD_MOD_TOOL_ID && workflows.length === 0) {
503
+ const hadMod = await FileSystemUtils.directoryExists(path.join(skillsDir, DASHBOARD_MOD_DIR));
504
+ await syncDashboardMod(skillsDir, TOSPEC_VERSION, false);
505
+ if (hadMod)
506
+ removed += 1;
507
+ }
508
+ }
509
+ if (removed > 0)
510
+ pruned.push({ name: tool.name, skillsDir: tool.skillsDir, removed });
511
+ }
512
+ return pruned;
513
+ }
410
514
  async createConfig(tospecPath, extendMode) {
411
515
  const configPath = path.join(tospecPath, 'config.yaml');
412
516
  if (resolveConfigFileInDir(tospecPath)) {
@@ -494,26 +598,5 @@ export class InitCommand {
494
598
  }
495
599
  this.say();
496
600
  }
497
- async removeLegacyCommands(projectPath) {
498
- let removed = 0;
499
- // Every workflow ships as a skill only. Slash commands duplicated each skill
500
- // under a second name in the agent's listing, so every location an earlier
501
- // version wrote them to is swept.
502
- const legacyPaths = [
503
- path.join(projectPath, '.agents', 'commands', 'tospec'),
504
- path.join(projectPath, '.claude', 'commands', 'tospec'),
505
- path.join(projectPath, '.claude', 'commands', 'tosx'),
506
- // Cursor support was removed; this stays so stale .cursor command files
507
- // are still swept away.
508
- ...ALL_WORKFLOWS.map((workflow) => path.join(projectPath, '.cursor', 'commands', `tospec-${workflow}.md`)),
509
- ];
510
- for (const legacyPath of legacyPaths) {
511
- if (fs.existsSync(legacyPath)) {
512
- await fs.promises.rm(legacyPath, { recursive: true, force: true });
513
- removed++;
514
- }
515
- }
516
- return removed;
517
- }
518
601
  }
519
602
  //# sourceMappingURL=init.js.map