@dedesfr/prompter 0.9.0 → 1.0.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 (216) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +105 -77
  3. package/dist/cli/index.js +25 -1
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +32 -9
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/login.d.ts +4 -0
  9. package/dist/commands/login.d.ts.map +1 -0
  10. package/dist/commands/login.js +56 -0
  11. package/dist/commands/login.js.map +1 -0
  12. package/dist/commands/logout.d.ts +4 -0
  13. package/dist/commands/logout.d.ts.map +1 -0
  14. package/dist/commands/logout.js +14 -0
  15. package/dist/commands/logout.js.map +1 -0
  16. package/dist/commands/update.d.ts.map +1 -1
  17. package/dist/commands/update.js +18 -5
  18. package/dist/commands/update.js.map +1 -1
  19. package/dist/commands/whoami.d.ts +4 -0
  20. package/dist/commands/whoami.d.ts.map +1 -0
  21. package/dist/commands/whoami.js +42 -0
  22. package/dist/commands/whoami.js.map +1 -0
  23. package/dist/core/auth-store.d.ts +10 -0
  24. package/dist/core/auth-store.d.ts.map +1 -0
  25. package/dist/core/auth-store.js +39 -0
  26. package/dist/core/auth-store.js.map +1 -0
  27. package/dist/core/registry.d.ts +18 -0
  28. package/dist/core/registry.d.ts.map +1 -0
  29. package/dist/core/registry.js +94 -0
  30. package/dist/core/registry.js.map +1 -0
  31. package/package.json +7 -1
  32. package/AGENTS.md +0 -123
  33. package/CLAUDE.md +0 -17
  34. package/build.js +0 -20
  35. package/convex-setup.md +0 -403
  36. package/prompt/ai-humanizer.md +0 -45
  37. package/prompt/api-contract-generator.md +0 -234
  38. package/prompt/apply.md +0 -17
  39. package/prompt/archive.md +0 -21
  40. package/prompt/design-system.md +0 -210
  41. package/prompt/document-explainer.md +0 -149
  42. package/prompt/epic-generator.md +0 -198
  43. package/prompt/epic-single.md +0 -47
  44. package/prompt/erd-generator.md +0 -130
  45. package/prompt/fsd-generator.md +0 -157
  46. package/prompt/prd-agent-generator.md +0 -147
  47. package/prompt/prd-generator.md +0 -195
  48. package/prompt/product-brief.md +0 -289
  49. package/prompt/proposal.md +0 -22
  50. package/prompt/qa-test-scenario.md +0 -133
  51. package/prompt/skill-creator.md +0 -350
  52. package/prompt/story-generator.md +0 -278
  53. package/prompt/story-single.md +0 -70
  54. package/prompt/tdd-generator.md +0 -294
  55. package/prompt/tdd-lite-generator.md +0 -224
  56. package/prompt/wireframe-generator.md +0 -219
  57. package/skills/ai-context-generator/SKILL.md +0 -54
  58. package/skills/ai-context-generator/references/AGENTS.template.md +0 -83
  59. package/skills/ai-context-generator/references/CLAUDE.template.md +0 -39
  60. package/skills/ai-context-generator/references/behavioral-guidelines.md +0 -71
  61. package/skills/ai-context-generator/references/discovery-checklist.md +0 -40
  62. package/skills/ai-context-generator/references/examples/AGENTS.good.md +0 -103
  63. package/skills/ai-context-generator/references/extraction-checklist.md +0 -23
  64. package/skills/ai-context-generator/references/overlays/laravel.md +0 -44
  65. package/skills/ai-humanizer/SKILL.md +0 -50
  66. package/skills/api-contract-generator/SKILL.md +0 -243
  67. package/skills/apply/SKILL.md +0 -23
  68. package/skills/archive/SKILL.md +0 -27
  69. package/skills/cerebro/SKILL.md +0 -187
  70. package/skills/cerebro/references/agents.md +0 -213
  71. package/skills/code-review/SKILL.md +0 -373
  72. package/skills/code-review/assets/report-template-agent.md +0 -212
  73. package/skills/code-review/assets/report-template-compact.md +0 -81
  74. package/skills/code-review/assets/report-template-full.md +0 -264
  75. package/skills/code-review/assets/report-template-human.md +0 -168
  76. package/skills/code-review/references/universal-patterns.md +0 -495
  77. package/skills/design-md/README.md +0 -34
  78. package/skills/design-md/SKILL.md +0 -172
  79. package/skills/design-md/examples/DESIGN.md +0 -154
  80. package/skills/design-system/SKILL.md +0 -216
  81. package/skills/design-system-generator/SKILL.md +0 -324
  82. package/skills/design-system-generator/assets/design-system-template.md +0 -348
  83. package/skills/design-system-generator/references/extraction-patterns.md +0 -321
  84. package/skills/doc-builder/SKILL.md +0 -115
  85. package/skills/doc-builder/references/ui-patterns.md +0 -394
  86. package/skills/document-explainer/SKILL.md +0 -155
  87. package/skills/document-translator/SKILL.md +0 -58
  88. package/skills/enhance/SKILL.md +0 -47
  89. package/skills/enhance-prompt/README.md +0 -34
  90. package/skills/enhance-prompt/SKILL.md +0 -204
  91. package/skills/enhance-prompt/references/KEYWORDS.md +0 -114
  92. package/skills/epic-generator/SKILL.md +0 -204
  93. package/skills/epic-single/SKILL.md +0 -63
  94. package/skills/erd-generator/SKILL.md +0 -138
  95. package/skills/feature-planner/SKILL.md +0 -305
  96. package/skills/feature-planner/assets/implementation-plan-template.md +0 -85
  97. package/skills/frontend-design/LICENSE.txt +0 -177
  98. package/skills/frontend-design/SKILL.md +0 -42
  99. package/skills/fsd-generator/SKILL.md +0 -163
  100. package/skills/gamma-builder/SKILL.md +0 -134
  101. package/skills/laravel-code-review/SKILL.md +0 -383
  102. package/skills/laravel-code-review/assets/report-template-agent.md +0 -195
  103. package/skills/laravel-code-review/assets/report-template-compact.md +0 -79
  104. package/skills/laravel-code-review/assets/report-template-full.md +0 -253
  105. package/skills/laravel-code-review/assets/report-template-human.md +0 -159
  106. package/skills/laravel-code-review/references/laravel-patterns.md +0 -571
  107. package/skills/laravel-code-review/references/php84-features.md +0 -442
  108. package/skills/mcp-builder/LICENSE.txt +0 -202
  109. package/skills/mcp-builder/SKILL.md +0 -236
  110. package/skills/mcp-builder/reference/evaluation.md +0 -602
  111. package/skills/mcp-builder/reference/mcp_best_practices.md +0 -249
  112. package/skills/mcp-builder/reference/node_mcp_server.md +0 -970
  113. package/skills/mcp-builder/reference/python_mcp_server.md +0 -719
  114. package/skills/mcp-builder/scripts/connections.py +0 -151
  115. package/skills/mcp-builder/scripts/evaluation.py +0 -373
  116. package/skills/mcp-builder/scripts/example_evaluation.xml +0 -22
  117. package/skills/mcp-builder/scripts/requirements.txt +0 -2
  118. package/skills/meeting-notes/SKILL.md +0 -159
  119. package/skills/meeting-notes/evals/evals.json +0 -23
  120. package/skills/prd-agent-generator/SKILL.md +0 -132
  121. package/skills/prd-generator/SKILL.md +0 -211
  122. package/skills/product-brief/SKILL.md +0 -141
  123. package/skills/project-orchestrator/SKILL.md +0 -487
  124. package/skills/project-orchestrator/assets/caddy-vps-setup.md +0 -180
  125. package/skills/project-orchestrator/assets/plan-summary-template.md +0 -159
  126. package/skills/prompter-specs/SKILL.md +0 -115
  127. package/skills/prompter-workflow/SKILL.md +0 -166
  128. package/skills/prompter-workflow/evals/evals.json +0 -89
  129. package/skills/proposal/SKILL.md +0 -28
  130. package/skills/qa-test-scenario/SKILL.md +0 -149
  131. package/skills/skill-creator/SKILL.md +0 -173
  132. package/skills/sph-generator/SKILL.md +0 -488
  133. package/skills/story-generator/SKILL.md +0 -285
  134. package/skills/story-single/SKILL.md +0 -86
  135. package/skills/tdd-generator/SKILL.md +0 -300
  136. package/skills/tdd-lite-generator/SKILL.md +0 -230
  137. package/skills/ui-ux-pro/SKILL.md +0 -199
  138. package/skills/ui-ux-pro/assets/design-spec-template.md +0 -173
  139. package/skills/ui-ux-pro/references/component-patterns.md +0 -255
  140. package/skills/ui-ux-pro/references/design-principles.md +0 -167
  141. package/skills/wireframe-generator/SKILL.md +0 -227
  142. package/src/cli/index.ts +0 -223
  143. package/src/commands/archive.ts +0 -302
  144. package/src/commands/change.ts +0 -292
  145. package/src/commands/config.ts +0 -233
  146. package/src/commands/guide.ts +0 -50
  147. package/src/commands/init.ts +0 -597
  148. package/src/commands/list.ts +0 -194
  149. package/src/commands/show.ts +0 -138
  150. package/src/commands/spec.ts +0 -251
  151. package/src/commands/update.ts +0 -129
  152. package/src/commands/upgrade.ts +0 -30
  153. package/src/commands/validate.ts +0 -326
  154. package/src/core/artifact-graph/graph.ts +0 -167
  155. package/src/core/artifact-graph/index.ts +0 -44
  156. package/src/core/artifact-graph/instruction-loader.ts +0 -302
  157. package/src/core/artifact-graph/resolver.ts +0 -226
  158. package/src/core/artifact-graph/schema.ts +0 -124
  159. package/src/core/artifact-graph/state.ts +0 -64
  160. package/src/core/artifact-graph/types.ts +0 -65
  161. package/src/core/completions/command-registry.ts +0 -382
  162. package/src/core/completions/completion-provider.ts +0 -128
  163. package/src/core/completions/generators/bash-generator.ts +0 -191
  164. package/src/core/completions/generators/fish-generator.ts +0 -188
  165. package/src/core/completions/generators/powershell-generator.ts +0 -223
  166. package/src/core/completions/generators/zsh-generator.ts +0 -281
  167. package/src/core/completions/templates/bash-templates.ts +0 -24
  168. package/src/core/completions/templates/fish-templates.ts +0 -40
  169. package/src/core/completions/templates/powershell-templates.ts +0 -25
  170. package/src/core/completions/templates/zsh-templates.ts +0 -36
  171. package/src/core/completions/types.ts +0 -90
  172. package/src/core/config-schema.ts +0 -230
  173. package/src/core/config.ts +0 -181
  174. package/src/core/configurators/slash/antigravity.ts +0 -10
  175. package/src/core/configurators/slash/base.ts +0 -109
  176. package/src/core/configurators/slash/claude.ts +0 -10
  177. package/src/core/configurators/slash/codex.ts +0 -10
  178. package/src/core/configurators/slash/droid.ts +0 -10
  179. package/src/core/configurators/slash/forge.ts +0 -10
  180. package/src/core/configurators/slash/github-copilot.ts +0 -10
  181. package/src/core/configurators/slash/index.ts +0 -10
  182. package/src/core/configurators/slash/kilocode.ts +0 -10
  183. package/src/core/configurators/slash/opencode.ts +0 -10
  184. package/src/core/configurators/slash/registry.ts +0 -51
  185. package/src/core/converters/json-converter.ts +0 -62
  186. package/src/core/global-config.ts +0 -136
  187. package/src/core/parsers/change-parser.ts +0 -234
  188. package/src/core/parsers/markdown-parser.ts +0 -237
  189. package/src/core/parsers/requirement-blocks.ts +0 -234
  190. package/src/core/prompt-templates.ts +0 -3504
  191. package/src/core/schemas/base.schema.ts +0 -20
  192. package/src/core/schemas/change.schema.ts +0 -42
  193. package/src/core/schemas/index.ts +0 -20
  194. package/src/core/schemas/spec.schema.ts +0 -17
  195. package/src/core/skill-discovery.ts +0 -68
  196. package/src/core/specs-apply.ts +0 -483
  197. package/src/core/styles/palette.ts +0 -8
  198. package/src/core/templates/agents-template.ts +0 -459
  199. package/src/core/templates/claude-template.ts +0 -2
  200. package/src/core/templates/index.ts +0 -3
  201. package/src/core/templates/project-template.ts +0 -32
  202. package/src/core/validation/constants.ts +0 -48
  203. package/src/core/validation/types.ts +0 -19
  204. package/src/core/validation/validator.ts +0 -449
  205. package/src/core/view.ts +0 -219
  206. package/src/index.ts +0 -1
  207. package/src/utils/change-metadata.ts +0 -171
  208. package/src/utils/change-utils.ts +0 -131
  209. package/src/utils/file-system.ts +0 -252
  210. package/src/utils/index.ts +0 -12
  211. package/src/utils/interactive.ts +0 -29
  212. package/src/utils/item-discovery.ts +0 -66
  213. package/src/utils/match.ts +0 -26
  214. package/src/utils/shell-detection.ts +0 -62
  215. package/src/utils/task-progress.ts +0 -43
  216. package/tsconfig.json +0 -28
