heraspec 0.1.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 (47) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +57 -0
  3. package/bin/heraspec.js +3803 -0
  4. package/bin/heraspec.js.map +7 -0
  5. package/dist/core/templates/skills/CHANGELOG.md +117 -0
  6. package/dist/core/templates/skills/README-template.md +58 -0
  7. package/dist/core/templates/skills/README.md +36 -0
  8. package/dist/core/templates/skills/content-optimization-skill.md +104 -0
  9. package/dist/core/templates/skills/data/charts.csv +26 -0
  10. package/dist/core/templates/skills/data/colors.csv +97 -0
  11. package/dist/core/templates/skills/data/landing.csv +31 -0
  12. package/dist/core/templates/skills/data/pages-proposed.csv +22 -0
  13. package/dist/core/templates/skills/data/pages.csv +10 -0
  14. package/dist/core/templates/skills/data/products.csv +97 -0
  15. package/dist/core/templates/skills/data/prompts.csv +24 -0
  16. package/dist/core/templates/skills/data/stacks/flutter.csv +53 -0
  17. package/dist/core/templates/skills/data/stacks/html-tailwind.csv +56 -0
  18. package/dist/core/templates/skills/data/stacks/nextjs.csv +53 -0
  19. package/dist/core/templates/skills/data/stacks/react-native.csv +52 -0
  20. package/dist/core/templates/skills/data/stacks/react.csv +54 -0
  21. package/dist/core/templates/skills/data/stacks/svelte.csv +54 -0
  22. package/dist/core/templates/skills/data/stacks/swiftui.csv +51 -0
  23. package/dist/core/templates/skills/data/stacks/vue.csv +50 -0
  24. package/dist/core/templates/skills/data/styles.csv +59 -0
  25. package/dist/core/templates/skills/data/typography.csv +58 -0
  26. package/dist/core/templates/skills/data/ux-guidelines.csv +100 -0
  27. package/dist/core/templates/skills/documents-skill.md +114 -0
  28. package/dist/core/templates/skills/e2e-test-skill.md +119 -0
  29. package/dist/core/templates/skills/integration-test-skill.md +118 -0
  30. package/dist/core/templates/skills/module-codebase-skill.md +110 -0
  31. package/dist/core/templates/skills/scripts/CODE_EXPLANATION.md +394 -0
  32. package/dist/core/templates/skills/scripts/SEARCH_ALGORITHMS_COMPARISON.md +421 -0
  33. package/dist/core/templates/skills/scripts/SEARCH_MODES_GUIDE.md +238 -0
  34. package/dist/core/templates/skills/scripts/core.py +385 -0
  35. package/dist/core/templates/skills/scripts/search.py +73 -0
  36. package/dist/core/templates/skills/suggestion-skill.md +118 -0
  37. package/dist/core/templates/skills/templates/accessibility-checklist.md +40 -0
  38. package/dist/core/templates/skills/templates/example-prompt-full-theme.md +333 -0
  39. package/dist/core/templates/skills/templates/page-types-guide.md +338 -0
  40. package/dist/core/templates/skills/templates/pages-proposed-summary.md +273 -0
  41. package/dist/core/templates/skills/templates/pre-delivery-checklist.md +42 -0
  42. package/dist/core/templates/skills/templates/prompt-template-full-theme.md +313 -0
  43. package/dist/core/templates/skills/templates/responsive-design.md +40 -0
  44. package/dist/core/templates/skills/ui-ux-skill.md +584 -0
  45. package/dist/core/templates/skills/unit-test-skill.md +111 -0
  46. package/dist/index.js +1736 -0
  47. package/package.json +71 -0
