@rashidee/co2 1.2.5
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.
- package/dist/index.js +25449 -0
- package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
- package/drizzle/0001_wise_wolf_cub.sql +13 -0
- package/drizzle/0002_secret_spyke.sql +16 -0
- package/drizzle/0003_workspace_config_singleton.sql +16 -0
- package/drizzle/0004_terminal_claim_v120.sql +11 -0
- package/drizzle/0005_brainstorm_v124.sql +10 -0
- package/drizzle/0006_project_v140.sql +6 -0
- package/drizzle/0007_prd_wizard_v124.sql +14 -0
- package/drizzle/0008_workflow_artifact_v126.sql +24 -0
- package/drizzle/0009_project_import_v150.sql +7 -0
- package/drizzle/0010_quality_v151.sql +3 -0
- package/drizzle/meta/0000_snapshot.json +1335 -0
- package/drizzle/meta/0001_snapshot.json +1416 -0
- package/drizzle/meta/0002_snapshot.json +1510 -0
- package/drizzle/meta/_journal.json +83 -0
- package/package.json +41 -0
- package/plugin/.claude-plugin/marketplace.json +36 -0
- package/plugin/.claude-plugin/plugin.json +22 -0
- package/plugin/README.md +625 -0
- package/plugin/SKILLS.md +350 -0
- package/plugin/co2.svg +5 -0
- package/plugin/commands/brainstorm-loop.md +129 -0
- package/plugin/skills/conductor-defect/SKILL.md +898 -0
- package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
- package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
- package/plugin/skills/depgen-k8s/SKILL.md +468 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
- package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
- package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
- package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
- package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
- package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
- package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
- package/plugin/skills/modelgen-relational/SKILL.md +707 -0
- package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
- package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
- package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
- package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
- package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
- package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
- package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
- package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
- package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
- package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
- package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
- package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
- package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
- package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
- package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
- package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
- package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
- package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
- package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
- package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
- package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
- package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
- package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
- package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
- package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
- package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
- package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
- package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
- package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
- package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
- package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
- package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
- package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
- package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
- package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
- package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
- package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
- package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
- package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
- package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
- package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
- package/plugin/skills/testgen-functional/SKILL.md +1381 -0
- package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
- package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
- package/plugin/skills/util-projectinit/SKILL.md +307 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
- package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
- package/plugin/skills/util-projectsync/SKILL.md +501 -0
- package/plugin/skills/util-updprd/SKILL.md +324 -0
- package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
- package/plugin/skills/util-ustagger/SKILL.md +154 -0
- package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
- package/static/assets/arc-DRKw7sjs.js +1 -0
- package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
- package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
- package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
- package/static/assets/channel-C2nVbuwv.js +1 -0
- package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
- package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
- package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
- package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
- package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
- package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
- package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
- package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
- package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
- package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
- package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
- package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
- package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
- package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
- package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
- package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
- package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
- package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
- package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
- package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
- package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
- package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
- package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
- package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
- package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
- package/static/assets/graph-C9eacEi8.js +1 -0
- package/static/assets/index-LZSQkLE2.css +41 -0
- package/static/assets/index-y7XDSgMG.js +495 -0
- package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
- package/static/assets/init-Gi6I4Gst.js +1 -0
- package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
- package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
- package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
- package/static/assets/katex-C5jXJg4s.js +257 -0
- package/static/assets/layout-DEXfKzaS.js +1 -0
- package/static/assets/linear-CLO--ryk.js +1 -0
- package/static/assets/map-Czzmt4hB.js +1 -0
- package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
- package/static/assets/ordinal-Cboi1Yqb.js +1 -0
- package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
- package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
- package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
- package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
- package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
- package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
- package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
- package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
- package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
- package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
- package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
- package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
- package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
- package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
- package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
- package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
- package/static/index.html +24 -0
|
@@ -0,0 +1,1391 @@
|
|
|
1
|
+
# Specification Template — TypeScript CLI Application
|
|
2
|
+
|
|
3
|
+
This is the authoritative template for the generated specification. The specification is
|
|
4
|
+
split into **two types of files**:
|
|
5
|
+
|
|
6
|
+
1. **`SPECIFICATION.md`** (root) — Table of Contents, shared infrastructure, and all
|
|
7
|
+
application-level sections. Generated once per application.
|
|
8
|
+
2. **`<command-name>/SPEC.md`** (per-command) — Self-contained command blueprint.
|
|
9
|
+
Generated once per command from PRD.md.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
specification/
|
|
13
|
+
├── SPECIFICATION.md
|
|
14
|
+
├── init/
|
|
15
|
+
│ └── SPEC.md
|
|
16
|
+
├── deploy/
|
|
17
|
+
│ └── SPEC.md
|
|
18
|
+
└── ...
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Placeholders use `{{VARIABLE}}` syntax and must be replaced with actual values.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Part A: Root SPECIFICATION.md
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Table of Contents
|
|
30
|
+
|
|
31
|
+
Generate a TOC with anchor links. Include a **Commands** section linking to each
|
|
32
|
+
command's `SPEC.md`:
|
|
33
|
+
|
|
34
|
+
```markdown
|
|
35
|
+
## Table of Contents
|
|
36
|
+
|
|
37
|
+
### Shared Infrastructure
|
|
38
|
+
- [1. Project Overview](#1-project-overview)
|
|
39
|
+
- [2. package.json Configuration](#2-packagejson-configuration)
|
|
40
|
+
- ...
|
|
41
|
+
|
|
42
|
+
### Commands
|
|
43
|
+
- [init](init/SPEC.md)
|
|
44
|
+
- [deploy](deploy/SPEC.md)
|
|
45
|
+
- [config](config/SPEC.md)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Section 1: Project Overview
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
# {{APPLICATION_NAME}} — Technical Specification
|
|
54
|
+
|
|
55
|
+
## 1. Project Overview
|
|
56
|
+
|
|
57
|
+
**Application Name**: {{APPLICATION_NAME}}
|
|
58
|
+
**Package Name**: {{PACKAGE_NAME}} (e.g., @scope/my-tool or my-tool)
|
|
59
|
+
**Binary Name**: {{BINARY_NAME}} (the executable command, e.g., my-tool)
|
|
60
|
+
**Node.js Version**: 22.x LTS (minimum)
|
|
61
|
+
**TypeScript**: 5.x
|
|
62
|
+
**Description**: {{APP_DESCRIPTION}}
|
|
63
|
+
**Versions Covered**: v0.1.0 — v{{LATEST_VERSION}}
|
|
64
|
+
|
|
65
|
+
### Optional Components (Auto-Determined)
|
|
66
|
+
**Prompts**: {{yes|no}}
|
|
67
|
+
**User Config**: {{yes|no}}
|
|
68
|
+
**Project Config**: {{yes|no}}
|
|
69
|
+
**Shell Execution**: {{yes|no}}
|
|
70
|
+
**HTTP Client**: {{yes|no}}
|
|
71
|
+
**Auto-update**: {{yes|no}}
|
|
72
|
+
**Binary Packaging**: {{yes|no}}
|
|
73
|
+
**Table Output**: {{yes|no}}
|
|
74
|
+
|
|
75
|
+
### Technology Stack
|
|
76
|
+
(Render core stack table from SKILL.md, plus selected optional versions)
|
|
77
|
+
|
|
78
|
+
### Commands
|
|
79
|
+
| Command | Description | Aliases | Version |
|
|
80
|
+
|----------|----------------------------|---------|---------|
|
|
81
|
+
| init | Initialize a new project | i | v0.1.0 |
|
|
82
|
+
| deploy | Deploy to environment | d | v0.1.0 |
|
|
83
|
+
| config | Manage user configuration | | v0.1.0 |
|
|
84
|
+
|
|
85
|
+
### Distribution Strategy
|
|
86
|
+
**npm package**: `npm install -g {{PACKAGE_NAME}}`
|
|
87
|
+
[If Binary Packaging = yes] **Standalone binaries**: GitHub Releases — Windows, macOS, Linux
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Section 2: package.json Configuration
|
|
93
|
+
|
|
94
|
+
Generate a complete `package.json` inside a fenced code block. Must be copy-pasteable.
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"name": "{{PACKAGE_NAME}}",
|
|
99
|
+
"version": "0.1.0",
|
|
100
|
+
"description": "{{APP_DESCRIPTION}}",
|
|
101
|
+
"type": "module",
|
|
102
|
+
"license": "MIT",
|
|
103
|
+
"engines": {
|
|
104
|
+
"node": ">=22.0.0"
|
|
105
|
+
},
|
|
106
|
+
"bin": {
|
|
107
|
+
"{{BINARY_NAME}}": "./dist/cli.js"
|
|
108
|
+
},
|
|
109
|
+
"files": [
|
|
110
|
+
"dist",
|
|
111
|
+
"README.md"
|
|
112
|
+
],
|
|
113
|
+
"scripts": {
|
|
114
|
+
"dev": "tsx watch src/cli.ts",
|
|
115
|
+
"build": "tsup",
|
|
116
|
+
"typecheck": "tsc --noEmit",
|
|
117
|
+
"test": "vitest run",
|
|
118
|
+
"test:watch": "vitest",
|
|
119
|
+
"lint": "eslint src --ext .ts",
|
|
120
|
+
"prepublishOnly": "npm run build && npm run typecheck && npm run test"
|
|
121
|
+
},
|
|
122
|
+
"dependencies": {
|
|
123
|
+
"commander": "^12.0.0",
|
|
124
|
+
"chalk": "^5.0.0",
|
|
125
|
+
"ora": "^8.0.0",
|
|
126
|
+
"zod": "^3.0.0"
|
|
127
|
+
// [conditional deps listed per section below]
|
|
128
|
+
},
|
|
129
|
+
"devDependencies": {
|
|
130
|
+
"typescript": "^5.0.0",
|
|
131
|
+
"tsup": "^8.0.0",
|
|
132
|
+
"vitest": "^2.0.0",
|
|
133
|
+
"@types/node": "^22.0.0",
|
|
134
|
+
"tsx": "^4.0.0",
|
|
135
|
+
"eslint": "^9.0.0",
|
|
136
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
137
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**[If Prompts = yes]** add to `dependencies`:
|
|
143
|
+
```json
|
|
144
|
+
"@inquirer/prompts": "^7.0.0"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**[If User Config = yes]** add to `dependencies`:
|
|
148
|
+
```json
|
|
149
|
+
"conf": "^13.0.0"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**[If Project Config = yes]** add to `dependencies`:
|
|
153
|
+
```json
|
|
154
|
+
"cosmiconfig": "^9.0.0"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**[If Shell Execution = yes]** add to `dependencies`:
|
|
158
|
+
```json
|
|
159
|
+
"execa": "^9.0.0"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**[If HTTP Client = yes]** add to `dependencies`:
|
|
163
|
+
```json
|
|
164
|
+
"got": "^14.0.0"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**[If Auto-update = yes]** add to `dependencies`:
|
|
168
|
+
```json
|
|
169
|
+
"update-notifier": "^7.0.0"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**[If Table Output = yes]** add to `dependencies`:
|
|
173
|
+
```json
|
|
174
|
+
"cli-table3": "^0.6.0",
|
|
175
|
+
"@types/cli-table3": "^0.6.0"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**[If Local Database = yes]** add to `dependencies` and `devDependencies`:
|
|
179
|
+
```json
|
|
180
|
+
// dependencies:
|
|
181
|
+
"better-sqlite3": "^11.0.0",
|
|
182
|
+
"drizzle-orm": "^0.36.0",
|
|
183
|
+
"env-paths": "^3.0.0",
|
|
184
|
+
|
|
185
|
+
// devDependencies:
|
|
186
|
+
"@types/better-sqlite3": "^7.0.0",
|
|
187
|
+
"drizzle-kit": "^0.28.0"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
And add scripts:
|
|
191
|
+
```json
|
|
192
|
+
"db:generate": "drizzle-kit generate",
|
|
193
|
+
"db:migrate": "drizzle-kit migrate",
|
|
194
|
+
"db:studio": "drizzle-kit studio"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**[If Binary Packaging = yes]** add to `devDependencies` and scripts:
|
|
198
|
+
```json
|
|
199
|
+
// devDependencies:
|
|
200
|
+
"@yao-pkg/pkg": "^5.0.0"
|
|
201
|
+
|
|
202
|
+
// scripts:
|
|
203
|
+
"pkg:linux": "pkg . --target node22-linux-x64 --output dist/bin/{{BINARY_NAME}}-linux",
|
|
204
|
+
"pkg:macos": "pkg . --target node22-macos-x64 --output dist/bin/{{BINARY_NAME}}-macos",
|
|
205
|
+
"pkg:windows": "pkg . --target node22-win-x64 --output dist/bin/{{BINARY_NAME}}.exe",
|
|
206
|
+
"pkg:all": "npm run pkg:linux && npm run pkg:macos && npm run pkg:windows"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Section 3: TypeScript & Build Configuration
|
|
212
|
+
|
|
213
|
+
### 3.1 tsconfig.json
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"compilerOptions": {
|
|
218
|
+
"target": "ES2022",
|
|
219
|
+
"module": "NodeNext",
|
|
220
|
+
"moduleResolution": "NodeNext",
|
|
221
|
+
"lib": ["ES2022"],
|
|
222
|
+
"outDir": "./dist",
|
|
223
|
+
"rootDir": "./src",
|
|
224
|
+
"strict": true,
|
|
225
|
+
"noUncheckedIndexedAccess": true,
|
|
226
|
+
"exactOptionalPropertyTypes": true,
|
|
227
|
+
"declaration": true,
|
|
228
|
+
"declarationMap": true,
|
|
229
|
+
"sourceMap": true,
|
|
230
|
+
"skipLibCheck": true,
|
|
231
|
+
"esModuleInterop": false,
|
|
232
|
+
"forceConsistentCasingInFileNames": true,
|
|
233
|
+
"resolveJsonModule": true
|
|
234
|
+
},
|
|
235
|
+
"include": ["src/**/*"],
|
|
236
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 3.2 tsup.config.ts
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { defineConfig } from 'tsup'
|
|
244
|
+
|
|
245
|
+
export default defineConfig({
|
|
246
|
+
entry: ['src/cli.ts'],
|
|
247
|
+
format: ['esm'],
|
|
248
|
+
target: 'node22',
|
|
249
|
+
platform: 'node',
|
|
250
|
+
clean: true,
|
|
251
|
+
sourcemap: process.env.NODE_ENV !== 'production',
|
|
252
|
+
minify: false,
|
|
253
|
+
shims: false,
|
|
254
|
+
dts: false,
|
|
255
|
+
banner: {
|
|
256
|
+
js: '#!/usr/bin/env node',
|
|
257
|
+
},
|
|
258
|
+
esbuildOptions(options) {
|
|
259
|
+
options.mainFields = ['module', 'main']
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### 3.3 .eslintrc.json
|
|
265
|
+
|
|
266
|
+
```json
|
|
267
|
+
{
|
|
268
|
+
"root": true,
|
|
269
|
+
"parser": "@typescript-eslint/parser",
|
|
270
|
+
"parserOptions": { "project": "./tsconfig.json" },
|
|
271
|
+
"plugins": ["@typescript-eslint"],
|
|
272
|
+
"extends": [
|
|
273
|
+
"eslint:recommended",
|
|
274
|
+
"plugin:@typescript-eslint/strict-type-checked"
|
|
275
|
+
],
|
|
276
|
+
"rules": {
|
|
277
|
+
"@typescript-eslint/no-explicit-any": "error",
|
|
278
|
+
"@typescript-eslint/no-floating-promises": "error",
|
|
279
|
+
"no-console": ["error", { "allow": [] }]
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 3.4 .gitignore
|
|
285
|
+
|
|
286
|
+
```gitignore
|
|
287
|
+
# Build output
|
|
288
|
+
dist/
|
|
289
|
+
*.d.ts
|
|
290
|
+
|
|
291
|
+
# Binaries
|
|
292
|
+
dist/bin/
|
|
293
|
+
|
|
294
|
+
# Dependencies
|
|
295
|
+
node_modules/
|
|
296
|
+
|
|
297
|
+
# Testing
|
|
298
|
+
coverage/
|
|
299
|
+
|
|
300
|
+
# Environment
|
|
301
|
+
.env
|
|
302
|
+
.env.*
|
|
303
|
+
|
|
304
|
+
# OS
|
|
305
|
+
.DS_Store
|
|
306
|
+
Thumbs.db
|
|
307
|
+
|
|
308
|
+
# IDE
|
|
309
|
+
.vscode/
|
|
310
|
+
.idea/
|
|
311
|
+
*.iml
|
|
312
|
+
|
|
313
|
+
# Logs
|
|
314
|
+
*.log
|
|
315
|
+
logs/
|
|
316
|
+
|
|
317
|
+
# Local development database [If Local Database = yes]
|
|
318
|
+
dev.db
|
|
319
|
+
dev.db-shm
|
|
320
|
+
dev.db-wal
|
|
321
|
+
.drizzle/
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Section 4: Application Entry Point
|
|
327
|
+
|
|
328
|
+
`src/cli.ts` is the Commander.js program root. It must:
|
|
329
|
+
1. Inject the shebang (`#!/usr/bin/env node`) via tsup banner
|
|
330
|
+
2. Optionally check for updates (non-blocking)
|
|
331
|
+
3. Create the root `Command` with name, description, version
|
|
332
|
+
4. Register global options shared by all commands
|
|
333
|
+
5. Import and register each command
|
|
334
|
+
6. Call `program.parse()`
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// src/cli.ts
|
|
338
|
+
import { Command } from 'commander'
|
|
339
|
+
import { createRequire } from 'node:module'
|
|
340
|
+
import { fileURLToPath } from 'node:url'
|
|
341
|
+
import { dirname, join } from 'node:path'
|
|
342
|
+
import { readFileSync } from 'node:fs'
|
|
343
|
+
import { registerInitCommand } from './commands/init/index.js'
|
|
344
|
+
import { registerDeployCommand } from './commands/deploy/index.js'
|
|
345
|
+
import { registerConfigCommand } from './commands/config/index.js'
|
|
346
|
+
// [If Auto-update = yes]:
|
|
347
|
+
import { checkForUpdates } from './utils/updater.js'
|
|
348
|
+
|
|
349
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
350
|
+
const __dirname = dirname(__filename)
|
|
351
|
+
|
|
352
|
+
const pkg = JSON.parse(
|
|
353
|
+
readFileSync(join(__dirname, '../package.json'), 'utf-8')
|
|
354
|
+
) as { name: string; version: string; description: string }
|
|
355
|
+
|
|
356
|
+
// [If Auto-update = yes]: Fire and forget — never blocks the command
|
|
357
|
+
void checkForUpdates(pkg.name, pkg.version)
|
|
358
|
+
|
|
359
|
+
const program = new Command()
|
|
360
|
+
|
|
361
|
+
program
|
|
362
|
+
.name('{{BINARY_NAME}}')
|
|
363
|
+
.description(pkg.description)
|
|
364
|
+
.version(pkg.version, '-V, --version', 'output the current version')
|
|
365
|
+
.option('-v, --verbose', 'enable verbose output', false)
|
|
366
|
+
.option('--json', 'output results as JSON', false)
|
|
367
|
+
.option('--no-color', 'disable color output')
|
|
368
|
+
.addHelpCommand(true)
|
|
369
|
+
|
|
370
|
+
registerInitCommand(program)
|
|
371
|
+
registerDeployCommand(program)
|
|
372
|
+
registerConfigCommand(program)
|
|
373
|
+
|
|
374
|
+
program.parseAsync(process.argv).catch((err: unknown) => {
|
|
375
|
+
// Top-level fallback — most errors are caught inside commands
|
|
376
|
+
console.error(String(err))
|
|
377
|
+
process.exit(1)
|
|
378
|
+
})
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**[If Background Daemon = yes]** insert the daemon detection block immediately before
|
|
382
|
+
`const program = new Command()`:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
// Must appear BEFORE Commander initialises — invisible to --help output
|
|
386
|
+
if (process.env['{{BINARY_NAME_UPPER}}_DAEMON_MODE'] === '1') {
|
|
387
|
+
const { runDaemon } = await import('./daemon/runner.js')
|
|
388
|
+
await runDaemon()
|
|
389
|
+
process.exit(0)
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
> See `references/command-patterns.md` for the complete Commander.js patterns: root
|
|
394
|
+
> program setup, global option propagation (`getGlobalOptions`), sub-command groups,
|
|
395
|
+
> `--dry-run`, confirmation prompts, tabular output, help text conventions, exit code
|
|
396
|
+
> table, and the `handleError` async wrapper used in every action handler.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Section 5: Project Directory Structure
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+
{{BINARY_NAME}}/
|
|
404
|
+
├── src/
|
|
405
|
+
│ ├── cli.ts ← Program entry point
|
|
406
|
+
│ ├── types/
|
|
407
|
+
│ │ └── index.ts ← Shared TypeScript types
|
|
408
|
+
│ ├── errors.ts ← CliError class + handleError()
|
|
409
|
+
│ ├── commands/
|
|
410
|
+
│ │ ├── init/
|
|
411
|
+
│ │ │ ├── index.ts ← registerInitCommand(program)
|
|
412
|
+
│ │ │ └── prompts.ts ← [If Prompts] Inquirer prompt flows
|
|
413
|
+
│ │ ├── deploy/
|
|
414
|
+
│ │ │ ├── index.ts
|
|
415
|
+
│ │ │ └── prompts.ts
|
|
416
|
+
│ │ └── config/ ← [If User Config] config sub-commands
|
|
417
|
+
│ │ └── index.ts
|
|
418
|
+
│ ├── services/
|
|
419
|
+
│ │ ├── init.service.ts ← InitService (no CLI I/O)
|
|
420
|
+
│ │ ├── deploy.service.ts ← DeployService
|
|
421
|
+
│ │ └── config.service.ts ← [If User Config] ConfigService
|
|
422
|
+
│ ├── config/
|
|
423
|
+
│ │ ├── user.config.ts ← [If User Config] conf setup
|
|
424
|
+
│ │ └── project.config.ts ← [If Project Config] cosmiconfig loader
|
|
425
|
+
│ ├── services/
|
|
426
|
+
│ │ └── http.client.ts ← [If HTTP Client] got instance
|
|
427
|
+
│ ├── db/ ← [If Local Database]
|
|
428
|
+
│ │ ├── client.ts ← better-sqlite3 singleton + migration runner
|
|
429
|
+
│ │ ├── path.ts ← OS data directory via env-paths
|
|
430
|
+
│ │ ├── schema.ts ← Drizzle table definitions (all entities)
|
|
431
|
+
│ │ └── repositories/
|
|
432
|
+
│ │ └── <entity>.repository.ts ← One file per entity type
|
|
433
|
+
│ ├── utils/
|
|
434
|
+
│ │ ├── signal.ts ← [If any Async] SIGINT/SIGTERM handler
|
|
435
|
+
│ │ ├── poll.ts ← [If Polling] polling loop utility
|
|
436
|
+
│ │ ├── batch.ts ← [If Inline Batch] chunk processor
|
|
437
|
+
│ │ ├── shell.ts ← [If Shell Execution] execa wrapper
|
|
438
|
+
│ │ └── updater.ts ← [If Auto-update] update-notifier
|
|
439
|
+
│ ├── daemon/
|
|
440
|
+
│ │ └── runner.ts ← [If Background Daemon] daemon event loop
|
|
441
|
+
│ └── ui/
|
|
442
|
+
│ ├── logger.ts ← chalk-based logger
|
|
443
|
+
│ ├── spinner.ts ← ora wrapper
|
|
444
|
+
│ ├── progress.ts ← [If Inline Batch] ProgressBar (ora + percentage)
|
|
445
|
+
│ ├── table.ts ← [If Table Output] cli-table3 wrapper
|
|
446
|
+
│ └── output.ts ← Unified output (human + --json)
|
|
447
|
+
├── test/
|
|
448
|
+
│ ├── commands/
|
|
449
|
+
│ │ ├── init.test.ts
|
|
450
|
+
│ │ └── deploy.test.ts
|
|
451
|
+
│ └── services/
|
|
452
|
+
│ ├── init.service.test.ts
|
|
453
|
+
│ └── deploy.service.test.ts
|
|
454
|
+
├── tsconfig.json
|
|
455
|
+
├── tsup.config.ts
|
|
456
|
+
├── vitest.config.ts
|
|
457
|
+
├── drizzle.config.ts ← [If Local Database]
|
|
458
|
+
├── package.json
|
|
459
|
+
├── .eslintrc.json
|
|
460
|
+
├── .gitignore
|
|
461
|
+
└── README.md
|
|
462
|
+
|
|
463
|
+
drizzle/ ← [If Local Database] Generated SQL migrations
|
|
464
|
+
├── 0000_initial_schema.sql
|
|
465
|
+
└── meta/
|
|
466
|
+
└── _journal.json
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Use actual command names from PRD.md throughout — never generic placeholders like
|
|
470
|
+
`command1`, `command2`.
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Section 6: Shared Types
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// src/types/index.ts
|
|
478
|
+
|
|
479
|
+
/** Global options available on every command */
|
|
480
|
+
export interface GlobalOptions {
|
|
481
|
+
verbose: boolean
|
|
482
|
+
json: boolean
|
|
483
|
+
color: boolean
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Standard success result shape for --json output */
|
|
487
|
+
export interface CliResult<T = unknown> {
|
|
488
|
+
success: true
|
|
489
|
+
data: T
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Standard error result shape for --json output */
|
|
493
|
+
export interface CliErrorResult {
|
|
494
|
+
success: false
|
|
495
|
+
error: {
|
|
496
|
+
code: string
|
|
497
|
+
message: string
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export type CliOutput<T = unknown> = CliResult<T> | CliErrorResult
|
|
502
|
+
|
|
503
|
+
// Add application-specific shared types from MODEL.md here
|
|
504
|
+
// e.g.:
|
|
505
|
+
// export interface ProjectConfig { ... }
|
|
506
|
+
// export interface DeployTarget { name: string; url: string }
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Section 7: Terminal UI Utilities
|
|
512
|
+
|
|
513
|
+
### 7.1 Logger
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
// src/ui/logger.ts
|
|
517
|
+
import chalk from 'chalk'
|
|
518
|
+
|
|
519
|
+
let verboseEnabled = false
|
|
520
|
+
|
|
521
|
+
export const logger = {
|
|
522
|
+
enableVerbose(): void { verboseEnabled = true },
|
|
523
|
+
|
|
524
|
+
info(message: string): void {
|
|
525
|
+
process.stdout.write(chalk.blue('ℹ') + ' ' + message + '\n')
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
success(message: string): void {
|
|
529
|
+
process.stdout.write(chalk.green('✔') + ' ' + message + '\n')
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
warn(message: string): void {
|
|
533
|
+
process.stderr.write(chalk.yellow('⚠') + ' ' + chalk.yellow(message) + '\n')
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
error(message: string): void {
|
|
537
|
+
process.stderr.write(chalk.red('✖') + ' ' + chalk.red(message) + '\n')
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
debug(message: string): void {
|
|
541
|
+
if (!verboseEnabled) return
|
|
542
|
+
process.stderr.write(chalk.gray('[debug] ') + chalk.gray(message) + '\n')
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
/** Print a section header */
|
|
546
|
+
heading(message: string): void {
|
|
547
|
+
process.stdout.write('\n' + chalk.bold(message) + '\n')
|
|
548
|
+
},
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### 7.2 Spinner
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
// src/ui/spinner.ts
|
|
556
|
+
import ora, { type Ora } from 'ora'
|
|
557
|
+
|
|
558
|
+
export function createSpinner(text: string): Ora {
|
|
559
|
+
return ora({ text, color: 'cyan' })
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export async function withSpinner<T>(
|
|
563
|
+
text: string,
|
|
564
|
+
fn: () => Promise<T>,
|
|
565
|
+
successText?: string,
|
|
566
|
+
): Promise<T> {
|
|
567
|
+
const spinner = createSpinner(text)
|
|
568
|
+
spinner.start()
|
|
569
|
+
try {
|
|
570
|
+
const result = await fn()
|
|
571
|
+
spinner.succeed(successText ?? text)
|
|
572
|
+
return result
|
|
573
|
+
} catch (error) {
|
|
574
|
+
spinner.fail()
|
|
575
|
+
throw error
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### 7.3 Output Handler
|
|
581
|
+
|
|
582
|
+
```typescript
|
|
583
|
+
// src/ui/output.ts
|
|
584
|
+
import type { CliOutput } from '../types/index.js'
|
|
585
|
+
import { logger } from './logger.js'
|
|
586
|
+
|
|
587
|
+
let jsonMode = false
|
|
588
|
+
|
|
589
|
+
export const output = {
|
|
590
|
+
enableJson(): void { jsonMode = true },
|
|
591
|
+
|
|
592
|
+
/** Print a success result — JSON if --json, formatted otherwise */
|
|
593
|
+
success<T>(data: T, humanSummary: string): void {
|
|
594
|
+
if (jsonMode) {
|
|
595
|
+
process.stdout.write(JSON.stringify({ success: true, data } satisfies CliOutput<T>, null, 2) + '\n')
|
|
596
|
+
} else {
|
|
597
|
+
logger.success(humanSummary)
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
/** Print an error result — JSON if --json, formatted otherwise */
|
|
602
|
+
failure(code: string, message: string): void {
|
|
603
|
+
if (jsonMode) {
|
|
604
|
+
process.stdout.write(JSON.stringify({ success: false, error: { code, message } } satisfies CliOutput, null, 2) + '\n')
|
|
605
|
+
} else {
|
|
606
|
+
logger.error(message)
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
/** Print raw human-readable text (not affected by --json) */
|
|
611
|
+
raw(text: string): void {
|
|
612
|
+
process.stdout.write(text + '\n')
|
|
613
|
+
},
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### 7.4 Table Output *(conditional — include only if Table Output = yes)*
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
// src/ui/table.ts
|
|
621
|
+
import Table from 'cli-table3'
|
|
622
|
+
|
|
623
|
+
export interface TableOptions {
|
|
624
|
+
head: string[]
|
|
625
|
+
rows: string[][]
|
|
626
|
+
compact?: boolean
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export function printTable({ head, rows, compact = false }: TableOptions): void {
|
|
630
|
+
const table = new Table({
|
|
631
|
+
head: head.map(h => `\u001b[1m${h}\u001b[0m`), // bold headers
|
|
632
|
+
style: compact ? { compact: true } : {},
|
|
633
|
+
})
|
|
634
|
+
for (const row of rows) {
|
|
635
|
+
table.push(row)
|
|
636
|
+
}
|
|
637
|
+
process.stdout.write(table.toString() + '\n')
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
## Section 8: Error Handling
|
|
644
|
+
|
|
645
|
+
```typescript
|
|
646
|
+
// src/errors.ts
|
|
647
|
+
import { output } from './ui/output.js'
|
|
648
|
+
import { logger } from './ui/logger.js'
|
|
649
|
+
|
|
650
|
+
export const ExitCodes = {
|
|
651
|
+
SUCCESS: 0,
|
|
652
|
+
GENERAL_ERROR: 1,
|
|
653
|
+
INVALID_INPUT: 2,
|
|
654
|
+
NOT_FOUND: 3,
|
|
655
|
+
UNAUTHORIZED: 4,
|
|
656
|
+
NETWORK_ERROR: 5,
|
|
657
|
+
CONFIG_ERROR: 6,
|
|
658
|
+
} as const
|
|
659
|
+
|
|
660
|
+
export type ExitCode = (typeof ExitCodes)[keyof typeof ExitCodes]
|
|
661
|
+
|
|
662
|
+
export class CliError extends Error {
|
|
663
|
+
constructor(
|
|
664
|
+
message: string,
|
|
665
|
+
public readonly code: string,
|
|
666
|
+
public readonly exitCode: ExitCode = ExitCodes.GENERAL_ERROR,
|
|
667
|
+
) {
|
|
668
|
+
super(message)
|
|
669
|
+
this.name = 'CliError'
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
static invalidInput(message: string): CliError {
|
|
673
|
+
return new CliError(message, 'INVALID_INPUT', ExitCodes.INVALID_INPUT)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
static notFound(resource: string): CliError {
|
|
677
|
+
return new CliError(`${resource} not found`, 'NOT_FOUND', ExitCodes.NOT_FOUND)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
static unauthorized(message = 'Not authenticated. Run `{{BINARY_NAME}} config login`.'): CliError {
|
|
681
|
+
return new CliError(message, 'UNAUTHORIZED', ExitCodes.UNAUTHORIZED)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
static networkError(message: string): CliError {
|
|
685
|
+
return new CliError(message, 'NETWORK_ERROR', ExitCodes.NETWORK_ERROR)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
static configError(message: string): CliError {
|
|
689
|
+
return new CliError(message, 'CONFIG_ERROR', ExitCodes.CONFIG_ERROR)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Wrap a command action handler to catch and format all errors.
|
|
695
|
+
* Usage: program.command('deploy').action(handleError(async (opts) => { ... }))
|
|
696
|
+
*/
|
|
697
|
+
export function handleError<TArgs extends unknown[]>(
|
|
698
|
+
fn: (...args: TArgs) => Promise<void>,
|
|
699
|
+
): (...args: TArgs) => Promise<void> {
|
|
700
|
+
return async (...args: TArgs): Promise<void> => {
|
|
701
|
+
try {
|
|
702
|
+
await fn(...args)
|
|
703
|
+
} catch (err: unknown) {
|
|
704
|
+
if (err instanceof CliError) {
|
|
705
|
+
output.failure(err.code, err.message)
|
|
706
|
+
process.exit(err.exitCode)
|
|
707
|
+
}
|
|
708
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
709
|
+
output.failure('UNKNOWN_ERROR', message)
|
|
710
|
+
logger.debug(err instanceof Error ? (err.stack ?? message) : message)
|
|
711
|
+
process.exit(ExitCodes.GENERAL_ERROR)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
## Section 9: User Configuration Management *(conditional — include only if User Config = yes)*
|
|
720
|
+
|
|
721
|
+
See `references/config-patterns.md` for the complete implementation. This section must
|
|
722
|
+
include:
|
|
723
|
+
|
|
724
|
+
- `src/config/user.config.ts` — `conf` instance with typed schema (Zod), typed getter/setter
|
|
725
|
+
accessors, and key migration logic
|
|
726
|
+
- `src/commands/config/index.ts` — `config get`, `config set`, `config list`, `config reset`
|
|
727
|
+
sub-commands using `commander`'s `.addCommand()` chain
|
|
728
|
+
- Complete code sample for all four sub-commands
|
|
729
|
+
- All config key names, types, and defaults extracted from MODEL.md
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## Section 10: Project Configuration Management *(conditional — include only if Project Config = yes)*
|
|
734
|
+
|
|
735
|
+
See `references/config-patterns.md` for the complete implementation. This section must
|
|
736
|
+
include:
|
|
737
|
+
|
|
738
|
+
- `src/config/project.config.ts` — cosmiconfig loader with named search path
|
|
739
|
+
(`{{BINARY_NAME}}rc`, `{{BINARY_NAME}}.config.js`, `package.json#{{BINARY_NAME}}`)
|
|
740
|
+
- Zod schema for project config validation
|
|
741
|
+
- Precedence merge function: CLI flags > env vars > project config > defaults
|
|
742
|
+
- `loadProjectConfig()` exported function used by commands that need project context
|
|
743
|
+
|
|
744
|
+
---
|
|
745
|
+
|
|
746
|
+
## Section 11: HTTP Client Setup *(conditional — include only if HTTP Client = yes)*
|
|
747
|
+
|
|
748
|
+
```typescript
|
|
749
|
+
// src/services/http.client.ts
|
|
750
|
+
import got, { type Got, HTTPError } from 'got'
|
|
751
|
+
import { getUserConfig } from '../config/user.config.js'
|
|
752
|
+
import { CliError } from '../errors.js'
|
|
753
|
+
|
|
754
|
+
export function createHttpClient(baseUrl?: string): Got {
|
|
755
|
+
const config = getUserConfig()
|
|
756
|
+
const token = config.get('apiToken')
|
|
757
|
+
|
|
758
|
+
return got.extend({
|
|
759
|
+
prefixUrl: baseUrl ?? config.get('apiBaseUrl'),
|
|
760
|
+
headers: {
|
|
761
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
762
|
+
'User-Agent': '{{PACKAGE_NAME}}/{{VERSION}}',
|
|
763
|
+
'Content-Type': 'application/json',
|
|
764
|
+
Accept: 'application/json',
|
|
765
|
+
},
|
|
766
|
+
retry: {
|
|
767
|
+
limit: 2,
|
|
768
|
+
methods: ['GET'],
|
|
769
|
+
statusCodes: [408, 429, 500, 502, 503, 504],
|
|
770
|
+
},
|
|
771
|
+
timeout: {
|
|
772
|
+
request: 30_000,
|
|
773
|
+
},
|
|
774
|
+
hooks: {
|
|
775
|
+
beforeError: [
|
|
776
|
+
(error) => {
|
|
777
|
+
if (error instanceof HTTPError) {
|
|
778
|
+
const { statusCode, body } = error.response
|
|
779
|
+
const message = typeof body === 'string'
|
|
780
|
+
? (JSON.parse(body) as { message?: string }).message ?? body
|
|
781
|
+
: 'HTTP request failed'
|
|
782
|
+
if (statusCode === 401) throw CliError.unauthorized()
|
|
783
|
+
if (statusCode === 404) throw CliError.notFound('Resource')
|
|
784
|
+
throw CliError.networkError(`HTTP ${statusCode.toString()}: ${message}`)
|
|
785
|
+
}
|
|
786
|
+
return error
|
|
787
|
+
},
|
|
788
|
+
],
|
|
789
|
+
},
|
|
790
|
+
})
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** Singleton HTTP client — initialized on first call */
|
|
794
|
+
let _client: Got | null = null
|
|
795
|
+
|
|
796
|
+
export function getHttpClient(): Got {
|
|
797
|
+
_client ??= createHttpClient()
|
|
798
|
+
return _client
|
|
799
|
+
}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
## Section 12: Shell Execution Utilities *(conditional — include only if Shell Execution = yes)*
|
|
805
|
+
|
|
806
|
+
```typescript
|
|
807
|
+
// src/utils/shell.ts
|
|
808
|
+
import { execa, type Options as ExecaOptions } from 'execa'
|
|
809
|
+
import { logger } from '../ui/logger.js'
|
|
810
|
+
import { CliError } from '../errors.js'
|
|
811
|
+
|
|
812
|
+
export interface RunOptions extends ExecaOptions {
|
|
813
|
+
/** If true, errors are logged but not thrown */
|
|
814
|
+
ignoreErrors?: boolean
|
|
815
|
+
/** Descriptive label for logging */
|
|
816
|
+
label?: string
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export async function run(
|
|
820
|
+
command: string,
|
|
821
|
+
args: readonly string[] = [],
|
|
822
|
+
options: RunOptions = {},
|
|
823
|
+
): Promise<string> {
|
|
824
|
+
const { ignoreErrors = false, label, ...execaOptions } = options
|
|
825
|
+
const displayCmd = [command, ...args].join(' ')
|
|
826
|
+
|
|
827
|
+
logger.debug(`${label ?? 'run'}: ${displayCmd}`)
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
const result = await execa(command, args, {
|
|
831
|
+
...execaOptions,
|
|
832
|
+
all: true,
|
|
833
|
+
})
|
|
834
|
+
return result.stdout
|
|
835
|
+
} catch (err: unknown) {
|
|
836
|
+
if (ignoreErrors) {
|
|
837
|
+
logger.debug(`Command failed (ignored): ${displayCmd}`)
|
|
838
|
+
return ''
|
|
839
|
+
}
|
|
840
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
841
|
+
throw new CliError(
|
|
842
|
+
`Command failed: ${displayCmd}\n${message}`,
|
|
843
|
+
'SHELL_ERROR',
|
|
844
|
+
)
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
## Section 13: Auto-Update Notifier *(conditional — include only if Auto-update = yes)*
|
|
852
|
+
|
|
853
|
+
```typescript
|
|
854
|
+
// src/utils/updater.ts
|
|
855
|
+
import updateNotifier from 'update-notifier'
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Non-blocking update check. Shows a notification on the NEXT run after
|
|
859
|
+
* an update is detected (update-notifier caches the check result for 24h).
|
|
860
|
+
*/
|
|
861
|
+
export function checkForUpdates(packageName: string, currentVersion: string): void {
|
|
862
|
+
// update-notifier is CommonJS — use createRequire for ESM compat
|
|
863
|
+
const notifier = updateNotifier({
|
|
864
|
+
pkg: { name: packageName, version: currentVersion },
|
|
865
|
+
updateCheckInterval: 1000 * 60 * 60 * 24, // 24 hours
|
|
866
|
+
})
|
|
867
|
+
notifier.notify({ isGlobal: true })
|
|
868
|
+
}
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
---
|
|
872
|
+
|
|
873
|
+
## Section 14: Testing Strategy
|
|
874
|
+
|
|
875
|
+
See `references/testing-patterns.md` for the full testing patterns.
|
|
876
|
+
|
|
877
|
+
### vitest.config.ts
|
|
878
|
+
|
|
879
|
+
```typescript
|
|
880
|
+
import { defineConfig } from 'vitest/config'
|
|
881
|
+
|
|
882
|
+
export default defineConfig({
|
|
883
|
+
test: {
|
|
884
|
+
globals: true,
|
|
885
|
+
environment: 'node',
|
|
886
|
+
include: ['test/**/*.test.ts'],
|
|
887
|
+
coverage: {
|
|
888
|
+
provider: 'v8',
|
|
889
|
+
include: ['src/**/*.ts'],
|
|
890
|
+
exclude: ['src/cli.ts', 'src/ui/**', 'src/**/*.d.ts'],
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
})
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
### Testing Approach
|
|
897
|
+
|
|
898
|
+
- **Service tests**: Pure unit tests — no file system, no network, no process.exit
|
|
899
|
+
Inject all dependencies via constructor; mock with `vi.fn()`
|
|
900
|
+
- **Command tests**: Integration-style — invoke Commander's `parseAsync` with
|
|
901
|
+
controlled `argv`, assert on `output.success`/`output.failure` calls
|
|
902
|
+
- **Config tests**: Use `tmp` directories; reset conf store in `beforeEach`
|
|
903
|
+
|
|
904
|
+
---
|
|
905
|
+
|
|
906
|
+
## Section 15: Packaging & Distribution *(conditional — include only if Binary Packaging = yes)*
|
|
907
|
+
|
|
908
|
+
See `references/packaging-patterns.md` for the full packaging guide. This section must
|
|
909
|
+
include:
|
|
910
|
+
|
|
911
|
+
- `pkg` field in `package.json` with scripts, assets, and targets
|
|
912
|
+
- Platform-specific npm scripts (`pkg:linux`, `pkg:macos`, `pkg:windows`, `pkg:all`)
|
|
913
|
+
- GitHub Actions workflow for automated release (triggered on `v*` tag)
|
|
914
|
+
- Checksum generation for release assets
|
|
915
|
+
- macOS code signing note (ad-hoc signing with `codesign`)
|
|
916
|
+
- Windows note (Windows Defender SmartScreen bypass for unsigned binaries)
|
|
917
|
+
|
|
918
|
+
For npm-only distribution, include:
|
|
919
|
+
- `"prepublishOnly"` script sequence (typecheck → test → build)
|
|
920
|
+
- `.npmignore` listing excluded files (`src/`, `test/`, config files)
|
|
921
|
+
- `"engines"` field asserting Node.js version requirement
|
|
922
|
+
|
|
923
|
+
---
|
|
924
|
+
|
|
925
|
+
## Section 16: Local Database *(conditional — include only if Local Database = yes)*
|
|
926
|
+
|
|
927
|
+
See `references/database-patterns.md` for the complete implementation. This section must
|
|
928
|
+
include:
|
|
929
|
+
|
|
930
|
+
### 16.1 drizzle.config.ts
|
|
931
|
+
|
|
932
|
+
```typescript
|
|
933
|
+
import type { Config } from 'drizzle-kit'
|
|
934
|
+
|
|
935
|
+
export default {
|
|
936
|
+
schema: './src/db/schema.ts',
|
|
937
|
+
out: './drizzle',
|
|
938
|
+
dialect: 'sqlite',
|
|
939
|
+
dbCredentials: {
|
|
940
|
+
url: './dev.db', // local dev only — production uses OS data directory
|
|
941
|
+
},
|
|
942
|
+
} satisfies Config
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
### 16.2 Database path resolution (`src/db/path.ts`)
|
|
946
|
+
|
|
947
|
+
`env-paths` wrapper that resolves the OS-appropriate data directory and ensures it
|
|
948
|
+
exists before returning the SQLite file path. Must produce:
|
|
949
|
+
- macOS: `~/Library/Application Support/{{BINARY_NAME}}/data.db`
|
|
950
|
+
- Linux: `~/.local/share/{{BINARY_NAME}}/data.db`
|
|
951
|
+
- Windows: `%APPDATA%\{{BINARY_NAME}}\data.db`
|
|
952
|
+
|
|
953
|
+
### 16.3 Schema (`src/db/schema.ts`)
|
|
954
|
+
|
|
955
|
+
Drizzle table definitions for **every entity type** derived from the model files in
|
|
956
|
+
`context/model/`. Use actual table names and columns from model definitions. Every
|
|
957
|
+
table must include:
|
|
958
|
+
- `id` — `text().primaryKey()` storing a UUIDv4 generated in the repository
|
|
959
|
+
- Timestamp columns as `integer('col', { mode: 'timestamp' })` mapping to `Date`
|
|
960
|
+
- JSON payload columns as `text()` — serialise/deserialise in the repository layer
|
|
961
|
+
|
|
962
|
+
### 16.4 Database client (`src/db/client.ts`)
|
|
963
|
+
|
|
964
|
+
Singleton `getDb()` function and `createDbClient(dbPath?)` factory:
|
|
965
|
+
- Opens `better-sqlite3` with `journal_mode = WAL`, `foreign_keys = ON`, `synchronous = NORMAL`
|
|
966
|
+
- Runs `migrate(db, { migrationsFolder })` on creation — idempotent, zero-penalty once migrations are current
|
|
967
|
+
- `closeDb()` for graceful shutdown and test cleanup
|
|
968
|
+
- `createDbClient(':memory:')` used in all tests
|
|
969
|
+
|
|
970
|
+
### 16.5 Repositories (`src/db/repositories/`)
|
|
971
|
+
|
|
972
|
+
One repository class per entity. Each class:
|
|
973
|
+
- Receives `DbClient` via constructor
|
|
974
|
+
- Exposes typed CRUD methods matching the user stories for that entity
|
|
975
|
+
- Never leaks Drizzle internals — public methods return plain typed objects
|
|
976
|
+
- Uses `db.transaction()` for multi-step writes
|
|
977
|
+
|
|
978
|
+
Include the complete repository code for every entity derived from the model files.
|
|
979
|
+
|
|
980
|
+
### 16.6 Migration files
|
|
981
|
+
|
|
982
|
+
List the initial migration SQL file (`drizzle/0000_initial_schema.sql`) that creates
|
|
983
|
+
all tables. The SQL must match the Drizzle schema exactly:
|
|
984
|
+
- Integer primary keys or `CHAR(36)` UUIDs
|
|
985
|
+
- `FOREIGN KEY` constraints matching the schema relationships
|
|
986
|
+
- Indexes matching the query patterns in the repositories
|
|
987
|
+
|
|
988
|
+
Remind the developer: run `npm run db:generate` after any schema change, commit the
|
|
989
|
+
generated file, and the CLI will apply it automatically on next run.
|
|
990
|
+
|
|
991
|
+
---
|
|
992
|
+
|
|
993
|
+
## Section 17: Async Patterns *(conditional — include only if any async pattern detected)*
|
|
994
|
+
|
|
995
|
+
See `references/async-patterns.md` for complete implementation. Include only the
|
|
996
|
+
sub-sections that apply. Multiple sub-sections may coexist in the same application.
|
|
997
|
+
|
|
998
|
+
### 17a. Shared Signal Handler *(include whenever any async pattern is yes)*
|
|
999
|
+
|
|
1000
|
+
```typescript
|
|
1001
|
+
// src/utils/signal.ts — generated whenever any async pattern is selected
|
|
1002
|
+
export type CleanupFn = () => void | Promise<void>
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Register SIGINT (Ctrl+C) and SIGTERM handlers. Returns a dispose function
|
|
1006
|
+
* that removes the listeners when the loop exits normally.
|
|
1007
|
+
* Exit codes: 130 for SIGINT (POSIX convention), 0 for SIGTERM (clean shutdown).
|
|
1008
|
+
*/
|
|
1009
|
+
export function onSignal(cleanup: CleanupFn): () => void {
|
|
1010
|
+
let handled = false
|
|
1011
|
+
const handler = async (signal: NodeJS.Signals): Promise<void> => {
|
|
1012
|
+
if (handled) return
|
|
1013
|
+
handled = true
|
|
1014
|
+
process.stderr.write('\n')
|
|
1015
|
+
try { await cleanup() } finally {
|
|
1016
|
+
process.exit(signal === 'SIGTERM' ? 0 : 130)
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
process.once('SIGINT', () => { void handler('SIGINT') })
|
|
1020
|
+
process.once('SIGTERM', () => { void handler('SIGTERM') })
|
|
1021
|
+
return () => {
|
|
1022
|
+
process.off('SIGINT', handler as never)
|
|
1023
|
+
process.off('SIGTERM', handler as never)
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
### 17b. Polling *(conditional — include only if Polling = yes)*
|
|
1029
|
+
|
|
1030
|
+
The spec must include:
|
|
1031
|
+
|
|
1032
|
+
- **`src/utils/poll.ts`** — Complete `poll<T>(options)` with `check`, `intervalMs`,
|
|
1033
|
+
`timeoutMs`, `label`, and `onTick`. Spinner managed internally. `onSignal` registered
|
|
1034
|
+
and disposed within the function. Returns `{ value: T, elapsedMs: number }`.
|
|
1035
|
+
- **Command integration** — For every user story with a `--watch`/`--follow`/`--wait`
|
|
1036
|
+
flag, show both code paths in the action handler:
|
|
1037
|
+
- Without flag → trigger, print result ID, `output.success()`, return
|
|
1038
|
+
- With flag → trigger → `poll()` → interpret terminal state → `output.success()` or exit 1
|
|
1039
|
+
- **Output contracts** — human and `--json` form for both paths (immediate and polled)
|
|
1040
|
+
- **Timeout** (exit 1, message "Polling timed out after Xs") and **Ctrl+C** (exit 130,
|
|
1041
|
+
message "Polling stopped — operation continues in the background")
|
|
1042
|
+
|
|
1043
|
+
### 17c. Inline Batch Processing *(conditional — include only if Inline Batch = yes)*
|
|
1044
|
+
|
|
1045
|
+
The spec must include:
|
|
1046
|
+
|
|
1047
|
+
- **`src/utils/batch.ts`** — Complete `runBatch<TInput, TOutput>(options)` with `total`,
|
|
1048
|
+
`items`, `process`, `concurrency`, `bail`, `onProgress`. Returns `BatchSummary`.
|
|
1049
|
+
- **`src/ui/progress.ts`** — `ProgressBar` wrapping ora: `update(BatchProgress)` shows
|
|
1050
|
+
`[pct%] n/total — ETA Xs`, plus `succeed()` / `fail()` / `stop()` methods.
|
|
1051
|
+
- **Service resume pattern** — when `Local Database = yes`, the service filters out
|
|
1052
|
+
already-processed IDs before returning the item list (checkpoint / resume support).
|
|
1053
|
+
- **Command options** — `--concurrency <n>`, `--bail`, `--dry-run` on every command
|
|
1054
|
+
using `runBatch`, mapped to `BatchOptions` fields.
|
|
1055
|
+
- **Exit code table**: all succeeded → 0; any failed → 1; Ctrl+C → 130.
|
|
1056
|
+
|
|
1057
|
+
### 17d. Background Daemon *(conditional — include only if Background Daemon = yes)*
|
|
1058
|
+
|
|
1059
|
+
The spec must include:
|
|
1060
|
+
|
|
1061
|
+
- **`src/cli.ts` modification** — daemon mode detection block checking
|
|
1062
|
+
`MY_TOOL_DAEMON_MODE === '1'` placed BEFORE `program.name(...)`. Show exact code.
|
|
1063
|
+
- **`src/daemon/runner.ts`** — `runDaemon()` with event loop, `onSignal`, `tick()` stub
|
|
1064
|
+
annotated with which NFRs define the actual work, and `TICK_INTERVAL_MS` constant
|
|
1065
|
+
derived from the NFRs (e.g. "checks every 30 seconds" → `30_000`).
|
|
1066
|
+
- **`src/services/daemon.service.ts`** — Complete `DaemonManager`:
|
|
1067
|
+
`start()`, `stop(timeoutMs)`, `isRunning()` (signal-0 check), `getStatus()`,
|
|
1068
|
+
`getRecentLogs(n)`. PID and start time stored in `conf`.
|
|
1069
|
+
- **`src/commands/daemon/index.ts`** — `start`, `stop`, `status`, `logs` sub-commands.
|
|
1070
|
+
- **`conf` schema additions** — `daemonPid: z.number().int().positive().optional()` and
|
|
1071
|
+
`daemonStartedAt: z.string().optional()` added to `UserConfigSchema`.
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
Each command from PRD.md gets its own `<command-name>/SPEC.md`.
|
|
1075
|
+
|
|
1076
|
+
---
|
|
1077
|
+
|
|
1078
|
+
## Command SPEC.md Structure
|
|
1079
|
+
|
|
1080
|
+
Each command SPEC.md follows the template below. See `references/command-patterns.md`
|
|
1081
|
+
for the full Commander.js patterns — the generated spec must draw from those patterns
|
|
1082
|
+
and substitute actual command names, flags, aliases, and option defaults from the
|
|
1083
|
+
`model/<command>/model.md` context file. Generic placeholders (e.g. `<n>`, `<template>`)
|
|
1084
|
+
must be replaced with the real argument names from the model.
|
|
1085
|
+
|
|
1086
|
+
```markdown
|
|
1087
|
+
# `{{command}}` Command — Specification
|
|
1088
|
+
|
|
1089
|
+
> Part of [{{APPLICATION_NAME}} Technical Specification](../SPECIFICATION.md)
|
|
1090
|
+
|
|
1091
|
+
## Overview
|
|
1092
|
+
|
|
1093
|
+
**Command**: `{{BINARY_NAME}} {{command}}`
|
|
1094
|
+
**Aliases**: `{{aliases}}`
|
|
1095
|
+
**Description**: {{description}}
|
|
1096
|
+
**Version**: {{version}}
|
|
1097
|
+
|
|
1098
|
+
## Traceability
|
|
1099
|
+
|
|
1100
|
+
### User Stories
|
|
1101
|
+
| ID | Version | Description |
|
|
1102
|
+
|-------------|---------|-------------------------------------|
|
|
1103
|
+
| USCLI00101 | v0.1.0 | As a user, I want to... |
|
|
1104
|
+
|
|
1105
|
+
### Non-Functional Requirements
|
|
1106
|
+
| ID | Version | Description |
|
|
1107
|
+
|-------------|---------|-------------------------------------|
|
|
1108
|
+
| NFRCLI0001 | v0.1.0 | Output must be filterable by --json |
|
|
1109
|
+
|
|
1110
|
+
### Constraints
|
|
1111
|
+
| ID | Version | Description |
|
|
1112
|
+
|-------------|---------|-------------------------------------|
|
|
1113
|
+
| CONSCLI001 | v0.1.0 | Must support --dry-run |
|
|
1114
|
+
|
|
1115
|
+
### Removed / Replaced
|
|
1116
|
+
| ID | Type | Removed In | Replaced By | Reason |
|
|
1117
|
+
|-------------|-------------|------------|-------------|--------|
|
|
1118
|
+
| _None._ | | | | |
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
## 2. Usage
|
|
1122
|
+
|
|
1123
|
+
```
|
|
1124
|
+
{{BINARY_NAME}} {{command}} [arguments] [options]
|
|
1125
|
+
|
|
1126
|
+
Arguments:
|
|
1127
|
+
<name> Project name (required)
|
|
1128
|
+
|
|
1129
|
+
Options:
|
|
1130
|
+
-t, --template Template to use (default: "default")
|
|
1131
|
+
-y, --yes Skip prompts and use defaults
|
|
1132
|
+
--dry-run Print what would happen without executing
|
|
1133
|
+
-h, --help Display help for command
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
## 3. Command Registration
|
|
1137
|
+
|
|
1138
|
+
```typescript
|
|
1139
|
+
// src/commands/{{command}}/index.ts
|
|
1140
|
+
import type { Command } from 'commander'
|
|
1141
|
+
import { handleError } from '../../errors.js'
|
|
1142
|
+
import { logger } from '../../ui/logger.js'
|
|
1143
|
+
import { output } from '../../ui/output.js'
|
|
1144
|
+
// [If Prompts = yes]:
|
|
1145
|
+
import { prompt{{Command}}Options } from './prompts.js'
|
|
1146
|
+
|
|
1147
|
+
export function register{{Command}}Command(program: Command): void {
|
|
1148
|
+
program
|
|
1149
|
+
.command('{{command}}')
|
|
1150
|
+
.alias('{{alias}}')
|
|
1151
|
+
.description('{{description}}')
|
|
1152
|
+
.argument('<name>', 'project name')
|
|
1153
|
+
.option('-t, --template <template>', 'template to use', 'default')
|
|
1154
|
+
.option('-y, --yes', 'skip prompts and use defaults', false)
|
|
1155
|
+
.option('--dry-run', 'preview without executing', false)
|
|
1156
|
+
.action(handleError(async (name: string, opts: {{Command}}Options) => {
|
|
1157
|
+
// Setup
|
|
1158
|
+
if (opts.verbose) logger.enableVerbose()
|
|
1159
|
+
if (opts.json) output.enableJson()
|
|
1160
|
+
|
|
1161
|
+
// [If Prompts = yes] Collect missing options interactively
|
|
1162
|
+
const resolvedOpts = opts.yes ? opts : await prompt{{Command}}Options(name, opts)
|
|
1163
|
+
|
|
1164
|
+
// Delegate to service
|
|
1165
|
+
const service = new {{Command}}Service()
|
|
1166
|
+
const result = await service.execute(name, resolvedOpts)
|
|
1167
|
+
|
|
1168
|
+
// Output
|
|
1169
|
+
output.success(result, `{{Command}} complete: ${name}`)
|
|
1170
|
+
}))
|
|
1171
|
+
}
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
## 4. Argument & Option Contracts
|
|
1175
|
+
|
|
1176
|
+
| Flag | Short | Type | Default | Required | Description |
|
|
1177
|
+
|--------------------|-------|-----------|-------------|----------|--------------------------|
|
|
1178
|
+
| `<name>` | — | `string` | — | Yes | Project name |
|
|
1179
|
+
| `--template` | `-t` | `string` | `"default"` | No | Template identifier |
|
|
1180
|
+
| `--yes` | `-y` | `boolean` | `false` | No | Skip all prompts |
|
|
1181
|
+
| `--dry-run` | — | `boolean` | `false` | No | Preview without changes |
|
|
1182
|
+
|
|
1183
|
+
### TypeScript Interface
|
|
1184
|
+
|
|
1185
|
+
```typescript
|
|
1186
|
+
export interface {{Command}}Options {
|
|
1187
|
+
template: string
|
|
1188
|
+
yes: boolean
|
|
1189
|
+
dryRun: boolean
|
|
1190
|
+
verbose: boolean
|
|
1191
|
+
json: boolean
|
|
1192
|
+
}
|
|
1193
|
+
```
|
|
1194
|
+
|
|
1195
|
+
### Zod Validation Schema
|
|
1196
|
+
|
|
1197
|
+
```typescript
|
|
1198
|
+
import { z } from 'zod'
|
|
1199
|
+
|
|
1200
|
+
export const {{command}}OptionsSchema = z.object({
|
|
1201
|
+
template: z.string().min(1).default('default'),
|
|
1202
|
+
yes: z.boolean().default(false),
|
|
1203
|
+
dryRun: z.boolean().default(false),
|
|
1204
|
+
verbose: z.boolean().default(false),
|
|
1205
|
+
json: z.boolean().default(false),
|
|
1206
|
+
})
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
## 5. Prompt Flow *(conditional — include only if Prompts = yes for this command)*
|
|
1210
|
+
|
|
1211
|
+
```typescript
|
|
1212
|
+
// src/commands/{{command}}/prompts.ts
|
|
1213
|
+
import { input, select, confirm } from '@inquirer/prompts'
|
|
1214
|
+
import type { {{Command}}Options } from './index.js'
|
|
1215
|
+
|
|
1216
|
+
export async function prompt{{Command}}Options(
|
|
1217
|
+
name: string,
|
|
1218
|
+
partial: Partial<{{Command}}Options>,
|
|
1219
|
+
): Promise<{{Command}}Options> {
|
|
1220
|
+
const template = partial.template ?? await select({
|
|
1221
|
+
message: 'Select a template:',
|
|
1222
|
+
choices: [
|
|
1223
|
+
{ name: 'Default', value: 'default', description: 'Minimal starter' },
|
|
1224
|
+
{ name: 'Full', value: 'full', description: 'All features enabled' },
|
|
1225
|
+
],
|
|
1226
|
+
})
|
|
1227
|
+
|
|
1228
|
+
const confirm_ = await confirm({
|
|
1229
|
+
message: `Create project "${name}" with template "${template}"?`,
|
|
1230
|
+
default: true,
|
|
1231
|
+
})
|
|
1232
|
+
|
|
1233
|
+
if (!confirm_) {
|
|
1234
|
+
process.stdout.write('Aborted.\n')
|
|
1235
|
+
process.exit(0)
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
return {
|
|
1239
|
+
template,
|
|
1240
|
+
yes: false,
|
|
1241
|
+
dryRun: partial.dryRun ?? false,
|
|
1242
|
+
verbose: partial.verbose ?? false,
|
|
1243
|
+
json: partial.json ?? false,
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
## 6. Service Interface
|
|
1249
|
+
|
|
1250
|
+
```typescript
|
|
1251
|
+
// src/services/{{command}}.service.ts (public interface)
|
|
1252
|
+
export interface I{{Command}}Service {
|
|
1253
|
+
execute(name: string, options: {{Command}}Options): Promise<{{Command}}Result>
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
export interface {{Command}}Result {
|
|
1257
|
+
name: string
|
|
1258
|
+
template: string
|
|
1259
|
+
path: string
|
|
1260
|
+
files: string[]
|
|
1261
|
+
dryRun: boolean
|
|
1262
|
+
}
|
|
1263
|
+
```
|
|
1264
|
+
|
|
1265
|
+
## 7. Service Implementation
|
|
1266
|
+
|
|
1267
|
+
```typescript
|
|
1268
|
+
// src/services/{{command}}.service.ts
|
|
1269
|
+
import { join } from 'node:path'
|
|
1270
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
1271
|
+
import { CliError } from '../errors.js'
|
|
1272
|
+
import { logger } from '../ui/logger.js'
|
|
1273
|
+
import type { {{Command}}Options, {{Command}}Result, I{{Command}}Service } from './{{command}}.service.js'
|
|
1274
|
+
|
|
1275
|
+
export class {{Command}}Service implements I{{Command}}Service {
|
|
1276
|
+
async execute(name: string, options: {{Command}}Options): Promise<{{Command}}Result> {
|
|
1277
|
+
logger.debug(`Executing {{command}} for "${name}" with template "${options.template}"`)
|
|
1278
|
+
|
|
1279
|
+
const targetPath = join(process.cwd(), name)
|
|
1280
|
+
|
|
1281
|
+
// Service has NO terminal output — returns data only
|
|
1282
|
+
if (options.dryRun) {
|
|
1283
|
+
return { name, template: options.template, path: targetPath, files: [], dryRun: true }
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// Real implementation (derive from user stories)
|
|
1287
|
+
// ...
|
|
1288
|
+
|
|
1289
|
+
return {
|
|
1290
|
+
name,
|
|
1291
|
+
template: options.template,
|
|
1292
|
+
path: targetPath,
|
|
1293
|
+
files: ['package.json', 'tsconfig.json', 'README.md'],
|
|
1294
|
+
dryRun: false,
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
```
|
|
1299
|
+
|
|
1300
|
+
## 8. Output Contract
|
|
1301
|
+
|
|
1302
|
+
### Human-readable output
|
|
1303
|
+
|
|
1304
|
+
```
|
|
1305
|
+
✔ {{Command}} complete: my-project
|
|
1306
|
+
|
|
1307
|
+
Created 3 files in /Users/you/my-project:
|
|
1308
|
+
• package.json
|
|
1309
|
+
• tsconfig.json
|
|
1310
|
+
• README.md
|
|
1311
|
+
|
|
1312
|
+
Run `cd my-project && npm install` to get started.
|
|
1313
|
+
```
|
|
1314
|
+
|
|
1315
|
+
### JSON output (`--json`)
|
|
1316
|
+
|
|
1317
|
+
```json
|
|
1318
|
+
{
|
|
1319
|
+
"success": true,
|
|
1320
|
+
"data": {
|
|
1321
|
+
"name": "my-project",
|
|
1322
|
+
"template": "default",
|
|
1323
|
+
"path": "/Users/you/my-project",
|
|
1324
|
+
"files": ["package.json", "tsconfig.json", "README.md"],
|
|
1325
|
+
"dryRun": false
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
### JSON error output
|
|
1331
|
+
|
|
1332
|
+
```json
|
|
1333
|
+
{
|
|
1334
|
+
"success": false,
|
|
1335
|
+
"error": {
|
|
1336
|
+
"code": "INVALID_INPUT",
|
|
1337
|
+
"message": "Project name cannot contain spaces"
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
### Async output contracts *(include only for commands with async patterns)*
|
|
1343
|
+
|
|
1344
|
+
**If this command uses `--watch` / `--follow` (Polling):**
|
|
1345
|
+
|
|
1346
|
+
| Scenario | Exit Code | Human output | JSON `success` |
|
|
1347
|
+
|----------|-----------|--------------|----------------|
|
|
1348
|
+
| Triggered, no --watch | 0 | `ℹ Job started: job-123` | `true` |
|
|
1349
|
+
| --watch, succeeded | 0 | `✔ Done in 14.2s` | `true` |
|
|
1350
|
+
| --watch, failed | 1 | `✖ Job failed: <reason>` | `false` |
|
|
1351
|
+
| --watch, timed out | 1 | `✖ Timed out after 300s` | `false` |
|
|
1352
|
+
| Ctrl+C during poll | 130 | `⚠ Polling stopped — continues in background` | N/A |
|
|
1353
|
+
|
|
1354
|
+
**If this command uses `runBatch` (Inline Batch):**
|
|
1355
|
+
|
|
1356
|
+
| Scenario | Exit Code | Human output |
|
|
1357
|
+
|----------|-----------|--------------|
|
|
1358
|
+
| All succeeded | 0 | `✔ Synced 432 resources in 24.1s` |
|
|
1359
|
+
| Partial failure | 1 | `✖ Completed with 3 error(s)` |
|
|
1360
|
+
| --bail triggered | 1 | `✖ Stopped after first error` |
|
|
1361
|
+
| Ctrl+C | 130 | `⚠ Batch interrupted — stopping after current chunk` |
|
|
1362
|
+
|
|
1363
|
+
## 9. Error Cases
|
|
1364
|
+
|
|
1365
|
+
| Scenario | Error Code | Exit Code | Message shown to user |
|
|
1366
|
+
|---------------------------------|-----------------|-----------|-----------------------------------------------|
|
|
1367
|
+
| Name contains invalid chars | `INVALID_INPUT` | 2 | "Project name may only contain..." |
|
|
1368
|
+
| Target directory already exists | `CONFLICT` | 1 | "Directory already exists: /path/to/dir" |
|
|
1369
|
+
| Template not found | `NOT_FOUND` | 3 | "Template 'xyz' not found" |
|
|
1370
|
+
| File system write failure | `IO_ERROR` | 1 | "Failed to create project files: <reason>" |
|
|
1371
|
+
|
|
1372
|
+
## 10. Tests
|
|
1373
|
+
|
|
1374
|
+
(See `references/testing-patterns.md` for full test patterns)
|
|
1375
|
+
|
|
1376
|
+
```typescript
|
|
1377
|
+
// test/commands/{{command}}.test.ts
|
|
1378
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
1379
|
+
import { {{Command}}Service } from '../../src/services/{{command}}.service.js'
|
|
1380
|
+
|
|
1381
|
+
describe('{{Command}}Service', () => {
|
|
1382
|
+
it('returns dryRun result without creating files', async () => {
|
|
1383
|
+
const service = new {{Command}}Service()
|
|
1384
|
+
const result = await service.execute('my-project', {
|
|
1385
|
+
template: 'default', yes: true, dryRun: true, verbose: false, json: false,
|
|
1386
|
+
})
|
|
1387
|
+
expect(result.dryRun).toBe(true)
|
|
1388
|
+
expect(result.files).toHaveLength(0)
|
|
1389
|
+
})
|
|
1390
|
+
})
|
|
1391
|
+
```
|