anchorspec 1.3.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 (273) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +230 -0
  3. package/bin/anchorspec.js +3 -0
  4. package/bin/openspec.js +3 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.js +455 -0
  7. package/dist/commands/change.d.ts +35 -0
  8. package/dist/commands/change.js +277 -0
  9. package/dist/commands/completion.d.ts +72 -0
  10. package/dist/commands/completion.js +257 -0
  11. package/dist/commands/config.d.ts +36 -0
  12. package/dist/commands/config.js +552 -0
  13. package/dist/commands/feedback.d.ts +9 -0
  14. package/dist/commands/feedback.js +183 -0
  15. package/dist/commands/schema.d.ts +6 -0
  16. package/dist/commands/schema.js +869 -0
  17. package/dist/commands/show.d.ts +14 -0
  18. package/dist/commands/show.js +132 -0
  19. package/dist/commands/spec.d.ts +15 -0
  20. package/dist/commands/spec.js +225 -0
  21. package/dist/commands/validate.d.ts +24 -0
  22. package/dist/commands/validate.js +294 -0
  23. package/dist/commands/workflow/index.d.ts +17 -0
  24. package/dist/commands/workflow/index.js +12 -0
  25. package/dist/commands/workflow/instructions.d.ts +29 -0
  26. package/dist/commands/workflow/instructions.js +381 -0
  27. package/dist/commands/workflow/new-change.d.ts +11 -0
  28. package/dist/commands/workflow/new-change.js +44 -0
  29. package/dist/commands/workflow/schemas.d.ts +10 -0
  30. package/dist/commands/workflow/schemas.js +34 -0
  31. package/dist/commands/workflow/shared.d.ts +57 -0
  32. package/dist/commands/workflow/shared.js +116 -0
  33. package/dist/commands/workflow/status.d.ts +14 -0
  34. package/dist/commands/workflow/status.js +75 -0
  35. package/dist/commands/workflow/templates.d.ts +16 -0
  36. package/dist/commands/workflow/templates.js +68 -0
  37. package/dist/core/archive.d.ts +11 -0
  38. package/dist/core/archive.js +318 -0
  39. package/dist/core/artifact-graph/graph.d.ts +56 -0
  40. package/dist/core/artifact-graph/graph.js +141 -0
  41. package/dist/core/artifact-graph/index.d.ts +7 -0
  42. package/dist/core/artifact-graph/index.js +13 -0
  43. package/dist/core/artifact-graph/instruction-loader.d.ts +143 -0
  44. package/dist/core/artifact-graph/instruction-loader.js +214 -0
  45. package/dist/core/artifact-graph/resolver.d.ts +81 -0
  46. package/dist/core/artifact-graph/resolver.js +257 -0
  47. package/dist/core/artifact-graph/schema.d.ts +13 -0
  48. package/dist/core/artifact-graph/schema.js +108 -0
  49. package/dist/core/artifact-graph/state.d.ts +12 -0
  50. package/dist/core/artifact-graph/state.js +54 -0
  51. package/dist/core/artifact-graph/types.d.ts +45 -0
  52. package/dist/core/artifact-graph/types.js +43 -0
  53. package/dist/core/available-tools.d.ts +17 -0
  54. package/dist/core/available-tools.js +43 -0
  55. package/dist/core/command-generation/adapters/amazon-q.d.ts +13 -0
  56. package/dist/core/command-generation/adapters/amazon-q.js +26 -0
  57. package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
  58. package/dist/core/command-generation/adapters/antigravity.js +26 -0
  59. package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
  60. package/dist/core/command-generation/adapters/auggie.js +27 -0
  61. package/dist/core/command-generation/adapters/bob.d.ts +14 -0
  62. package/dist/core/command-generation/adapters/bob.js +45 -0
  63. package/dist/core/command-generation/adapters/claude.d.ts +13 -0
  64. package/dist/core/command-generation/adapters/claude.js +50 -0
  65. package/dist/core/command-generation/adapters/cline.d.ts +14 -0
  66. package/dist/core/command-generation/adapters/cline.js +27 -0
  67. package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
  68. package/dist/core/command-generation/adapters/codebuddy.js +28 -0
  69. package/dist/core/command-generation/adapters/codex.d.ts +16 -0
  70. package/dist/core/command-generation/adapters/codex.js +39 -0
  71. package/dist/core/command-generation/adapters/continue.d.ts +13 -0
  72. package/dist/core/command-generation/adapters/continue.js +28 -0
  73. package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
  74. package/dist/core/command-generation/adapters/costrict.js +27 -0
  75. package/dist/core/command-generation/adapters/crush.d.ts +13 -0
  76. package/dist/core/command-generation/adapters/crush.js +30 -0
  77. package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
  78. package/dist/core/command-generation/adapters/cursor.js +44 -0
  79. package/dist/core/command-generation/adapters/factory.d.ts +13 -0
  80. package/dist/core/command-generation/adapters/factory.js +27 -0
  81. package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
  82. package/dist/core/command-generation/adapters/gemini.js +26 -0
  83. package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
  84. package/dist/core/command-generation/adapters/github-copilot.js +26 -0
  85. package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
  86. package/dist/core/command-generation/adapters/iflow.js +29 -0
  87. package/dist/core/command-generation/adapters/index.d.ts +32 -0
  88. package/dist/core/command-generation/adapters/index.js +32 -0
  89. package/dist/core/command-generation/adapters/junie.d.ts +13 -0
  90. package/dist/core/command-generation/adapters/junie.js +26 -0
  91. package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
  92. package/dist/core/command-generation/adapters/kilocode.js +23 -0
  93. package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
  94. package/dist/core/command-generation/adapters/kiro.js +26 -0
  95. package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
  96. package/dist/core/command-generation/adapters/lingma.js +30 -0
  97. package/dist/core/command-generation/adapters/opencode.d.ts +13 -0
  98. package/dist/core/command-generation/adapters/opencode.js +29 -0
  99. package/dist/core/command-generation/adapters/pi.d.ts +18 -0
  100. package/dist/core/command-generation/adapters/pi.js +55 -0
  101. package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
  102. package/dist/core/command-generation/adapters/qoder.js +30 -0
  103. package/dist/core/command-generation/adapters/qwen.d.ts +13 -0
  104. package/dist/core/command-generation/adapters/qwen.js +26 -0
  105. package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
  106. package/dist/core/command-generation/adapters/roocode.js +27 -0
  107. package/dist/core/command-generation/adapters/windsurf.d.ts +14 -0
  108. package/dist/core/command-generation/adapters/windsurf.js +51 -0
  109. package/dist/core/command-generation/generator.d.ts +21 -0
  110. package/dist/core/command-generation/generator.js +27 -0
  111. package/dist/core/command-generation/index.d.ts +22 -0
  112. package/dist/core/command-generation/index.js +24 -0
  113. package/dist/core/command-generation/registry.d.ts +36 -0
  114. package/dist/core/command-generation/registry.js +98 -0
  115. package/dist/core/command-generation/types.d.ts +56 -0
  116. package/dist/core/command-generation/types.js +8 -0
  117. package/dist/core/completions/command-registry.d.ts +7 -0
  118. package/dist/core/completions/command-registry.js +461 -0
  119. package/dist/core/completions/completion-provider.d.ts +60 -0
  120. package/dist/core/completions/completion-provider.js +102 -0
  121. package/dist/core/completions/factory.d.ts +64 -0
  122. package/dist/core/completions/factory.js +75 -0
  123. package/dist/core/completions/generators/bash-generator.d.ts +32 -0
  124. package/dist/core/completions/generators/bash-generator.js +174 -0
  125. package/dist/core/completions/generators/fish-generator.d.ts +32 -0
  126. package/dist/core/completions/generators/fish-generator.js +157 -0
  127. package/dist/core/completions/generators/powershell-generator.d.ts +33 -0
  128. package/dist/core/completions/generators/powershell-generator.js +207 -0
  129. package/dist/core/completions/generators/zsh-generator.d.ts +44 -0
  130. package/dist/core/completions/generators/zsh-generator.js +250 -0
  131. package/dist/core/completions/installers/bash-installer.d.ts +87 -0
  132. package/dist/core/completions/installers/bash-installer.js +318 -0
  133. package/dist/core/completions/installers/fish-installer.d.ts +43 -0
  134. package/dist/core/completions/installers/fish-installer.js +143 -0
  135. package/dist/core/completions/installers/powershell-installer.d.ts +102 -0
  136. package/dist/core/completions/installers/powershell-installer.js +387 -0
  137. package/dist/core/completions/installers/zsh-installer.d.ts +125 -0
  138. package/dist/core/completions/installers/zsh-installer.js +449 -0
  139. package/dist/core/completions/templates/bash-templates.d.ts +6 -0
  140. package/dist/core/completions/templates/bash-templates.js +24 -0
  141. package/dist/core/completions/templates/fish-templates.d.ts +7 -0
  142. package/dist/core/completions/templates/fish-templates.js +39 -0
  143. package/dist/core/completions/templates/powershell-templates.d.ts +6 -0
  144. package/dist/core/completions/templates/powershell-templates.js +25 -0
  145. package/dist/core/completions/templates/zsh-templates.d.ts +6 -0
  146. package/dist/core/completions/templates/zsh-templates.js +36 -0
  147. package/dist/core/completions/types.d.ts +79 -0
  148. package/dist/core/completions/types.js +2 -0
  149. package/dist/core/config-prompts.d.ts +9 -0
  150. package/dist/core/config-prompts.js +34 -0
  151. package/dist/core/config-schema.d.ts +86 -0
  152. package/dist/core/config-schema.js +213 -0
  153. package/dist/core/config.d.ts +18 -0
  154. package/dist/core/config.js +37 -0
  155. package/dist/core/converters/json-converter.d.ts +6 -0
  156. package/dist/core/converters/json-converter.js +51 -0
  157. package/dist/core/global-config.d.ts +44 -0
  158. package/dist/core/global-config.js +125 -0
  159. package/dist/core/index.d.ts +2 -0
  160. package/dist/core/index.js +3 -0
  161. package/dist/core/init.d.ts +37 -0
  162. package/dist/core/init.js +593 -0
  163. package/dist/core/legacy-cleanup.d.ts +162 -0
  164. package/dist/core/legacy-cleanup.js +514 -0
  165. package/dist/core/list.d.ts +9 -0
  166. package/dist/core/list.js +171 -0
  167. package/dist/core/migration.d.ts +23 -0
  168. package/dist/core/migration.js +108 -0
  169. package/dist/core/parsers/change-parser.d.ts +13 -0
  170. package/dist/core/parsers/change-parser.js +193 -0
  171. package/dist/core/parsers/markdown-parser.d.ts +22 -0
  172. package/dist/core/parsers/markdown-parser.js +187 -0
  173. package/dist/core/parsers/requirement-blocks.d.ts +37 -0
  174. package/dist/core/parsers/requirement-blocks.js +201 -0
  175. package/dist/core/profile-sync-drift.d.ts +38 -0
  176. package/dist/core/profile-sync-drift.js +200 -0
  177. package/dist/core/profiles.d.ts +26 -0
  178. package/dist/core/profiles.js +40 -0
  179. package/dist/core/project-config.d.ts +64 -0
  180. package/dist/core/project-config.js +223 -0
  181. package/dist/core/schemas/base.schema.d.ts +13 -0
  182. package/dist/core/schemas/base.schema.js +13 -0
  183. package/dist/core/schemas/change.schema.d.ts +73 -0
  184. package/dist/core/schemas/change.schema.js +31 -0
  185. package/dist/core/schemas/index.d.ts +4 -0
  186. package/dist/core/schemas/index.js +4 -0
  187. package/dist/core/schemas/spec.schema.d.ts +18 -0
  188. package/dist/core/schemas/spec.schema.js +15 -0
  189. package/dist/core/shared/index.d.ts +8 -0
  190. package/dist/core/shared/index.js +8 -0
  191. package/dist/core/shared/skill-generation.d.ts +49 -0
  192. package/dist/core/shared/skill-generation.js +96 -0
  193. package/dist/core/shared/tool-detection.d.ts +71 -0
  194. package/dist/core/shared/tool-detection.js +158 -0
  195. package/dist/core/specs-apply.d.ts +73 -0
  196. package/dist/core/specs-apply.js +384 -0
  197. package/dist/core/styles/palette.d.ts +7 -0
  198. package/dist/core/styles/palette.js +8 -0
  199. package/dist/core/templates/index.d.ts +8 -0
  200. package/dist/core/templates/index.js +9 -0
  201. package/dist/core/templates/skill-templates.d.ts +19 -0
  202. package/dist/core/templates/skill-templates.js +18 -0
  203. package/dist/core/templates/types.d.ts +19 -0
  204. package/dist/core/templates/types.js +5 -0
  205. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  206. package/dist/core/templates/workflows/apply-change.js +308 -0
  207. package/dist/core/templates/workflows/archive-change.d.ts +10 -0
  208. package/dist/core/templates/workflows/archive-change.js +271 -0
  209. package/dist/core/templates/workflows/bulk-archive-change.d.ts +10 -0
  210. package/dist/core/templates/workflows/bulk-archive-change.js +488 -0
  211. package/dist/core/templates/workflows/continue-change.d.ts +10 -0
  212. package/dist/core/templates/workflows/continue-change.js +232 -0
  213. package/dist/core/templates/workflows/explore.d.ts +10 -0
  214. package/dist/core/templates/workflows/explore.js +461 -0
  215. package/dist/core/templates/workflows/feedback.d.ts +9 -0
  216. package/dist/core/templates/workflows/feedback.js +108 -0
  217. package/dist/core/templates/workflows/ff-change.d.ts +10 -0
  218. package/dist/core/templates/workflows/ff-change.js +198 -0
  219. package/dist/core/templates/workflows/new-change.d.ts +10 -0
  220. package/dist/core/templates/workflows/new-change.js +143 -0
  221. package/dist/core/templates/workflows/onboard.d.ts +10 -0
  222. package/dist/core/templates/workflows/onboard.js +565 -0
  223. package/dist/core/templates/workflows/propose.d.ts +10 -0
  224. package/dist/core/templates/workflows/propose.js +216 -0
  225. package/dist/core/templates/workflows/sync-specs.d.ts +10 -0
  226. package/dist/core/templates/workflows/sync-specs.js +272 -0
  227. package/dist/core/templates/workflows/verify-change.d.ts +10 -0
  228. package/dist/core/templates/workflows/verify-change.js +332 -0
  229. package/dist/core/update.d.ts +77 -0
  230. package/dist/core/update.js +537 -0
  231. package/dist/core/validation/constants.d.ts +34 -0
  232. package/dist/core/validation/constants.js +40 -0
  233. package/dist/core/validation/types.d.ts +18 -0
  234. package/dist/core/validation/types.js +2 -0
  235. package/dist/core/validation/validator.d.ts +33 -0
  236. package/dist/core/validation/validator.js +409 -0
  237. package/dist/core/view.d.ts +8 -0
  238. package/dist/core/view.js +168 -0
  239. package/dist/index.d.ts +3 -0
  240. package/dist/index.js +3 -0
  241. package/dist/prompts/searchable-multi-select.d.ts +28 -0
  242. package/dist/prompts/searchable-multi-select.js +159 -0
  243. package/dist/ui/ascii-patterns.d.ts +16 -0
  244. package/dist/ui/ascii-patterns.js +133 -0
  245. package/dist/ui/welcome-screen.d.ts +10 -0
  246. package/dist/ui/welcome-screen.js +146 -0
  247. package/dist/utils/change-metadata.d.ts +51 -0
  248. package/dist/utils/change-metadata.js +147 -0
  249. package/dist/utils/change-utils.d.ts +62 -0
  250. package/dist/utils/change-utils.js +121 -0
  251. package/dist/utils/command-references.d.ts +18 -0
  252. package/dist/utils/command-references.js +20 -0
  253. package/dist/utils/file-system.d.ts +36 -0
  254. package/dist/utils/file-system.js +281 -0
  255. package/dist/utils/index.d.ts +6 -0
  256. package/dist/utils/index.js +9 -0
  257. package/dist/utils/interactive.d.ts +18 -0
  258. package/dist/utils/interactive.js +21 -0
  259. package/dist/utils/item-discovery.d.ts +4 -0
  260. package/dist/utils/item-discovery.js +72 -0
  261. package/dist/utils/match.d.ts +3 -0
  262. package/dist/utils/match.js +22 -0
  263. package/dist/utils/shell-detection.d.ts +20 -0
  264. package/dist/utils/shell-detection.js +41 -0
  265. package/dist/utils/task-progress.d.ts +8 -0
  266. package/dist/utils/task-progress.js +36 -0
  267. package/package.json +78 -0
  268. package/schemas/spec-driven/schema.yaml +153 -0
  269. package/schemas/spec-driven/templates/design.md +19 -0
  270. package/schemas/spec-driven/templates/proposal.md +23 -0
  271. package/schemas/spec-driven/templates/spec.md +8 -0
  272. package/schemas/spec-driven/templates/tasks.md +9 -0
  273. package/scripts/postinstall.js +83 -0
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Options for creating a change.
3
+ */
4
+ export interface CreateChangeOptions {
5
+ /** The workflow schema to use (default: 'spec-driven') */
6
+ schema?: string;
7
+ }
8
+ /**
9
+ * Result of creating a change.
10
+ */
11
+ export interface CreateChangeResult {
12
+ /** The schema that was actually used (resolved from options, config, or default) */
13
+ schema: string;
14
+ }
15
+ /**
16
+ * Result of validating a change name.
17
+ */
18
+ export interface ValidationResult {
19
+ valid: boolean;
20
+ error?: string;
21
+ }
22
+ /**
23
+ * Validates that a change name follows kebab-case conventions.
24
+ *
25
+ * Valid names:
26
+ * - Start with a lowercase letter
27
+ * - Contain only lowercase letters, numbers, and hyphens
28
+ * - Do not start or end with a hyphen
29
+ * - Do not contain consecutive hyphens
30
+ *
31
+ * @param name - The change name to validate
32
+ * @returns Validation result with `valid: true` or `valid: false` with an error message
33
+ *
34
+ * @example
35
+ * validateChangeName('add-auth') // { valid: true }
36
+ * validateChangeName('Add-Auth') // { valid: false, error: '...' }
37
+ */
38
+ export declare function validateChangeName(name: string): ValidationResult;
39
+ /**
40
+ * Creates a new change directory with metadata file.
41
+ *
42
+ * @param projectRoot - The root directory of the project (where `openspec/` lives)
43
+ * @param name - The change name (must be valid kebab-case)
44
+ * @param options - Optional settings for the change
45
+ * @throws Error if the change name is invalid
46
+ * @throws Error if the schema name is invalid
47
+ * @throws Error if the change directory already exists
48
+ *
49
+ * @returns Result containing the resolved schema name
50
+ *
51
+ * @example
52
+ * // Creates openspec/changes/add-auth/ with default schema
53
+ * const result = await createChange('/path/to/project', 'add-auth')
54
+ * console.log(result.schema) // 'spec-driven' or value from config
55
+ *
56
+ * @example
57
+ * // Creates openspec/changes/add-auth/ with custom schema
58
+ * const result = await createChange('/path/to/project', 'add-auth', { schema: 'my-workflow' })
59
+ * console.log(result.schema) // 'my-workflow'
60
+ */
61
+ export declare function createChange(projectRoot: string, name: string, options?: CreateChangeOptions): Promise<CreateChangeResult>;
62
+ //# sourceMappingURL=change-utils.d.ts.map
@@ -0,0 +1,121 @@
1
+ import path from 'path';
2
+ import { FileSystemUtils } from './file-system.js';
3
+ import { writeChangeMetadata, validateSchemaName } from './change-metadata.js';
4
+ import { readProjectConfig } from '../core/project-config.js';
5
+ const DEFAULT_SCHEMA = 'spec-driven';
6
+ /**
7
+ * Validates that a change name follows kebab-case conventions.
8
+ *
9
+ * Valid names:
10
+ * - Start with a lowercase letter
11
+ * - Contain only lowercase letters, numbers, and hyphens
12
+ * - Do not start or end with a hyphen
13
+ * - Do not contain consecutive hyphens
14
+ *
15
+ * @param name - The change name to validate
16
+ * @returns Validation result with `valid: true` or `valid: false` with an error message
17
+ *
18
+ * @example
19
+ * validateChangeName('add-auth') // { valid: true }
20
+ * validateChangeName('Add-Auth') // { valid: false, error: '...' }
21
+ */
22
+ export function validateChangeName(name) {
23
+ // Pattern: starts with lowercase letter, followed by lowercase letters/numbers,
24
+ // optionally followed by hyphen + lowercase letters/numbers (repeatable)
25
+ const kebabCasePattern = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
26
+ if (!name) {
27
+ return { valid: false, error: 'Change name cannot be empty' };
28
+ }
29
+ if (!kebabCasePattern.test(name)) {
30
+ // Provide specific error messages for common mistakes
31
+ if (/[A-Z]/.test(name)) {
32
+ return { valid: false, error: 'Change name must be lowercase (use kebab-case)' };
33
+ }
34
+ if (/\s/.test(name)) {
35
+ return { valid: false, error: 'Change name cannot contain spaces (use hyphens instead)' };
36
+ }
37
+ if (/_/.test(name)) {
38
+ return { valid: false, error: 'Change name cannot contain underscores (use hyphens instead)' };
39
+ }
40
+ if (name.startsWith('-')) {
41
+ return { valid: false, error: 'Change name cannot start with a hyphen' };
42
+ }
43
+ if (name.endsWith('-')) {
44
+ return { valid: false, error: 'Change name cannot end with a hyphen' };
45
+ }
46
+ if (/--/.test(name)) {
47
+ return { valid: false, error: 'Change name cannot contain consecutive hyphens' };
48
+ }
49
+ if (/[^a-z0-9-]/.test(name)) {
50
+ return { valid: false, error: 'Change name can only contain lowercase letters, numbers, and hyphens' };
51
+ }
52
+ if (/^[0-9]/.test(name)) {
53
+ return { valid: false, error: 'Change name must start with a letter' };
54
+ }
55
+ return { valid: false, error: 'Change name must follow kebab-case convention (e.g., add-auth, refactor-db)' };
56
+ }
57
+ return { valid: true };
58
+ }
59
+ /**
60
+ * Creates a new change directory with metadata file.
61
+ *
62
+ * @param projectRoot - The root directory of the project (where `anchorspec/` lives)
63
+ * @param name - The change name (must be valid kebab-case)
64
+ * @param options - Optional settings for the change
65
+ * @throws Error if the change name is invalid
66
+ * @throws Error if the schema name is invalid
67
+ * @throws Error if the change directory already exists
68
+ *
69
+ * @returns Result containing the resolved schema name
70
+ *
71
+ * @example
72
+ * // Creates anchorspec/changes/add-auth/ with default schema
73
+ * const result = await createChange('/path/to/project', 'add-auth')
74
+ * console.log(result.schema) // 'spec-driven' or value from config
75
+ *
76
+ * @example
77
+ * // Creates anchorspec/changes/add-auth/ with custom schema
78
+ * const result = await createChange('/path/to/project', 'add-auth', { schema: 'my-workflow' })
79
+ * console.log(result.schema) // 'my-workflow'
80
+ */
81
+ export async function createChange(projectRoot, name, options = {}) {
82
+ // Validate the name first
83
+ const validation = validateChangeName(name);
84
+ if (!validation.valid) {
85
+ throw new Error(validation.error);
86
+ }
87
+ // Determine schema: explicit option → project config → hardcoded default
88
+ let schemaName;
89
+ if (options.schema) {
90
+ schemaName = options.schema;
91
+ }
92
+ else {
93
+ // Try to read from project config
94
+ try {
95
+ const config = readProjectConfig(projectRoot);
96
+ schemaName = config?.schema ?? DEFAULT_SCHEMA;
97
+ }
98
+ catch {
99
+ // If config read fails, use default
100
+ schemaName = DEFAULT_SCHEMA;
101
+ }
102
+ }
103
+ // Validate the resolved schema
104
+ validateSchemaName(schemaName, projectRoot);
105
+ // Build the change directory path
106
+ const changeDir = path.join(projectRoot, 'anchorspec', 'changes', name);
107
+ // Check if change already exists
108
+ if (await FileSystemUtils.directoryExists(changeDir)) {
109
+ throw new Error(`Change '${name}' already exists at ${changeDir}`);
110
+ }
111
+ // Create the directory (including parent directories if needed)
112
+ await FileSystemUtils.createDirectory(changeDir);
113
+ // Write metadata file with schema and creation date
114
+ const today = new Date().toISOString().split('T')[0];
115
+ writeChangeMetadata(changeDir, {
116
+ schema: schemaName,
117
+ created: today,
118
+ }, projectRoot);
119
+ return { schema: schemaName };
120
+ }
121
+ //# sourceMappingURL=change-utils.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Command Reference Utilities
3
+ *
4
+ * Utilities for transforming command references to tool-specific formats.
5
+ */
6
+ /**
7
+ * Transforms colon-based command references to hyphen-based format.
8
+ * Converts `/opsx:` patterns to `/opsx-` for tools that use hyphen syntax.
9
+ *
10
+ * @param text - The text containing command references
11
+ * @returns Text with command references transformed to hyphen format
12
+ *
13
+ * @example
14
+ * transformToHyphenCommands('/opsx:new') // returns '/opsx-new'
15
+ * transformToHyphenCommands('Use /opsx:apply to implement') // returns 'Use /opsx-apply to implement'
16
+ */
17
+ export declare function transformToHyphenCommands(text: string): string;
18
+ //# sourceMappingURL=command-references.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Command Reference Utilities
3
+ *
4
+ * Utilities for transforming command references to tool-specific formats.
5
+ */
6
+ /**
7
+ * Transforms colon-based command references to hyphen-based format.
8
+ * Converts `/ansx:` patterns to `/ansx-` for tools that use hyphen syntax.
9
+ *
10
+ * @param text - The text containing command references
11
+ * @returns Text with command references transformed to hyphen format
12
+ *
13
+ * @example
14
+ * transformToHyphenCommands('/ansx:new') // returns '/ansx-new'
15
+ * transformToHyphenCommands('Use /ansx:apply to implement') // returns 'Use /ansx-apply to implement'
16
+ */
17
+ export function transformToHyphenCommands(text) {
18
+ return text.replace(/\/ansx:/g, '/ansx-');
19
+ }
20
+ //# sourceMappingURL=command-references.js.map
@@ -0,0 +1,36 @@
1
+ export declare class FileSystemUtils {
2
+ /**
3
+ * Converts a path to use forward slashes (POSIX style).
4
+ * Essential for cross-platform compatibility with glob libraries like fast-glob.
5
+ */
6
+ static toPosixPath(p: string): string;
7
+ private static isWindowsBasePath;
8
+ private static normalizeSegments;
9
+ static joinPath(basePath: string, ...segments: string[]): string;
10
+ static createDirectory(dirPath: string): Promise<void>;
11
+ static fileExists(filePath: string): Promise<boolean>;
12
+ /**
13
+ * Finds the first existing parent directory by walking up the directory tree.
14
+ * @param dirPath Starting directory path
15
+ * @returns The first existing directory path, or null if root is reached without finding one
16
+ */
17
+ private static findFirstExistingDirectory;
18
+ static canWriteFile(filePath: string): Promise<boolean>;
19
+ static directoryExists(dirPath: string): Promise<boolean>;
20
+ static writeFile(filePath: string, content: string): Promise<void>;
21
+ static readFile(filePath: string): Promise<string>;
22
+ static updateFileWithMarkers(filePath: string, content: string, startMarker: string, endMarker: string): Promise<void>;
23
+ static ensureWritePermissions(dirPath: string): Promise<boolean>;
24
+ }
25
+ /**
26
+ * Removes a marker block from file content.
27
+ * Only removes markers that are on their own lines (ignores inline mentions).
28
+ * Cleans up double blank lines that may result from removal.
29
+ *
30
+ * @param content - File content with markers
31
+ * @param startMarker - The start marker string
32
+ * @param endMarker - The end marker string
33
+ * @returns Content with marker block removed, or original content if markers not found/invalid
34
+ */
35
+ export declare function removeMarkerBlock(content: string, startMarker: string, endMarker: string): string;
36
+ //# sourceMappingURL=file-system.d.ts.map
@@ -0,0 +1,281 @@
1
+ import { promises as fs, constants as fsConstants } from 'fs';
2
+ import path from 'path';
3
+ function isMarkerOnOwnLine(content, markerIndex, markerLength) {
4
+ let leftIndex = markerIndex - 1;
5
+ while (leftIndex >= 0 && content[leftIndex] !== '\n') {
6
+ const char = content[leftIndex];
7
+ if (char !== ' ' && char !== '\t' && char !== '\r') {
8
+ return false;
9
+ }
10
+ leftIndex--;
11
+ }
12
+ let rightIndex = markerIndex + markerLength;
13
+ while (rightIndex < content.length && content[rightIndex] !== '\n') {
14
+ const char = content[rightIndex];
15
+ if (char !== ' ' && char !== '\t' && char !== '\r') {
16
+ return false;
17
+ }
18
+ rightIndex++;
19
+ }
20
+ return true;
21
+ }
22
+ function findMarkerIndex(content, marker, fromIndex = 0) {
23
+ let currentIndex = content.indexOf(marker, fromIndex);
24
+ while (currentIndex !== -1) {
25
+ if (isMarkerOnOwnLine(content, currentIndex, marker.length)) {
26
+ return currentIndex;
27
+ }
28
+ currentIndex = content.indexOf(marker, currentIndex + marker.length);
29
+ }
30
+ return -1;
31
+ }
32
+ export class FileSystemUtils {
33
+ /**
34
+ * Converts a path to use forward slashes (POSIX style).
35
+ * Essential for cross-platform compatibility with glob libraries like fast-glob.
36
+ */
37
+ static toPosixPath(p) {
38
+ return p.replace(/\\/g, '/');
39
+ }
40
+ static isWindowsBasePath(basePath) {
41
+ return /^[A-Za-z]:[\\/]/.test(basePath) || basePath.startsWith('\\');
42
+ }
43
+ static normalizeSegments(segments) {
44
+ return segments
45
+ .flatMap((segment) => segment.split(/[\\/]+/u))
46
+ .filter((part) => part.length > 0);
47
+ }
48
+ static joinPath(basePath, ...segments) {
49
+ const normalizedSegments = this.normalizeSegments(segments);
50
+ if (this.isWindowsBasePath(basePath)) {
51
+ const normalizedBasePath = path.win32.normalize(basePath);
52
+ return normalizedSegments.length
53
+ ? path.win32.join(normalizedBasePath, ...normalizedSegments)
54
+ : normalizedBasePath;
55
+ }
56
+ const posixBasePath = basePath.replace(/\\/g, '/');
57
+ return normalizedSegments.length
58
+ ? path.posix.join(posixBasePath, ...normalizedSegments)
59
+ : path.posix.normalize(posixBasePath);
60
+ }
61
+ static async createDirectory(dirPath) {
62
+ await fs.mkdir(dirPath, { recursive: true });
63
+ }
64
+ static async fileExists(filePath) {
65
+ try {
66
+ await fs.access(filePath);
67
+ return true;
68
+ }
69
+ catch (error) {
70
+ if (error.code !== 'ENOENT') {
71
+ console.debug(`Unable to check if file exists at ${filePath}: ${error.message}`);
72
+ }
73
+ return false;
74
+ }
75
+ }
76
+ /**
77
+ * Finds the first existing parent directory by walking up the directory tree.
78
+ * @param dirPath Starting directory path
79
+ * @returns The first existing directory path, or null if root is reached without finding one
80
+ */
81
+ static async findFirstExistingDirectory(dirPath) {
82
+ let currentDir = dirPath;
83
+ while (true) {
84
+ try {
85
+ const stats = await fs.stat(currentDir);
86
+ if (stats.isDirectory()) {
87
+ return currentDir;
88
+ }
89
+ // Path component exists but is not a directory (edge case)
90
+ console.debug(`Path component ${currentDir} exists but is not a directory`);
91
+ return null;
92
+ }
93
+ catch (error) {
94
+ if (error.code === 'ENOENT') {
95
+ // Directory doesn't exist, move up one level
96
+ const parentDir = path.dirname(currentDir);
97
+ if (parentDir === currentDir) {
98
+ // Reached filesystem root without finding existing directory
99
+ return null;
100
+ }
101
+ currentDir = parentDir;
102
+ }
103
+ else {
104
+ // Unexpected error (permissions, I/O error, etc.)
105
+ console.debug(`Error checking directory ${currentDir}: ${error.message}`);
106
+ return null;
107
+ }
108
+ }
109
+ }
110
+ }
111
+ static async canWriteFile(filePath) {
112
+ try {
113
+ const stats = await fs.stat(filePath);
114
+ if (!stats.isFile()) {
115
+ return true;
116
+ }
117
+ // On Windows, stats.mode doesn't reliably indicate write permissions.
118
+ // Use fs.access with W_OK to check actual write permissions cross-platform.
119
+ try {
120
+ await fs.access(filePath, fsConstants.W_OK);
121
+ return true;
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
127
+ catch (error) {
128
+ if (error.code === 'ENOENT') {
129
+ // File doesn't exist - find first existing parent directory and check its permissions
130
+ const parentDir = path.dirname(filePath);
131
+ const existingDir = await this.findFirstExistingDirectory(parentDir);
132
+ if (existingDir === null) {
133
+ // No existing parent directory found (edge case)
134
+ return false;
135
+ }
136
+ // Check if the existing parent directory is writable
137
+ try {
138
+ await fs.access(existingDir, fsConstants.W_OK);
139
+ return true;
140
+ }
141
+ catch {
142
+ return false;
143
+ }
144
+ }
145
+ console.debug(`Unable to determine write permissions for ${filePath}: ${error.message}`);
146
+ return false;
147
+ }
148
+ }
149
+ static async directoryExists(dirPath) {
150
+ try {
151
+ const stats = await fs.stat(dirPath);
152
+ return stats.isDirectory();
153
+ }
154
+ catch (error) {
155
+ if (error.code !== 'ENOENT') {
156
+ console.debug(`Unable to check if directory exists at ${dirPath}: ${error.message}`);
157
+ }
158
+ return false;
159
+ }
160
+ }
161
+ static async writeFile(filePath, content) {
162
+ const dir = path.dirname(filePath);
163
+ await this.createDirectory(dir);
164
+ await fs.writeFile(filePath, content, 'utf-8');
165
+ }
166
+ static async readFile(filePath) {
167
+ return await fs.readFile(filePath, 'utf-8');
168
+ }
169
+ static async updateFileWithMarkers(filePath, content, startMarker, endMarker) {
170
+ let existingContent = '';
171
+ if (await this.fileExists(filePath)) {
172
+ existingContent = await this.readFile(filePath);
173
+ const startIndex = findMarkerIndex(existingContent, startMarker);
174
+ const endIndex = startIndex !== -1
175
+ ? findMarkerIndex(existingContent, endMarker, startIndex + startMarker.length)
176
+ : findMarkerIndex(existingContent, endMarker);
177
+ if (startIndex !== -1 && endIndex !== -1) {
178
+ if (endIndex < startIndex) {
179
+ throw new Error(`Invalid marker state in ${filePath}. End marker appears before start marker.`);
180
+ }
181
+ const before = existingContent.substring(0, startIndex);
182
+ const after = existingContent.substring(endIndex + endMarker.length);
183
+ existingContent = before + startMarker + '\n' + content + '\n' + endMarker + after;
184
+ }
185
+ else if (startIndex === -1 && endIndex === -1) {
186
+ existingContent = startMarker + '\n' + content + '\n' + endMarker + '\n\n' + existingContent;
187
+ }
188
+ else {
189
+ throw new Error(`Invalid marker state in ${filePath}. Found start: ${startIndex !== -1}, Found end: ${endIndex !== -1}`);
190
+ }
191
+ }
192
+ else {
193
+ existingContent = startMarker + '\n' + content + '\n' + endMarker;
194
+ }
195
+ await this.writeFile(filePath, existingContent);
196
+ }
197
+ static async ensureWritePermissions(dirPath) {
198
+ try {
199
+ // If directory doesn't exist, check parent directory permissions
200
+ if (!await this.directoryExists(dirPath)) {
201
+ const parentDir = path.dirname(dirPath);
202
+ if (!await this.directoryExists(parentDir)) {
203
+ await this.createDirectory(parentDir);
204
+ }
205
+ return await this.ensureWritePermissions(parentDir);
206
+ }
207
+ const testFile = path.join(dirPath, '.anchorspec-test-' + Date.now() + '-' + Math.random().toString(36).slice(2));
208
+ await fs.writeFile(testFile, '');
209
+ // On Windows, file may be temporarily locked by antivirus or indexing services.
210
+ // Retry unlink with a small delay if it fails.
211
+ const maxRetries = 3;
212
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
213
+ try {
214
+ await fs.unlink(testFile);
215
+ break;
216
+ }
217
+ catch (unlinkError) {
218
+ if (attempt === maxRetries - 1) {
219
+ // Last attempt failed, but we successfully wrote the file, so permissions are OK
220
+ // Just log and continue - the temp file will be cleaned up eventually
221
+ console.debug(`Could not clean up test file ${testFile}: ${unlinkError.message}`);
222
+ }
223
+ else {
224
+ // Wait briefly before retrying (Windows file lock release)
225
+ await new Promise((resolve) => setTimeout(resolve, 50));
226
+ }
227
+ }
228
+ }
229
+ return true;
230
+ }
231
+ catch (error) {
232
+ console.debug(`Insufficient permissions to write to ${dirPath}: ${error.message}`);
233
+ return false;
234
+ }
235
+ }
236
+ }
237
+ /**
238
+ * Removes a marker block from file content.
239
+ * Only removes markers that are on their own lines (ignores inline mentions).
240
+ * Cleans up double blank lines that may result from removal.
241
+ *
242
+ * @param content - File content with markers
243
+ * @param startMarker - The start marker string
244
+ * @param endMarker - The end marker string
245
+ * @returns Content with marker block removed, or original content if markers not found/invalid
246
+ */
247
+ export function removeMarkerBlock(content, startMarker, endMarker) {
248
+ const startIndex = findMarkerIndex(content, startMarker);
249
+ const endIndex = startIndex !== -1
250
+ ? findMarkerIndex(content, endMarker, startIndex + startMarker.length)
251
+ : findMarkerIndex(content, endMarker);
252
+ if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
253
+ return content;
254
+ }
255
+ // Find the start of the line containing the start marker
256
+ let lineStart = startIndex;
257
+ while (lineStart > 0 && content[lineStart - 1] !== '\n') {
258
+ lineStart--;
259
+ }
260
+ // Find the end of the line containing the end marker
261
+ let lineEnd = endIndex + endMarker.length;
262
+ while (lineEnd < content.length && content[lineEnd] !== '\n') {
263
+ lineEnd++;
264
+ }
265
+ // Include the trailing newline if present
266
+ if (lineEnd < content.length && content[lineEnd] === '\n') {
267
+ lineEnd++;
268
+ }
269
+ const before = content.substring(0, lineStart);
270
+ const after = content.substring(lineEnd);
271
+ // Clean up double blank lines (handle both Unix \n and Windows \r\n)
272
+ let result = before + after;
273
+ result = result.replace(/(\r?\n){3,}/g, '\n\n');
274
+ // Trim trailing whitespace but preserve leading whitespace and original newline style
275
+ if (result.trimEnd() === '') {
276
+ return '';
277
+ }
278
+ const newline = content.includes('\r\n') ? '\r\n' : '\n';
279
+ return result.trimEnd() + newline;
280
+ }
281
+ //# sourceMappingURL=file-system.js.map
@@ -0,0 +1,6 @@
1
+ export { validateChangeName, createChange } from './change-utils.js';
2
+ export type { ValidationResult, CreateChangeOptions } from './change-utils.js';
3
+ export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, } from './change-metadata.js';
4
+ export { FileSystemUtils, removeMarkerBlock } from './file-system.js';
5
+ export { transformToHyphenCommands } from './command-references.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ // Shared utilities
2
+ export { validateChangeName, createChange } from './change-utils.js';
3
+ // Change metadata utilities
4
+ export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, } from './change-metadata.js';
5
+ // File system utilities
6
+ export { FileSystemUtils, removeMarkerBlock } from './file-system.js';
7
+ // Command reference utilities
8
+ export { transformToHyphenCommands } from './command-references.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,18 @@
1
+ export type InteractiveOptions = {
2
+ /**
3
+ * Explicit "disable prompts" flag passed by internal callers.
4
+ */
5
+ noInteractive?: boolean;
6
+ /**
7
+ * Commander-style negated option: `--no-interactive` sets this to false.
8
+ */
9
+ interactive?: boolean;
10
+ };
11
+ /**
12
+ * Resolves whether non-interactive mode is requested.
13
+ * Handles both explicit `noInteractive: true` and Commander.js style `interactive: false`.
14
+ * Use this helper instead of manually checking options.noInteractive to avoid bugs.
15
+ */
16
+ export declare function resolveNoInteractive(value?: boolean | InteractiveOptions): boolean;
17
+ export declare function isInteractive(value?: boolean | InteractiveOptions): boolean;
18
+ //# sourceMappingURL=interactive.d.ts.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Resolves whether non-interactive mode is requested.
3
+ * Handles both explicit `noInteractive: true` and Commander.js style `interactive: false`.
4
+ * Use this helper instead of manually checking options.noInteractive to avoid bugs.
5
+ */
6
+ export function resolveNoInteractive(value) {
7
+ if (typeof value === 'boolean')
8
+ return value;
9
+ return value?.noInteractive === true || value?.interactive === false;
10
+ }
11
+ export function isInteractive(value) {
12
+ if (resolveNoInteractive(value))
13
+ return false;
14
+ if (process.env.OPEN_SPEC_INTERACTIVE === '0')
15
+ return false;
16
+ // Respect the standard CI environment variable (set by GitHub Actions, GitLab CI, Travis, etc.)
17
+ if ('CI' in process.env)
18
+ return false;
19
+ return !!process.stdin.isTTY;
20
+ }
21
+ //# sourceMappingURL=interactive.js.map
@@ -0,0 +1,4 @@
1
+ export declare function getActiveChangeIds(root?: string): Promise<string[]>;
2
+ export declare function getSpecIds(root?: string): Promise<string[]>;
3
+ export declare function getArchivedChangeIds(root?: string): Promise<string[]>;
4
+ //# sourceMappingURL=item-discovery.d.ts.map
@@ -0,0 +1,72 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ export async function getActiveChangeIds(root = process.cwd()) {
4
+ const changesPath = path.join(root, 'anchorspec', 'changes');
5
+ try {
6
+ const entries = await fs.readdir(changesPath, { withFileTypes: true });
7
+ const result = [];
8
+ for (const entry of entries) {
9
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'archive')
10
+ continue;
11
+ const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
12
+ try {
13
+ await fs.access(proposalPath);
14
+ result.push(entry.name);
15
+ }
16
+ catch {
17
+ // skip directories without proposal.md
18
+ }
19
+ }
20
+ return result.sort();
21
+ }
22
+ catch {
23
+ return [];
24
+ }
25
+ }
26
+ export async function getSpecIds(root = process.cwd()) {
27
+ const specsPath = path.join(root, 'anchorspec', 'specs');
28
+ const result = [];
29
+ try {
30
+ const entries = await fs.readdir(specsPath, { withFileTypes: true });
31
+ for (const entry of entries) {
32
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
33
+ continue;
34
+ const specFile = path.join(specsPath, entry.name, 'spec.md');
35
+ try {
36
+ await fs.access(specFile);
37
+ result.push(entry.name);
38
+ }
39
+ catch {
40
+ // ignore
41
+ }
42
+ }
43
+ }
44
+ catch {
45
+ // ignore
46
+ }
47
+ return result.sort();
48
+ }
49
+ export async function getArchivedChangeIds(root = process.cwd()) {
50
+ const archivePath = path.join(root, 'anchorspec', 'changes', 'archive');
51
+ try {
52
+ const entries = await fs.readdir(archivePath, { withFileTypes: true });
53
+ const result = [];
54
+ for (const entry of entries) {
55
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
56
+ continue;
57
+ const proposalPath = path.join(archivePath, entry.name, 'proposal.md');
58
+ try {
59
+ await fs.access(proposalPath);
60
+ result.push(entry.name);
61
+ }
62
+ catch {
63
+ // skip directories without proposal.md
64
+ }
65
+ }
66
+ return result.sort();
67
+ }
68
+ catch {
69
+ return [];
70
+ }
71
+ }
72
+ //# sourceMappingURL=item-discovery.js.map