@@ -0,0 +1,3803 @@
1
+ #!/usr/bin/env node
2
+ // src/cli/index.ts
3
+ import { Command } from "commander";
4
+ import { createRequire as createRequire2 } from "module";
5
+ import { fileURLToPath as fileURLToPath2 } from "url";
6
+ import { dirname as dirname2, join as join2 } from "path";
7
+
8
+ // src/core/init.ts
9
+ import ora from "ora";
10
+ import chalk from "chalk";
11
+ import path2 from "path";
12
+
13
+ // src/utils/file-system.ts
14
+ import { promises as fs } from "fs";
15
+ import path from "path";
16
+ var FileSystemUtils = class {
17
+ static async createDirectory(dirPath) {
18
+ await fs.mkdir(dirPath, { recursive: true });
19
+ }
20
+ static async fileExists(filePath) {
21
+ try {
22
+ await fs.access(filePath);
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+ static async readFile(filePath) {
29
+ return await fs.readFile(filePath, "utf-8");
30
+ }
31
+ static async writeFile(filePath, content) {
32
+ await fs.writeFile(filePath, content, "utf-8");
33
+ }
34
+ static async readDirectory(dirPath) {
35
+ return await fs.readdir(dirPath);
36
+ }
37
+ static async stat(filePath) {
38
+ return await fs.stat(filePath);
39
+ }
40
+ static async copyFile(src, dest) {
41
+ await fs.copyFile(src, dest);
42
+ }
43
+ static async copyDirectory(src, dest) {
44
+ await fs.mkdir(dest, { recursive: true });
45
+ const entries = await fs.readdir(src, { withFileTypes: true });
46
+ for (const entry of entries) {
47
+ const srcPath = path.join(src, entry.name);
48
+ const destPath = path.join(dest, entry.name);
49
+ if (entry.isDirectory()) {
50
+ await this.copyDirectory(srcPath, destPath);
51
+ } else {
52
+ await fs.copyFile(srcPath, destPath);
53
+ }
54
+ }
55
+ }
56
+ static async removeFile(filePath) {
57
+ await fs.unlink(filePath);
58
+ }
59
+ static async removeDirectory(dirPath, recursive = true) {
60
+ if (typeof fs.rm === "function") {
61
+ await fs.rm(dirPath, { recursive, force: true });
62
+ } else {
63
+ await fs.rmdir(dirPath, { recursive });
64
+ }
65
+ }
66
+ static async moveFile(src, dest) {
67
+ await fs.rename(src, dest);
68
+ }
69
+ static joinPath(...segments) {
70
+ return path.join(...segments);
71
+ }
72
+ static resolvePath(...segments) {
73
+ return path.resolve(...segments);
74
+ }
75
+ static getDirectoryName(filePath) {
76
+ return path.dirname(filePath);
77
+ }
78
+ static getBaseName(filePath) {
79
+ return path.basename(filePath);
80
+ }
81
+ };
82
+
83
+ // src/core/templates/index.ts
84
+ var PROJECT_TEMPLATE = `# HeraSpec Project
85
+
86
+ ## Overview
87
+ Describe your project here.
88
+
89
+ ## Project Types
90
+ - wordpress-plugin
91
+ - wordpress-theme
92
+ - perfex-module
93
+ - laravel-package
94
+ - node-service
95
+ - generic-webapp
96
+ - backend-api
97
+ - frontend-app
98
+ - multi-stack
99
+
100
+ ## Tech Stack
101
+ List your technologies here (e.g., PHP 8.1, WordPress 6.0, Laravel 10, etc.)
102
+
103
+ ## Conventions
104
+ Define coding standards, architectural patterns, and conventions to follow.
105
+
106
+ `;
107
+ var AGENTS_TEMPLATE = `# HeraSpec \u2014 AI Agent Instructions
108
+
109
+ This document defines the workflow for AI agents working with HeraSpec.
110
+
111
+ ## Core Workflow
112
+
113
+ ### Step 1 \u2014 Create a Change
114
+
115
+ **When creating changes, ALWAYS read heraspec/project.md first to understand:**
116
+ - Project types being used
117
+ - Tech stack and conventions
118
+ - Existing architecture patterns
119
+ - Coding standards
120
+
121
+ **Then scaffold:**
122
+ - \`heraspec/changes/<slug>/\` - Create proposal.md, tasks.md, design.md (optional)
123
+ - \`heraspec/specs/<slug>/\` - Create delta specs here (NOT inside changes folder)
124
+
125
+ **If user asks to create changes based on project.md:**
126
+ 1. Read \`heraspec/project.md\` thoroughly
127
+ 2. Identify all features/capabilities mentioned
128
+ 3. Create separate changes for each major feature
129
+ 4. Ensure each change follows project.md conventions
130
+ 5. Use correct project types and skills from project.md
131
+
132
+ ### Step 2 \u2014 Refine Specs
133
+ - Update delta specs in \`heraspec/specs/<slug>/\`
134
+ - Never modify source-of-truth specs directly
135
+
136
+ ### Step 3 \u2014 Approval
137
+ - Wait for user: "Specs approved."
138
+
139
+ ### Step 4 \u2014 Implementation
140
+
141
+ **CRITICAL: When implementing tasks, ALWAYS use Skills system:**
142
+
143
+ 1. **Read task line** to identify skill:
144
+ \`\`\`markdown
145
+ ## 1. Perfex module \u2013 Category Management (projectType: perfex-module, skill: module-codebase)
146
+ - [ ] 1.1 Create module structure
147
+ \`\`\`
148
+
149
+ 2. **Find skill folder**:
150
+ - Project-specific: \`heraspec/skills/<project-type>/<skill-name>/\`
151
+ - Cross-cutting: \`heraspec/skills/<skill-name>/\`
152
+
153
+ 3. **Read skill.md**:
154
+ - Understand purpose, steps, inputs, outputs
155
+ - Follow tone, rules, and limitations
156
+ - Check available templates and scripts
157
+
158
+ 4. **Use skill resources**:
159
+ - Run scripts from \`scripts/\` folder if needed
160
+ - Use templates from \`templates/\` folder
161
+ - Reference examples from \`examples/\` folder
162
+
163
+ 5. **Implement following skill.md guidance**:
164
+ - Follow step-by-step process
165
+ - Use correct naming conventions
166
+ - Apply code style rules
167
+ - Respect limitations
168
+
169
+ **Example workflow:**
170
+ - Task: \`(projectType: perfex-module, skill: module-codebase)\`
171
+ - Agent reads: \`heraspec/skills/perfex-module/module-codebase/skill.md\`
172
+ - Agent follows: Steps, uses templates, runs scripts
173
+ - Agent implements: According to skill.md guidelines
174
+
175
+ **Special case - UI/UX skill:**
176
+ - Task: \`(skill: ui-ux)\`
177
+ - Agent reads: \`heraspec/skills/ui-ux/skill.md\`
178
+ - Agent MUST use search scripts before implementing:
179
+ \`\`\`bash
180
+ # Search for design intelligence
181
+ python3 heraspec/skills/ui-ux/scripts/search.py "<keyword>" --domain <domain>
182
+ python3 heraspec/skills/ui-ux/scripts/search.py "<keyword>" --stack <stack>
183
+ \`\`\`
184
+ - Agent synthesizes search results
185
+ - Agent implements with proper colors, fonts, styles from search results
186
+ - Agent verifies with pre-delivery checklist
187
+
188
+ - Follow tasks.md
189
+ - Mark tasks completed: \`- [x]\`
190
+
191
+ ### Step 5 \u2014 Archive
192
+ - Run: \`heraspec archive <slug> --yes\`
193
+ - This merges delta specs into source specs
194
+ - Moves change folder to archives
195
+
196
+ ## Spec Format
197
+
198
+ Specs must include:
199
+ - \`## Meta\` section with project type, domain, stack
200
+ - \`## Purpose\`
201
+ - \`## Requirements\` with scenarios
202
+
203
+ ## Delta Spec Format
204
+
205
+ Delta specs use:
206
+ - \`## ADDED Requirements\`
207
+ - \`## MODIFIED Requirements\`
208
+ - \`## REMOVED Requirements\`
209
+
210
+ ## Tasks Format
211
+
212
+ Tasks grouped by project type and skill:
213
+ \`\`\`
214
+ ## 1. WordPress plugin \u2013 admin settings page (projectType: wordpress-plugin, skill: admin-settings-page)
215
+ - [ ] Task description
216
+ \`\`\`
217
+
218
+ ## Skills System
219
+
220
+ **CRITICAL: Always use Skills when implementing tasks!**
221
+
222
+ ### How Skills Work
223
+
224
+ 1. **Tasks reference skills**: \`(projectType: perfex-module, skill: module-codebase)\`
225
+ 2. **Find skill folder**:
226
+ - Project-specific: \`heraspec/skills/<project-type>/<skill-name>/\`
227
+ - Cross-cutting: \`heraspec/skills/<skill-name>/\`
228
+ 3. **Read skill.md**: Understand purpose, steps, inputs, outputs, rules
229
+ 4. **Use skill resources**: Scripts, templates, examples
230
+ 5. **Implement following skill.md**: Follow step-by-step process
231
+
232
+ ### Skill Discovery
233
+
234
+ - List all skills: Check \`heraspec/skills/\` directory
235
+ - Project-specific skills: \`heraspec/skills/<project-type>/\`
236
+ - Cross-cutting skills: \`heraspec/skills/<skill-name>/\` (root level)
237
+
238
+ ### When Change Has Multiple Skills
239
+
240
+ **Important**: Each task group uses ONE skill. When working on a task group, agent MUST use that skill's skill.md.
241
+
242
+ Example with multiple skills in one change:
243
+ \`\`\`
244
+ ## 1. Perfex module \u2013 Feature (projectType: perfex-module, skill: module-codebase)
245
+ - [ ] Task 1.1 Create module structure
246
+ - [ ] Task 1.2 Configure registration
247
+
248
+ ## 2. UI/UX \u2013 Admin Interface (skill: ui-ux)
249
+ - [ ] Task 2.1 Design color palette
250
+ - [ ] Task 2.2 Create component styles
251
+
252
+ ## 3. Documents \u2013 User Guide (skill: documents)
253
+ - [ ] Task 3.1 Write technical docs
254
+ \`\`\`
255
+
256
+ **Agent workflow:**
257
+ 1. **For task group 1** (module-codebase):
258
+ - Read: \`heraspec/skills/perfex-module/module-codebase/skill.md\`
259
+ - Follow: Module codebase process
260
+ - Use: Module codebase templates/scripts
261
+ - Implement: Tasks 1.1, 1.2
262
+
263
+ 2. **For task group 2** (ui-ux):
264
+ - Read: \`heraspec/skills/ui-ux/skill.md\`
265
+ - Follow: UI/UX process
266
+ - Use: UI/UX templates/scripts
267
+ - Implement: Tasks 2.1, 2.2
268
+
269
+ 3. **For task group 3** (documents):
270
+ - Read: \`heraspec/skills/documents/skill.md\`
271
+ - Follow: Documents process
272
+ - Use: Documents templates/scripts
273
+ - Implement: Task 3.1
274
+
275
+ **Key rule**: Switch skill.md when switching task groups!
276
+
277
+ ## Rules
278
+
279
+ 1. **Specs first, tasks second, implementation last.**
280
+ 2. **Always use Skills**: When task has skill tag, MUST read and follow skill.md
281
+ 3. Never modify source-of-truth specs directly.
282
+ 4. Delta specs go in \`heraspec/specs/<slug>/\` (NOT in changes folder).
283
+ 5. Always wait for approval before implementation.
284
+ 6. **One skill per task group**: Each task group should use one skill consistently.
285
+
286
+ `;
287
+ var SKILLS_SECTION_TEMPLATE = `## Skills System
288
+
289
+ **CRITICAL: When implementing tasks, ALWAYS use Skills system:**
290
+
291
+ 1. **Read task line** to identify skill:
292
+ \`\`\`markdown
293
+ ## 1. Perfex module \u2013 Category Management (projectType: perfex-module, skill: module-codebase)
294
+ - [ ] 1.1 Create module structure
295
+ \`\`\`
296
+
297
+ 2. **Find skill folder**:
298
+ - Project-specific: \`heraspec/skills/<project-type>/<skill-name>/\`
299
+ - Cross-cutting: \`heraspec/skills/<skill-name>/\`
300
+
301
+ 3. **Read skill.md**:
302
+ - Understand purpose, steps, inputs, outputs
303
+ - Follow tone, rules, and limitations
304
+ - Check available templates and scripts
305
+
306
+ 4. **Use skill resources**:
307
+ - Run scripts from \`scripts/\` folder if needed
308
+ - Use templates from \`templates/\` folder
309
+ - Reference examples from \`examples/\` folder
310
+
311
+ 5. **Implement following skill.md guidance**:
312
+ - Follow step-by-step process
313
+ - Use correct naming conventions
314
+ - Apply code style rules
315
+ - Respect limitations
316
+
317
+ **Example workflow:**
318
+ - Task: \`(projectType: perfex-module, skill: module-codebase)\`
319
+ - Agent reads: \`heraspec/skills/perfex-module/module-codebase/skill.md\`
320
+ - Agent follows: Steps, uses templates, runs scripts
321
+ - Agent implements: According to skill.md guidelines
322
+
323
+ **Special case - UI/UX skill:**
324
+ - Task: \`(skill: ui-ux)\`
325
+ - Agent reads: \`heraspec/skills/ui-ux/skill.md\`
326
+ - Agent MUST use search scripts before implementing:
327
+ \`\`\`bash
328
+ # Search for design intelligence (BM25 - default, fast)
329
+ python3 heraspec/skills/ui-ux/scripts/search.py "<keyword>" --domain <domain>
330
+
331
+ # For better semantic results, use Vector or Hybrid mode:
332
+ python3 heraspec/skills/ui-ux/scripts/search.py "<keyword>" --domain <domain> --mode vector
333
+ python3 heraspec/skills/ui-ux/scripts/search.py "<keyword>" --domain <domain> --mode hybrid
334
+
335
+ # Stack-specific search
336
+ python3 heraspec/skills/ui-ux/scripts/search.py "<keyword>" --stack <stack>
337
+ \`\`\`
338
+ - **Search Modes:**
339
+ - **BM25 (default)**: Fast keyword-based search, zero dependencies
340
+ - **Vector**: Semantic search, ~15-20% better results (requires: pip install sentence-transformers scikit-learn)
341
+ - **Hybrid**: Best of both, ~25% better results (requires: pip install sentence-transformers scikit-learn)
342
+ - Agent synthesizes search results
343
+ - Agent implements with proper colors, fonts, styles from search results
344
+ - Agent verifies with pre-delivery checklist
345
+
346
+ ### Skill Discovery
347
+
348
+ - List all skills: Check \`heraspec/skills/\` directory
349
+ - Project-specific skills: \`heraspec/skills/<project-type>/\`
350
+ - Cross-cutting skills: \`heraspec/skills/<skill-name>/\` (root level)
351
+
352
+ ### When Change Has Multiple Skills
353
+
354
+ **Important**: Each task group uses ONE skill. When working on a task group, agent MUST use that skill's skill.md.
355
+
356
+ Example with multiple skills in one change:
357
+ \`\`\`
358
+ ## 1. Perfex module \u2013 Feature (projectType: perfex-module, skill: module-codebase)
359
+ - [ ] Task 1.1 Create module structure
360
+ - [ ] Task 1.2 Configure registration
361
+
362
+ ## 2. UI/UX \u2013 Admin Interface (skill: ui-ux)
363
+ - [ ] Task 2.1 Design color palette
364
+ - [ ] Task 2.2 Create component styles
365
+
366
+ ## 3. Documents \u2013 User Guide (skill: documents)
367
+ - [ ] Task 3.1 Write technical docs
368
+ \`\`\`
369
+
370
+ **Agent workflow:**
371
+ 1. **For task group 1** (module-codebase):
372
+ - Read: \`heraspec/skills/perfex-module/module-codebase/skill.md\`
373
+ - Follow: Module codebase process
374
+ - Use: Module codebase templates/scripts
375
+ - Implement: Tasks 1.1, 1.2
376
+
377
+ 2. **For task group 2** (ui-ux):
378
+ - Read: \`heraspec/skills/ui-ux/skill.md\`
379
+ - Follow: UI/UX process
380
+ - Use: UI/UX templates/scripts
381
+ - Implement: Tasks 2.1, 2.2
382
+
383
+ 3. **For task group 3** (documents):
384
+ - Read: \`heraspec/skills/documents/skill.md\`
385
+ - Follow: Documents process
386
+ - Use: Documents templates/scripts
387
+ - Implement: Task 3.1
388
+
389
+ **Key rule**: Switch skill.md when switching task groups!
390
+ `;
391
+ var CONFIG_TEMPLATE = `# HeraSpec Configuration
392
+
393
+ projectTypes:
394
+ - wordpress-plugin
395
+ # Add other project types as needed
396
+
397
+ defaultDomain: global
398
+
399
+ `;
400
+ var PROPOSAL_TEMPLATE = `# Change Proposal: <slug>
401
+
402
+ ## Purpose
403
+ Describe why this change is needed.
404
+
405
+ ## Scope
406
+ What will be changed?
407
+
408
+ ## Project Types
409
+ - wordpress-plugin
410
+ - perfex-module
411
+
412
+ ## Impact
413
+ What parts of the system will be affected?
414
+
415
+ `;
416
+ var TASKS_TEMPLATE = `# Tasks
417
+
418
+ ## 1. WordPress plugin \u2013 feature name (projectType: wordpress-plugin, skill: admin-settings-page)
419
+ - [ ] Task 1.1
420
+ - [ ] Task 1.2
421
+
422
+ ## 2. Perfex module \u2013 feature name (projectType: perfex-module, skill: module-registration)
423
+ - [ ] Task 2.1
424
+
425
+ `;
426
+ var TemplateManager = class {
427
+ static getProjectTemplate() {
428
+ return PROJECT_TEMPLATE;
429
+ }
430
+ static getAgentsTemplate() {
431
+ return AGENTS_TEMPLATE;
432
+ }
433
+ static getConfigTemplate() {
434
+ return CONFIG_TEMPLATE;
435
+ }
436
+ static getProposalTemplate(slug) {
437
+ return PROPOSAL_TEMPLATE.replace("<slug>", slug);
438
+ }
439
+ static getTasksTemplate() {
440
+ return TASKS_TEMPLATE;
441
+ }
442
+ static getSkillsSection() {
443
+ return SKILLS_SECTION_TEMPLATE;
444
+ }
445
+ };
446
+
447
+ // src/core/config.ts
448
+ var HERASPEC_DIR_NAME = "heraspec";
449
+ var SPECS_DIR_NAME = "specs";
450
+ var CHANGES_DIR_NAME = "changes";
451
+ var ARCHIVES_DIR_NAME = "archives";
452
+ var SKILLS_DIR_NAME = "skills";
453
+ var PROJECT_TYPES = [
454
+ "wordpress-plugin",
455
+ "wordpress-theme",
456
+ "perfex-module",
457
+ "laravel-package",
458
+ "node-service",
459
+ "generic-webapp",
460
+ "backend-api",
461
+ "frontend-app",
462
+ "multi-stack"
463
+ ];
464
+ var HERASPEC_MARKERS = {
465
+ PROJECT_MD: "project.md",
466
+ AGENTS_MD: "AGENTS.heraspec.md",
467
+ CONFIG_YAML: "config.yaml",
468
+ PROPOSAL_MD: "proposal.md",
469
+ TASKS_MD: "tasks.md",
470
+ DESIGN_MD: "design.md"
471
+ };
472
+
473
+ // src/core/init.ts
474
+ var InitCommand = class {
475
+ async execute(targetPath = ".") {
476
+ const resolvedPath = path2.resolve(targetPath);
477
+ const heraspecPath = path2.join(resolvedPath, HERASPEC_DIR_NAME);
478
+ const alreadyInitialized = await FileSystemUtils.fileExists(
479
+ path2.join(heraspecPath, HERASPEC_MARKERS.PROJECT_MD)
480
+ );
481
+ const spinner = ora({
482
+ text: alreadyInitialized ? "Updating HeraSpec..." : "Initializing HeraSpec...",
483
+ color: "blue"
484
+ }).start();
485
+ try {
486
+ await FileSystemUtils.createDirectory(heraspecPath);
487
+ await FileSystemUtils.createDirectory(path2.join(heraspecPath, SPECS_DIR_NAME));
488
+ await FileSystemUtils.createDirectory(path2.join(heraspecPath, CHANGES_DIR_NAME));
489
+ await FileSystemUtils.createDirectory(path2.join(heraspecPath, ARCHIVES_DIR_NAME));
490
+ await FileSystemUtils.createDirectory(path2.join(heraspecPath, SKILLS_DIR_NAME));
491
+ const skillsReadmePath = path2.join(heraspecPath, SKILLS_DIR_NAME, "README.md");
492
+ if (!await FileSystemUtils.fileExists(skillsReadmePath)) {
493
+ const skillsReadme = await this.getSkillsReadmeTemplate();
494
+ await FileSystemUtils.writeFile(skillsReadmePath, skillsReadme);
495
+ }
496
+ const uiuxGuidePath = path2.join(heraspecPath, SKILLS_DIR_NAME, "UI_UX_SKILL_QUICK_REFERENCE.md");
497
+ if (!await FileSystemUtils.fileExists(uiuxGuidePath)) {
498
+ const uiuxGuide = await this.getUIUXQuickReference();
499
+ await FileSystemUtils.writeFile(uiuxGuidePath, uiuxGuide);
500
+ }
501
+ await this.createTemplateFiles(heraspecPath, alreadyInitialized);
502
+ const agentsPath = path2.join(resolvedPath, HERASPEC_MARKERS.AGENTS_MD);
503
+ await this.updateAgentsFile(agentsPath, alreadyInitialized);
504
+ await this.updateRelatedMarkdownFiles(resolvedPath);
505
+ spinner.succeed(
506
+ chalk.green(
507
+ alreadyInitialized ? "HeraSpec updated successfully" : "HeraSpec initialized successfully"
508
+ )
509
+ );
510
+ console.log();
511
+ console.log(chalk.cyan("Next steps:"));
512
+ console.log(
513
+ chalk.gray("1. Review and update heraspec/project.md with your project details")
514
+ );
515
+ console.log(
516
+ chalk.gray('2. Create your first change: "Create a HeraSpec change to..."')
517
+ );
518
+ console.log(
519
+ chalk.gray("3. List changes: heraspec list")
520
+ );
521
+ } catch (error) {
522
+ spinner.fail(chalk.red(`Error: ${error.message}`));
523
+ throw error;
524
+ }
525
+ }
526
+ async createTemplateFiles(heraspecPath, skipExisting) {
527
+ const projectMdPath = path2.join(heraspecPath, HERASPEC_MARKERS.PROJECT_MD);
528
+ const configYamlPath = path2.join(heraspecPath, HERASPEC_MARKERS.CONFIG_YAML);
529
+ if (!await FileSystemUtils.fileExists(projectMdPath) || !skipExisting) {
530
+ await FileSystemUtils.writeFile(
531
+ projectMdPath,
532
+ TemplateManager.getProjectTemplate()
533
+ );
534
+ }
535
+ if (!await FileSystemUtils.fileExists(configYamlPath) || !skipExisting) {
536
+ await FileSystemUtils.writeFile(
537
+ configYamlPath,
538
+ TemplateManager.getConfigTemplate()
539
+ );
540
+ }
541
+ }
542
+ async updateAgentsFile(agentsPath, alreadyInitialized) {
543
+ const skillsSectionMarker = "## Skills System";
544
+ const skillsSectionEndMarker = "**Key rule**: Switch skill.md when switching task groups!";
545
+ if (!alreadyInitialized) {
546
+ await FileSystemUtils.writeFile(
547
+ agentsPath,
548
+ TemplateManager.getAgentsTemplate()
549
+ );
550
+ return;
551
+ }
552
+ let existingContent = "";
553
+ if (await FileSystemUtils.fileExists(agentsPath)) {
554
+ existingContent = await FileSystemUtils.readFile(agentsPath);
555
+ }
556
+ if (existingContent.includes(skillsSectionMarker)) {
557
+ const skillsSection = await this.getSkillsSection();
558
+ const updatedContent = this.replaceSkillsSection(existingContent, skillsSection);
559
+ await FileSystemUtils.writeFile(agentsPath, updatedContent);
560
+ } else {
561
+ const skillsSection = await this.getSkillsSection();
562
+ const updatedContent = this.appendSkillsSection(existingContent, skillsSection);
563
+ await FileSystemUtils.writeFile(agentsPath, updatedContent);
564
+ }
565
+ }
566
+ replaceSkillsSection(existingContent, newSkillsSection) {
567
+ const startMarker = "## Skills System";
568
+ const startIndex = existingContent.indexOf(startMarker);
569
+ if (startIndex === -1) {
570
+ return this.appendSkillsSection(existingContent, newSkillsSection);
571
+ }
572
+ let endIndex = existingContent.indexOf("\n## ", startIndex + startMarker.length);
573
+ if (endIndex === -1) {
574
+ endIndex = existingContent.length;
575
+ }
576
+ const before = existingContent.substring(0, startIndex).trimEnd();
577
+ const after = existingContent.substring(endIndex);
578
+ return before + "\n\n" + newSkillsSection + (after.trimStart().startsWith("\n") ? "" : "\n\n") + after;
579
+ }
580
+ appendSkillsSection(existingContent, skillsSection) {
581
+ const rulesMarker = "\n## Rules\n";
582
+ const rulesIndex = existingContent.indexOf(rulesMarker);
583
+ if (rulesIndex !== -1) {
584
+ const before = existingContent.substring(0, rulesIndex).trimEnd();
585
+ const after = existingContent.substring(rulesIndex);
586
+ return before + "\n\n" + skillsSection + "\n\n" + after;
587
+ }
588
+ const rulesMarker2 = "## Rules";
589
+ const rulesIndex2 = existingContent.indexOf(rulesMarker2);
590
+ if (rulesIndex2 !== -1 && rulesIndex2 > 0) {
591
+ const before = existingContent.substring(0, rulesIndex2).trimEnd();
592
+ const after = existingContent.substring(rulesIndex2);
593
+ return before + "\n\n" + skillsSection + "\n\n" + after;
594
+ }
595
+ return existingContent.trimEnd() + "\n\n" + skillsSection;
596
+ }
597
+ async getSkillsSection() {
598
+ return TemplateManager.getSkillsSection();
599
+ }
600
+ async getSkillsReadmeTemplate() {
601
+ return `# Skills Directory
602
+
603
+ This directory contains reusable skills for HeraSpec projects.
604
+
605
+ ## What Are Skills?
606
+
607
+ Skills are reusable patterns and workflows that help AI agents implement tasks consistently. Each skill contains:
608
+
609
+ - **skill.md**: Complete guide on how to use the skill
610
+ - **templates/**: Reusable templates
611
+ - **scripts/**: Automation scripts
612
+ - **examples/**: Good and bad examples
613
+
614
+ ## How Agents Use Skills
615
+
616
+ When a task has a skill tag:
617
+ \`\`\`markdown
618
+ ## 1. Feature (projectType: perfex-module, skill: module-codebase)
619
+ - [ ] Task 1.1
620
+ \`\`\`
621
+
622
+ The agent will:
623
+ 1. Find skill folder: \`heraspec/skills/perfex-module/module-codebase/\`
624
+ 2. Read \`skill.md\` to understand process
625
+ 3. Use templates and scripts from skill folder
626
+ 4. Follow guidelines in skill.md
627
+
628
+ ## Available Skills
629
+
630
+ Run \`heraspec skill list\` to see all available skills.
631
+
632
+ ## UI/UX Skill - Creating Full Theme Packages
633
+
634
+ The **UI/UX skill** is particularly useful for creating complete website themes with multiple pages.
635
+
636
+ ### Quick Start
637
+
638
+ When you need to create a full website package, use prompts like:
639
+
640
+ \`\`\`
641
+ T\u1EA1o g\xF3i website \u0111\u1EA7y \u0111\u1EE7 cho [PRODUCT_TYPE] v\u1EDBi style [STYLE_KEYWORDS].
642
+ S\u1EED d\u1EE5ng skill ui-ux v\u1EDBi hybrid mode \u0111\u1EC3 search design intelligence.
643
+ T\u1EA1o c\xE1c trang: home, about, [other pages].
644
+ Stack: [html-tailwind/react/nextjs].
645
+ \u0110\u1EA3m b\u1EA3o responsive, accessible, consistent design system.
646
+ \`\`\`
647
+
648
+ ### Prompt Templates
649
+
650
+ For detailed prompt examples and templates, see:
651
+ - **Example Prompts**: \`heraspec/skills/ui-ux/templates/example-prompt-full-theme.md\`
652
+ - **Prompt Templates**: \`heraspec/skills/ui-ux/templates/prompt-template-full-theme.md\`
653
+
654
+ These templates include:
655
+ - Ready-to-use prompts for different website types (E-commerce, SaaS, Service, Blog, Portfolio)
656
+ - Step-by-step instructions
657
+ - Search command examples
658
+ - Best practices
659
+
660
+ ### Search Modes
661
+
662
+ UI/UX skill supports 3 search modes:
663
+ - **BM25 (default)**: Fast keyword-based search, zero dependencies
664
+ - **Vector**: Semantic search, ~15-20% better results (requires: \`pip install sentence-transformers scikit-learn\`)
665
+ - **Hybrid**: Best of both, ~25% better results (requires: \`pip install sentence-transformers scikit-learn\`)
666
+
667
+ **Usage:**
668
+ \`\`\`bash
669
+ # BM25 (default)
670
+ python3 heraspec/skills/ui-ux/scripts/search.py "minimalism" --domain style
671
+
672
+ # Vector (semantic)
673
+ python3 heraspec/skills/ui-ux/scripts/search.py "elegant dark theme" --domain style --mode vector
674
+
675
+ # Hybrid (best)
676
+ python3 heraspec/skills/ui-ux/scripts/search.py "modern minimal design" --domain style --mode hybrid
677
+ \`\`\`
678
+
679
+ ### Multi-Page Support
680
+
681
+ Default page set includes:
682
+ 1. Home
683
+ 2. About
684
+ 3. Post Details
685
+ 4. Category
686
+ 5. Pricing
687
+ 6. FAQ
688
+ 7. Contact
689
+ 8. Product Category (e-commerce)
690
+ 9. Product Details (e-commerce)
691
+
692
+ Search page types:
693
+ \`\`\`bash
694
+ python3 heraspec/skills/ui-ux/scripts/search.py "home homepage" --domain pages
695
+ python3 heraspec/skills/ui-ux/scripts/search.py "pricing plans" --domain pages
696
+ \`\`\`
697
+
698
+ ### Adding UI/UX Skill to Your Project
699
+
700
+ 1. Copy skill from HeraSpec core:
701
+ \`\`\`bash
702
+ # Copy UI/UX skill
703
+ cp -r /path/to/HeraSpec/src/core/templates/skills/ui-ux-skill.md heraspec/skills/ui-ux/
704
+ cp -r /path/to/HeraSpec/src/core/templates/skills/scripts heraspec/skills/ui-ux/
705
+ cp -r /path/to/HeraSpec/src/core/templates/skills/data heraspec/skills/ui-ux/
706
+ cp -r /path/to/HeraSpec/src/core/templates/skills/templates heraspec/skills/ui-ux/
707
+ \`\`\`
708
+
709
+ 2. Or use \`heraspec skill add ui-ux\` (if available)
710
+
711
+ 3. Read \`heraspec/skills/ui-ux/ui-ux-skill.md\` for complete documentation
712
+
713
+ ## Creating New Skills
714
+
715
+ 1. Create skill folder structure
716
+ 2. Write \`skill.md\` following the template
717
+ 3. Add templates, scripts, examples as needed
718
+
719
+ See \`docs/SKILLS_STRUCTURE_PROPOSAL.md\` for detailed structure.
720
+ `;
721
+ }
722
+ async getUIUXQuickReference() {
723
+ return `# UI/UX Skill - Quick Reference Guide
724
+
725
+ Quick guide for creating prompts to build full theme packages with multiple pages using the ui-ux skill.
726
+
727
+ ## \u{1F4CB} Basic Prompt Template
728
+
729
+ \`\`\`
730
+ Create a complete website package for [PRODUCT_TYPE] with the following requirements:
731
+
732
+ **Project Information:**
733
+ - Product type: [SaaS / E-commerce / Service / Portfolio / etc.]
734
+ - Style: [minimal / elegant / modern / bold / etc.]
735
+ - Industry: [Healthcare / Fintech / Beauty / etc.]
736
+ - Stack: [html-tailwind / react / nextjs / etc.]
737
+ - Pages to create: home, about, [add other pages if needed]
738
+
739
+ **Process:**
740
+ 1. Use skill ui-ux to search design intelligence with hybrid mode
741
+ 2. Create shared components first (Header, Footer, Button, Card)
742
+ 3. Implement pages in order
743
+ 4. Ensure consistency in colors, typography, spacing
744
+ 5. Verify with pre-delivery checklist
745
+
746
+ **Quality Requirements:**
747
+ - \u2705 Consistent design system
748
+ - \u2705 Responsive (320px, 768px, 1024px, 1440px)
749
+ - \u2705 Accessible (WCAG AA minimum)
750
+ - \u2705 Performance optimized
751
+ \`\`\`
752
+
753
+ ## \u{1F3AF} Specific Prompt Examples
754
+
755
+ ### E-Commerce
756
+ \`\`\`
757
+ Create a complete website package for an online fashion store.
758
+
759
+ Product type: E-commerce Luxury
760
+ Style: elegant, premium, sophisticated
761
+ Stack: Next.js with Tailwind CSS
762
+ Pages: home, about, product category, product details, cart, checkout, thank you, faq, contact
763
+
764
+ Use skill ui-ux with hybrid mode. Focus on conversion optimization.
765
+ \`\`\`
766
+
767
+ ### SaaS
768
+ \`\`\`
769
+ Create a complete website package for a project management SaaS platform.
770
+
771
+ Product type: SaaS (General)
772
+ Style: modern, clean, professional
773
+ Stack: React with Tailwind CSS
774
+ Pages: home, about, pricing, features, faq, contact, login, register, dashboard
775
+
776
+ Use skill ui-ux with hybrid mode. Ensure professional and trustworthy.
777
+ \`\`\`
778
+
779
+ ### Service Business
780
+ \`\`\`
781
+ Create a complete website package for a healthcare service.
782
+
783
+ Product type: Beauty & Wellness Service
784
+ Style: elegant, minimal, soft, professional
785
+ Stack: html-tailwind
786
+ Pages: home, about, services, blog listing, post details, category, pricing, faq, contact
787
+
788
+ Use skill ui-ux with hybrid mode. Focus on trust and credibility.
789
+ \`\`\`
790
+
791
+ ## \u{1F50D} Search Modes
792
+
793
+ ### BM25 (Default)
794
+ \`\`\`bash
795
+ python3 heraspec/skills/ui-ux/scripts/search.py "minimalism" --domain style
796
+ \`\`\`
797
+ - \u2705 Fast, zero dependencies
798
+ - \u2705 Best for exact keyword matches
799
+
800
+ ### Vector (Semantic)
801
+ \`\`\`bash
802
+ python3 heraspec/skills/ui-ux/scripts/search.py "elegant dark theme" --domain style --mode vector
803
+ \`\`\`
804
+ - \u2705 Understands meaning and synonyms
805
+ - \u2705 ~15-20% better results
806
+ - \u26A0\uFE0F Requires: \`pip install sentence-transformers scikit-learn\`
807
+
808
+ ### Hybrid (Best)
809
+ \`\`\`bash
810
+ python3 heraspec/skills/ui-ux/scripts/search.py "modern minimal design" --domain style --mode hybrid
811
+ \`\`\`
812
+ - \u2705 Combines BM25 + Vector
813
+ - \u2705 ~25% better results
814
+ - \u26A0\uFE0F Requires: \`pip install sentence-transformers scikit-learn\`
815
+
816
+ ## \u{1F4C4} Default Page Set
817
+
818
+ When creating a "complete website package", the default set includes 9 pages:
819
+
820
+ 1. **Home** - Main homepage
821
+ 2. **About** - Company/story page
822
+ 3. **Post Details** - Blog/article detail
823
+ 4. **Category** - Blog/category listing
824
+ 5. **Pricing** - Pricing plans
825
+ 6. **FAQ** - Frequently asked questions
826
+ 7. **Contact** - Contact form
827
+ 8. **Product Category** - E-commerce category (if applicable)
828
+ 9. **Product Details** - E-commerce product detail (if applicable)
829
+
830
+ ## \u{1F527} Search Page Types
831
+
832
+ \`\`\`bash
833
+ # Home page
834
+ python3 heraspec/skills/ui-ux/scripts/search.py "home homepage" --domain pages
835
+
836
+ # About page
837
+ python3 heraspec/skills/ui-ux/scripts/search.py "about company story" --domain pages
838
+
839
+ # Pricing page
840
+ python3 heraspec/skills/ui-ux/scripts/search.py "pricing plans tiers" --domain pages
841
+
842
+ # E-commerce pages
843
+ python3 heraspec/skills/ui-ux/scripts/search.py "product-category shop catalog" --domain pages
844
+ python3 heraspec/skills/ui-ux/scripts/search.py "product-detail single-product" --domain pages
845
+ \`\`\`
846
+
847
+ ## \u{1F4DA} Detailed Documentation
848
+
849
+ After copying UI/UX skill to your project, see:
850
+ - \`heraspec/skills/ui-ux/ui-ux-skill.md\` - Complete skill documentation
851
+ - \`heraspec/skills/ui-ux/templates/example-prompt-full-theme.md\` - Detailed prompt examples
852
+ - \`heraspec/skills/ui-ux/templates/prompt-template-full-theme.md\` - Copy-paste templates
853
+
854
+ ## \u{1F4A1} Tips
855
+
856
+ 1. **Always mention "skill ui-ux"** - Agent will know to use this skill
857
+ 2. **Encourage using hybrid mode** - Best results
858
+ 3. **List all pages clearly** - Agent knows exact scope
859
+ 4. **Require consistency** - Ensures unified design system
860
+ 5. **Mention pre-delivery checklist** - Agent will verify before delivering
861
+
862
+ ## \u{1F680} Quick Start
863
+
864
+ 1. Copy UI/UX skill to project:
865
+ \`\`\`bash
866
+ cp -r /path/to/HeraSpec/src/core/templates/skills/ui-ux-skill.md heraspec/skills/ui-ux/
867
+ cp -r /path/to/HeraSpec/src/core/templates/skills/scripts heraspec/skills/ui-ux/
868
+ cp -r /path/to/HeraSpec/src/core/templates/skills/data heraspec/skills/ui-ux/
869
+ cp -r /path/to/HeraSpec/src/core/templates/skills/templates heraspec/skills/ui-ux/
870
+ \`\`\`
871
+
872
+ 2. Use prompt template from above
873
+
874
+ 3. Agent will automatically:
875
+ - Search design intelligence with skill ui-ux
876
+ - Create shared components
877
+ - Implement each page
878
+ - Verify with checklist
879
+ `;
880
+ }
881
+ /**
882
+ * Update related markdown files in the project (README.md, etc.)
883
+ */
884
+ async updateRelatedMarkdownFiles(projectPath) {
885
+ const readmePath = path2.join(projectPath, "README.md");
886
+ if (await FileSystemUtils.fileExists(readmePath)) {
887
+ await this.updateReadmeFile(readmePath);
888
+ }
889
+ }
890
+ /**
891
+ * Update README.md with HeraSpec information
892
+ */
893
+ async updateReadmeFile(readmePath) {
894
+ const existingContent = await FileSystemUtils.readFile(readmePath);
895
+ const heraspecSection = this.getHeraSpecReadmeSection();
896
+ const sectionMarkers = [
897
+ "## HeraSpec",
898
+ "## HeraSpec Development",
899
+ "### HeraSpec",
900
+ "### HeraSpec Development",
901
+ "<!-- HeraSpec Section -->"
902
+ ];
903
+ let hasHeraSpecSection = false;
904
+ let sectionStartIndex = -1;
905
+ let sectionEndIndex = -1;
906
+ for (const marker of sectionMarkers) {
907
+ const index = existingContent.indexOf(marker);
908
+ if (index !== -1) {
909
+ hasHeraSpecSection = true;
910
+ sectionStartIndex = index;
911
+ sectionEndIndex = existingContent.indexOf("\n## ", index + marker.length);
912
+ if (sectionEndIndex === -1) {
913
+ sectionEndIndex = existingContent.indexOf("\n### ", index + marker.length);
914
+ }
915
+ if (sectionEndIndex === -1) {
916
+ sectionEndIndex = existingContent.length;
917
+ }
918
+ break;
919
+ }
920
+ }
921
+ if (hasHeraSpecSection && sectionStartIndex !== -1) {
922
+ const before = existingContent.substring(0, sectionStartIndex).trimEnd();
923
+ const after = existingContent.substring(sectionEndIndex);
924
+ const updatedContent = before + "\n\n" + heraspecSection + (after.trimStart().startsWith("\n") ? "" : "\n\n") + after;
925
+ await FileSystemUtils.writeFile(readmePath, updatedContent);
926
+ } else {
927
+ const insertBeforeMarkers = [
928
+ "\n## Development",
929
+ "\n## Setup",
930
+ "\n## Contributing",
931
+ "\n## Installation",
932
+ "\n## Getting Started"
933
+ ];
934
+ let inserted = false;
935
+ for (const marker of insertBeforeMarkers) {
936
+ const index = existingContent.indexOf(marker);
937
+ if (index !== -1) {
938
+ const before = existingContent.substring(0, index).trimEnd();
939
+ const after = existingContent.substring(index);
940
+ const updatedContent = before + "\n\n" + heraspecSection + "\n\n" + after;
941
+ await FileSystemUtils.writeFile(readmePath, updatedContent);
942
+ inserted = true;
943
+ break;
944
+ }
945
+ }
946
+ if (!inserted) {
947
+ const updatedContent = existingContent.trimEnd() + "\n\n" + heraspecSection;
948
+ await FileSystemUtils.writeFile(readmePath, updatedContent);
949
+ }
950
+ }
951
+ }
952
+ /**
953
+ * Get HeraSpec section content for README.md
954
+ */
955
+ getHeraSpecReadmeSection() {
956
+ return `<!-- HeraSpec Section -->
957
+ ## HeraSpec Development
958
+
959
+ This project uses [HeraSpec](https://github.com/your-org/heraspec) for spec-driven development.
960
+
961
+ ### Quick Start
962
+
963
+ \`\`\`bash
964
+ # Initialize HeraSpec (if not already done)
965
+ heraspec init
966
+
967
+ # List active changes
968
+ heraspec list
969
+
970
+ # View a change
971
+ heraspec show <change-name>
972
+
973
+ # Validate changes
974
+ heraspec validate <change-name>
975
+ \`\`\`
976
+
977
+ ### Project Structure
978
+
979
+ - \`heraspec/project.md\` - Project overview and configuration
980
+ - \`heraspec/specs/\` - Source of truth specifications
981
+ - \`heraspec/changes/\` - Active changes in progress
982
+ - \`heraspec/skills/\` - Reusable skills for AI agents
983
+ - \`AGENTS.heraspec.md\` - AI agent instructions
984
+
985
+ ### Working with Changes
986
+
987
+ 1. **Create a change**: Ask AI to create a HeraSpec change, or create manually
988
+ 2. **Refine specs**: Review and update delta specs in \`heraspec/specs/<change-name>/\`
989
+ 3. **Implement**: Follow tasks in \`heraspec/changes/<change-name>/tasks.md\`
990
+ 4. **Archive**: Run \`heraspec archive <change-name> --yes\` when complete
991
+
992
+ ### Skills
993
+
994
+ Add skills to your project:
995
+
996
+ \`\`\`bash
997
+ # List available skills
998
+ heraspec skill list
999
+
1000
+ # Add a skill
1001
+ heraspec skill add ui-ux
1002
+ heraspec skill add unit-test
1003
+
1004
+ # View skill details
1005
+ heraspec skill show ui-ux
1006
+ \`\`\`
1007
+
1008
+ For more information, see the [HeraSpec documentation](https://github.com/your-org/heraspec/docs).
1009
+
1010
+ ---
1011
+
1012
+ *This section is automatically updated by \`heraspec init\`. Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}*`;
1013
+ }
1014
+ };
1015
+
1016
+ // src/core/list.ts
1017
+ import path3 from "path";
1018
+ var ListCommand = class {
1019
+ async execute(targetPath = ".", mode = "changes") {
1020
+ const heraspecPath = path3.join(targetPath, HERASPEC_DIR_NAME);
1021
+ if (mode === "changes") {
1022
+ await this.listChanges(heraspecPath);
1023
+ } else {
1024
+ await this.listSpecs(heraspecPath);
1025
+ }
1026
+ }
1027
+ async listChanges(heraspecPath) {
1028
+ const changesDir = path3.join(heraspecPath, CHANGES_DIR_NAME);
1029
+ try {
1030
+ await FileSystemUtils.stat(changesDir);
1031
+ } catch {
1032
+ console.log('No HeraSpec changes directory found. Run "heraspec init" first.');
1033
+ return;
1034
+ }
1035
+ const entries = await FileSystemUtils.readDirectory(changesDir);
1036
+ const changeDirs = [];
1037
+ for (const entry of entries) {
1038
+ const entryPath = path3.join(changesDir, entry);
1039
+ const stats = await FileSystemUtils.stat(entryPath);
1040
+ if (stats.isDirectory() && entry !== ARCHIVES_DIR_NAME) {
1041
+ changeDirs.push(entry);
1042
+ }
1043
+ }
1044
+ if (changeDirs.length === 0) {
1045
+ console.log("No active changes found.");
1046
+ return;
1047
+ }
1048
+ changeDirs.sort();
1049
+ console.log("\nActive changes:");
1050
+ console.log("\u2500".repeat(50));
1051
+ for (const change of changeDirs) {
1052
+ console.log(` \u2022 ${change}`);
1053
+ }
1054
+ console.log();
1055
+ }
1056
+ async listSpecs(heraspecPath) {
1057
+ const specsDir = path3.join(heraspecPath, SPECS_DIR_NAME);
1058
+ try {
1059
+ await FileSystemUtils.stat(specsDir);
1060
+ } catch {
1061
+ console.log('No HeraSpec specs directory found. Run "heraspec init" first.');
1062
+ return;
1063
+ }
1064
+ const specs = await this.findSpecFiles(specsDir, "");
1065
+ if (specs.length === 0) {
1066
+ console.log("No specs found.");
1067
+ return;
1068
+ }
1069
+ specs.sort();
1070
+ console.log("\nSpecs:");
1071
+ console.log("\u2500".repeat(50));
1072
+ for (const spec of specs) {
1073
+ console.log(` \u2022 ${spec}`);
1074
+ }
1075
+ console.log();
1076
+ }
1077
+ async findSpecFiles(dir, prefix) {
1078
+ const specs = [];
1079
+ const entries = await FileSystemUtils.readDirectory(dir);
1080
+ for (const entry of entries) {
1081
+ const entryPath = path3.join(dir, entry);
1082
+ const stats = await FileSystemUtils.stat(entryPath);
1083
+ if (stats.isDirectory()) {
1084
+ const subSpecs = await this.findSpecFiles(
1085
+ entryPath,
1086
+ prefix ? `${prefix}/${entry}` : entry
1087
+ );
1088
+ specs.push(...subSpecs);
1089
+ } else if (entry === "spec.md") {
1090
+ specs.push(prefix || "global");
1091
+ }
1092
+ }
1093
+ return specs;
1094
+ }
1095
+ };
1096
+
1097
+ // src/core/archive.ts
1098
+ import path4 from "path";
1099
+ import ora2 from "ora";
1100
+ import chalk2 from "chalk";
1101
+
1102
+ // src/core/parsers/markdown-parser.ts
1103
+ var MarkdownParser = class _MarkdownParser {
1104
+ lines;
1105
+ constructor(content) {
1106
+ this.lines = _MarkdownParser.normalizeContent(content).split("\n");
1107
+ }
1108
+ static normalizeContent(content) {
1109
+ return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1110
+ }
1111
+ parseSpec(name) {
1112
+ const sections = this.parseSections();
1113
+ const meta = this.parseMeta(sections);
1114
+ const purpose = this.findSection(sections, "Purpose")?.content.join("\n").trim() || "";
1115
+ const requirementsSection = this.findSection(sections, "Requirements");
1116
+ if (!purpose) {
1117
+ throw new Error("Spec must have a Purpose section");
1118
+ }
1119
+ if (!requirementsSection) {
1120
+ throw new Error("Spec must have a Requirements section");
1121
+ }
1122
+ const requirements = this.parseRequirements(requirementsSection);
1123
+ return {
1124
+ name,
1125
+ meta,
1126
+ overview: purpose.trim(),
1127
+ requirements,
1128
+ metadata: {
1129
+ version: "1.0.0",
1130
+ format: "heraspec"
1131
+ }
1132
+ };
1133
+ }
1134
+ parseDeltaSpec(content) {
1135
+ const sections = this.parseSections();
1136
+ const added = this.findSection(sections, "ADDED Requirements");
1137
+ const modified = this.findSection(sections, "MODIFIED Requirements");
1138
+ const removed = this.findSection(sections, "REMOVED Requirements");
1139
+ return {
1140
+ added: added ? this.parseDeltaRequirements(added) : [],
1141
+ modified: modified ? this.parseDeltaRequirements(modified) : [],
1142
+ removed: removed ? this.parseDeltaRequirements(removed) : []
1143
+ };
1144
+ }
1145
+ parseMeta(sections) {
1146
+ const metaSection = this.findSection(sections, "Meta");
1147
+ if (!metaSection) {
1148
+ return void 0;
1149
+ }
1150
+ const meta = {};
1151
+ const content = metaSection.content.join("\n");
1152
+ const projectTypeMatch = content.match(/project type:\s*(.+)/i);
1153
+ if (projectTypeMatch) {
1154
+ const types = projectTypeMatch[1].split("|").map((t) => t.trim());
1155
+ meta.projectType = types.length === 1 ? types[0] : types;
1156
+ }
1157
+ const domainMatch = content.match(/domain:\s*(.+)/i);
1158
+ if (domainMatch) {
1159
+ meta.domain = domainMatch[1].trim();
1160
+ }
1161
+ const stackMatch = content.match(/stack:\s*(.+)/i);
1162
+ if (stackMatch) {
1163
+ const stacks = stackMatch[1].split("|").map((s) => s.trim());
1164
+ meta.stack = stacks.length === 1 ? stacks[0] : stacks;
1165
+ }
1166
+ return Object.keys(meta).length > 0 ? meta : void 0;
1167
+ }
1168
+ parseSections() {
1169
+ const sections = [];
1170
+ let currentSection = null;
1171
+ for (const line of this.lines) {
1172
+ const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
1173
+ if (headerMatch) {
1174
+ if (currentSection) {
1175
+ sections.push(currentSection);
1176
+ }
1177
+ currentSection = {
1178
+ level: headerMatch[1].length,
1179
+ title: headerMatch[2].trim(),
1180
+ content: []
1181
+ };
1182
+ } else if (currentSection) {
1183
+ currentSection.content.push(line);
1184
+ }
1185
+ }
1186
+ if (currentSection) {
1187
+ sections.push(currentSection);
1188
+ }
1189
+ return sections;
1190
+ }
1191
+ findSection(sections, title) {
1192
+ return sections.find(
1193
+ (s) => s.title.toLowerCase() === title.toLowerCase() || s.title.toLowerCase().includes(title.toLowerCase())
1194
+ );
1195
+ }
1196
+ parseRequirements(section) {
1197
+ const requirements = [];
1198
+ const lines = section.content;
1199
+ let currentRequirement = null;
1200
+ let inRequirement = false;
1201
+ let inScenario = false;
1202
+ let currentScenario = null;
1203
+ for (let i = 0; i < lines.length; i++) {
1204
+ const line = lines[i];
1205
+ const reqMatch = line.match(/^###\s+Requirement:\s*(.+)$/i);
1206
+ if (reqMatch) {
1207
+ if (currentRequirement) {
1208
+ requirements.push(currentRequirement);
1209
+ }
1210
+ currentRequirement = {
1211
+ name: reqMatch[1].trim(),
1212
+ description: "",
1213
+ scenarios: []
1214
+ };
1215
+ inRequirement = true;
1216
+ continue;
1217
+ }
1218
+ const scenarioMatch = line.match(/^####\s+Scenario:\s*(.+)$/i);
1219
+ if (scenarioMatch && currentRequirement) {
1220
+ if (currentScenario) {
1221
+ currentRequirement.scenarios.push(currentScenario);
1222
+ }
1223
+ currentScenario = {
1224
+ name: scenarioMatch[1].trim(),
1225
+ steps: []
1226
+ };
1227
+ inScenario = true;
1228
+ continue;
1229
+ }
1230
+ if (currentScenario && line.match(/^-\s*(GIVEN|WHEN|THEN|AND|BUT)\s+/i)) {
1231
+ const step = line.replace(/^-\s*/, "").trim();
1232
+ currentScenario.steps.push(step);
1233
+ continue;
1234
+ }
1235
+ if (currentRequirement && !inScenario) {
1236
+ const trimmed = line.trim();
1237
+ if (trimmed && !trimmed.startsWith("#")) {
1238
+ currentRequirement.description += (currentRequirement.description ? "\n" : "") + trimmed;
1239
+ }
1240
+ }
1241
+ }
1242
+ if (currentScenario && currentRequirement) {
1243
+ currentRequirement.scenarios.push(currentScenario);
1244
+ }
1245
+ if (currentRequirement) {
1246
+ requirements.push(currentRequirement);
1247
+ }
1248
+ return requirements;
1249
+ }
1250
+ parseDeltaRequirements(section) {
1251
+ return this.parseRequirements(section);
1252
+ }
1253
+ };
1254
+
1255
+ // src/core/archive.ts
1256
+ var ArchiveCommand = class {
1257
+ async execute(changeName, options) {
1258
+ if (!changeName) {
1259
+ console.error("Error: Please specify a change name");
1260
+ console.log("Usage: heraspec archive <change-name> [--yes]");
1261
+ process.exitCode = 1;
1262
+ return;
1263
+ }
1264
+ const changePath = path4.join(".", HERASPEC_DIR_NAME, CHANGES_DIR_NAME, changeName);
1265
+ if (!await FileSystemUtils.fileExists(changePath)) {
1266
+ console.error(`Error: Change "${changeName}" not found`);
1267
+ process.exitCode = 1;
1268
+ return;
1269
+ }
1270
+ if (!options?.yes) {
1271
+ console.log(`
1272
+ This will archive "${changeName}" and merge delta specs into source specs.`);
1273
+ console.log("This action cannot be undone.\n");
1274
+ console.error("Error: Please use --yes flag to confirm");
1275
+ process.exitCode = 1;
1276
+ return;
1277
+ }
1278
+ const spinner = ora2({
1279
+ text: `Archiving change "${changeName}"...`,
1280
+ color: "blue"
1281
+ }).start();
1282
+ try {
1283
+ await this.mergeDeltaSpecs(changePath, changeName);
1284
+ const specsDir = path4.join(
1285
+ ".",
1286
+ HERASPEC_DIR_NAME,
1287
+ SPECS_DIR_NAME,
1288
+ changeName
1289
+ );
1290
+ if (await FileSystemUtils.fileExists(specsDir)) {
1291
+ await FileSystemUtils.removeDirectory(specsDir, true);
1292
+ }
1293
+ const archiveDir = path4.join(
1294
+ ".",
1295
+ HERASPEC_DIR_NAME,
1296
+ CHANGES_DIR_NAME,
1297
+ ARCHIVES_DIR_NAME
1298
+ );
1299
+ await FileSystemUtils.createDirectory(archiveDir);
1300
+ const datePrefix = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1301
+ const archivePath = path4.join(archiveDir, `${datePrefix}-${changeName}`);
1302
+ await FileSystemUtils.createDirectory(archivePath);
1303
+ await this.moveChangeToArchive(changePath, archivePath);
1304
+ await FileSystemUtils.removeDirectory(changePath, true);
1305
+ spinner.succeed(chalk2.green(`Change "${changeName}" archived successfully`));
1306
+ } catch (error) {
1307
+ spinner.fail(chalk2.red(`Error: ${error.message}`));
1308
+ throw error;
1309
+ }
1310
+ }
1311
+ async mergeDeltaSpecs(changePath, changeName) {
1312
+ const deltaSpecsDir = path4.join(
1313
+ ".",
1314
+ HERASPEC_DIR_NAME,
1315
+ SPECS_DIR_NAME,
1316
+ changeName
1317
+ );
1318
+ if (!await FileSystemUtils.fileExists(deltaSpecsDir)) {
1319
+ return;
1320
+ }
1321
+ const deltaSpecs = await this.findDeltaSpecFiles(deltaSpecsDir);
1322
+ for (const deltaSpec of deltaSpecs) {
1323
+ const relativePath = path4.relative(deltaSpecsDir, deltaSpec.path);
1324
+ const targetSpecPath = path4.join(
1325
+ ".",
1326
+ HERASPEC_DIR_NAME,
1327
+ SPECS_DIR_NAME,
1328
+ relativePath
1329
+ );
1330
+ const deltaContent = await FileSystemUtils.readFile(deltaSpec.path);
1331
+ const parser = new MarkdownParser(deltaContent);
1332
+ const delta = parser.parseDeltaSpec(deltaContent);
1333
+ let targetContent = "";
1334
+ if (await FileSystemUtils.fileExists(targetSpecPath)) {
1335
+ targetContent = await FileSystemUtils.readFile(targetSpecPath);
1336
+ }
1337
+ const mergedContent = this.mergeDeltaIntoSpec(targetContent, delta, deltaSpec.name);
1338
+ await FileSystemUtils.createDirectory(path4.dirname(targetSpecPath));
1339
+ await FileSystemUtils.writeFile(targetSpecPath, mergedContent);
1340
+ }
1341
+ }
1342
+ mergeDeltaIntoSpec(existingContent, delta, specName) {
1343
+ let merged = existingContent || `# Spec: ${specName}
1344
+
1345
+ ## Purpose
1346
+
1347
+ ## Requirements
1348
+
1349
+ `;
1350
+ if (delta.added.length > 0) {
1351
+ merged += "\n## ADDED Requirements\n\n";
1352
+ for (const req of delta.added) {
1353
+ merged += `### Requirement: ${req.name}
1354
+ ${req.description}
1355
+
1356
+ `;
1357
+ }
1358
+ }
1359
+ return merged;
1360
+ }
1361
+ async moveChangeToArchive(sourcePath, archivePath) {
1362
+ const entries = await FileSystemUtils.readDirectory(sourcePath);
1363
+ for (const entry of entries) {
1364
+ const sourceEntry = path4.join(sourcePath, entry);
1365
+ const archiveEntry = path4.join(archivePath, entry);
1366
+ const stats = await FileSystemUtils.stat(sourceEntry);
1367
+ if (stats.isDirectory()) {
1368
+ await FileSystemUtils.createDirectory(archiveEntry);
1369
+ await this.moveChangeToArchive(sourceEntry, archiveEntry);
1370
+ } else {
1371
+ await FileSystemUtils.moveFile(sourceEntry, archiveEntry);
1372
+ }
1373
+ }
1374
+ }
1375
+ async findDeltaSpecFiles(dir, prefix = "") {
1376
+ const specs = [];
1377
+ const entries = await FileSystemUtils.readDirectory(dir);
1378
+ for (const entry of entries) {
1379
+ const entryPath = path4.join(dir, entry);
1380
+ const stats = await FileSystemUtils.stat(entryPath);
1381
+ if (stats.isDirectory()) {
1382
+ const subSpecs = await this.findDeltaSpecFiles(
1383
+ entryPath,
1384
+ prefix ? `${prefix}/${entry}` : entry
1385
+ );
1386
+ specs.push(...subSpecs);
1387
+ } else if (entry.endsWith(".md")) {
1388
+ specs.push({
1389
+ name: prefix || path4.basename(entry, ".md"),
1390
+ path: entryPath
1391
+ });
1392
+ }
1393
+ }
1394
+ return specs;
1395
+ }
1396
+ };
1397
+
1398
+ // src/core/restore.ts
1399
+ import path5 from "path";
1400
+ import ora3 from "ora";
1401
+ import chalk3 from "chalk";
1402
+ var RestoreCommand = class {
1403
+ async execute(archiveName, options) {
1404
+ const archivesDir = path5.join(
1405
+ ".",
1406
+ HERASPEC_DIR_NAME,
1407
+ CHANGES_DIR_NAME,
1408
+ ARCHIVES_DIR_NAME
1409
+ );
1410
+ if (!await FileSystemUtils.fileExists(archivesDir)) {
1411
+ console.error("Error: No archives directory found. No archives to restore.");
1412
+ process.exitCode = 1;
1413
+ return;
1414
+ }
1415
+ if (!archiveName) {
1416
+ const archives = await this.listArchives(archivesDir);
1417
+ if (archives.length === 0) {
1418
+ console.log("No archived changes found.");
1419
+ return;
1420
+ }
1421
+ console.log("\nAvailable archives:");
1422
+ console.log("\u2500".repeat(60));
1423
+ archives.forEach((archive, index) => {
1424
+ console.log(`${index + 1}. ${archive}`);
1425
+ });
1426
+ console.log();
1427
+ console.error("Error: Please specify archive name to restore");
1428
+ console.log(`Usage: heraspec restore <archive-name>`);
1429
+ console.log(`Example: heraspec restore 2025-01-15-add-two-factor-auth`);
1430
+ process.exitCode = 1;
1431
+ return;
1432
+ }
1433
+ const archivePath = path5.join(archivesDir, archiveName);
1434
+ if (!await FileSystemUtils.fileExists(archivePath)) {
1435
+ console.error(`Error: Archive "${archiveName}" not found`);
1436
+ console.log("\nAvailable archives:");
1437
+ const archives = await this.listArchives(archivesDir);
1438
+ archives.forEach((archive) => {
1439
+ console.log(` - ${archive}`);
1440
+ });
1441
+ process.exitCode = 1;
1442
+ return;
1443
+ }
1444
+ const changeName = this.extractChangeName(archiveName);
1445
+ const changePath = path5.join(
1446
+ ".",
1447
+ HERASPEC_DIR_NAME,
1448
+ CHANGES_DIR_NAME,
1449
+ changeName
1450
+ );
1451
+ if (await FileSystemUtils.fileExists(changePath)) {
1452
+ console.error(`Error: Change "${changeName}" already exists in active changes.`);
1453
+ console.log("Please remove or rename the existing change first.");
1454
+ process.exitCode = 1;
1455
+ return;
1456
+ }
1457
+ if (!options?.yes) {
1458
+ console.log(`
1459
+ This will restore archive "${archiveName}" to active change "${changeName}".`);
1460
+ console.log("Note: This will not revert spec changes that were merged during archive.\n");
1461
+ console.error("Error: Please use --yes flag to confirm");
1462
+ process.exitCode = 1;
1463
+ return;
1464
+ }
1465
+ const spinner = ora3({
1466
+ text: `Restoring archive "${archiveName}"...`,
1467
+ color: "blue"
1468
+ }).start();
1469
+ try {
1470
+ const changesDir = path5.join(
1471
+ ".",
1472
+ HERASPEC_DIR_NAME,
1473
+ CHANGES_DIR_NAME
1474
+ );
1475
+ await FileSystemUtils.createDirectory(changesDir);
1476
+ await FileSystemUtils.createDirectory(changePath);
1477
+ await this.moveArchiveToChanges(archivePath, changePath);
1478
+ await FileSystemUtils.removeDirectory(archivePath, true);
1479
+ spinner.succeed(
1480
+ chalk3.green(`Archive "${archiveName}" restored successfully to "${changeName}"`)
1481
+ );
1482
+ console.log();
1483
+ console.log(chalk3.cyan("Note:"));
1484
+ console.log(
1485
+ chalk3.gray(
1486
+ "Spec changes that were merged during archive are still in source specs."
1487
+ )
1488
+ );
1489
+ console.log(
1490
+ chalk3.gray(
1491
+ "You may need to manually review and revert spec changes if needed."
1492
+ )
1493
+ );
1494
+ } catch (error) {
1495
+ spinner.fail(chalk3.red(`Error: ${error.message}`));
1496
+ throw error;
1497
+ }
1498
+ }
1499
+ async listArchives(archivesDir) {
1500
+ const archives = [];
1501
+ const entries = await FileSystemUtils.readDirectory(archivesDir);
1502
+ for (const entry of entries) {
1503
+ const entryPath = path5.join(archivesDir, entry);
1504
+ const stats = await FileSystemUtils.stat(entryPath);
1505
+ if (stats.isDirectory()) {
1506
+ archives.push(entry);
1507
+ }
1508
+ }
1509
+ return archives.sort();
1510
+ }
1511
+ extractChangeName(archiveName) {
1512
+ const datePattern = /^\d{4}-\d{2}-\d{2}-/;
1513
+ return archiveName.replace(datePattern, "");
1514
+ }
1515
+ async moveArchiveToChanges(sourcePath, destPath) {
1516
+ const entries = await FileSystemUtils.readDirectory(sourcePath);
1517
+ for (const entry of entries) {
1518
+ const sourceEntry = path5.join(sourcePath, entry);
1519
+ const destEntry = path5.join(destPath, entry);
1520
+ const stats = await FileSystemUtils.stat(sourceEntry);
1521
+ if (stats.isDirectory()) {
1522
+ await FileSystemUtils.createDirectory(destEntry);
1523
+ await this.moveArchiveToChanges(sourceEntry, destEntry);
1524
+ } else {
1525
+ await FileSystemUtils.moveFile(sourceEntry, destEntry);
1526
+ }
1527
+ }
1528
+ }
1529
+ };
1530
+
1531
+ // src/commands/show.ts
1532
+ import path8 from "path";
1533
+ import chalk4 from "chalk";
1534
+
1535
+ // src/utils/task-parser.ts
1536
+ import { readFileSync as readFileSync2 } from "fs";
1537
+
1538
+ // src/core/skills/skill-parser.ts
1539
+ import { readFileSync } from "fs";
1540
+ import path6 from "path";
1541
+ var SkillParser = class {
1542
+ static parseSkill(skillPath, skillName) {
1543
+ const skillMdPath = path6.join(skillPath, "skill.md");
1544
+ try {
1545
+ const content = readFileSync(skillMdPath, "utf-8");
1546
+ return this.parseSkillContent(content, skillName, skillPath);
1547
+ } catch (error) {
1548
+ throw new Error(`Failed to parse skill "${skillName}": ${error instanceof Error ? error.message : "Unknown error"}`);
1549
+ }
1550
+ }
1551
+ static parseSkillContent(content, skillName, skillPath) {
1552
+ const lines = content.split("\n");
1553
+ const skill = {
1554
+ name: skillName,
1555
+ skillPath,
1556
+ purpose: "",
1557
+ whenToUse: [],
1558
+ steps: [],
1559
+ inputs: [],
1560
+ outputs: [],
1561
+ toneAndRules: {},
1562
+ templates: [],
1563
+ scripts: [],
1564
+ examples: [],
1565
+ relatedSkills: []
1566
+ };
1567
+ let currentSection = "";
1568
+ let currentList = [];
1569
+ for (let i = 0; i < lines.length; i++) {
1570
+ const line = lines[i].trim();
1571
+ if (line.match(/^##+\s+/)) {
1572
+ this.saveList(currentSection, currentList, skill);
1573
+ currentList = [];
1574
+ const sectionName = line.replace(/^##+\s+/, "").toLowerCase();
1575
+ currentSection = sectionName;
1576
+ if (sectionName.includes("m\u1EE5c \u0111\xEDch") || sectionName.includes("purpose")) {
1577
+ if (i + 1 < lines.length && lines[i + 1].trim()) {
1578
+ skill.purpose = lines[i + 1].trim();
1579
+ }
1580
+ }
1581
+ continue;
1582
+ }
1583
+ if (line.startsWith("- ") || line.startsWith("* ")) {
1584
+ const item = line.replace(/^[-*]\s+/, "").trim();
1585
+ if (item) {
1586
+ currentList.push(item);
1587
+ }
1588
+ continue;
1589
+ }
1590
+ if (line.match(/^\d+\.\s+/)) {
1591
+ const step = line.replace(/^\d+\.\s+/, "").trim();
1592
+ if (step) {
1593
+ skill.steps = skill.steps || [];
1594
+ skill.steps.push(step);
1595
+ }
1596
+ continue;
1597
+ }
1598
+ if (currentSection.includes("input")) {
1599
+ if (line.startsWith("- ")) {
1600
+ skill.inputs = skill.inputs || [];
1601
+ skill.inputs.push(line.replace(/^-\s+/, ""));
1602
+ }
1603
+ }
1604
+ if (currentSection.includes("output")) {
1605
+ if (line.startsWith("- ")) {
1606
+ skill.outputs = skill.outputs || [];
1607
+ skill.outputs.push(line.replace(/^-\s+/, ""));
1608
+ }
1609
+ }
1610
+ if (currentSection.includes("template")) {
1611
+ if (line.includes(".php") || line.includes(".md") || line.includes(".scss") || line.includes(".js") || line.includes(".sh")) {
1612
+ skill.templates = skill.templates || [];
1613
+ const templateName = line.match(/`([^`]+)`/) || line.match(/\*\*([^*]+)\*\*/);
1614
+ if (templateName) {
1615
+ skill.templates.push(templateName[1]);
1616
+ }
1617
+ }
1618
+ }
1619
+ if (currentSection.includes("script")) {
1620
+ if (line.includes(".sh") || line.includes(".py") || line.includes(".js")) {
1621
+ skill.scripts = skill.scripts || [];
1622
+ const scriptName = line.match(/`([^`]+)`/) || line.match(/\*\*([^*]+)\*\*/);
1623
+ if (scriptName) {
1624
+ skill.scripts.push(scriptName[1]);
1625
+ }
1626
+ }
1627
+ }
1628
+ }
1629
+ this.saveList(currentSection, currentList, skill);
1630
+ return {
1631
+ name: skill.name || skillName,
1632
+ skillPath: skill.skillPath || skillPath,
1633
+ purpose: skill.purpose || "No description available",
1634
+ whenToUse: skill.whenToUse || [],
1635
+ steps: skill.steps || [],
1636
+ inputs: skill.inputs || [],
1637
+ outputs: skill.outputs || [],
1638
+ toneAndRules: skill.toneAndRules || {},
1639
+ templates: skill.templates || [],
1640
+ scripts: skill.scripts || [],
1641
+ examples: skill.examples || [],
1642
+ relatedSkills: skill.relatedSkills || []
1643
+ };
1644
+ }
1645
+ static saveList(section, list, skill) {
1646
+ if (list.length === 0) return;
1647
+ if (section.includes("khi n\xE0o") || section.includes("when")) {
1648
+ skill.whenToUse = list;
1649
+ } else if (section.includes("input")) {
1650
+ skill.inputs = list;
1651
+ } else if (section.includes("output")) {
1652
+ skill.outputs = list;
1653
+ } else if (section.includes("h\u1EA1n ch\u1EBF") || section.includes("limitation")) {
1654
+ skill.toneAndRules = skill.toneAndRules || {};
1655
+ skill.toneAndRules.limitations = list;
1656
+ } else if (section.includes("li\xEAn k\u1EBFt") || section.includes("related")) {
1657
+ skill.relatedSkills = list;
1658
+ }
1659
+ }
1660
+ };
1661
+
1662
+ // src/core/skills/skill-manager.ts
1663
+ import path7 from "path";
1664
+ var SkillManager = class {
1665
+ /**
1666
+ * Find skill path for a given project type and skill name
1667
+ */
1668
+ static async findSkillPath(projectType, skillName, projectPath = ".") {
1669
+ const projectSkillPath = path7.join(
1670
+ projectPath,
1671
+ HERASPEC_DIR_NAME,
1672
+ SKILLS_DIR_NAME,
1673
+ projectType,
1674
+ skillName
1675
+ );
1676
+ if (await FileSystemUtils.fileExists(path7.join(projectSkillPath, "skill.md"))) {
1677
+ return projectSkillPath;
1678
+ }
1679
+ const crossCuttingPath = path7.join(
1680
+ projectPath,
1681
+ HERASPEC_DIR_NAME,
1682
+ SKILLS_DIR_NAME,
1683
+ skillName
1684
+ );
1685
+ if (await FileSystemUtils.fileExists(path7.join(crossCuttingPath, "skill.md"))) {
1686
+ return crossCuttingPath;
1687
+ }
1688
+ return null;
1689
+ }
1690
+ /**
1691
+ * Load skill information
1692
+ */
1693
+ static async loadSkill(projectType, skillName, projectPath = ".") {
1694
+ const skillPath = await this.findSkillPath(projectType, skillName, projectPath);
1695
+ if (!skillPath) {
1696
+ return null;
1697
+ }
1698
+ try {
1699
+ return SkillParser.parseSkill(skillPath, skillName);
1700
+ } catch (error) {
1701
+ console.error(`Failed to load skill "${skillName}": ${error instanceof Error ? error.message : "Unknown error"}`);
1702
+ return null;
1703
+ }
1704
+ }
1705
+ /**
1706
+ * List all available skills
1707
+ */
1708
+ static async listSkills(projectPath = ".") {
1709
+ const skillsDir = path7.join(projectPath, HERASPEC_DIR_NAME, SKILLS_DIR_NAME);
1710
+ if (!await FileSystemUtils.fileExists(skillsDir)) {
1711
+ return [];
1712
+ }
1713
+ const skills = [];
1714
+ const entries = await FileSystemUtils.readDirectory(skillsDir);
1715
+ for (const entry of entries) {
1716
+ const entryPath = path7.join(skillsDir, entry);
1717
+ const stats = await FileSystemUtils.stat(entryPath);
1718
+ if (stats.isDirectory()) {
1719
+ const isProjectType = PROJECT_TYPES.includes(entry);
1720
+ if (isProjectType) {
1721
+ const projectSkills = await this.listSkillsInDirectory(entryPath, entry);
1722
+ skills.push(...projectSkills);
1723
+ } else {
1724
+ const skillMdPath = path7.join(entryPath, "skill.md");
1725
+ if (await FileSystemUtils.fileExists(skillMdPath)) {
1726
+ skills.push({
1727
+ skillName: entry,
1728
+ path: entryPath
1729
+ });
1730
+ }
1731
+ }
1732
+ }
1733
+ }
1734
+ return skills;
1735
+ }
1736
+ /**
1737
+ * Extract skill from task line
1738
+ */
1739
+ static extractSkillFromTask(taskLine) {
1740
+ const match = taskLine.match(/\(projectType:\s*([^,)]+)(?:,\s*skill:\s*([^)]+))?\)/i);
1741
+ if (match) {
1742
+ return {
1743
+ projectType: match[1]?.trim(),
1744
+ skill: match[2]?.trim()
1745
+ };
1746
+ }
1747
+ const skillOnlyMatch = taskLine.match(/\(skill:\s*([^)]+)\)/i);
1748
+ if (skillOnlyMatch) {
1749
+ return {
1750
+ skill: skillOnlyMatch[1]?.trim()
1751
+ };
1752
+ }
1753
+ return null;
1754
+ }
1755
+ static async listSkillsInDirectory(dir, projectType) {
1756
+ const skills = [];
1757
+ const entries = await FileSystemUtils.readDirectory(dir);
1758
+ for (const entry of entries) {
1759
+ const entryPath = path7.join(dir, entry);
1760
+ const stats = await FileSystemUtils.stat(entryPath);
1761
+ if (stats.isDirectory()) {
1762
+ const skillMdPath = path7.join(entryPath, "skill.md");
1763
+ if (await FileSystemUtils.fileExists(skillMdPath)) {
1764
+ skills.push({
1765
+ projectType,
1766
+ skillName: entry,
1767
+ path: entryPath
1768
+ });
1769
+ }
1770
+ }
1771
+ }
1772
+ return skills;
1773
+ }
1774
+ };
1775
+
1776
+ // src/utils/task-parser.ts
1777
+ var TaskParser = class {
1778
+ static parseTasks(filePath) {
1779
+ const content = readFileSync2(filePath, "utf-8");
1780
+ const lines = content.split("\n");
1781
+ const groups = [];
1782
+ const skillsUsed = /* @__PURE__ */ new Set();
1783
+ let currentGroup = null;
1784
+ for (const line of lines) {
1785
+ const groupMatch = line.match(/^##+\s+\d+\.\s+(.+?)\s*\((.+)\)/);
1786
+ if (groupMatch) {
1787
+ if (currentGroup) {
1788
+ groups.push(currentGroup);
1789
+ }
1790
+ const title = groupMatch[1].trim();
1791
+ const params = groupMatch[2];
1792
+ const skillInfo = SkillManager.extractSkillFromTask(`(${params})`);
1793
+ currentGroup = {
1794
+ title,
1795
+ projectType: skillInfo?.projectType,
1796
+ skill: skillInfo?.skill,
1797
+ tasks: []
1798
+ };
1799
+ if (skillInfo?.skill) {
1800
+ const skillKey = skillInfo.projectType ? `${skillInfo.projectType}:${skillInfo.skill}` : skillInfo.skill;
1801
+ skillsUsed.add(skillKey);
1802
+ }
1803
+ continue;
1804
+ }
1805
+ const taskMatch = line.match(/^-\s+\[([ x])\]\s+(.+)/);
1806
+ if (taskMatch && currentGroup) {
1807
+ const completed = taskMatch[1] === "x";
1808
+ const description = taskMatch[2].trim();
1809
+ const idMatch = description.match(/^(\d+\.\d+)\s+/);
1810
+ const id = idMatch ? idMatch[1] : `${currentGroup.tasks.length + 1}`;
1811
+ const taskDesc = idMatch ? description.replace(/^\d+\.\d+\s+/, "") : description;
1812
+ currentGroup.tasks.push({
1813
+ id,
1814
+ description: taskDesc,
1815
+ completed
1816
+ });
1817
+ }
1818
+ }
1819
+ if (currentGroup) {
1820
+ groups.push(currentGroup);
1821
+ }
1822
+ const skillsArray = Array.from(skillsUsed).map((skillKey) => {
1823
+ const [projectType, skill] = skillKey.includes(":") ? skillKey.split(":") : [void 0, skillKey];
1824
+ return { projectType, skill };
1825
+ });
1826
+ return {
1827
+ groups,
1828
+ skillsUsed: skillsArray
1829
+ };
1830
+ }
1831
+ static async getSkillsForTasks(filePath) {
1832
+ const parsed = this.parseTasks(filePath);
1833
+ const skills = [];
1834
+ for (const skillInfo of parsed.skillsUsed) {
1835
+ if (skillInfo.skill) {
1836
+ const skillPath = await SkillManager.findSkillPath(
1837
+ skillInfo.projectType || "",
1838
+ skillInfo.skill,
1839
+ "."
1840
+ );
1841
+ if (skillPath) {
1842
+ skills.push({
1843
+ projectType: skillInfo.projectType,
1844
+ skill: skillInfo.skill,
1845
+ path: skillPath
1846
+ });
1847
+ }
1848
+ }
1849
+ }
1850
+ return skills;
1851
+ }
1852
+ };
1853
+
1854
+ // src/commands/show.ts
1855
+ var ShowCommand = class {
1856
+ async execute(itemName) {
1857
+ if (!itemName) {
1858
+ console.error("Error: Please specify a change or spec name");
1859
+ console.log("Usage: heraspec show <change-name>");
1860
+ process.exitCode = 1;
1861
+ return;
1862
+ }
1863
+ const changePath = path8.join(
1864
+ ".",
1865
+ HERASPEC_DIR_NAME,
1866
+ CHANGES_DIR_NAME,
1867
+ itemName
1868
+ );
1869
+ if (await FileSystemUtils.fileExists(changePath)) {
1870
+ await this.showChange(itemName, changePath);
1871
+ return;
1872
+ }
1873
+ const specPath = this.findSpecPath(itemName);
1874
+ if (specPath && await FileSystemUtils.fileExists(specPath)) {
1875
+ await this.showSpec(specPath);
1876
+ return;
1877
+ }
1878
+ console.error(`Error: Change or spec "${itemName}" not found`);
1879
+ process.exitCode = 1;
1880
+ }
1881
+ async showChange(changeName, changePath) {
1882
+ console.log(`
1883
+ \u{1F4CB} Change: ${changeName}
1884
+ `);
1885
+ console.log("\u2550".repeat(60));
1886
+ const proposalPath = path8.join(changePath, HERASPEC_MARKERS.PROPOSAL_MD);
1887
+ if (await FileSystemUtils.fileExists(proposalPath)) {
1888
+ console.log("\n## Proposal\n");
1889
+ const proposal = await FileSystemUtils.readFile(proposalPath);
1890
+ console.log(proposal);
1891
+ }
1892
+ const tasksPath = path8.join(changePath, HERASPEC_MARKERS.TASKS_MD);
1893
+ if (await FileSystemUtils.fileExists(tasksPath)) {
1894
+ console.log("\n## Tasks\n");
1895
+ const tasks = await FileSystemUtils.readFile(tasksPath);
1896
+ console.log(tasks);
1897
+ try {
1898
+ const parsedTasks = TaskParser.parseTasks(tasksPath);
1899
+ if (parsedTasks.skillsUsed.length > 0) {
1900
+ console.log("\n## Skills Used in This Change\n");
1901
+ for (const skillInfo of parsedTasks.skillsUsed) {
1902
+ if (skillInfo.skill) {
1903
+ const skillLabel = skillInfo.projectType ? `${skillInfo.projectType}/${skillInfo.skill}` : skillInfo.skill;
1904
+ console.log(chalk4.cyan(` \u2022 ${skillLabel}`));
1905
+ console.log(chalk4.gray(` Location: heraspec/skills/${skillInfo.projectType ? skillInfo.projectType + "/" : ""}${skillInfo.skill}/`));
1906
+ }
1907
+ }
1908
+ console.log();
1909
+ console.log(chalk4.yellow("\u{1F4A1} Tip: Read skill.md files to understand how to implement tasks."));
1910
+ console.log();
1911
+ }
1912
+ } catch (error) {
1913
+ }
1914
+ }
1915
+ const designPath = path8.join(changePath, HERASPEC_MARKERS.DESIGN_MD);
1916
+ if (await FileSystemUtils.fileExists(designPath)) {
1917
+ console.log("\n## Design\n");
1918
+ const design = await FileSystemUtils.readFile(designPath);
1919
+ console.log(design);
1920
+ }
1921
+ const specsPath = path8.join(
1922
+ ".",
1923
+ HERASPEC_DIR_NAME,
1924
+ SPECS_DIR_NAME,
1925
+ changeName
1926
+ );
1927
+ if (await FileSystemUtils.fileExists(specsPath)) {
1928
+ const deltaSpecs = await this.findDeltaSpecs(specsPath);
1929
+ if (deltaSpecs.length > 0) {
1930
+ console.log("\n## Delta Specs\n");
1931
+ for (const spec of deltaSpecs) {
1932
+ console.log(`
1933
+ ### ${spec.name}
1934
+ `);
1935
+ const content = await FileSystemUtils.readFile(spec.path);
1936
+ console.log(content);
1937
+ }
1938
+ }
1939
+ }
1940
+ console.log("\n" + "\u2550".repeat(60) + "\n");
1941
+ }
1942
+ async showSpec(specPath) {
1943
+ console.log("\n\u{1F4C4} Spec\n");
1944
+ console.log("\u2550".repeat(60));
1945
+ const content = await FileSystemUtils.readFile(specPath);
1946
+ console.log(content);
1947
+ console.log("\u2550".repeat(60) + "\n");
1948
+ }
1949
+ findSpecPath(specName) {
1950
+ const basePath = path8.join(".", HERASPEC_DIR_NAME, SPECS_DIR_NAME);
1951
+ const paths = [
1952
+ path8.join(basePath, `${specName}.md`),
1953
+ path8.join(basePath, specName, "spec.md"),
1954
+ path8.join(basePath, ...specName.split("/"), "spec.md")
1955
+ ];
1956
+ return paths[0];
1957
+ }
1958
+ async findDeltaSpecs(dir, prefix = "") {
1959
+ const specs = [];
1960
+ const entries = await FileSystemUtils.readDirectory(dir);
1961
+ for (const entry of entries) {
1962
+ const entryPath = path8.join(dir, entry);
1963
+ const stats = await FileSystemUtils.stat(entryPath);
1964
+ if (stats.isDirectory()) {
1965
+ const subSpecs = await this.findDeltaSpecs(
1966
+ entryPath,
1967
+ prefix ? `${prefix}/${entry}` : entry
1968
+ );
1969
+ specs.push(...subSpecs);
1970
+ } else if (entry === "spec.md" || entry.endsWith(".md")) {
1971
+ specs.push({
1972
+ name: prefix || "global",
1973
+ path: entryPath
1974
+ });
1975
+ }
1976
+ }
1977
+ return specs;
1978
+ }
1979
+ };
1980
+
1981
+ // src/commands/validate.ts
1982
+ import path10 from "path";
1983
+ import chalk5 from "chalk";
1984
+
1985
+ // src/core/validation/validator.ts
1986
+ import { readFileSync as readFileSync3 } from "fs";
1987
+ import path9 from "path";
1988
+
1989
+ // src/core/schemas/base.schema.ts
1990
+ import { z } from "zod";
1991
+ var ProjectTypeSchema = z.enum([
1992
+ "wordpress-plugin",
1993
+ "wordpress-theme",
1994
+ "perfex-module",
1995
+ "laravel-package",
1996
+ "node-service",
1997
+ "generic-webapp",
1998
+ "backend-api",
1999
+ "frontend-app",
2000
+ "multi-stack"
2001
+ ]);
2002
+ var SpecMetaSchema = z.object({
2003
+ projectType: z.union([
2004
+ ProjectTypeSchema,
2005
+ z.array(ProjectTypeSchema)
2006
+ ]).optional(),
2007
+ domain: z.string().optional(),
2008
+ stack: z.union([
2009
+ z.string(),
2010
+ z.array(z.string())
2011
+ ]).optional()
2012
+ }).optional();
2013
+ var ScenarioSchema = z.object({
2014
+ name: z.string(),
2015
+ steps: z.array(z.string()).min(1)
2016
+ });
2017
+ var RequirementSchema = z.object({
2018
+ id: z.string().optional(),
2019
+ name: z.string(),
2020
+ description: z.string(),
2021
+ scenarios: z.array(ScenarioSchema).optional(),
2022
+ constraints: z.array(z.string()).optional()
2023
+ });
2024
+
2025
+ // src/core/schemas/spec.schema.ts
2026
+ import { z as z2 } from "zod";
2027
+ var SpecSchema = z2.object({
2028
+ name: z2.string().min(1),
2029
+ meta: SpecMetaSchema,
2030
+ overview: z2.string().min(1),
2031
+ requirements: z2.array(RequirementSchema).min(1),
2032
+ metadata: z2.object({
2033
+ version: z2.string().default("1.0.0"),
2034
+ format: z2.literal("heraspec"),
2035
+ sourcePath: z2.string().optional()
2036
+ }).optional()
2037
+ });
2038
+
2039
+ // src/core/schemas/change.schema.ts
2040
+ import { z as z3 } from "zod";
2041
+ var DeltaTypeSchema = z3.enum(["ADDED", "MODIFIED", "REMOVED"]);
2042
+ var DeltaRequirementSchema = z3.object({
2043
+ type: DeltaTypeSchema,
2044
+ requirement: z3.object({
2045
+ id: z3.string().optional(),
2046
+ name: z3.string(),
2047
+ description: z3.string(),
2048
+ scenarios: z3.array(z3.object({
2049
+ name: z3.string(),
2050
+ steps: z3.array(z3.string())
2051
+ })).optional(),
2052
+ constraints: z3.array(z3.string()).optional()
2053
+ })
2054
+ });
2055
+ var ChangeSchema = z3.object({
2056
+ name: z3.string(),
2057
+ proposal: z3.string().min(1),
2058
+ tasks: z3.array(z3.string()).optional(),
2059
+ design: z3.string().optional()
2060
+ });
2061
+
2062
+ // src/core/validation/validator.ts
2063
+ var Validator = class {
2064
+ strictMode;
2065
+ constructor(strictMode = false) {
2066
+ this.strictMode = strictMode;
2067
+ }
2068
+ async validateSpec(filePath) {
2069
+ const errors = [];
2070
+ const warnings = [];
2071
+ try {
2072
+ const content = readFileSync3(filePath, "utf-8");
2073
+ const parser = new MarkdownParser(content);
2074
+ const specName = this.extractNameFromPath(filePath);
2075
+ const spec = parser.parseSpec(specName);
2076
+ const result = SpecSchema.safeParse(spec);
2077
+ if (!result.success) {
2078
+ result.error.errors.forEach((err) => {
2079
+ errors.push(`${err.path.join(".")}: ${err.message}`);
2080
+ });
2081
+ }
2082
+ if (spec.requirements.length === 0) {
2083
+ errors.push("Spec must have at least one requirement");
2084
+ }
2085
+ for (const req of spec.requirements) {
2086
+ if (!req.description || req.description.trim().length === 0) {
2087
+ errors.push(`Requirement "${req.name}" must have a description`);
2088
+ }
2089
+ if (!req.scenarios || req.scenarios.length === 0) {
2090
+ warnings.push(`Requirement "${req.name}" has no scenarios`);
2091
+ }
2092
+ }
2093
+ return {
2094
+ valid: errors.length === 0,
2095
+ errors,
2096
+ warnings
2097
+ };
2098
+ } catch (error) {
2099
+ return {
2100
+ valid: false,
2101
+ errors: [error instanceof Error ? error.message : "Unknown error"],
2102
+ warnings: []
2103
+ };
2104
+ }
2105
+ }
2106
+ async validateChange(changePath) {
2107
+ const errors = [];
2108
+ const warnings = [];
2109
+ const suggestions = [];
2110
+ const proposalPath = path9.join(changePath, "proposal.md");
2111
+ if (!await FileSystemUtils.fileExists(proposalPath)) {
2112
+ errors.push({
2113
+ message: "Change must have a proposal.md file",
2114
+ path: proposalPath,
2115
+ suggestion: `Create proposal.md in ${changePath} with: # Change Proposal: [name]
2116
+
2117
+ ## Purpose
2118
+ [Description]
2119
+
2120
+ ## Scope
2121
+ [What will change]`,
2122
+ autoFixable: false
2123
+ });
2124
+ suggestions.push(`Create proposal.md file at ${proposalPath}`);
2125
+ }
2126
+ const tasksPath = path9.join(changePath, "tasks.md");
2127
+ if (!await FileSystemUtils.fileExists(tasksPath)) {
2128
+ warnings.push({
2129
+ message: "Change has no tasks.md file",
2130
+ path: tasksPath,
2131
+ suggestion: `Create tasks.md with implementation tasks grouped by project type and skill`,
2132
+ autoFixable: false
2133
+ });
2134
+ }
2135
+ const changeName = path9.basename(changePath);
2136
+ const specsDir = path9.join(
2137
+ ".",
2138
+ HERASPEC_DIR_NAME,
2139
+ SPECS_DIR_NAME,
2140
+ changeName
2141
+ );
2142
+ if (await FileSystemUtils.fileExists(specsDir)) {
2143
+ const deltaSpecs = await this.findDeltaSpecs(specsDir);
2144
+ for (const specPath of deltaSpecs) {
2145
+ const report = await this.validateDeltaSpec(specPath);
2146
+ errors.push(...report.errors);
2147
+ warnings.push(...report.warnings);
2148
+ if (report.suggestions) {
2149
+ suggestions.push(...report.suggestions);
2150
+ }
2151
+ }
2152
+ } else {
2153
+ warnings.push({
2154
+ message: "No delta specs found for this change",
2155
+ suggestion: `Create delta specs in ${specsDir}/spec.md using ADDED/MODIFIED/REMOVED sections`,
2156
+ autoFixable: false
2157
+ });
2158
+ }
2159
+ return {
2160
+ valid: errors.length === 0,
2161
+ errors,
2162
+ warnings,
2163
+ suggestions: suggestions.length > 0 ? suggestions : void 0
2164
+ };
2165
+ }
2166
+ async validateDeltaSpec(filePath) {
2167
+ const errors = [];
2168
+ const warnings = [];
2169
+ const suggestions = [];
2170
+ try {
2171
+ const content = readFileSync3(filePath, "utf-8");
2172
+ const parser = new MarkdownParser(content);
2173
+ const delta = parser.parseDeltaSpec(content);
2174
+ if (delta.added.length === 0 && delta.modified.length === 0 && delta.removed.length === 0) {
2175
+ warnings.push({
2176
+ message: "Delta spec has no changes",
2177
+ suggestion: "Add at least one section: ## ADDED Requirements, ## MODIFIED Requirements, or ## REMOVED Requirements",
2178
+ autoFixable: false
2179
+ });
2180
+ }
2181
+ if (!content.includes("## ADDED") && !content.includes("## MODIFIED") && !content.includes("## REMOVED")) {
2182
+ warnings.push({
2183
+ message: "Delta spec may not follow proper format",
2184
+ suggestion: "Use sections: ## ADDED Requirements, ## MODIFIED Requirements, ## REMOVED Requirements",
2185
+ autoFixable: false
2186
+ });
2187
+ }
2188
+ return {
2189
+ valid: errors.length === 0,
2190
+ errors,
2191
+ warnings,
2192
+ suggestions: suggestions.length > 0 ? suggestions : void 0
2193
+ };
2194
+ } catch (error) {
2195
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
2196
+ const suggestion = this.getParseErrorSuggestion(errorMessage);
2197
+ return {
2198
+ valid: false,
2199
+ errors: [{
2200
+ message: errorMessage,
2201
+ suggestion,
2202
+ autoFixable: false
2203
+ }],
2204
+ warnings: [],
2205
+ suggestions: suggestion ? [suggestion] : void 0
2206
+ };
2207
+ }
2208
+ }
2209
+ extractNameFromPath(filePath) {
2210
+ const baseName = path9.basename(filePath, ".md");
2211
+ return baseName === "spec" ? path9.basename(path9.dirname(filePath)) : baseName;
2212
+ }
2213
+ async findDeltaSpecs(dir) {
2214
+ const specs = [];
2215
+ const entries = await FileSystemUtils.readDirectory(dir);
2216
+ for (const entry of entries) {
2217
+ const entryPath = path9.join(dir, entry);
2218
+ const stats = await FileSystemUtils.stat(entryPath);
2219
+ if (stats.isDirectory()) {
2220
+ const subSpecs = await this.findDeltaSpecs(entryPath);
2221
+ specs.push(...subSpecs);
2222
+ } else if (entry.endsWith(".md")) {
2223
+ specs.push(entryPath);
2224
+ }
2225
+ }
2226
+ return specs;
2227
+ }
2228
+ };
2229
+
2230
+ // src/commands/validate.ts
2231
+ var ValidateCommand = class {
2232
+ async execute(itemName, options) {
2233
+ const validator = new Validator(options?.strict || false);
2234
+ if (!itemName) {
2235
+ console.error("Error: Please specify a change or spec name");
2236
+ console.log("Usage: heraspec validate <change-name>");
2237
+ process.exitCode = 1;
2238
+ return;
2239
+ }
2240
+ const changePath = path10.join(".", HERASPEC_DIR_NAME, CHANGES_DIR_NAME, itemName);
2241
+ if (await FileSystemUtils.fileExists(changePath)) {
2242
+ const report = await validator.validateChange(changePath);
2243
+ this.printReport(itemName, report);
2244
+ if (!report.valid) {
2245
+ process.exitCode = 1;
2246
+ }
2247
+ return;
2248
+ }
2249
+ const specPath = this.findSpecPath(itemName);
2250
+ if (specPath && await FileSystemUtils.fileExists(specPath)) {
2251
+ const report = await validator.validateSpec(specPath);
2252
+ this.printReport(itemName, report);
2253
+ if (!report.valid) {
2254
+ process.exitCode = 1;
2255
+ }
2256
+ return;
2257
+ }
2258
+ console.error(`Error: Change or spec "${itemName}" not found`);
2259
+ process.exitCode = 1;
2260
+ }
2261
+ findSpecPath(specName) {
2262
+ const basePath = path10.join(".", HERASPEC_DIR_NAME, SPECS_DIR_NAME);
2263
+ return path10.join(basePath, `${specName}.md`);
2264
+ }
2265
+ printReport(itemName, report) {
2266
+ console.log(`
2267
+ Validation Report: ${itemName}
2268
+ `);
2269
+ console.log("\u2500".repeat(60));
2270
+ if (report.valid) {
2271
+ console.log(chalk5.green("\u2713 Valid"));
2272
+ } else {
2273
+ console.log(chalk5.red("\u2717 Invalid"));
2274
+ }
2275
+ if (report.errors && report.errors.length > 0) {
2276
+ console.log(chalk5.red("\nErrors:"));
2277
+ report.errors.forEach((err) => {
2278
+ console.log(chalk5.red(` \u2022 ${err.message || err}`));
2279
+ if (err.suggestion) {
2280
+ console.log(chalk5.gray(` \u{1F4A1} Suggestion: ${err.suggestion}`));
2281
+ }
2282
+ if (err.path) {
2283
+ console.log(chalk5.gray(` \u{1F4CD} Path: ${err.path}`));
2284
+ }
2285
+ });
2286
+ }
2287
+ if (report.warnings && report.warnings.length > 0) {
2288
+ console.log(chalk5.yellow("\nWarnings:"));
2289
+ report.warnings.forEach((warn) => {
2290
+ console.log(chalk5.yellow(` \u2022 ${warn.message || warn}`));
2291
+ if (warn.suggestion) {
2292
+ console.log(chalk5.gray(` \u{1F4A1} Suggestion: ${warn.suggestion}`));
2293
+ }
2294
+ if (warn.path) {
2295
+ console.log(chalk5.gray(` \u{1F4CD} Path: ${warn.path}`));
2296
+ }
2297
+ });
2298
+ }
2299
+ if (report.suggestions && report.suggestions.length > 0) {
2300
+ console.log(chalk5.cyan("\n\u{1F4A1} Quick Fixes:"));
2301
+ report.suggestions.forEach((suggestion) => {
2302
+ console.log(chalk5.cyan(` \u2022 ${suggestion}`));
2303
+ });
2304
+ }
2305
+ if (!report.valid && report.errors && report.errors.length > 0) {
2306
+ console.log(chalk5.gray("\n\u{1F4DD} Next Steps:"));
2307
+ console.log(chalk5.gray(" 1. Review errors above"));
2308
+ console.log(chalk5.gray(" 2. Apply suggested fixes"));
2309
+ console.log(chalk5.gray(" 3. Run validation again: heraspec validate " + itemName));
2310
+ }
2311
+ console.log("\n" + "\u2500".repeat(60) + "\n");
2312
+ }
2313
+ };
2314
+
2315
+ // src/commands/skill.ts
2316
+ import path11 from "path";
2317
+ import ora4 from "ora";
2318
+
2319
+ // src/core/templates/skills-template-map.ts
2320
+ var SKILL_TEMPLATE_MAP = {
2321
+ // Cross-cutting skills
2322
+ "ui-ux": {
2323
+ templateFileName: "ui-ux-skill.md",
2324
+ isCrossCutting: true,
2325
+ resourceDirs: ["scripts", "templates", "data"]
2326
+ },
2327
+ "documents": {
2328
+ templateFileName: "documents-skill.md",
2329
+ isCrossCutting: true
2330
+ },
2331
+ "content-optimization": {
2332
+ templateFileName: "content-optimization-skill.md",
2333
+ isCrossCutting: true
2334
+ },
2335
+ "unit-test": {
2336
+ templateFileName: "unit-test-skill.md",
2337
+ isCrossCutting: true
2338
+ },
2339
+ "integration-test": {
2340
+ templateFileName: "integration-test-skill.md",
2341
+ isCrossCutting: true
2342
+ },
2343
+ "e2e-test": {
2344
+ templateFileName: "e2e-test-skill.md",
2345
+ isCrossCutting: true
2346
+ },
2347
+ "suggestion": {
2348
+ templateFileName: "suggestion-skill.md",
2349
+ isCrossCutting: true
2350
+ },
2351
+ // Perfex module skills
2352
+ "perfex-module:module-codebase": {
2353
+ templateFileName: "module-codebase-skill.md",
2354
+ isCrossCutting: false,
2355
+ projectType: "perfex-module"
2356
+ }
2357
+ };
2358
+ function getSkillTemplateInfo(skillName, projectType) {
2359
+ if (projectType) {
2360
+ const key = `${projectType}:${skillName}`;
2361
+ if (SKILL_TEMPLATE_MAP[key]) {
2362
+ return SKILL_TEMPLATE_MAP[key];
2363
+ }
2364
+ }
2365
+ if (SKILL_TEMPLATE_MAP[skillName]) {
2366
+ return SKILL_TEMPLATE_MAP[skillName];
2367
+ }
2368
+ return null;
2369
+ }
2370
+ function getAllSkillTemplates() {
2371
+ const result = [];
2372
+ for (const [key, info] of Object.entries(SKILL_TEMPLATE_MAP)) {
2373
+ if (info.isCrossCutting) {
2374
+ result.push({ skillName: key, info });
2375
+ } else {
2376
+ const [projectType, skillName] = key.split(":");
2377
+ result.push({ skillName, projectType, info });
2378
+ }
2379
+ }
2380
+ return result;
2381
+ }
2382
+
2383
+ // src/commands/skill.ts
2384
+ import { fileURLToPath } from "url";
2385
+ import { dirname, join } from "path";
2386
+ import { createRequire } from "module";
2387
+ var require2 = createRequire(import.meta.url);
2388
+ var __filename = fileURLToPath(import.meta.url);
2389
+ var __dirname = dirname(__filename);
2390
+ async function getCoreTemplatesDir() {
2391
+ const possiblePaths = [];
2392
+ try {
2393
+ const packageJsonPath2 = require2.resolve("../package.json");
2394
+ const packageDir = path11.dirname(packageJsonPath2);
2395
+ possiblePaths.push(
2396
+ join(packageDir, "src", "core", "templates", "skills"),
2397
+ // Source (when linked, this is the actual source)
2398
+ join(packageDir, "dist", "core", "templates", "skills")
2399
+ // Built (templates copied during build)
2400
+ );
2401
+ } catch {
2402
+ }
2403
+ try {
2404
+ const packageJsonPath2 = require2.resolve("heraspec/package.json");
2405
+ const packageDir = path11.dirname(packageJsonPath2);
2406
+ possiblePaths.push(
2407
+ join(packageDir, "dist", "core", "templates", "skills"),
2408
+ // Built
2409
+ join(packageDir, "src", "core", "templates", "skills")
2410
+ // Source (if available)
2411
+ );
2412
+ } catch {
2413
+ }
2414
+ possiblePaths.push(
2415
+ // Source version (for development) - when running from source: src/commands/skill.ts
2416
+ join(__dirname, "..", "..", "src", "core", "templates", "skills"),
2417
+ // Built version - when running from built: dist/commands/skill.js
2418
+ join(__dirname, "..", "core", "templates", "skills"),
2419
+ // Alternative: from project root (when running from HeraSpec source)
2420
+ join(process.cwd(), "src", "core", "templates", "skills")
2421
+ );
2422
+ for (const possiblePath of possiblePaths) {
2423
+ if (await FileSystemUtils.fileExists(possiblePath)) {
2424
+ return possiblePath;
2425
+ }
2426
+ }
2427
+ return null;
2428
+ }
2429
+ var SkillCommand = class {
2430
+ async list(projectPath = ".") {
2431
+ const skills = await SkillManager.listSkills(projectPath);
2432
+ if (skills.length === 0) {
2433
+ console.log("No skills found. Skills will be created as needed.");
2434
+ console.log("See docs/SKILLS_STRUCTURE_PROPOSAL.md for skill structure.");
2435
+ return;
2436
+ }
2437
+ console.log("\nAvailable Skills:\n");
2438
+ console.log("\u2550".repeat(60));
2439
+ const byProjectType = {};
2440
+ const crossCutting = [];
2441
+ for (const skill of skills) {
2442
+ if (skill.projectType) {
2443
+ if (!byProjectType[skill.projectType]) {
2444
+ byProjectType[skill.projectType] = [];
2445
+ }
2446
+ byProjectType[skill.projectType].push({
2447
+ skillName: skill.skillName,
2448
+ path: skill.path
2449
+ });
2450
+ } else {
2451
+ crossCutting.push({
2452
+ skillName: skill.skillName,
2453
+ path: skill.path
2454
+ });
2455
+ }
2456
+ }
2457
+ for (const [projectType, projectSkills] of Object.entries(byProjectType)) {
2458
+ console.log(`
2459
+ \u{1F4E6} ${projectType}:`);
2460
+ for (const skill of projectSkills) {
2461
+ console.log(` \u2022 ${skill.skillName}`);
2462
+ }
2463
+ }
2464
+ if (crossCutting.length > 0) {
2465
+ console.log(`
2466
+ \u{1F527} Cross-cutting skills:`);
2467
+ for (const skill of crossCutting) {
2468
+ console.log(` \u2022 ${skill.skillName}`);
2469
+ }
2470
+ }
2471
+ console.log("\n" + "\u2550".repeat(60) + "\n");
2472
+ }
2473
+ async show(skillName, projectType, projectPath = ".") {
2474
+ if (!skillName) {
2475
+ console.error("Error: Please specify a skill name");
2476
+ console.log("Usage: heraspec skill show <skill-name> [--project-type <type>]");
2477
+ process.exitCode = 1;
2478
+ return;
2479
+ }
2480
+ let skillInfo = null;
2481
+ if (projectType) {
2482
+ skillInfo = await SkillManager.loadSkill(projectType, skillName, projectPath);
2483
+ } else {
2484
+ const skills = await SkillManager.listSkills(projectPath);
2485
+ const found = skills.find((s) => s.skillName === skillName);
2486
+ if (found) {
2487
+ try {
2488
+ skillInfo = SkillParser.parseSkill(found.path, skillName);
2489
+ } catch (error) {
2490
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
2491
+ process.exitCode = 1;
2492
+ return;
2493
+ }
2494
+ }
2495
+ }
2496
+ if (!skillInfo) {
2497
+ console.error(`Error: Skill "${skillName}" not found`);
2498
+ if (projectType) {
2499
+ console.log(`Searched in: heraspec/skills/${projectType}/${skillName}/`);
2500
+ } else {
2501
+ console.log("Searched in: heraspec/skills/");
2502
+ }
2503
+ process.exitCode = 1;
2504
+ return;
2505
+ }
2506
+ console.log(`
2507
+ \u{1F4DA} Skill: ${skillInfo.name}
2508
+ `);
2509
+ console.log("\u2550".repeat(60));
2510
+ console.log(`
2511
+ \u{1F4CD} Path: ${skillInfo.skillPath}
2512
+ `);
2513
+ if (skillInfo.purpose) {
2514
+ console.log("## Purpose");
2515
+ console.log(skillInfo.purpose);
2516
+ console.log();
2517
+ }
2518
+ if (skillInfo.whenToUse.length > 0) {
2519
+ console.log("## When to Use");
2520
+ skillInfo.whenToUse.forEach((item) => {
2521
+ console.log(`- ${item}`);
2522
+ });
2523
+ console.log();
2524
+ }
2525
+ if (skillInfo.steps.length > 0) {
2526
+ console.log("## Steps");
2527
+ skillInfo.steps.forEach((step, index) => {
2528
+ console.log(`${index + 1}. ${step}`);
2529
+ });
2530
+ console.log();
2531
+ }
2532
+ if (skillInfo.inputs.length > 0) {
2533
+ console.log("## Inputs");
2534
+ skillInfo.inputs.forEach((input) => {
2535
+ console.log(`- ${input}`);
2536
+ });
2537
+ console.log();
2538
+ }
2539
+ if (skillInfo.outputs.length > 0) {
2540
+ console.log("## Outputs");
2541
+ skillInfo.outputs.forEach((output) => {
2542
+ console.log(`- ${output}`);
2543
+ });
2544
+ console.log();
2545
+ }
2546
+ if (skillInfo.templates.length > 0) {
2547
+ console.log("## Available Templates");
2548
+ skillInfo.templates.forEach((template) => {
2549
+ console.log(`- ${template}`);
2550
+ });
2551
+ console.log();
2552
+ }
2553
+ if (skillInfo.scripts.length > 0) {
2554
+ console.log("## Available Scripts");
2555
+ skillInfo.scripts.forEach((script) => {
2556
+ console.log(`- ${script}`);
2557
+ });
2558
+ console.log();
2559
+ }
2560
+ if (skillInfo.toneAndRules.limitations && skillInfo.toneAndRules.limitations.length > 0) {
2561
+ console.log("## Limitations");
2562
+ skillInfo.toneAndRules.limitations.forEach((limitation) => {
2563
+ console.log(`- ${limitation}`);
2564
+ });
2565
+ console.log();
2566
+ }
2567
+ const skillMdPath = path11.join(skillInfo.skillPath, "skill.md");
2568
+ if (await FileSystemUtils.fileExists(skillMdPath)) {
2569
+ console.log("\u2550".repeat(60));
2570
+ console.log("\n## Full skill.md Content\n");
2571
+ const content = await FileSystemUtils.readFile(skillMdPath);
2572
+ console.log(content);
2573
+ }
2574
+ console.log("\n" + "\u2550".repeat(60) + "\n");
2575
+ }
2576
+ async repair(projectPath = ".") {
2577
+ const spinner = ora4("Repairing skills structure...").start();
2578
+ try {
2579
+ const skillsDir = path11.join(projectPath, HERASPEC_DIR_NAME, SKILLS_DIR_NAME);
2580
+ if (!await FileSystemUtils.fileExists(skillsDir)) {
2581
+ spinner.fail('Skills directory does not exist. Run "heraspec init" first.');
2582
+ process.exitCode = 1;
2583
+ return;
2584
+ }
2585
+ const skills = await SkillManager.listSkills(projectPath);
2586
+ let fixed = 0;
2587
+ let errors = 0;
2588
+ for (const skill of skills) {
2589
+ const skillPath = skill.path;
2590
+ const skillMdPath = path11.join(skillPath, "skill.md");
2591
+ if (!await FileSystemUtils.fileExists(skillMdPath)) {
2592
+ spinner.warn(`Missing skill.md in ${skillPath}`);
2593
+ errors++;
2594
+ continue;
2595
+ }
2596
+ const standardDirs = ["templates", "scripts", "examples"];
2597
+ for (const dir of standardDirs) {
2598
+ const dirPath = path11.join(skillPath, dir);
2599
+ if (!await FileSystemUtils.fileExists(dirPath)) {
2600
+ await FileSystemUtils.createDirectory(dirPath);
2601
+ fixed++;
2602
+ }
2603
+ }
2604
+ try {
2605
+ const content = await FileSystemUtils.readFile(skillMdPath);
2606
+ if (!content.includes("## Purpose") && !content.includes("# Skill:")) {
2607
+ spinner.warn(`Invalid skill.md structure in ${skillPath}`);
2608
+ }
2609
+ } catch (error) {
2610
+ spinner.warn(`Cannot read skill.md in ${skillPath}: ${error instanceof Error ? error.message : "Unknown error"}`);
2611
+ errors++;
2612
+ }
2613
+ }
2614
+ if (errors === 0 && fixed === 0) {
2615
+ spinner.succeed("All skills are properly structured");
2616
+ } else {
2617
+ spinner.succeed(`Repaired ${fixed} issues, found ${errors} errors`);
2618
+ }
2619
+ } catch (error) {
2620
+ spinner.fail(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
2621
+ process.exitCode = 1;
2622
+ }
2623
+ }
2624
+ async add(skillName, projectType, projectPath = ".") {
2625
+ if (!skillName) {
2626
+ console.error("Error: Please specify a skill name");
2627
+ console.log("Usage: heraspec skills add <skill-name> [--project-type <type>]");
2628
+ process.exitCode = 1;
2629
+ return;
2630
+ }
2631
+ const spinner = ora4(`Adding skill "${skillName}"...`).start();
2632
+ try {
2633
+ const templateInfo = getSkillTemplateInfo(skillName, projectType);
2634
+ if (!templateInfo) {
2635
+ spinner.fail(`Skill template "${skillName}" not found`);
2636
+ console.log("\nAvailable skills:");
2637
+ const allTemplates = getAllSkillTemplates();
2638
+ for (const { skillName: name, projectType: pt } of allTemplates) {
2639
+ if (pt) {
2640
+ console.log(` - ${name} (projectType: ${pt})`);
2641
+ } else {
2642
+ console.log(` - ${name} (cross-cutting)`);
2643
+ }
2644
+ }
2645
+ process.exitCode = 1;
2646
+ return;
2647
+ }
2648
+ const skillsDir = path11.join(projectPath, HERASPEC_DIR_NAME, SKILLS_DIR_NAME);
2649
+ let destPath;
2650
+ if (templateInfo.isCrossCutting) {
2651
+ destPath = path11.join(skillsDir, skillName);
2652
+ } else {
2653
+ if (!templateInfo.projectType) {
2654
+ spinner.fail("Project type is required for this skill");
2655
+ process.exitCode = 1;
2656
+ return;
2657
+ }
2658
+ destPath = path11.join(skillsDir, templateInfo.projectType, skillName);
2659
+ }
2660
+ const isUpdate = await FileSystemUtils.fileExists(destPath);
2661
+ if (isUpdate) {
2662
+ spinner.info(`Skill "${skillName}" already exists at ${destPath}`);
2663
+ spinner.start(`Removing old version to update with latest...`);
2664
+ try {
2665
+ await FileSystemUtils.removeDirectory(destPath, true);
2666
+ spinner.succeed(`Removed old skill "${skillName}"`);
2667
+ } catch (error) {
2668
+ spinner.fail(`Failed to remove old skill: ${error instanceof Error ? error.message : "Unknown error"}`);
2669
+ process.exitCode = 1;
2670
+ return;
2671
+ }
2672
+ spinner.start(`Adding updated skill "${skillName}"...`);
2673
+ }
2674
+ await FileSystemUtils.createDirectory(destPath);
2675
+ const coreTemplatesDir = await getCoreTemplatesDir();
2676
+ if (!coreTemplatesDir) {
2677
+ spinner.fail("Cannot find HeraSpec templates directory. Make sure you are running from HeraSpec project or have templates installed.");
2678
+ process.exitCode = 1;
2679
+ return;
2680
+ }
2681
+ const templateFile = path11.join(coreTemplatesDir, templateInfo.templateFileName);
2682
+ if (!await FileSystemUtils.fileExists(templateFile)) {
2683
+ spinner.fail(`Template file not found: ${templateFile}`);
2684
+ process.exitCode = 1;
2685
+ return;
2686
+ }
2687
+ await FileSystemUtils.copyFile(templateFile, path11.join(destPath, "skill.md"));
2688
+ if (templateInfo.resourceDirs) {
2689
+ for (const resourceDir of templateInfo.resourceDirs) {
2690
+ const srcResourceDir = path11.join(coreTemplatesDir, resourceDir);
2691
+ const destResourceDir = path11.join(destPath, resourceDir);
2692
+ if (await FileSystemUtils.fileExists(srcResourceDir)) {
2693
+ await FileSystemUtils.copyDirectory(srcResourceDir, destResourceDir);
2694
+ }
2695
+ }
2696
+ }
2697
+ await FileSystemUtils.createDirectory(path11.join(destPath, "templates"));
2698
+ await FileSystemUtils.createDirectory(path11.join(destPath, "scripts"));
2699
+ await FileSystemUtils.createDirectory(path11.join(destPath, "examples"));
2700
+ const successMessage = isUpdate ? `Skill "${skillName}" updated successfully` : `Skill "${skillName}" added successfully`;
2701
+ spinner.succeed(successMessage);
2702
+ console.log(`
2703
+ \u{1F4CD} Location: ${destPath}`);
2704
+ if (isUpdate) {
2705
+ console.log(`
2706
+ \u2728 Skill has been updated with latest features and improvements.`);
2707
+ }
2708
+ console.log(`
2709
+ \u{1F4A1} Run "heraspec skill show ${skillName}${projectType ? ` --project-type ${projectType}` : ""}" to view details
2710
+ `);
2711
+ } catch (error) {
2712
+ spinner.fail(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
2713
+ process.exitCode = 1;
2714
+ }
2715
+ }
2716
+ };
2717
+
2718
+ // src/commands/helper.ts
2719
+ import chalk6 from "chalk";
2720
+ var HelperCommand = class {
2721
+ async execute() {
2722
+ console.log(chalk6.cyan.bold("\n\u{1F4DA} HeraSpec Helper - Usage Guide\n"));
2723
+ console.log("\u2550".repeat(70) + "\n");
2724
+ this.showQuickStart();
2725
+ this.showCommands();
2726
+ this.showExamplePrompts();
2727
+ this.showWorkflow();
2728
+ this.showTips();
2729
+ console.log("\n" + "\u2550".repeat(70));
2730
+ console.log(chalk6.gray("\n\u{1F4A1} Tip: See details at docs/README.md (available in multiple languages)\n"));
2731
+ }
2732
+ showQuickStart() {
2733
+ console.log(chalk6.yellow.bold("\u{1F680} Quick Start\n"));
2734
+ console.log(chalk6.white("1. Initialize a new project:"));
2735
+ console.log(chalk6.gray(" cd my-project"));
2736
+ console.log(chalk6.cyan(" heraspec init\n"));
2737
+ console.log(chalk6.white("2. Configure project.md:"));
2738
+ console.log(chalk6.gray(" Edit heraspec/project.md with your project information\n"));
2739
+ console.log(chalk6.white("3. Create your first change (using AI):"));
2740
+ console.log(chalk6.cyan(' "Create a HeraSpec change to add feature X"\n'));
2741
+ console.log(chalk6.white("4. View list:"));
2742
+ console.log(chalk6.cyan(" heraspec list\n"));
2743
+ console.log("\u2500".repeat(70) + "\n");
2744
+ }
2745
+ showCommands() {
2746
+ console.log(chalk6.yellow.bold("\u26A1 Main CLI Commands\n"));
2747
+ const commands = [
2748
+ { cmd: "heraspec init [path]", desc: "Initialize HeraSpec in project" },
2749
+ { cmd: "heraspec list", desc: "List changes (default)" },
2750
+ { cmd: "heraspec list --specs", desc: "List specs" },
2751
+ { cmd: "heraspec show [name]", desc: "Show change or spec details" },
2752
+ { cmd: "heraspec validate [name]", desc: "Validate change or spec" },
2753
+ { cmd: "heraspec archive [name]", desc: "Archive completed change" },
2754
+ { cmd: "heraspec restore [name]", desc: "Restore change from archive" },
2755
+ { cmd: "heraspec skill list", desc: "List available skills" },
2756
+ { cmd: "heraspec skill show <name>", desc: "Show skill details" },
2757
+ { cmd: "heraspec make docs", desc: "Generate product documentation" },
2758
+ { cmd: "heraspec make docs --agent <name>", desc: "Generate docs with AI agent" },
2759
+ { cmd: "heraspec make test", desc: "Generate test cases from specs" },
2760
+ { cmd: "heraspec make test --type <type>", desc: "Generate tests (unit/integration/e2e)" },
2761
+ { cmd: "heraspec suggest", desc: "Suggest new features for project" },
2762
+ { cmd: "heraspec view", desc: "View dashboard overview" },
2763
+ { cmd: "heraspec helper", desc: "Show this guide" }
2764
+ ];
2765
+ commands.forEach(({ cmd, desc }) => {
2766
+ console.log(chalk6.cyan(` ${cmd.padEnd(35)}`) + chalk6.white(desc));
2767
+ });
2768
+ console.log("\n" + "\u2500".repeat(70) + "\n");
2769
+ }
2770
+ showExamplePrompts() {
2771
+ console.log(chalk6.yellow.bold("\u{1F4AC} Example Prompts for AI\n"));
2772
+ console.log(chalk6.white.bold("1. Simple Change Creation:\n"));
2773
+ console.log(chalk6.gray(' "Create a HeraSpec change to add 2FA authentication feature"\n'));
2774
+ console.log(chalk6.gray(' "Create HeraSpec change for order management module"\n'));
2775
+ console.log(chalk6.white.bold("2. Create Change Based on project.md:\n"));
2776
+ console.log(chalk6.gray(' "Read heraspec/project.md and create HeraSpec changes for all\n'));
2777
+ console.log(chalk6.gray(' features mentioned in it"\n'));
2778
+ console.log(chalk6.gray(' "Based on project.md, create changes to implement by phase"\n'));
2779
+ console.log(chalk6.white.bold("3. Detailed Change Creation:\n"));
2780
+ console.log(chalk6.gray(' "Create HeraSpec change with the following steps:\n'));
2781
+ console.log(chalk6.gray(" 1. Read and analyze heraspec/project.md\n"));
2782
+ console.log(chalk6.gray(" 2. Identify features to build\n"));
2783
+ console.log(chalk6.gray(" 3. For each feature, create a separate change\n"));
2784
+ console.log(chalk6.gray(" 4. Each change needs proposal.md, tasks.md, specs/\n"));
2785
+ console.log(chalk6.gray(' 5. Follow conventions in project.md"\n'));
2786
+ console.log(chalk6.white.bold("4. Prompt With Project Type and Skill:\n"));
2787
+ console.log(chalk6.gray(' "Create change for WordPress plugin with skill admin-settings-page"\n'));
2788
+ console.log(chalk6.gray(' "Create change for Perfex module with skill module-codebase"\n'));
2789
+ console.log(chalk6.gray(' "Create UI/UX change with skill ui-ux for admin interface"\n'));
2790
+ console.log(chalk6.white.bold("5. View and Validate:\n"));
2791
+ console.log(chalk6.gray(' "View change add-user-auth"\n'));
2792
+ console.log(chalk6.gray(' "Validate change add-user-auth --strict"\n'));
2793
+ console.log(chalk6.gray(' "List all changes"\n'));
2794
+ console.log(chalk6.white.bold("6. Implementation:\n"));
2795
+ console.log(chalk6.gray(' "Specs approved, start implementing change add-user-auth"\n'));
2796
+ console.log(chalk6.gray(' "Execute tasks in change add-user-auth"\n'));
2797
+ console.log(chalk6.white.bold("7. Generate Documentation:\n"));
2798
+ console.log(chalk6.gray(' "Generate product documentation from specs"\n'));
2799
+ console.log(chalk6.gray(" heraspec make docs\n"));
2800
+ console.log(chalk6.gray(' "Generate docs with specific AI agent"\n'));
2801
+ console.log(chalk6.gray(" heraspec make docs --agent chatgpt\n"));
2802
+ console.log(chalk6.gray(" heraspec make docs --agent claude\n"));
2803
+ console.log("\u2500".repeat(70) + "\n");
2804
+ }
2805
+ showWorkflow() {
2806
+ console.log(chalk6.yellow.bold("\u{1F504} Workflow\n"));
2807
+ const steps = [
2808
+ {
2809
+ step: "1. Create Change",
2810
+ actions: [
2811
+ "AI or you create change directory",
2812
+ "Write proposal.md describing purpose",
2813
+ "Create tasks.md with task list",
2814
+ "Write delta specs in specs/"
2815
+ ],
2816
+ prompt: '"Create HeraSpec change to..."'
2817
+ },
2818
+ {
2819
+ step: "2. Refine Specs",
2820
+ actions: [
2821
+ "Review: heraspec show <name>",
2822
+ "Ask AI to edit if needed",
2823
+ "Validate: heraspec validate <name>"
2824
+ ],
2825
+ prompt: '"Edit specs in change..."'
2826
+ },
2827
+ {
2828
+ step: "3. Approval",
2829
+ actions: ['Wait for user: "Specs approved."'],
2830
+ prompt: 'You confirm: "Specs approved."'
2831
+ },
2832
+ {
2833
+ step: "4. Implementation",
2834
+ actions: [
2835
+ "AI reads tasks.md and skill.md",
2836
+ "Execute each task",
2837
+ "Mark completed: - [x]"
2838
+ ],
2839
+ prompt: '"Specs approved, implement change..."'
2840
+ },
2841
+ {
2842
+ step: "5. Archive",
2843
+ actions: [
2844
+ "Review: heraspec show <name>",
2845
+ "Archive: heraspec archive <name> --yes",
2846
+ "Specs merged into source specs"
2847
+ ],
2848
+ prompt: "heraspec archive <name> --yes"
2849
+ },
2850
+ {
2851
+ step: "6. Generate Documentation",
2852
+ actions: [
2853
+ "Generate product docs: heraspec make docs",
2854
+ "With specific agent: heraspec make docs --agent <name>",
2855
+ "Output: documentation/product-documentation.txt"
2856
+ ],
2857
+ prompt: "heraspec make docs --agent chatgpt"
2858
+ }
2859
+ ];
2860
+ steps.forEach(({ step, actions, prompt }) => {
2861
+ console.log(chalk6.cyan.bold(` ${step}`));
2862
+ actions.forEach((action) => {
2863
+ console.log(chalk6.white(` \u2022 ${action}`));
2864
+ });
2865
+ console.log(chalk6.gray(` \u{1F4AC} ${prompt}
2866
+ `));
2867
+ });
2868
+ console.log("\u2500".repeat(70) + "\n");
2869
+ }
2870
+ showTips() {
2871
+ console.log(chalk6.yellow.bold("\u{1F4A1} Tips & Best Practices\n"));
2872
+ const tips = [
2873
+ "Always read heraspec/project.md before creating change",
2874
+ "Use Skills system when implementing (each task has skill tag)",
2875
+ "Validate specs before requesting approval",
2876
+ "One change should focus on one specific feature",
2877
+ "Name change: kebab-case, verb-led (add-, create-, implement-)",
2878
+ "Delta specs only change in change folder",
2879
+ "Do not edit source-of-truth specs directly",
2880
+ "Archive change after completion to merge specs",
2881
+ "Use heraspec skill list to view available skills",
2882
+ "UI/UX tasks: Use search scripts in skill ui-ux",
2883
+ "Generate product docs: heraspec make docs (default agent: chatgpt)",
2884
+ "Specify AI agent: heraspec make docs --agent <name> (chatgpt, claude, etc.)",
2885
+ "Generate tests: heraspec make test (default: unit)",
2886
+ "Test types: unit (individual functions), integration (components), e2e (user flows)",
2887
+ "Generate code: heraspec make code (generates code skeletons from specs)",
2888
+ "Validate with suggestions: heraspec validate <name> (includes fix suggestions)",
2889
+ "Get feature suggestions: heraspec suggest (analyzes project and suggests enhancements)",
2890
+ "Use test skills: unit-test, integration-test, e2e-test for test implementation"
2891
+ ];
2892
+ tips.forEach((tip) => {
2893
+ console.log(chalk6.white(` \u2022 ${tip}`));
2894
+ });
2895
+ console.log("");
2896
+ }
2897
+ };
2898
+
2899
+ // src/commands/make-docs.ts
2900
+ import path12 from "path";
2901
+ import chalk7 from "chalk";
2902
+ import ora5 from "ora";
2903
+ var MakeDocsCommand = class {
2904
+ async execute(projectPath = ".", agent = "chatgpt") {
2905
+ const spinner = ora5(`Generating product documentation (Agent: ${agent})...`).start();
2906
+ try {
2907
+ const resolvedPath = path12.resolve(projectPath);
2908
+ const specsDir = path12.join(resolvedPath, HERASPEC_DIR_NAME, SPECS_DIR_NAME);
2909
+ const docsDir = path12.join(resolvedPath, "documentation");
2910
+ const projectMdPath = path12.join(resolvedPath, HERASPEC_DIR_NAME, HERASPEC_MARKERS.PROJECT_MD);
2911
+ if (!await FileSystemUtils.fileExists(specsDir)) {
2912
+ spinner.fail('Specs directory not found. Run "heraspec init" first.');
2913
+ process.exitCode = 1;
2914
+ return;
2915
+ }
2916
+ await FileSystemUtils.createDirectory(docsDir);
2917
+ let projectInfo = {};
2918
+ if (await FileSystemUtils.fileExists(projectMdPath)) {
2919
+ const projectContent = await FileSystemUtils.readFile(projectMdPath);
2920
+ projectInfo = this.extractProjectInfo(projectContent);
2921
+ }
2922
+ const specFiles = await this.findSpecFiles(specsDir);
2923
+ if (specFiles.length === 0) {
2924
+ spinner.warn('No specs found. Create specs first using "heraspec list --specs".');
2925
+ return;
2926
+ }
2927
+ const specs = await this.readSpecs(specFiles);
2928
+ const docContent = this.generateProductDocumentation(specs, projectInfo, agent);
2929
+ const docFilePath = path12.join(docsDir, "product-documentation.txt");
2930
+ await FileSystemUtils.writeFile(docFilePath, docContent);
2931
+ spinner.succeed(`Product documentation generated: ${docFilePath}`);
2932
+ console.log(chalk7.gray(`
2933
+ Found ${specFiles.length} feature(s)`));
2934
+ } catch (error) {
2935
+ spinner.fail(`Error: ${error.message}`);
2936
+ process.exitCode = 1;
2937
+ throw error;
2938
+ }
2939
+ }
2940
+ /**
2941
+ * Extract project information from project.md
2942
+ */
2943
+ extractProjectInfo(projectContent) {
2944
+ const info = {};
2945
+ const nameMatch = projectContent.match(/^#\s+(.+)$/m);
2946
+ if (nameMatch) {
2947
+ info.name = nameMatch[1].trim();
2948
+ }
2949
+ const lines = projectContent.split("\n");
2950
+ let descriptionStart = false;
2951
+ const descriptionLines = [];
2952
+ for (const line of lines) {
2953
+ if (line.startsWith("#") && !descriptionStart) {
2954
+ descriptionStart = true;
2955
+ continue;
2956
+ }
2957
+ if (descriptionStart && line.trim() && !line.startsWith("#")) {
2958
+ descriptionLines.push(line.trim());
2959
+ if (descriptionLines.length >= 3) break;
2960
+ }
2961
+ }
2962
+ if (descriptionLines.length > 0) {
2963
+ info.description = descriptionLines.join(" ");
2964
+ }
2965
+ return info;
2966
+ }
2967
+ /**
2968
+ * Find all spec files recursively
2969
+ */
2970
+ async findSpecFiles(specsDir) {
2971
+ const specFiles = [];
2972
+ await this.findSpecFilesRecursive(specsDir, specFiles);
2973
+ return specFiles.sort();
2974
+ }
2975
+ /**
2976
+ * Recursively find spec files
2977
+ */
2978
+ async findSpecFilesRecursive(dir, files) {
2979
+ const fs2 = await import("fs/promises");
2980
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
2981
+ for (const entry of entries) {
2982
+ const entryPath = path12.join(dir, entry.name);
2983
+ if (entry.isDirectory()) {
2984
+ await this.findSpecFilesRecursive(entryPath, files);
2985
+ } else if (entry.name.endsWith(".md")) {
2986
+ files.push(entryPath);
2987
+ }
2988
+ }
2989
+ }
2990
+ /**
2991
+ * Read all spec files
2992
+ */
2993
+ async readSpecs(specFiles) {
2994
+ const specs = [];
2995
+ for (const specFile of specFiles) {
2996
+ const content = await FileSystemUtils.readFile(specFile);
2997
+ const name = this.extractFeatureName(specFile, content);
2998
+ specs.push({
2999
+ path: specFile,
3000
+ content,
3001
+ name
3002
+ });
3003
+ }
3004
+ return specs;
3005
+ }
3006
+ /**
3007
+ * Extract feature name from spec file
3008
+ */
3009
+ extractFeatureName(filePath, content) {
3010
+ const headingMatch = content.match(/^#+\s+(.+)$/m);
3011
+ if (headingMatch) {
3012
+ return headingMatch[1].trim();
3013
+ }
3014
+ return path12.basename(filePath, ".md").split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
3015
+ }
3016
+ /**
3017
+ * Generate product documentation from specs
3018
+ */
3019
+ generateProductDocumentation(specs, projectInfo, agent = "chatgpt") {
3020
+ const lines = [];
3021
+ const productName = projectInfo.name || "Product";
3022
+ lines.push(`# ${productName}`);
3023
+ lines.push("");
3024
+ if (projectInfo.description) {
3025
+ lines.push(projectInfo.description);
3026
+ lines.push("");
3027
+ }
3028
+ lines.push("---");
3029
+ lines.push("");
3030
+ lines.push("## Overview");
3031
+ lines.push("");
3032
+ lines.push(`This product includes ${specs.length} main feature${specs.length > 1 ? "s" : ""}:`);
3033
+ lines.push("");
3034
+ specs.forEach((spec, index) => {
3035
+ lines.push(`${index + 1}. ${spec.name}`);
3036
+ });
3037
+ lines.push("");
3038
+ lines.push("---");
3039
+ lines.push("");
3040
+ lines.push("## Features");
3041
+ lines.push("");
3042
+ specs.forEach((spec, index) => {
3043
+ const featureInfo = this.extractFeatureInfo(spec.content);
3044
+ lines.push(`### ${index + 1}. ${spec.name}`);
3045
+ lines.push("");
3046
+ if (featureInfo.description) {
3047
+ lines.push(featureInfo.description);
3048
+ lines.push("");
3049
+ }
3050
+ if (featureInfo.capabilities.length > 0) {
3051
+ lines.push("**Key Capabilities:**");
3052
+ lines.push("");
3053
+ featureInfo.capabilities.forEach((cap) => {
3054
+ lines.push(`- ${cap}`);
3055
+ });
3056
+ lines.push("");
3057
+ }
3058
+ if (featureInfo.useCases.length > 0) {
3059
+ lines.push("**Use Cases:**");
3060
+ lines.push("");
3061
+ featureInfo.useCases.forEach((useCase) => {
3062
+ lines.push(`- ${useCase}`);
3063
+ });
3064
+ lines.push("");
3065
+ }
3066
+ if (index < specs.length - 1) {
3067
+ lines.push("---");
3068
+ lines.push("");
3069
+ }
3070
+ });
3071
+ lines.push("");
3072
+ lines.push("---");
3073
+ lines.push("");
3074
+ lines.push(`*This documentation is auto-generated from product specifications using ${agent}.*`);
3075
+ lines.push("");
3076
+ return lines.join("\n");
3077
+ }
3078
+ /**
3079
+ * Extract user-friendly information from spec content
3080
+ */
3081
+ extractFeatureInfo(content) {
3082
+ const info = {
3083
+ description: "",
3084
+ capabilities: [],
3085
+ useCases: []
3086
+ };
3087
+ const lines = content.split("\n");
3088
+ let inPurpose = false;
3089
+ let inRequirements = false;
3090
+ let inScenarios = false;
3091
+ let currentSection = "";
3092
+ for (let i = 0; i < lines.length; i++) {
3093
+ const line = lines[i];
3094
+ const trimmed = line.trim();
3095
+ if (trimmed.match(/^##+\s+Purpose/i)) {
3096
+ inPurpose = true;
3097
+ inRequirements = false;
3098
+ inScenarios = false;
3099
+ continue;
3100
+ } else if (trimmed.match(/^##+\s+Requirements?/i)) {
3101
+ inPurpose = false;
3102
+ inRequirements = true;
3103
+ inScenarios = false;
3104
+ continue;
3105
+ } else if (trimmed.match(/^##+\s+Scenarios?/i)) {
3106
+ inPurpose = false;
3107
+ inRequirements = false;
3108
+ inScenarios = true;
3109
+ continue;
3110
+ } else if (trimmed.match(/^##+/)) {
3111
+ inPurpose = false;
3112
+ inRequirements = false;
3113
+ inScenarios = false;
3114
+ continue;
3115
+ }
3116
+ if (inPurpose && trimmed && !trimmed.startsWith("#")) {
3117
+ if (!info.description) {
3118
+ info.description = trimmed;
3119
+ } else if (info.description.length < 300) {
3120
+ info.description += " " + trimmed;
3121
+ }
3122
+ }
3123
+ if (inRequirements && trimmed.startsWith("-")) {
3124
+ const requirement = trimmed.replace(/^-\s*/, "").trim();
3125
+ if (requirement) {
3126
+ const userFriendly = this.makeUserFriendly(requirement);
3127
+ info.capabilities.push(userFriendly);
3128
+ }
3129
+ }
3130
+ if (inScenarios && trimmed.match(/^[*-]\s*.+:/)) {
3131
+ const scenario = trimmed.replace(/^[*-]\s*/, "").split(":")[0].trim();
3132
+ if (scenario) {
3133
+ const userFriendly = this.makeUserFriendly(scenario);
3134
+ info.useCases.push(userFriendly);
3135
+ }
3136
+ }
3137
+ }
3138
+ if (!info.description) {
3139
+ for (const line of lines) {
3140
+ const trimmed = line.trim();
3141
+ if (trimmed && !trimmed.startsWith("#") && trimmed.length > 20) {
3142
+ info.description = trimmed;
3143
+ break;
3144
+ }
3145
+ }
3146
+ }
3147
+ return info;
3148
+ }
3149
+ /**
3150
+ * Make technical text more user-friendly
3151
+ */
3152
+ makeUserFriendly(text) {
3153
+ let result = text.replace(/\([^)]*\)/g, "").replace(/\[([^\]]+)\]/g, "$1").replace(/\{([^}]+)\}/g, "$1").replace(/\b(GET|POST|PUT|DELETE|API|REST|HTTP)\b/gi, "").replace(/\b(endpoint|route|method|parameter|query|body)\b/gi, "").trim();
3154
+ if (result) {
3155
+ result = result.charAt(0).toUpperCase() + result.slice(1);
3156
+ }
3157
+ result = result.replace(/\s+/g, " ");
3158
+ return result;
3159
+ }
3160
+ };
3161
+
3162
+ // src/commands/make-test.ts
3163
+ import path13 from "path";
3164
+ import chalk8 from "chalk";
3165
+ import ora6 from "ora";
3166
+ var MakeTestCommand = class {
3167
+ async execute(projectPath = ".", testType = "unit") {
3168
+ const spinner = ora6(`Generating ${testType} tests...`).start();
3169
+ try {
3170
+ const resolvedPath = path13.resolve(projectPath);
3171
+ const specsDir = path13.join(resolvedPath, HERASPEC_DIR_NAME, SPECS_DIR_NAME);
3172
+ const projectMdPath = path13.join(resolvedPath, HERASPEC_DIR_NAME, HERASPEC_MARKERS.PROJECT_MD);
3173
+ if (!await FileSystemUtils.fileExists(specsDir)) {
3174
+ spinner.fail('Specs directory not found. Run "heraspec init" first.');
3175
+ process.exitCode = 1;
3176
+ return;
3177
+ }
3178
+ const testDir = this.getTestDirectory(resolvedPath, testType);
3179
+ await FileSystemUtils.createDirectory(testDir);
3180
+ let projectInfo = {};
3181
+ if (await FileSystemUtils.fileExists(projectMdPath)) {
3182
+ const projectContent = await FileSystemUtils.readFile(projectMdPath);
3183
+ projectInfo = this.extractProjectInfo(projectContent);
3184
+ }
3185
+ const specFiles = await this.findSpecFiles(specsDir);
3186
+ if (specFiles.length === 0) {
3187
+ spinner.warn('No specs found. Create specs first using "heraspec list --specs".');
3188
+ return;
3189
+ }
3190
+ const specs = await this.readSpecs(specFiles);
3191
+ const testFilesCreated = await this.generateTestFiles(specs, testDir, testType, projectInfo);
3192
+ spinner.succeed(`Generated ${testFilesCreated.length} test file(s) in ${testDir}`);
3193
+ console.log(chalk8.gray(`
3194
+ Test type: ${testType}`));
3195
+ console.log(chalk8.gray(`Specs analyzed: ${specFiles.length}`));
3196
+ } catch (error) {
3197
+ spinner.fail(`Error: ${error.message}`);
3198
+ process.exitCode = 1;
3199
+ throw error;
3200
+ }
3201
+ }
3202
+ /**
3203
+ * Get test directory path based on project type and test type
3204
+ */
3205
+ getTestDirectory(projectPath, testType) {
3206
+ const testDirs = {
3207
+ unit: "tests/unit",
3208
+ integration: "tests/integration",
3209
+ e2e: "tests/e2e"
3210
+ };
3211
+ const baseDir = testDirs[testType] || "tests";
3212
+ return path13.join(projectPath, baseDir);
3213
+ }
3214
+ /**
3215
+ * Extract project information from project.md
3216
+ */
3217
+ extractProjectInfo(projectContent) {
3218
+ const info = {};
3219
+ const projectTypeMatch = projectContent.match(/project[-\s]type[s]?[:\s]+([^\n]+)/i);
3220
+ if (projectTypeMatch) {
3221
+ info.projectType = projectTypeMatch[1].trim();
3222
+ }
3223
+ const stackMatch = projectContent.match(/tech[-\s]stack[:\s]+([^\n]+)/i);
3224
+ if (stackMatch) {
3225
+ info.stack = stackMatch[1].split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3226
+ }
3227
+ return info;
3228
+ }
3229
+ /**
3230
+ * Find all spec files recursively
3231
+ */
3232
+ async findSpecFiles(specsDir) {
3233
+ const specFiles = [];
3234
+ await this.findSpecFilesRecursive(specsDir, specFiles);
3235
+ return specFiles.sort();
3236
+ }
3237
+ /**
3238
+ * Recursively find spec files
3239
+ */
3240
+ async findSpecFilesRecursive(dir, files) {
3241
+ const fs2 = await import("fs/promises");
3242
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
3243
+ for (const entry of entries) {
3244
+ const entryPath = path13.join(dir, entry.name);
3245
+ if (entry.isDirectory()) {
3246
+ await this.findSpecFilesRecursive(entryPath, files);
3247
+ } else if (entry.name.endsWith(".md")) {
3248
+ files.push(entryPath);
3249
+ }
3250
+ }
3251
+ }
3252
+ /**
3253
+ * Read all spec files
3254
+ */
3255
+ async readSpecs(specFiles) {
3256
+ const specs = [];
3257
+ for (const specFile of specFiles) {
3258
+ const content = await FileSystemUtils.readFile(specFile);
3259
+ const name = this.extractFeatureName(specFile, content);
3260
+ specs.push({
3261
+ path: specFile,
3262
+ content,
3263
+ name
3264
+ });
3265
+ }
3266
+ return specs;
3267
+ }
3268
+ /**
3269
+ * Extract feature name from spec file
3270
+ */
3271
+ extractFeatureName(filePath, content) {
3272
+ const headingMatch = content.match(/^#+\s+(.+)$/m);
3273
+ if (headingMatch) {
3274
+ return headingMatch[1].trim();
3275
+ }
3276
+ return path13.basename(filePath, ".md");
3277
+ }
3278
+ /**
3279
+ * Generate test files from specs
3280
+ */
3281
+ async generateTestFiles(specs, testDir, testType, projectInfo) {
3282
+ const createdFiles = [];
3283
+ for (const spec of specs) {
3284
+ const testContent = this.generateTestContent(spec, testType, projectInfo);
3285
+ const testFileName = this.getTestFileName(spec.name, testType, projectInfo);
3286
+ const testFilePath = path13.join(testDir, testFileName);
3287
+ await FileSystemUtils.writeFile(testFilePath, testContent);
3288
+ createdFiles.push(testFilePath);
3289
+ }
3290
+ return createdFiles;
3291
+ }
3292
+ /**
3293
+ * Generate test file content
3294
+ */
3295
+ generateTestContent(spec, testType, projectInfo) {
3296
+ const featureInfo = this.extractFeatureInfo(spec.content);
3297
+ const testFramework = this.determineTestFramework(projectInfo);
3298
+ const lines = [];
3299
+ lines.push(`/**`);
3300
+ lines.push(` * ${testType.charAt(0).toUpperCase() + testType.slice(1)} tests for: ${spec.name}`);
3301
+ lines.push(` * Generated from spec: ${path13.basename(spec.path)}`);
3302
+ lines.push(` */`);
3303
+ lines.push("");
3304
+ if (testFramework === "jest") {
3305
+ lines.push(`describe('${spec.name}', () => {`);
3306
+ lines.push(" // TODO: Implement test cases based on spec requirements");
3307
+ lines.push("");
3308
+ if (featureInfo.requirements.length > 0) {
3309
+ featureInfo.requirements.forEach((req, index) => {
3310
+ lines.push(` test('should ${req}', () => {`);
3311
+ lines.push(` // Test implementation needed`);
3312
+ lines.push(` expect(true).toBe(true);`);
3313
+ lines.push(` });`);
3314
+ if (index < featureInfo.requirements.length - 1) {
3315
+ lines.push("");
3316
+ }
3317
+ });
3318
+ }
3319
+ lines.push("});");
3320
+ } else if (testFramework === "phpunit") {
3321
+ lines.push(`class ${this.toPascalCase(spec.name)}Test extends TestCase`);
3322
+ lines.push("{");
3323
+ lines.push(" // TODO: Implement test cases based on spec requirements");
3324
+ lines.push("");
3325
+ if (featureInfo.requirements.length > 0) {
3326
+ featureInfo.requirements.forEach((req, index) => {
3327
+ lines.push(` public function test_${this.toSnakeCase(req)}()`);
3328
+ lines.push(" {");
3329
+ lines.push(" // Test implementation needed");
3330
+ lines.push(" $this->assertTrue(true);");
3331
+ lines.push(" }");
3332
+ if (index < featureInfo.requirements.length - 1) {
3333
+ lines.push("");
3334
+ }
3335
+ });
3336
+ }
3337
+ lines.push("}");
3338
+ } else {
3339
+ lines.push(`// ${testType.charAt(0).toUpperCase() + testType.slice(1)} tests for: ${spec.name}`);
3340
+ lines.push("");
3341
+ lines.push("// TODO: Implement test cases based on spec requirements");
3342
+ lines.push("");
3343
+ if (featureInfo.requirements.length > 0) {
3344
+ featureInfo.requirements.forEach((req) => {
3345
+ lines.push(`// Test: ${req}`);
3346
+ });
3347
+ }
3348
+ }
3349
+ return lines.join("\n");
3350
+ }
3351
+ /**
3352
+ * Determine test framework based on project info
3353
+ */
3354
+ determineTestFramework(projectInfo) {
3355
+ if (projectInfo.stack) {
3356
+ if (projectInfo.stack.some((s) => s.toLowerCase().includes("php"))) {
3357
+ return "phpunit";
3358
+ }
3359
+ if (projectInfo.stack.some((s) => s.toLowerCase().includes("node") || s.toLowerCase().includes("javascript"))) {
3360
+ return "jest";
3361
+ }
3362
+ }
3363
+ return "jest";
3364
+ }
3365
+ /**
3366
+ * Get test file name
3367
+ */
3368
+ getTestFileName(featureName, testType, projectInfo) {
3369
+ const framework = this.determineTestFramework(projectInfo);
3370
+ const baseName = this.toKebabCase(featureName);
3371
+ if (framework === "phpunit") {
3372
+ return `${this.toPascalCase(featureName)}Test.php`;
3373
+ } else if (framework === "jest") {
3374
+ return `${baseName}.test.js`;
3375
+ }
3376
+ return `${baseName}.test.js`;
3377
+ }
3378
+ /**
3379
+ * Extract feature info from spec content
3380
+ */
3381
+ extractFeatureInfo(content) {
3382
+ const info = {
3383
+ requirements: []
3384
+ };
3385
+ const lines = content.split("\n");
3386
+ let inRequirements = false;
3387
+ for (const line of lines) {
3388
+ const trimmed = line.trim();
3389
+ if (trimmed.match(/^##+\s+Requirements?/i)) {
3390
+ inRequirements = true;
3391
+ continue;
3392
+ } else if (trimmed.match(/^##+/) && inRequirements) {
3393
+ break;
3394
+ }
3395
+ if (inRequirements && trimmed.startsWith("-")) {
3396
+ const requirement = trimmed.replace(/^-\s*/, "").trim();
3397
+ if (requirement) {
3398
+ info.requirements.push(requirement);
3399
+ }
3400
+ }
3401
+ }
3402
+ return info;
3403
+ }
3404
+ /**
3405
+ * Convert to kebab-case
3406
+ */
3407
+ toKebabCase(str) {
3408
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
3409
+ }
3410
+ /**
3411
+ * Convert to PascalCase
3412
+ */
3413
+ toPascalCase(str) {
3414
+ return str.split(/[^a-zA-Z0-9]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
3415
+ }
3416
+ /**
3417
+ * Convert to snake_case
3418
+ */
3419
+ toSnakeCase(str) {
3420
+ return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
3421
+ }
3422
+ };
3423
+
3424
+ // src/commands/make-code.ts
3425
+ import path14 from "path";
3426
+ import chalk9 from "chalk";
3427
+ import ora7 from "ora";
3428
+ var MakeCodeCommand = class {
3429
+ async execute(projectPath = ".", specName) {
3430
+ const spinner = ora7("Generating code from specs...").start();
3431
+ try {
3432
+ const resolvedPath = path14.resolve(projectPath);
3433
+ const specsDir = path14.join(resolvedPath, HERASPEC_DIR_NAME, SPECS_DIR_NAME);
3434
+ const projectMdPath = path14.join(resolvedPath, HERASPEC_DIR_NAME, HERASPEC_MARKERS.PROJECT_MD);
3435
+ if (!await FileSystemUtils.fileExists(specsDir)) {
3436
+ spinner.fail('Specs directory not found. Run "heraspec init" first.');
3437
+ process.exitCode = 1;
3438
+ return;
3439
+ }
3440
+ let projectInfo = {};
3441
+ if (await FileSystemUtils.fileExists(projectMdPath)) {
3442
+ const projectContent = await FileSystemUtils.readFile(projectMdPath);
3443
+ projectInfo = this.extractProjectInfo(projectContent);
3444
+ }
3445
+ let specFiles = [];
3446
+ if (specName) {
3447
+ const specPath = this.findSpecPath(specsDir, specName);
3448
+ if (specPath && await FileSystemUtils.fileExists(specPath)) {
3449
+ specFiles = [specPath];
3450
+ } else {
3451
+ spinner.fail(`Spec "${specName}" not found`);
3452
+ process.exitCode = 1;
3453
+ return;
3454
+ }
3455
+ } else {
3456
+ specFiles = await this.findSpecFiles(specsDir);
3457
+ }
3458
+ if (specFiles.length === 0) {
3459
+ spinner.warn('No specs found. Create specs first using "heraspec list --specs".');
3460
+ return;
3461
+ }
3462
+ const specs = await this.readSpecs(specFiles);
3463
+ const generatedFiles = await this.generateCodeFiles(specs, resolvedPath, projectInfo);
3464
+ spinner.succeed(`Generated ${generatedFiles.length} code file(s)`);
3465
+ console.log(chalk9.gray(`
3466
+ Specs analyzed: ${specFiles.length}`));
3467
+ generatedFiles.forEach((file) => {
3468
+ console.log(chalk9.cyan(` \u2713 ${path14.relative(resolvedPath, file)}`));
3469
+ });
3470
+ } catch (error) {
3471
+ spinner.fail(`Error: ${error.message}`);
3472
+ process.exitCode = 1;
3473
+ throw error;
3474
+ }
3475
+ }
3476
+ extractProjectInfo(content) {
3477
+ const projectTypeMatch = content.match(/project[-\s]type[s]?[:\s]+([^\n]+)/i);
3478
+ const stackMatch = content.match(/tech[-\s]stack[:\s]+([^\n]+)/i);
3479
+ return {
3480
+ projectType: projectTypeMatch ? projectTypeMatch[1].trim().split(",")[0].trim() : void 0,
3481
+ stack: stackMatch ? stackMatch[1].split(",").map((s) => s.trim()).filter(Boolean) : void 0
3482
+ };
3483
+ }
3484
+ findSpecPath(specsDir, specName) {
3485
+ const directPath = path14.join(specsDir, `${specName}.md`);
3486
+ const nestedPath = path14.join(specsDir, specName, "spec.md");
3487
+ return directPath;
3488
+ }
3489
+ async findSpecFiles(specsDir) {
3490
+ const specFiles = [];
3491
+ await this.findSpecFilesRecursive(specsDir, specFiles);
3492
+ return specFiles.sort();
3493
+ }
3494
+ async findSpecFilesRecursive(dir, files) {
3495
+ const fs2 = await import("fs/promises");
3496
+ const entries = await fs2.readdir(dir, { withFileTypes: true });
3497
+ for (const entry of entries) {
3498
+ const entryPath = path14.join(dir, entry.name);
3499
+ if (entry.isDirectory()) {
3500
+ await this.findSpecFilesRecursive(entryPath, files);
3501
+ } else if (entry.name.endsWith(".md") && entry.name !== "README.md") {
3502
+ files.push(entryPath);
3503
+ }
3504
+ }
3505
+ }
3506
+ async readSpecs(specFiles) {
3507
+ const specs = [];
3508
+ for (const specFile of specFiles) {
3509
+ const content = await FileSystemUtils.readFile(specFile);
3510
+ const parser = new MarkdownParser(content);
3511
+ const specName = path14.basename(specFile, ".md");
3512
+ try {
3513
+ const parsed = parser.parseSpec(specName);
3514
+ specs.push({
3515
+ path: specFile,
3516
+ content,
3517
+ name: parsed.name,
3518
+ parsed
3519
+ });
3520
+ } catch (error) {
3521
+ console.warn(chalk9.yellow(`Warning: Could not parse spec file ${specFile}. Skipping.`));
3522
+ }
3523
+ }
3524
+ return specs;
3525
+ }
3526
+ async generateCodeFiles(specs, projectPath, projectInfo) {
3527
+ const generatedFiles = [];
3528
+ const projectType = projectInfo.projectType || "generic-webapp";
3529
+ for (const spec of specs) {
3530
+ const codeContent = this.generateCodeContent(spec, projectType, projectInfo);
3531
+ const outputPath = this.getOutputPath(projectPath, spec.name, projectType);
3532
+ await FileSystemUtils.createDirectory(path14.dirname(outputPath));
3533
+ await FileSystemUtils.writeFile(outputPath, codeContent);
3534
+ generatedFiles.push(outputPath);
3535
+ }
3536
+ return generatedFiles;
3537
+ }
3538
+ getOutputPath(projectPath, specName, projectType) {
3539
+ const sanitizedName = specName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
3540
+ if (projectType.includes("wordpress-plugin")) {
3541
+ return path14.join(projectPath, "includes", `class-${sanitizedName}.php`);
3542
+ } else if (projectType.includes("perfex-module")) {
3543
+ return path14.join(projectPath, "modules", sanitizedName, `${sanitizedName}.php`);
3544
+ } else if (projectType.includes("laravel")) {
3545
+ return path14.join(projectPath, "app", "Services", `${this.toPascalCase(sanitizedName)}Service.php`);
3546
+ } else if (projectType.includes("node")) {
3547
+ return path14.join(projectPath, "src", "services", `${sanitizedName}.js`);
3548
+ } else {
3549
+ return path14.join(projectPath, "src", `${sanitizedName}.ts`);
3550
+ }
3551
+ }
3552
+ generateCodeContent(spec, projectType, projectInfo) {
3553
+ const lines = [];
3554
+ const className = this.toPascalCase(spec.name.replace(/[^a-zA-Z0-9]/g, ""));
3555
+ lines.push(`/**`);
3556
+ lines.push(` * ${spec.name}`);
3557
+ lines.push(` * Generated from spec: ${path14.basename(spec.path)}`);
3558
+ lines.push(` *`);
3559
+ lines.push(` * ${spec.parsed.overview || "Generated code skeleton"}`);
3560
+ lines.push(` */`);
3561
+ lines.push("");
3562
+ if (projectType.includes("wordpress-plugin") || projectType.includes("perfex-module")) {
3563
+ lines.push(`<?php`);
3564
+ lines.push("");
3565
+ lines.push(`class ${className} {`);
3566
+ lines.push("");
3567
+ spec.parsed.requirements.forEach((req) => {
3568
+ const methodName = this.toCamelCase(req.name.replace(/[^a-zA-Z0-9]/g, ""));
3569
+ lines.push(` /**`);
3570
+ lines.push(` * ${req.description || req.name}`);
3571
+ lines.push(` */`);
3572
+ lines.push(` public function ${methodName}() {`);
3573
+ lines.push(` // TODO: Implement ${req.name}`);
3574
+ lines.push(` }`);
3575
+ lines.push("");
3576
+ });
3577
+ lines.push("}");
3578
+ } else if (projectType.includes("laravel")) {
3579
+ lines.push(`<?php`);
3580
+ lines.push("");
3581
+ lines.push(`namespace App\\Services;`);
3582
+ lines.push("");
3583
+ lines.push(`class ${className}Service {`);
3584
+ lines.push("");
3585
+ spec.parsed.requirements.forEach((req) => {
3586
+ const methodName = this.toCamelCase(req.name.replace(/[^a-zA-Z0-9]/g, ""));
3587
+ lines.push(` /**`);
3588
+ lines.push(` * ${req.description || req.name}`);
3589
+ lines.push(` */`);
3590
+ lines.push(` public function ${methodName}() {`);
3591
+ lines.push(` // TODO: Implement ${req.name}`);
3592
+ lines.push(` }`);
3593
+ lines.push("");
3594
+ });
3595
+ lines.push("}");
3596
+ } else if (projectType.includes("node")) {
3597
+ lines.push(`/**`);
3598
+ lines.push(` * ${spec.name} service`);
3599
+ lines.push(` */`);
3600
+ lines.push("");
3601
+ lines.push(`class ${className} {`);
3602
+ lines.push("");
3603
+ spec.parsed.requirements.forEach((req) => {
3604
+ const methodName = this.toCamelCase(req.name.replace(/[^a-zA-Z0-9]/g, ""));
3605
+ lines.push(` /**`);
3606
+ lines.push(` * ${req.description || req.name}`);
3607
+ lines.push(` */`);
3608
+ lines.push(` ${methodName}() {`);
3609
+ lines.push(` // TODO: Implement ${req.name}`);
3610
+ lines.push(` }`);
3611
+ lines.push("");
3612
+ });
3613
+ lines.push("}");
3614
+ lines.push("");
3615
+ lines.push(`module.exports = ${className};`);
3616
+ } else {
3617
+ lines.push(`export class ${className} {`);
3618
+ lines.push("");
3619
+ spec.parsed.requirements.forEach((req) => {
3620
+ const methodName = this.toCamelCase(req.name.replace(/[^a-zA-Z0-9]/g, ""));
3621
+ lines.push(` /**`);
3622
+ lines.push(` * ${req.description || req.name}`);
3623
+ lines.push(` */`);
3624
+ lines.push(` ${methodName}(): void {`);
3625
+ lines.push(` // TODO: Implement ${req.name}`);
3626
+ lines.push(` }`);
3627
+ lines.push("");
3628
+ });
3629
+ lines.push("}");
3630
+ }
3631
+ lines.push("");
3632
+ lines.push(`// Generated by HeraSpec - Update this file based on implementation requirements`);
3633
+ return lines.join("\n");
3634
+ }
3635
+ toPascalCase(str) {
3636
+ return str.replace(/[^a-zA-Z0-9]/g, " ").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
3637
+ }
3638
+ toCamelCase(str) {
3639
+ const pascal = this.toPascalCase(str);
3640
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
3641
+ }
3642
+ };
3643
+
3644
+ // src/cli/index.ts
3645
+ var require3 = createRequire2(import.meta.url);
3646
+ var __filename2 = fileURLToPath2(import.meta.url);
3647
+ var __dirname2 = dirname2(__filename2);
3648
+ var packageJsonPath = join2(__dirname2, "..", "package.json");
3649
+ var { version } = require3(packageJsonPath);
3650
+ var program = new Command();
3651
+ program.name("heraspec").description("Universal spec-first development framework + CLI").version(version);
3652
+ program.command("init [path]").description("Initialize HeraSpec in your project").action(async (targetPath = ".") => {
3653
+ try {
3654
+ const initCommand = new InitCommand();
3655
+ await initCommand.execute(targetPath);
3656
+ } catch (error) {
3657
+ console.error(`Error: ${error.message}`);
3658
+ process.exit(1);
3659
+ }
3660
+ });
3661
+ program.command("list").description("List items (changes by default). Use --specs to list specs.").option("--specs", "List specs instead of changes").option("--changes", "List changes explicitly (default)").action(async (options) => {
3662
+ try {
3663
+ const listCommand = new ListCommand();
3664
+ const mode = options?.specs ? "specs" : "changes";
3665
+ await listCommand.execute(".", mode);
3666
+ } catch (error) {
3667
+ console.error(`Error: ${error.message}`);
3668
+ process.exit(1);
3669
+ }
3670
+ });
3671
+ program.command("show [item-name]").description("Show a change or spec").action(async (itemName) => {
3672
+ try {
3673
+ const showCommand = new ShowCommand();
3674
+ await showCommand.execute(itemName);
3675
+ } catch (error) {
3676
+ console.error(`Error: ${error.message}`);
3677
+ process.exit(1);
3678
+ }
3679
+ });
3680
+ program.command("validate [item-name]").description("Validate a change or spec").option("--strict", "Enable strict validation mode").action(async (itemName, options) => {
3681
+ try {
3682
+ const validateCommand = new ValidateCommand();
3683
+ await validateCommand.execute(itemName, options);
3684
+ } catch (error) {
3685
+ console.error(`Error: ${error.message}`);
3686
+ process.exit(1);
3687
+ }
3688
+ });
3689
+ program.command("archive [change-name]").description("Archive a completed change and update main specs").option("-y, --yes", "Skip confirmation prompts").action(async (changeName, options) => {
3690
+ try {
3691
+ const archiveCommand = new ArchiveCommand();
3692
+ await archiveCommand.execute(changeName, options);
3693
+ } catch (error) {
3694
+ console.error(`Error: ${error.message}`);
3695
+ process.exit(1);
3696
+ }
3697
+ });
3698
+ program.command("restore [archive-name]").description("Restore an archived change back to active changes").option("-y, --yes", "Skip confirmation prompts").action(async (archiveName, options) => {
3699
+ try {
3700
+ const restoreCommand = new RestoreCommand();
3701
+ await restoreCommand.execute(archiveName, options);
3702
+ } catch (error) {
3703
+ console.error(`Error: ${error.message}`);
3704
+ process.exit(1);
3705
+ }
3706
+ });
3707
+ program.command("view").description("Display an interactive dashboard of specs and changes").action(async () => {
3708
+ try {
3709
+ const listCommand = new ListCommand();
3710
+ console.log("\n\u{1F4CA} HeraSpec Dashboard\n");
3711
+ console.log("\u2550".repeat(60));
3712
+ await listCommand.execute(".", "changes");
3713
+ await listCommand.execute(".", "specs");
3714
+ } catch (error) {
3715
+ console.error(`Error: ${error.message}`);
3716
+ process.exit(1);
3717
+ }
3718
+ });
3719
+ program.command("helper").description("Show usage guide, example prompts, and workflow instructions").action(async () => {
3720
+ try {
3721
+ const helperCommand = new HelperCommand();
3722
+ await helperCommand.execute();
3723
+ } catch (error) {
3724
+ console.error(`Error: ${error.message}`);
3725
+ process.exit(1);
3726
+ }
3727
+ });
3728
+ var makeCmd = program.command("make").description("Generate project artifacts");
3729
+ makeCmd.command("docs").description("Generate project documentation from specs").option("--agent <agent>", "Specify AI agent for documentation (default: chatgpt)", "chatgpt").action(async (options) => {
3730
+ try {
3731
+ const makeDocsCommand = new MakeDocsCommand();
3732
+ await makeDocsCommand.execute(".", options?.agent || "chatgpt");
3733
+ } catch (error) {
3734
+ console.error(`Error: ${error.message}`);
3735
+ process.exit(1);
3736
+ }
3737
+ });
3738
+ makeCmd.command("test").description("Generate test cases from specs").option("--type <type>", "Test type: unit, integration, e2e (default: unit)", "unit").action(async (options) => {
3739
+ try {
3740
+ const makeTestCommand = new MakeTestCommand();
3741
+ await makeTestCommand.execute(".", options?.type || "unit");
3742
+ } catch (error) {
3743
+ console.error(`Error: ${error.message}`);
3744
+ process.exit(1);
3745
+ }
3746
+ });
3747
+ makeCmd.command("code").description("Generate code skeletons from specs").argument("[spec-name]", "Specific spec to generate code for (optional)").action(async (specName) => {
3748
+ try {
3749
+ const makeCodeCommand = new MakeCodeCommand();
3750
+ await makeCodeCommand.execute(".", specName);
3751
+ } catch (error) {
3752
+ console.error(`Error: ${error.message}`);
3753
+ process.exit(1);
3754
+ }
3755
+ });
3756
+ program.command("suggest").description("Analyze project and suggest new features").action(async () => {
3757
+ try {
3758
+ const suggestCommand = new SuggestCommand();
3759
+ await suggestCommand.execute(".");
3760
+ } catch (error) {
3761
+ console.error(`Error: ${error.message}`);
3762
+ process.exit(1);
3763
+ }
3764
+ });
3765
+ var skillCmd = program.command("skill").description("Manage and view skills");
3766
+ skillCmd.command("list").description("List all available skills").action(async () => {
3767
+ try {
3768
+ const skillCommand = new SkillCommand();
3769
+ await skillCommand.list(".");
3770
+ } catch (error) {
3771
+ console.error(`Error: ${error.message}`);
3772
+ process.exit(1);
3773
+ }
3774
+ });
3775
+ skillCmd.command("show <skill-name>").description("Show detailed information about a skill").option("--project-type <type>", "Specify project type for project-specific skills").action(async (skillName, options) => {
3776
+ try {
3777
+ const skillCommand = new SkillCommand();
3778
+ await skillCommand.show(skillName, options?.projectType, ".");
3779
+ } catch (error) {
3780
+ console.error(`Error: ${error.message}`);
3781
+ process.exit(1);
3782
+ }
3783
+ });
3784
+ skillCmd.command("repair").description("Repair skills structure to match HeraSpec standards").action(async () => {
3785
+ try {
3786
+ const skillCommand = new SkillCommand();
3787
+ await skillCommand.repair(".");
3788
+ } catch (error) {
3789
+ console.error(`Error: ${error.message}`);
3790
+ process.exit(1);
3791
+ }
3792
+ });
3793
+ skillCmd.command("add <skill-name>").description("Add a default skill to the project from HeraSpec templates").option("--project-type <type>", "Specify project type for project-specific skills").action(async (skillName, options) => {
3794
+ try {
3795
+ const skillCommand = new SkillCommand();
3796
+ await skillCommand.add(skillName, options?.projectType, ".");
3797
+ } catch (error) {
3798
+ console.error(`Error: ${error.message}`);
3799
+ process.exit(1);
3800
+ }
3801
+ });
3802
+ program.parse();
3803
+ //# sourceMappingURL=heraspec.js.map