@@ -1,171 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
- import * as yaml from 'yaml';
4
- import { ChangeMetadataSchema, type ChangeMetadata } from '../core/artifact-graph/types.js';
5
- import { listSchemas } from '../core/artifact-graph/resolver.js';
6
-
7
- const METADATA_FILENAME = '.prompter.yaml';
8
-
9
- /**
10
- * Error thrown when change metadata validation fails.
11
- */
12
- export class ChangeMetadataError extends Error {
13
- constructor(
14
- message: string,
15
- public readonly metadataPath: string,
16
- public readonly cause?: Error
17
- ) {
18
- super(message);
19
- this.name = 'ChangeMetadataError';
20
- }
21
- }
22
-
23
- /**
24
- * Validates that a schema name is valid (exists in available schemas).
25
- *
26
- * @param schemaName - The schema name to validate
27
- * @returns The validated schema name
28
- * @throws Error if schema is not found
29
- */
30
- export function validateSchemaName(schemaName: string): string {
31
- const availableSchemas = listSchemas();
32
- if (!availableSchemas.includes(schemaName)) {
33
- throw new Error(
34
- `Unknown schema '${schemaName}'. Available: ${availableSchemas.join(', ')}`
35
- );
36
- }
37
- return schemaName;
38
- }
39
-
40
- /**
41
- * Writes change metadata to .prompter.yaml in the change directory.
42
- *
43
- * @param changeDir - The path to the change directory
44
- * @param metadata - The metadata to write
45
- * @throws ChangeMetadataError if validation fails or write fails
46
- */
47
- export function writeChangeMetadata(
48
- changeDir: string,
49
- metadata: ChangeMetadata
50
- ): void {
51
- const metaPath = path.join(changeDir, METADATA_FILENAME);
52
-
53
- // Validate schema exists
54
- validateSchemaName(metadata.schema);
55
-
56
- // Validate with Zod
57
- const parseResult = ChangeMetadataSchema.safeParse(metadata);
58
- if (!parseResult.success) {
59
- throw new ChangeMetadataError(
60
- `Invalid metadata: ${parseResult.error.message}`,
61
- metaPath
62
- );
63
- }
64
-
65
- // Write YAML file
66
- const content = yaml.stringify(parseResult.data);
67
- try {
68
- fs.writeFileSync(metaPath, content, 'utf-8');
69
- } catch (err) {
70
- const ioError = err instanceof Error ? err : new Error(String(err));
71
- throw new ChangeMetadataError(
72
- `Failed to write metadata: ${ioError.message}`,
73
- metaPath,
74
- ioError
75
- );
76
- }
77
- }
78
-
79
- /**
80
- * Reads change metadata from .prompter.yaml in the change directory.
81
- *
82
- * @param changeDir - The path to the change directory
83
- * @returns The validated metadata, or null if no metadata file exists
84
- * @throws ChangeMetadataError if the file exists but is invalid
85
- */
86
- export function readChangeMetadata(changeDir: string): ChangeMetadata | null {
87
- const metaPath = path.join(changeDir, METADATA_FILENAME);
88
-
89
- if (!fs.existsSync(metaPath)) {
90
- return null;
91
- }
92
-
93
- let content: string;
94
- try {
95
- content = fs.readFileSync(metaPath, 'utf-8');
96
- } catch (err) {
97
- const ioError = err instanceof Error ? err : new Error(String(err));
98
- throw new ChangeMetadataError(
99
- `Failed to read metadata: ${ioError.message}`,
100
- metaPath,
101
- ioError
102
- );
103
- }
104
-
105
- let parsed: unknown;
106
- try {
107
- parsed = yaml.parse(content);
108
- } catch (err) {
109
- const parseError = err instanceof Error ? err : new Error(String(err));
110
- throw new ChangeMetadataError(
111
- `Invalid YAML in metadata file: ${parseError.message}`,
112
- metaPath,
113
- parseError
114
- );
115
- }
116
-
117
- // Validate with Zod
118
- const parseResult = ChangeMetadataSchema.safeParse(parsed);
119
- if (!parseResult.success) {
120
- throw new ChangeMetadataError(
121
- `Invalid metadata: ${parseResult.error.message}`,
122
- metaPath
123
- );
124
- }
125
-
126
- // Validate that the schema exists
127
- const availableSchemas = listSchemas();
128
- if (!availableSchemas.includes(parseResult.data.schema)) {
129
- throw new ChangeMetadataError(
130
- `Unknown schema '${parseResult.data.schema}'. Available: ${availableSchemas.join(', ')}`,
131
- metaPath
132
- );
133
- }
134
-
135
- return parseResult.data;
136
- }
137
-
138
- /**
139
- * Resolves the schema for a change, with explicit override taking precedence.
140
- *
141
- * Resolution order:
142
- * 1. Explicit schema (if provided)
143
- * 2. Schema from .prompter.yaml metadata (if exists)
144
- * 3. Default 'spec-driven'
145
- *
146
- * @param changeDir - The path to the change directory
147
- * @param explicitSchema - Optional explicit schema override
148
- * @returns The resolved schema name
149
- */
150
- export function resolveSchemaForChange(
151
- changeDir: string,
152
- explicitSchema?: string
153
- ): string {
154
- // 1. Explicit override wins
155
- if (explicitSchema) {
156
- return explicitSchema;
157
- }
158
-
159
- // 2. Try reading from metadata
160
- try {
161
- const metadata = readChangeMetadata(changeDir);
162
- if (metadata?.schema) {
163
- return metadata.schema;
164
- }
165
- } catch {
166
- // If metadata read fails, fall back to default
167
- }
168
-
169
- // 3. Default
170
- return 'spec-driven';
171
- }
@@ -1,131 +0,0 @@
1
- import path from 'path';
2
- import { FileSystemUtils } from './file-system.js';
3
- import { writeChangeMetadata, validateSchemaName } from './change-metadata.js';
4
-
5
- const DEFAULT_SCHEMA = 'spec-driven';
6
-
7
- /**
8
- * Options for creating a change.
9
- */
10
- export interface CreateChangeOptions {
11
- /** The workflow schema to use (default: 'spec-driven') */
12
- schema?: string;
13
- }
14
-
15
- /**
16
- * Result of validating a change name.
17
- */
18
- export interface ValidationResult {
19
- valid: boolean;
20
- error?: string;
21
- }
22
-
23
- /**
24
- * Validates that a change name follows kebab-case conventions.
25
- *
26
- * Valid names:
27
- * - Start with a lowercase letter
28
- * - Contain only lowercase letters, numbers, and hyphens
29
- * - Do not start or end with a hyphen
30
- * - Do not contain consecutive hyphens
31
- *
32
- * @param name - The change name to validate
33
- * @returns Validation result with `valid: true` or `valid: false` with an error message
34
- *
35
- * @example
36
- * validateChangeName('add-auth') // { valid: true }
37
- * validateChangeName('Add-Auth') // { valid: false, error: '...' }
38
- */
39
- export function validateChangeName(name: string): ValidationResult {
40
- // Pattern: starts with lowercase letter, followed by lowercase letters/numbers,
41
- // optionally followed by hyphen + lowercase letters/numbers (repeatable)
42
- const kebabCasePattern = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
43
-
44
- if (!name) {
45
- return { valid: false, error: 'Change name cannot be empty' };
46
- }
47
-
48
- if (!kebabCasePattern.test(name)) {
49
- // Provide specific error messages for common mistakes
50
- if (/[A-Z]/.test(name)) {
51
- return { valid: false, error: 'Change name must be lowercase (use kebab-case)' };
52
- }
53
- if (/\s/.test(name)) {
54
- return { valid: false, error: 'Change name cannot contain spaces (use hyphens instead)' };
55
- }
56
- if (/_/.test(name)) {
57
- return { valid: false, error: 'Change name cannot contain underscores (use hyphens instead)' };
58
- }
59
- if (name.startsWith('-')) {
60
- return { valid: false, error: 'Change name cannot start with a hyphen' };
61
- }
62
- if (name.endsWith('-')) {
63
- return { valid: false, error: 'Change name cannot end with a hyphen' };
64
- }
65
- if (/--/.test(name)) {
66
- return { valid: false, error: 'Change name cannot contain consecutive hyphens' };
67
- }
68
- if (/[^a-z0-9-]/.test(name)) {
69
- return { valid: false, error: 'Change name can only contain lowercase letters, numbers, and hyphens' };
70
- }
71
- if (/^[0-9]/.test(name)) {
72
- return { valid: false, error: 'Change name must start with a letter' };
73
- }
74
-
75
- return { valid: false, error: 'Change name must follow kebab-case convention (e.g., add-auth, refactor-db)' };
76
- }
77
-
78
- return { valid: true };
79
- }
80
-
81
- /**
82
- * Creates a new change directory with metadata file.
83
- *
84
- * @param projectRoot - The root directory of the project (where `prompter/` lives)
85
- * @param name - The change name (must be valid kebab-case)
86
- * @param options - Optional settings for the change
87
- * @throws Error if the change name is invalid
88
- * @throws Error if the schema name is invalid
89
- * @throws Error if the change directory already exists
90
- *
91
- * @example
92
- * // Creates prompter/changes/add-auth/ with default schema
93
- * await createChange('/path/to/project', 'add-auth')
94
- *
95
- * @example
96
- * // Creates prompter/changes/add-auth/ with TDD schema
97
- * await createChange('/path/to/project', 'add-auth', { schema: 'tdd' })
98
- */
99
- export async function createChange(
100
- projectRoot: string,
101
- name: string,
102
- options: CreateChangeOptions = {}
103
- ): Promise<void> {
104
- // Validate the name first
105
- const validation = validateChangeName(name);
106
- if (!validation.valid) {
107
- throw new Error(validation.error);
108
- }
109
-
110
- // Determine schema (validate if provided)
111
- const schemaName = options.schema ?? DEFAULT_SCHEMA;
112
- validateSchemaName(schemaName);
113
-
114
- // Build the change directory path
115
- const changeDir = path.join(projectRoot, 'prompter', 'changes', name);
116
-
117
- // Check if change already exists
118
- if (await FileSystemUtils.directoryExists(changeDir)) {
119
- throw new Error(`Change '${name}' already exists at ${changeDir}`);
120
- }
121
-
122
- // Create the directory (including parent directories if needed)
123
- await FileSystemUtils.createDirectory(changeDir);
124
-
125
- // Write metadata file with schema and creation date
126
- const today = new Date().toISOString().split('T')[0];
127
- writeChangeMetadata(changeDir, {
128
- schema: schemaName,
129
- created: today,
130
- });
131
- }
@@ -1,252 +0,0 @@
1
- import { promises as fs, constants as fsConstants } from 'fs';
2
- import path from 'path';
3
-
4
- function isMarkerOnOwnLine(content: string, markerIndex: number, markerLength: number): boolean {
5
- let leftIndex = markerIndex - 1;
6
- while (leftIndex >= 0 && content[leftIndex] !== '\n') {
7
- const char = content[leftIndex];
8
- if (char !== ' ' && char !== '\t' && char !== '\r') {
9
- return false;
10
- }
11
- leftIndex--;
12
- }
13
-
14
- let rightIndex = markerIndex + markerLength;
15
- while (rightIndex < content.length && content[rightIndex] !== '\n') {
16
- const char = content[rightIndex];
17
- if (char !== ' ' && char !== '\t' && char !== '\r') {
18
- return false;
19
- }
20
- rightIndex++;
21
- }
22
-
23
- return true;
24
- }
25
-
26
- function findMarkerIndex(
27
- content: string,
28
- marker: string,
29
- fromIndex = 0
30
- ): number {
31
- let currentIndex = content.indexOf(marker, fromIndex);
32
-
33
- while (currentIndex !== -1) {
34
- if (isMarkerOnOwnLine(content, currentIndex, marker.length)) {
35
- return currentIndex;
36
- }
37
-
38
- currentIndex = content.indexOf(marker, currentIndex + marker.length);
39
- }
40
-
41
- return -1;
42
- }
43
-
44
- export class FileSystemUtils {
45
- /**
46
- * Converts a path to use forward slashes (POSIX style).
47
- * Essential for cross-platform compatibility with glob libraries like fast-glob.
48
- */
49
- static toPosixPath(p: string): string {
50
- return p.replace(/\\/g, '/');
51
- }
52
-
53
- private static isWindowsBasePath(basePath: string): boolean {
54
- return /^[A-Za-z]:[\\/]/.test(basePath) || basePath.startsWith('\\');
55
- }
56
-
57
- private static normalizeSegments(segments: string[]): string[] {
58
- return segments
59
- .flatMap((segment) => segment.split(/[\\/]+/u))
60
- .filter((part) => part.length > 0);
61
- }
62
-
63
- static joinPath(basePath: string, ...segments: string[]): string {
64
- const normalizedSegments = this.normalizeSegments(segments);
65
-
66
- if (this.isWindowsBasePath(basePath)) {
67
- const normalizedBasePath = path.win32.normalize(basePath);
68
- return normalizedSegments.length
69
- ? path.win32.join(normalizedBasePath, ...normalizedSegments)
70
- : normalizedBasePath;
71
- }
72
-
73
- const posixBasePath = basePath.replace(/\\/g, '/');
74
-
75
- return normalizedSegments.length
76
- ? path.posix.join(posixBasePath, ...normalizedSegments)
77
- : path.posix.normalize(posixBasePath);
78
- }
79
-
80
- static async createDirectory(dirPath: string): Promise<void> {
81
- await fs.mkdir(dirPath, { recursive: true });
82
- }
83
-
84
- static async fileExists(filePath: string): Promise<boolean> {
85
- try {
86
- await fs.access(filePath);
87
- return true;
88
- } catch (error: any) {
89
- if (error.code !== 'ENOENT') {
90
- console.debug(`Unable to check if file exists at ${filePath}: ${error.message}`);
91
- }
92
- return false;
93
- }
94
- }
95
-
96
- /**
97
- * Finds the first existing parent directory by walking up the directory tree.
98
- * @param dirPath Starting directory path
99
- * @returns The first existing directory path, or null if root is reached without finding one
100
- */
101
- private static async findFirstExistingDirectory(dirPath: string): Promise<string | null> {
102
- let currentDir = dirPath;
103
-
104
- while (true) {
105
- try {
106
- const stats = await fs.stat(currentDir);
107
- if (stats.isDirectory()) {
108
- return currentDir;
109
- }
110
- // Path component exists but is not a directory (edge case)
111
- console.debug(`Path component ${currentDir} exists but is not a directory`);
112
- return null;
113
- } catch (error: any) {
114
- if (error.code === 'ENOENT') {
115
- // Directory doesn't exist, move up one level
116
- const parentDir = path.dirname(currentDir);
117
- if (parentDir === currentDir) {
118
- // Reached filesystem root without finding existing directory
119
- return null;
120
- }
121
- currentDir = parentDir;
122
- } else {
123
- // Unexpected error (permissions, I/O error, etc.)
124
- console.debug(`Error checking directory ${currentDir}: ${error.message}`);
125
- return null;
126
- }
127
- }
128
- }
129
- }
130
-
131
- static async canWriteFile(filePath: string): Promise<boolean> {
132
- try {
133
- const stats = await fs.stat(filePath);
134
-
135
- if (!stats.isFile()) {
136
- return true;
137
- }
138
-
139
- // On Windows, stats.mode doesn't reliably indicate write permissions.
140
- // Use fs.access with W_OK to check actual write permissions cross-platform.
141
- try {
142
- await fs.access(filePath, fsConstants.W_OK);
143
- return true;
144
- } catch {
145
- return false;
146
- }
147
- } catch (error: any) {
148
- if (error.code === 'ENOENT') {
149
- // File doesn't exist - find first existing parent directory and check its permissions
150
- const parentDir = path.dirname(filePath);
151
- const existingDir = await this.findFirstExistingDirectory(parentDir);
152
-
153
- if (existingDir === null) {
154
- // No existing parent directory found (edge case)
155
- return false;
156
- }
157
-
158
- // Check if the existing parent directory is writable
159
- try {
160
- await fs.access(existingDir, fsConstants.W_OK);
161
- return true;
162
- } catch {
163
- return false;
164
- }
165
- }
166
-
167
- console.debug(`Unable to determine write permissions for ${filePath}: ${error.message}`);
168
- return false;
169
- }
170
- }
171
-
172
- static async directoryExists(dirPath: string): Promise<boolean> {
173
- try {
174
- const stats = await fs.stat(dirPath);
175
- return stats.isDirectory();
176
- } catch (error: any) {
177
- if (error.code !== 'ENOENT') {
178
- console.debug(`Unable to check if directory exists at ${dirPath}: ${error.message}`);
179
- }
180
- return false;
181
- }
182
- }
183
-
184
- static async writeFile(filePath: string, content: string): Promise<void> {
185
- const dir = path.dirname(filePath);
186
- await this.createDirectory(dir);
187
- await fs.writeFile(filePath, content, 'utf-8');
188
- }
189
-
190
- static async readFile(filePath: string): Promise<string> {
191
- return await fs.readFile(filePath, 'utf-8');
192
- }
193
-
194
- static async updateFileWithMarkers(
195
- filePath: string,
196
- content: string,
197
- startMarker: string,
198
- endMarker: string
199
- ): Promise<void> {
200
- let existingContent = '';
201
-
202
- if (await this.fileExists(filePath)) {
203
- existingContent = await this.readFile(filePath);
204
-
205
- const startIndex = findMarkerIndex(existingContent, startMarker);
206
- const endIndex = startIndex !== -1
207
- ? findMarkerIndex(existingContent, endMarker, startIndex + startMarker.length)
208
- : findMarkerIndex(existingContent, endMarker);
209
-
210
- if (startIndex !== -1 && endIndex !== -1) {
211
- if (endIndex < startIndex) {
212
- throw new Error(
213
- `Invalid marker state in ${filePath}. End marker appears before start marker.`
214
- );
215
- }
216
-
217
- const before = existingContent.substring(0, startIndex);
218
- const after = existingContent.substring(endIndex + endMarker.length);
219
- existingContent = before + startMarker + '\n' + content + '\n' + endMarker + after;
220
- } else if (startIndex === -1 && endIndex === -1) {
221
- existingContent = startMarker + '\n' + content + '\n' + endMarker + '\n\n' + existingContent;
222
- } else {
223
- throw new Error(`Invalid marker state in ${filePath}. Found start: ${startIndex !== -1}, Found end: ${endIndex !== -1}`);
224
- }
225
- } else {
226
- existingContent = startMarker + '\n' + content + '\n' + endMarker;
227
- }
228
-
229
- await this.writeFile(filePath, existingContent);
230
- }
231
-
232
- static async ensureWritePermissions(dirPath: string): Promise<boolean> {
233
- try {
234
- // If directory doesn't exist, check parent directory permissions
235
- if (!await this.directoryExists(dirPath)) {
236
- const parentDir = path.dirname(dirPath);
237
- if (!await this.directoryExists(parentDir)) {
238
- await this.createDirectory(parentDir);
239
- }
240
- return await this.ensureWritePermissions(parentDir);
241
- }
242
-
243
- const testFile = path.join(dirPath, '.prompter-test-' + Date.now());
244
- await fs.writeFile(testFile, '');
245
- await fs.unlink(testFile);
246
- return true;
247
- } catch (error: any) {
248
- console.debug(`Insufficient permissions to write to ${dirPath}: ${error.message}`);
249
- return false;
250
- }
251
- }
252
- }
@@ -1,12 +0,0 @@
1
- // Shared utilities
2
- export { validateChangeName, createChange } from './change-utils.js';
3
- export type { ValidationResult, CreateChangeOptions } from './change-utils.js';
4
-
5
- // Change metadata utilities
6
- export {
7
- readChangeMetadata,
8
- writeChangeMetadata,
9
- resolveSchemaForChange,
10
- validateSchemaName,
11
- ChangeMetadataError,
12
- } from './change-metadata.js';
@@ -1,29 +0,0 @@
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
- /**
13
- * Resolves whether non-interactive mode is requested.
14
- * Handles both explicit `noInteractive: true` and Commander.js style `interactive: false`.
15
- * Use this helper instead of manually checking options.noInteractive to avoid bugs.
16
- */
17
- export function resolveNoInteractive(value?: boolean | InteractiveOptions): boolean {
18
- if (typeof value === 'boolean') return value;
19
- return value?.noInteractive === true || value?.interactive === false;
20
- }
21
-
22
- export function isInteractive(value?: boolean | InteractiveOptions): boolean {
23
- if (resolveNoInteractive(value)) return false;
24
- if (process.env.OPEN_SPEC_INTERACTIVE === '0') return false;
25
- // Respect the standard CI environment variable (set by GitHub Actions, GitLab CI, Travis, etc.)
26
- if ('CI' in process.env) return false;
27
- return !!process.stdin.isTTY;
28
- }
29
-
@@ -1,66 +0,0 @@
1
- import { promises as fs } from 'fs';
2
- import path from 'path';
3
-
4
- export async function getActiveChangeIds(root: string = process.cwd()): Promise<string[]> {
5
- const changesPath = path.join(root, 'prompter', 'changes');
6
- try {
7
- const entries = await fs.readdir(changesPath, { withFileTypes: true });
8
- const result: string[] = [];
9
- for (const entry of entries) {
10
- if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'archive') continue;
11
- const proposalPath = path.join(changesPath, entry.name, 'proposal.md');
12
- try {
13
- await fs.access(proposalPath);
14
- result.push(entry.name);
15
- } catch {
16
- // skip directories without proposal.md
17
- }
18
- }
19
- return result.sort();
20
- } catch {
21
- return [];
22
- }
23
- }
24
-
25
- export async function getSpecIds(root: string = process.cwd()): Promise<string[]> {
26
- const specsPath = path.join(root, 'prompter', 'specs');
27
- const result: string[] = [];
28
- try {
29
- const entries = await fs.readdir(specsPath, { withFileTypes: true });
30
- for (const entry of entries) {
31
- if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
32
- const specFile = path.join(specsPath, entry.name, 'spec.md');
33
- try {
34
- await fs.access(specFile);
35
- result.push(entry.name);
36
- } catch {
37
- // ignore
38
- }
39
- }
40
- } catch {
41
- // ignore
42
- }
43
- return result.sort();
44
- }
45
-
46
- export async function getArchivedChangeIds(root: string = process.cwd()): Promise<string[]> {
47
- const archivePath = path.join(root, 'prompter', 'changes', 'archive');
48
- try {
49
- const entries = await fs.readdir(archivePath, { withFileTypes: true });
50
- const result: string[] = [];
51
- for (const entry of entries) {
52
- if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
53
- const proposalPath = path.join(archivePath, entry.name, 'proposal.md');
54
- try {
55
- await fs.access(proposalPath);
56
- result.push(entry.name);
57
- } catch {
58
- // skip directories without proposal.md
59
- }
60
- }
61
- return result.sort();
62
- } catch {
63
- return [];
64
- }
65
- }
66
-
@@ -1,26 +0,0 @@
1
- export function nearestMatches(input: string, candidates: string[], max: number = 5): string[] {
2
- const scored = candidates.map(candidate => ({ candidate, distance: levenshtein(input, candidate) }));
3
- scored.sort((a, b) => a.distance - b.distance);
4
- return scored.slice(0, max).map(s => s.candidate);
5
- }
6
-
7
- export function levenshtein(a: string, b: string): number {
8
- const m = a.length;
9
- const n = b.length;
10
- const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
11
- for (let i = 0; i <= m; i++) dp[i][0] = i;
12
- for (let j = 0; j <= n; j++) dp[0][j] = j;
13
- for (let i = 1; i <= m; i++) {
14
- for (let j = 1; j <= n; j++) {
15
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
16
- dp[i][j] = Math.min(
17
- dp[i - 1][j] + 1,
18
- dp[i][j - 1] + 1,
19
- dp[i - 1][j - 1] + cost
20
- );
21
- }
22
- }
23
- return dp[m][n];
24
- }
25
-
26
-