@zhuan-ai/zhuanspec 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +461 -0
  3. package/README.zh.md +434 -0
  4. package/bin/zhuanspec.js +3 -0
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.js +356 -0
  7. package/dist/commands/artifact-workflow.d.ts +13 -0
  8. package/dist/commands/artifact-workflow.js +916 -0
  9. package/dist/commands/change.d.ts +35 -0
  10. package/dist/commands/change.js +277 -0
  11. package/dist/commands/completion.d.ts +72 -0
  12. package/dist/commands/completion.js +221 -0
  13. package/dist/commands/config.d.ts +8 -0
  14. package/dist/commands/config.js +198 -0
  15. package/dist/commands/show.d.ts +14 -0
  16. package/dist/commands/show.js +132 -0
  17. package/dist/commands/spec.d.ts +15 -0
  18. package/dist/commands/spec.js +225 -0
  19. package/dist/commands/validate.d.ts +24 -0
  20. package/dist/commands/validate.js +294 -0
  21. package/dist/core/archive.d.ts +30 -0
  22. package/dist/core/archive.js +438 -0
  23. package/dist/core/artifact-graph/graph.d.ts +56 -0
  24. package/dist/core/artifact-graph/graph.js +141 -0
  25. package/dist/core/artifact-graph/index.d.ts +7 -0
  26. package/dist/core/artifact-graph/index.js +13 -0
  27. package/dist/core/artifact-graph/instruction-loader.d.ts +134 -0
  28. package/dist/core/artifact-graph/instruction-loader.js +180 -0
  29. package/dist/core/artifact-graph/resolver.d.ts +61 -0
  30. package/dist/core/artifact-graph/resolver.js +187 -0
  31. package/dist/core/artifact-graph/schema.d.ts +13 -0
  32. package/dist/core/artifact-graph/schema.js +108 -0
  33. package/dist/core/artifact-graph/state.d.ts +12 -0
  34. package/dist/core/artifact-graph/state.js +54 -0
  35. package/dist/core/artifact-graph/types.d.ts +45 -0
  36. package/dist/core/artifact-graph/types.js +43 -0
  37. package/dist/core/completions/command-registry.d.ts +7 -0
  38. package/dist/core/completions/command-registry.js +362 -0
  39. package/dist/core/completions/completion-provider.d.ts +60 -0
  40. package/dist/core/completions/completion-provider.js +102 -0
  41. package/dist/core/completions/factory.d.ts +51 -0
  42. package/dist/core/completions/factory.js +57 -0
  43. package/dist/core/completions/generators/zsh-generator.d.ts +58 -0
  44. package/dist/core/completions/generators/zsh-generator.js +319 -0
  45. package/dist/core/completions/installers/zsh-installer.d.ts +136 -0
  46. package/dist/core/completions/installers/zsh-installer.js +449 -0
  47. package/dist/core/completions/types.d.ts +78 -0
  48. package/dist/core/completions/types.js +2 -0
  49. package/dist/core/config-schema.d.ts +76 -0
  50. package/dist/core/config-schema.js +200 -0
  51. package/dist/core/config.d.ts +16 -0
  52. package/dist/core/config.js +29 -0
  53. package/dist/core/configurators/agents.d.ts +8 -0
  54. package/dist/core/configurators/agents.js +15 -0
  55. package/dist/core/configurators/base.d.ts +7 -0
  56. package/dist/core/configurators/base.js +2 -0
  57. package/dist/core/configurators/claude.d.ts +8 -0
  58. package/dist/core/configurators/claude.js +15 -0
  59. package/dist/core/configurators/cline.d.ts +8 -0
  60. package/dist/core/configurators/cline.js +15 -0
  61. package/dist/core/configurators/codebuddy.d.ts +8 -0
  62. package/dist/core/configurators/codebuddy.js +15 -0
  63. package/dist/core/configurators/costrict.d.ts +8 -0
  64. package/dist/core/configurators/costrict.js +15 -0
  65. package/dist/core/configurators/iflow.d.ts +8 -0
  66. package/dist/core/configurators/iflow.js +15 -0
  67. package/dist/core/configurators/qoder.d.ts +30 -0
  68. package/dist/core/configurators/qoder.js +42 -0
  69. package/dist/core/configurators/qwen.d.ts +24 -0
  70. package/dist/core/configurators/qwen.js +37 -0
  71. package/dist/core/configurators/registry.d.ts +9 -0
  72. package/dist/core/configurators/registry.js +43 -0
  73. package/dist/core/configurators/slash/amazon-q.d.ts +9 -0
  74. package/dist/core/configurators/slash/amazon-q.js +46 -0
  75. package/dist/core/configurators/slash/antigravity.d.ts +9 -0
  76. package/dist/core/configurators/slash/antigravity.js +23 -0
  77. package/dist/core/configurators/slash/auggie.d.ts +9 -0
  78. package/dist/core/configurators/slash/auggie.js +31 -0
  79. package/dist/core/configurators/slash/base.d.ts +19 -0
  80. package/dist/core/configurators/slash/base.js +69 -0
  81. package/dist/core/configurators/slash/claude.d.ts +9 -0
  82. package/dist/core/configurators/slash/claude.js +37 -0
  83. package/dist/core/configurators/slash/cline.d.ts +9 -0
  84. package/dist/core/configurators/slash/cline.js +23 -0
  85. package/dist/core/configurators/slash/codebuddy.d.ts +9 -0
  86. package/dist/core/configurators/slash/codebuddy.js +37 -0
  87. package/dist/core/configurators/slash/codex.d.ts +14 -0
  88. package/dist/core/configurators/slash/codex.js +109 -0
  89. package/dist/core/configurators/slash/costrict.d.ts +9 -0
  90. package/dist/core/configurators/slash/costrict.js +31 -0
  91. package/dist/core/configurators/slash/crush.d.ts +9 -0
  92. package/dist/core/configurators/slash/crush.js +37 -0
  93. package/dist/core/configurators/slash/cursor.d.ts +9 -0
  94. package/dist/core/configurators/slash/cursor.js +37 -0
  95. package/dist/core/configurators/slash/factory.d.ts +10 -0
  96. package/dist/core/configurators/slash/factory.js +35 -0
  97. package/dist/core/configurators/slash/gemini.d.ts +9 -0
  98. package/dist/core/configurators/slash/gemini.js +22 -0
  99. package/dist/core/configurators/slash/github-copilot.d.ts +9 -0
  100. package/dist/core/configurators/slash/github-copilot.js +34 -0
  101. package/dist/core/configurators/slash/iflow.d.ts +9 -0
  102. package/dist/core/configurators/slash/iflow.js +37 -0
  103. package/dist/core/configurators/slash/kilocode.d.ts +9 -0
  104. package/dist/core/configurators/slash/kilocode.js +17 -0
  105. package/dist/core/configurators/slash/opencode.d.ts +12 -0
  106. package/dist/core/configurators/slash/opencode.js +72 -0
  107. package/dist/core/configurators/slash/qoder.d.ts +35 -0
  108. package/dist/core/configurators/slash/qoder.js +76 -0
  109. package/dist/core/configurators/slash/qwen.d.ts +32 -0
  110. package/dist/core/configurators/slash/qwen.js +49 -0
  111. package/dist/core/configurators/slash/registry.d.ts +8 -0
  112. package/dist/core/configurators/slash/registry.js +75 -0
  113. package/dist/core/configurators/slash/roocode.d.ts +9 -0
  114. package/dist/core/configurators/slash/roocode.js +23 -0
  115. package/dist/core/configurators/slash/toml-base.d.ts +10 -0
  116. package/dist/core/configurators/slash/toml-base.js +53 -0
  117. package/dist/core/configurators/slash/windsurf.d.ts +9 -0
  118. package/dist/core/configurators/slash/windsurf.js +23 -0
  119. package/dist/core/converters/json-converter.d.ts +6 -0
  120. package/dist/core/converters/json-converter.js +51 -0
  121. package/dist/core/global-config.d.ts +39 -0
  122. package/dist/core/global-config.js +115 -0
  123. package/dist/core/index.d.ts +2 -0
  124. package/dist/core/index.js +3 -0
  125. package/dist/core/init.d.ts +60 -0
  126. package/dist/core/init.js +861 -0
  127. package/dist/core/list.d.ts +9 -0
  128. package/dist/core/list.js +171 -0
  129. package/dist/core/parsers/change-parser.d.ts +13 -0
  130. package/dist/core/parsers/change-parser.js +193 -0
  131. package/dist/core/parsers/markdown-parser.d.ts +22 -0
  132. package/dist/core/parsers/markdown-parser.js +187 -0
  133. package/dist/core/parsers/requirement-blocks.d.ts +37 -0
  134. package/dist/core/parsers/requirement-blocks.js +201 -0
  135. package/dist/core/project-config.d.ts +34 -0
  136. package/dist/core/project-config.js +79 -0
  137. package/dist/core/schemas/base.schema.d.ts +13 -0
  138. package/dist/core/schemas/base.schema.js +13 -0
  139. package/dist/core/schemas/change.schema.d.ts +73 -0
  140. package/dist/core/schemas/change.schema.js +31 -0
  141. package/dist/core/schemas/index.d.ts +4 -0
  142. package/dist/core/schemas/index.js +4 -0
  143. package/dist/core/schemas/spec.schema.d.ts +18 -0
  144. package/dist/core/schemas/spec.schema.js +15 -0
  145. package/dist/core/skill-discovery.d.ts +24 -0
  146. package/dist/core/skill-discovery.js +153 -0
  147. package/dist/core/specs-apply.d.ts +73 -0
  148. package/dist/core/specs-apply.js +384 -0
  149. package/dist/core/styles/palette.d.ts +7 -0
  150. package/dist/core/styles/palette.js +8 -0
  151. package/dist/core/templates/agents-root-stub.d.ts +2 -0
  152. package/dist/core/templates/agents-root-stub.js +17 -0
  153. package/dist/core/templates/agents-template.d.ts +2 -0
  154. package/dist/core/templates/agents-template.js +706 -0
  155. package/dist/core/templates/claude-template.d.ts +2 -0
  156. package/dist/core/templates/claude-template.js +2 -0
  157. package/dist/core/templates/cline-template.d.ts +2 -0
  158. package/dist/core/templates/cline-template.js +2 -0
  159. package/dist/core/templates/costrict-template.d.ts +2 -0
  160. package/dist/core/templates/costrict-template.js +2 -0
  161. package/dist/core/templates/index.d.ts +17 -0
  162. package/dist/core/templates/index.js +37 -0
  163. package/dist/core/templates/project-template.d.ts +8 -0
  164. package/dist/core/templates/project-template.js +32 -0
  165. package/dist/core/templates/skill-templates.d.ts +103 -0
  166. package/dist/core/templates/skill-templates.js +2131 -0
  167. package/dist/core/templates/slash-command-templates.d.ts +4 -0
  168. package/dist/core/templates/slash-command-templates.js +81 -0
  169. package/dist/core/update.d.ts +4 -0
  170. package/dist/core/update.js +88 -0
  171. package/dist/core/validation/constants.d.ts +34 -0
  172. package/dist/core/validation/constants.js +40 -0
  173. package/dist/core/validation/types.d.ts +18 -0
  174. package/dist/core/validation/types.js +2 -0
  175. package/dist/core/validation/validator.d.ts +33 -0
  176. package/dist/core/validation/validator.js +409 -0
  177. package/dist/core/view.d.ts +8 -0
  178. package/dist/core/view.js +168 -0
  179. package/dist/index.d.ts +3 -0
  180. package/dist/index.js +3 -0
  181. package/dist/utils/change-metadata.d.ts +47 -0
  182. package/dist/utils/change-metadata.js +130 -0
  183. package/dist/utils/change-utils.d.ts +51 -0
  184. package/dist/utils/change-utils.js +100 -0
  185. package/dist/utils/file-system.d.ts +19 -0
  186. package/dist/utils/file-system.js +177 -0
  187. package/dist/utils/index.d.ts +4 -0
  188. package/dist/utils/index.js +5 -0
  189. package/dist/utils/interactive.d.ts +18 -0
  190. package/dist/utils/interactive.js +21 -0
  191. package/dist/utils/item-discovery.d.ts +4 -0
  192. package/dist/utils/item-discovery.js +72 -0
  193. package/dist/utils/match.d.ts +3 -0
  194. package/dist/utils/match.js +22 -0
  195. package/dist/utils/shell-detection.d.ts +20 -0
  196. package/dist/utils/shell-detection.js +41 -0
  197. package/dist/utils/task-progress.d.ts +8 -0
  198. package/dist/utils/task-progress.js +36 -0
  199. package/package.json +81 -0
  200. package/schemas/spec-driven/schema.yaml +205 -0
  201. package/schemas/spec-driven/templates/design.md +19 -0
  202. package/schemas/spec-driven/templates/proposal.md +43 -0
  203. package/schemas/spec-driven/templates/spec.md +8 -0
  204. package/schemas/spec-driven/templates/tasks.md +25 -0
  205. package/schemas/tdd/schema.yaml +213 -0
  206. package/schemas/tdd/templates/docs.md +0 -0
  207. package/schemas/tdd/templates/implementation.md +11 -0
  208. package/schemas/tdd/templates/spec.md +11 -0
  209. package/schemas/tdd/templates/test.md +11 -0
  210. package/scripts/postinstall.js +147 -0
@@ -0,0 +1,861 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from 'fs';
4
+ import { createPrompt, isBackspaceKey, isDownKey, isEnterKey, isSpaceKey, isUpKey, useKeypress, usePagination, useState, } from '@inquirer/core';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import { execSync } from 'child_process';
8
+ import { FileSystemUtils } from '../utils/file-system.js';
9
+ import { TemplateManager } from './templates/index.js';
10
+ import { ToolRegistry } from './configurators/registry.js';
11
+ import { SlashCommandRegistry } from './configurators/slash/registry.js';
12
+ import { AI_TOOLS, ZHUANSPEC_DIR_NAME, ZHUANSPEC_MARKERS, } from './config.js';
13
+ import { PALETTE } from './styles/palette.js';
14
+ const PROGRESS_SPINNER = {
15
+ interval: 80,
16
+ frames: ['░░░', '▒░░', '▒▒░', '▒▒▒', '▓▒▒', '▓▓▒', '▓▓▓', '▒▓▓', '░▒▓'],
17
+ };
18
+ const LETTER_MAP = {
19
+ O: [' ████ ', '██ ██', '██ ██', '██ ██', ' ████ '],
20
+ P: ['█████ ', '██ ██', '█████ ', '██ ', '██ '],
21
+ E: ['██████', '██ ', '█████ ', '██ ', '██████'],
22
+ N: ['██ ██', '███ ██', '██ ███', '██ ██', '██ ██'],
23
+ S: [' █████', '██ ', ' ████ ', ' ██', '█████ '],
24
+ C: [' █████', '██ ', '██ ', '██ ', ' █████'],
25
+ ' ': [' ', ' ', ' ', ' ', ' '],
26
+ };
27
+ const sanitizeToolLabel = (raw) => raw.replace(/✅/gu, '✔').trim();
28
+ const parseToolLabel = (raw) => {
29
+ const sanitized = sanitizeToolLabel(raw);
30
+ const match = sanitized.match(/^(.*?)\s*\((.+)\)$/u);
31
+ if (!match) {
32
+ return { primary: sanitized };
33
+ }
34
+ return {
35
+ primary: match[1].trim(),
36
+ annotation: match[2].trim(),
37
+ };
38
+ };
39
+ const isSelectableChoice = (choice) => choice.selectable;
40
+ const ROOT_STUB_CHOICE_VALUE = '__root_stub__';
41
+ const OTHER_TOOLS_HEADING_VALUE = '__heading-other__';
42
+ const LIST_SPACER_VALUE = '__list-spacer__';
43
+ const ARCH_REPO_URL = 'http://gitlab.zhuanspirit.com/zz-kf/spec_repo.git';
44
+ const ARCH_REPO_BRANCH = 'spec_repo-feature-6612-2';
45
+ const toolSelectionWizard = createPrompt((config, done) => {
46
+ const totalSteps = 3;
47
+ const [step, setStep] = useState('intro');
48
+ const selectableChoices = config.choices.filter(isSelectableChoice);
49
+ const initialCursorIndex = config.choices.findIndex((choice) => choice.selectable);
50
+ const [cursor, setCursor] = useState(initialCursorIndex === -1 ? 0 : initialCursorIndex);
51
+ const [selected, setSelected] = useState(() => {
52
+ const initial = new Set((config.initialSelected ?? []).filter((value) => selectableChoices.some((choice) => choice.value === value)));
53
+ return selectableChoices
54
+ .map((choice) => choice.value)
55
+ .filter((value) => initial.has(value));
56
+ });
57
+ const [error, setError] = useState(null);
58
+ const selectedSet = new Set(selected);
59
+ const pageSize = Math.max(config.choices.length, 1);
60
+ const updateSelected = (next) => {
61
+ const ordered = selectableChoices
62
+ .map((choice) => choice.value)
63
+ .filter((value) => next.has(value));
64
+ setSelected(ordered);
65
+ };
66
+ const page = usePagination({
67
+ items: config.choices,
68
+ active: cursor,
69
+ pageSize,
70
+ loop: false,
71
+ renderItem: ({ item, isActive }) => {
72
+ if (!item.selectable) {
73
+ const prefix = item.kind === 'info' ? ' ' : '';
74
+ const textColor = item.kind === 'heading' ? PALETTE.lightGray : PALETTE.midGray;
75
+ return `${PALETTE.midGray(' ')} ${PALETTE.midGray(' ')} ${textColor(`${prefix}${item.label.primary}`)}`;
76
+ }
77
+ const isSelected = selectedSet.has(item.value);
78
+ const cursorSymbol = isActive
79
+ ? PALETTE.white('›')
80
+ : PALETTE.midGray(' ');
81
+ const indicator = isSelected
82
+ ? PALETTE.white('◉')
83
+ : PALETTE.midGray('○');
84
+ const nameColor = isActive ? PALETTE.white : PALETTE.midGray;
85
+ const annotation = item.label.annotation
86
+ ? PALETTE.midGray(` (${item.label.annotation})`)
87
+ : '';
88
+ const configuredNote = item.configured
89
+ ? PALETTE.midGray(' (已配置)')
90
+ : '';
91
+ const label = `${nameColor(item.label.primary)}${annotation}${configuredNote}`;
92
+ return `${cursorSymbol} ${indicator} ${label}`;
93
+ },
94
+ });
95
+ const moveCursor = (direction) => {
96
+ if (selectableChoices.length === 0) {
97
+ return;
98
+ }
99
+ let nextIndex = cursor;
100
+ while (true) {
101
+ nextIndex = nextIndex + direction;
102
+ if (nextIndex < 0 || nextIndex >= config.choices.length) {
103
+ return;
104
+ }
105
+ if (config.choices[nextIndex]?.selectable) {
106
+ setCursor(nextIndex);
107
+ return;
108
+ }
109
+ }
110
+ };
111
+ useKeypress((key) => {
112
+ if (step === 'intro') {
113
+ if (isEnterKey(key)) {
114
+ setStep('select');
115
+ }
116
+ return;
117
+ }
118
+ if (step === 'select') {
119
+ if (isUpKey(key)) {
120
+ moveCursor(-1);
121
+ setError(null);
122
+ return;
123
+ }
124
+ if (isDownKey(key)) {
125
+ moveCursor(1);
126
+ setError(null);
127
+ return;
128
+ }
129
+ if (isSpaceKey(key)) {
130
+ const current = config.choices[cursor];
131
+ if (!current || !current.selectable)
132
+ return;
133
+ const next = new Set(selected);
134
+ if (next.has(current.value)) {
135
+ next.delete(current.value);
136
+ }
137
+ else {
138
+ next.add(current.value);
139
+ }
140
+ updateSelected(next);
141
+ setError(null);
142
+ return;
143
+ }
144
+ if (isEnterKey(key)) {
145
+ const current = config.choices[cursor];
146
+ if (current &&
147
+ current.selectable &&
148
+ !selectedSet.has(current.value)) {
149
+ const next = new Set(selected);
150
+ next.add(current.value);
151
+ updateSelected(next);
152
+ }
153
+ setStep('review');
154
+ setError(null);
155
+ return;
156
+ }
157
+ if (key.name === 'escape') {
158
+ const next = new Set();
159
+ updateSelected(next);
160
+ setError(null);
161
+ }
162
+ return;
163
+ }
164
+ if (step === 'review') {
165
+ if (isEnterKey(key)) {
166
+ const finalSelection = config.choices
167
+ .map((choice) => choice.value)
168
+ .filter((value) => selectedSet.has(value) && value !== ROOT_STUB_CHOICE_VALUE);
169
+ done(finalSelection);
170
+ return;
171
+ }
172
+ if (isBackspaceKey(key) || key.name === 'escape') {
173
+ setStep('select');
174
+ setError(null);
175
+ }
176
+ }
177
+ });
178
+ const rootStubChoice = selectableChoices.find((choice) => choice.value === ROOT_STUB_CHOICE_VALUE);
179
+ const rootStubSelected = rootStubChoice
180
+ ? selectedSet.has(ROOT_STUB_CHOICE_VALUE)
181
+ : false;
182
+ const nativeChoices = selectableChoices.filter((choice) => choice.value !== ROOT_STUB_CHOICE_VALUE);
183
+ const selectedNativeChoices = nativeChoices.filter((choice) => selectedSet.has(choice.value));
184
+ const formatSummaryLabel = (choice) => {
185
+ const annotation = choice.label.annotation
186
+ ? PALETTE.midGray(` (${choice.label.annotation})`)
187
+ : '';
188
+ const configuredNote = choice.configured
189
+ ? PALETTE.midGray(' (already configured)')
190
+ : '';
191
+ return `${PALETTE.white(choice.label.primary)}${annotation}${configuredNote}`;
192
+ };
193
+ const stepIndex = step === 'intro' ? 1 : step === 'select' ? 2 : 3;
194
+ const lines = [];
195
+ lines.push(PALETTE.midGray(`Step ${stepIndex}/${totalSteps}`));
196
+ lines.push('');
197
+ if (step === 'intro') {
198
+ const introHeadline = config.extendMode
199
+ ? 'Extend your ZhuanSpec tooling'
200
+ : 'Configure your ZhuanSpec tooling';
201
+ const introBody = config.extendMode
202
+ ? 'We detected an existing setup. We will help you refresh or add integrations.'
203
+ : "Let's get your AI assistants connected so they understand ZhuanSpec.";
204
+ lines.push(PALETTE.white(introHeadline));
205
+ lines.push(PALETTE.midGray(introBody));
206
+ lines.push('');
207
+ lines.push(PALETTE.midGray('Press Enter to continue.'));
208
+ }
209
+ else if (step === 'select') {
210
+ lines.push(PALETTE.white(config.baseMessage));
211
+ lines.push(PALETTE.midGray('Use ↑/↓ to move · Space to toggle · Enter selects highlighted tool and reviews.'));
212
+ lines.push('');
213
+ lines.push(page);
214
+ lines.push('');
215
+ lines.push(PALETTE.midGray('Selected configuration:'));
216
+ if (rootStubSelected && rootStubChoice) {
217
+ lines.push(` ${PALETTE.white('-')} ${formatSummaryLabel(rootStubChoice)}`);
218
+ }
219
+ if (selectedNativeChoices.length === 0) {
220
+ lines.push(` ${PALETTE.midGray('- 未选择原生支持的提供者')}`);
221
+ }
222
+ else {
223
+ selectedNativeChoices.forEach((choice) => {
224
+ lines.push(` ${PALETTE.white('-')} ${formatSummaryLabel(choice)}`);
225
+ });
226
+ }
227
+ }
228
+ else {
229
+ lines.push(PALETTE.white('审查选择'));
230
+ lines.push(PALETTE.midGray('按 Enter 确认或按 Backspace 调整。'));
231
+ lines.push('');
232
+ if (rootStubSelected && rootStubChoice) {
233
+ lines.push(`${PALETTE.white('▌')} ${formatSummaryLabel(rootStubChoice)}`);
234
+ }
235
+ if (selectedNativeChoices.length === 0) {
236
+ lines.push(PALETTE.midGray('未选择原生支持的提供者。仍将应用通用说明。'));
237
+ }
238
+ else {
239
+ selectedNativeChoices.forEach((choice) => {
240
+ lines.push(`${PALETTE.white('▌')} ${formatSummaryLabel(choice)}`);
241
+ });
242
+ }
243
+ }
244
+ if (error) {
245
+ return [lines.join('\n'), chalk.red(error)];
246
+ }
247
+ return lines.join('\n');
248
+ });
249
+ export class InitCommand {
250
+ prompt;
251
+ toolsArg;
252
+ businessDirection;
253
+ constructor(options = {}) {
254
+ this.prompt = options.prompt ?? ((config) => toolSelectionWizard(config));
255
+ this.toolsArg = options.tools;
256
+ this.businessDirection = options.businessDirection;
257
+ }
258
+ async execute(targetPath) {
259
+ const projectPath = path.resolve(targetPath);
260
+ const zhuanspecDir = ZHUANSPEC_DIR_NAME;
261
+ const zhuanspecPath = path.join(projectPath, zhuanspecDir);
262
+ // Validation happens silently in the background
263
+ const extendMode = await this.validate(projectPath, zhuanspecPath);
264
+ const existingToolStates = await this.getExistingToolStates(projectPath, extendMode);
265
+ this.renderBanner(extendMode);
266
+ // Get configuration (after validation to avoid prompts if validation fails)
267
+ const config = await this.getConfiguration(existingToolStates, extendMode);
268
+ const availableTools = AI_TOOLS.filter((tool) => tool.available);
269
+ const selectedIds = new Set(config.aiTools);
270
+ const selectedTools = availableTools.filter((tool) => selectedIds.has(tool.value));
271
+ const created = selectedTools.filter((tool) => !existingToolStates[tool.value]);
272
+ const refreshed = selectedTools.filter((tool) => existingToolStates[tool.value]);
273
+ const skippedExisting = availableTools.filter((tool) => !selectedIds.has(tool.value) && existingToolStates[tool.value]);
274
+ const skipped = availableTools.filter((tool) => !selectedIds.has(tool.value) && !existingToolStates[tool.value]);
275
+ // Step 1: Create directory structure
276
+ if (!extendMode) {
277
+ const structureSpinner = this.startSpinner('正在创建 ZhuanSpec 结构...');
278
+ await this.createDirectoryStructure(zhuanspecPath);
279
+ await this.generateFiles(zhuanspecPath, config);
280
+ structureSpinner.stopAndPersist({
281
+ symbol: PALETTE.white('▌'),
282
+ text: PALETTE.white('ZhuanSpec 结构已创建'),
283
+ });
284
+ }
285
+ else {
286
+ ora({ stream: process.stdout }).info(PALETTE.midGray('ℹ ZhuanSpec 已初始化。正在检查缺失的文件...'));
287
+ await this.createDirectoryStructure(zhuanspecPath);
288
+ await this.ensureTemplateFiles(zhuanspecPath, config);
289
+ }
290
+ // Step 2: Configure AI tools
291
+ const toolSpinner = this.startSpinner('正在配置 AI 工具...');
292
+ const rootStubStatus = await this.configureAITools(projectPath, zhuanspecDir, config.aiTools);
293
+ toolSpinner.stopAndPersist({
294
+ symbol: PALETTE.white('▌'),
295
+ text: PALETTE.white('AI 工具已配置'),
296
+ });
297
+ // Step 3: Select business direction and apply architecture file
298
+ await this.promptForBusinessDirection();
299
+ if (this.businessDirection) {
300
+ const archSpinner = this.startSpinner(`正在获取 ${this.businessDirection} 业务架构文档...`);
301
+ const architectureContent = await this.fetchArchitectureFile();
302
+ if (architectureContent) {
303
+ const projectMdPath = path.join(zhuanspecPath, 'project.md');
304
+ const projectMdExists = await FileSystemUtils.fileExists(projectMdPath);
305
+ await FileSystemUtils.writeFile(projectMdPath, architectureContent);
306
+ archSpinner.stopAndPersist({
307
+ symbol: PALETTE.white('▌'),
308
+ text: PALETTE.white(projectMdExists
309
+ ? `已使用 ${this.businessDirection} 业务架构文档覆盖 project.md`
310
+ : `已应用 ${this.businessDirection} 业务架构文档到 project.md`),
311
+ });
312
+ }
313
+ else {
314
+ archSpinner.stopAndPersist({
315
+ symbol: PALETTE.midGray('▌'),
316
+ text: PALETTE.midGray(`未找到 ${this.businessDirection} 业务架构文档,使用默认模板`),
317
+ });
318
+ }
319
+ }
320
+ // Success message
321
+ this.displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode, rootStubStatus);
322
+ }
323
+ async validate(projectPath, _zhuanspecPath) {
324
+ const extendMode = await FileSystemUtils.directoryExists(_zhuanspecPath);
325
+ // Check write permissions
326
+ if (!(await FileSystemUtils.ensureWritePermissions(projectPath))) {
327
+ throw new Error(`Insufficient permissions to write to ${projectPath}`);
328
+ }
329
+ return extendMode;
330
+ }
331
+ async getConfiguration(existingTools, extendMode) {
332
+ const selectedTools = await this.getSelectedTools(existingTools, extendMode);
333
+ return { aiTools: selectedTools };
334
+ }
335
+ async getSelectedTools(existingTools, extendMode) {
336
+ const nonInteractiveSelection = this.resolveToolsArg();
337
+ if (nonInteractiveSelection !== null) {
338
+ return nonInteractiveSelection;
339
+ }
340
+ // Fall back to interactive mode
341
+ return this.promptForAITools(existingTools, extendMode);
342
+ }
343
+ resolveToolsArg() {
344
+ if (typeof this.toolsArg === 'undefined') {
345
+ return null;
346
+ }
347
+ const raw = this.toolsArg.trim();
348
+ if (raw.length === 0) {
349
+ throw new Error('The --tools option requires a value. Use "all", "none", or a comma-separated list of tool IDs.');
350
+ }
351
+ const availableTools = AI_TOOLS.filter((tool) => tool.available);
352
+ const availableValues = availableTools.map((tool) => tool.value);
353
+ const availableSet = new Set(availableValues);
354
+ const availableList = ['all', 'none', ...availableValues].join(', ');
355
+ const lowerRaw = raw.toLowerCase();
356
+ if (lowerRaw === 'all') {
357
+ return availableValues;
358
+ }
359
+ if (lowerRaw === 'none') {
360
+ return [];
361
+ }
362
+ const tokens = raw
363
+ .split(',')
364
+ .map((token) => token.trim())
365
+ .filter((token) => token.length > 0);
366
+ if (tokens.length === 0) {
367
+ throw new Error('The --tools option requires at least one tool ID when not using "all" or "none".');
368
+ }
369
+ const normalizedTokens = tokens.map((token) => token.toLowerCase());
370
+ if (normalizedTokens.some((token) => token === 'all' || token === 'none')) {
371
+ throw new Error('Cannot combine reserved values "all" or "none" with specific tool IDs.');
372
+ }
373
+ const invalidTokens = tokens.filter((_token, index) => !availableSet.has(normalizedTokens[index]));
374
+ if (invalidTokens.length > 0) {
375
+ throw new Error(`Invalid tool(s): ${invalidTokens.join(', ')}. Available values: ${availableList}`);
376
+ }
377
+ const deduped = [];
378
+ for (const token of normalizedTokens) {
379
+ if (!deduped.includes(token)) {
380
+ deduped.push(token);
381
+ }
382
+ }
383
+ return deduped;
384
+ }
385
+ async promptForAITools(existingTools, extendMode) {
386
+ const availableTools = AI_TOOLS.filter((tool) => tool.available);
387
+ const baseMessage = extendMode
388
+ ? '您想添加或刷新哪些原生支持的 AI 工具?'
389
+ : '您使用哪些原生支持的 AI 工具?';
390
+ const initialNativeSelection = extendMode
391
+ ? availableTools
392
+ .filter((tool) => existingTools[tool.value])
393
+ .map((tool) => tool.value)
394
+ : [];
395
+ const initialSelected = Array.from(new Set(initialNativeSelection));
396
+ const choices = [
397
+ {
398
+ kind: 'heading',
399
+ value: '__heading-native__',
400
+ label: {
401
+ primary: '原生支持的提供者(✔ 提供 ZhuanSpec 自定义斜杠命令)',
402
+ },
403
+ selectable: false,
404
+ },
405
+ ...availableTools.map((tool) => ({
406
+ kind: 'option',
407
+ value: tool.value,
408
+ label: parseToolLabel(tool.name),
409
+ configured: Boolean(existingTools[tool.value]),
410
+ selectable: true,
411
+ })),
412
+ ...(availableTools.length
413
+ ? [
414
+ {
415
+ kind: 'info',
416
+ value: LIST_SPACER_VALUE,
417
+ label: { primary: '' },
418
+ selectable: false,
419
+ },
420
+ ]
421
+ : []),
422
+ {
423
+ kind: 'heading',
424
+ value: OTHER_TOOLS_HEADING_VALUE,
425
+ label: {
426
+ primary: '其他工具(为 Amp、VS Code、GitHub Copilot 等使用通用 AGENTS.md)',
427
+ },
428
+ selectable: false,
429
+ },
430
+ {
431
+ kind: 'option',
432
+ value: ROOT_STUB_CHOICE_VALUE,
433
+ label: {
434
+ primary: '通用 AGENTS.md',
435
+ annotation: '始终可用',
436
+ },
437
+ configured: extendMode,
438
+ selectable: true,
439
+ },
440
+ ];
441
+ return this.prompt({
442
+ extendMode,
443
+ baseMessage,
444
+ choices,
445
+ initialSelected,
446
+ });
447
+ }
448
+ async getExistingToolStates(projectPath, extendMode) {
449
+ // Fresh initialization - no tools configured yet
450
+ if (!extendMode) {
451
+ return Object.fromEntries(AI_TOOLS.map(t => [t.value, false]));
452
+ }
453
+ // Extend mode - check all tools in parallel for better performance
454
+ const entries = await Promise.all(AI_TOOLS.map(async (t) => [t.value, await this.isToolConfigured(projectPath, t.value)]));
455
+ return Object.fromEntries(entries);
456
+ }
457
+ async isToolConfigured(projectPath, toolId) {
458
+ // A tool is only considered "configured by ZhuanSpec" if its files contain ZhuanSpec markers.
459
+ // For tools with both config files and slash commands, BOTH must have markers.
460
+ // For slash commands, at least one file with markers is sufficient (not all required).
461
+ // Helper to check if a file exists and contains ZhuanSpec markers
462
+ const fileHasMarkers = async (absolutePath) => {
463
+ try {
464
+ const content = await FileSystemUtils.readFile(absolutePath);
465
+ return content.includes(ZHUANSPEC_MARKERS.start) && content.includes(ZHUANSPEC_MARKERS.end);
466
+ }
467
+ catch {
468
+ return false;
469
+ }
470
+ };
471
+ let hasConfigFile = false;
472
+ let hasSlashCommands = false;
473
+ // Check if the tool has a config file with ZhuanSpec markers
474
+ const configFile = ToolRegistry.get(toolId)?.configFileName;
475
+ if (configFile) {
476
+ const configPath = path.join(projectPath, configFile);
477
+ hasConfigFile = (await FileSystemUtils.fileExists(configPath)) && (await fileHasMarkers(configPath));
478
+ }
479
+ // Check if any slash command file exists with ZhuanSpec markers
480
+ const slashConfigurator = SlashCommandRegistry.get(toolId);
481
+ if (slashConfigurator) {
482
+ for (const target of slashConfigurator.getTargets()) {
483
+ const absolute = slashConfigurator.resolveAbsolutePath(projectPath, target.id);
484
+ if ((await FileSystemUtils.fileExists(absolute)) && (await fileHasMarkers(absolute))) {
485
+ hasSlashCommands = true;
486
+ break; // At least one file with markers is sufficient
487
+ }
488
+ }
489
+ }
490
+ // Tool is only configured if BOTH exist with markers
491
+ // OR if the tool has no config file requirement (slash commands only)
492
+ // OR if the tool has no slash commands requirement (config file only)
493
+ const hasConfigFileRequirement = configFile !== undefined;
494
+ const hasSlashCommandRequirement = slashConfigurator !== undefined;
495
+ if (hasConfigFileRequirement && hasSlashCommandRequirement) {
496
+ // Both are required - both must be present with markers
497
+ return hasConfigFile && hasSlashCommands;
498
+ }
499
+ else if (hasConfigFileRequirement) {
500
+ // Only config file required
501
+ return hasConfigFile;
502
+ }
503
+ else if (hasSlashCommandRequirement) {
504
+ // Only slash commands required
505
+ return hasSlashCommands;
506
+ }
507
+ return false;
508
+ }
509
+ async createDirectoryStructure(zhuanspecPath) {
510
+ const directories = [
511
+ zhuanspecPath,
512
+ path.join(zhuanspecPath, 'specs'),
513
+ path.join(zhuanspecPath, 'changes'),
514
+ path.join(zhuanspecPath, 'changes', 'archive'),
515
+ ];
516
+ for (const dir of directories) {
517
+ await FileSystemUtils.createDirectory(dir);
518
+ }
519
+ }
520
+ async generateFiles(zhuanspecPath, config) {
521
+ await this.writeTemplateFiles(zhuanspecPath, config, false);
522
+ }
523
+ async ensureTemplateFiles(zhuanspecPath, config) {
524
+ await this.writeTemplateFiles(zhuanspecPath, config, true);
525
+ }
526
+ getRepoCwd() {
527
+ const moduleDir = path.dirname(new URL(import.meta.url).pathname);
528
+ const zhuanspecRoot = path.resolve(moduleDir, '..', '..');
529
+ return path.resolve(zhuanspecRoot, '..');
530
+ }
531
+ fetchAvailableBusinessDirections() {
532
+ const dedupe = (items) => Array.from(new Set(items)).sort();
533
+ const remoteFiles = this.fetchRemoteArchitectureFiles();
534
+ const remoteDirections = remoteFiles
535
+ .map((f) => {
536
+ const match = f.match(/^(.+)_project_architect\.md$/);
537
+ return match ? match[1] : null;
538
+ })
539
+ .filter((d) => d !== null);
540
+ if (remoteDirections.length > 0) {
541
+ return dedupe(remoteDirections);
542
+ }
543
+ const repoCwd = this.getRepoCwd();
544
+ // Fallback: try git ls-tree from local branch/ref
545
+ try {
546
+ const output = execSync(`git ls-tree --name-only ${ARCH_REPO_BRANCH} specs/common/`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'], cwd: repoCwd });
547
+ const files = output.trim().split('\n').filter(Boolean);
548
+ const directions = files
549
+ .map((f) => {
550
+ const basename = path.basename(f);
551
+ const match = basename.match(/^(.+)_project_architect\.md$/);
552
+ return match ? match[1] : null;
553
+ })
554
+ .filter((d) => d !== null);
555
+ if (directions.length > 0) {
556
+ return dedupe(directions);
557
+ }
558
+ }
559
+ catch {
560
+ // Ignore
561
+ }
562
+ // Fallback: try current git HEAD tree
563
+ try {
564
+ const output = execSync('git ls-tree --name-only HEAD specs/common/', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'], cwd: repoCwd });
565
+ const files = output.trim().split('\n').filter(Boolean);
566
+ const directions = files
567
+ .map((f) => {
568
+ const basename = path.basename(f);
569
+ const match = basename.match(/^(.+)_project_architect\.md$/);
570
+ return match ? match[1] : null;
571
+ })
572
+ .filter((d) => d !== null);
573
+ if (directions.length > 0) {
574
+ return dedupe(directions);
575
+ }
576
+ }
577
+ catch {
578
+ // Ignore
579
+ }
580
+ // Fallback: try local file system
581
+ try {
582
+ const commonDir = path.resolve(repoCwd, 'specs', 'common');
583
+ const entries = execSync(`ls ${commonDir}`, {
584
+ encoding: 'utf-8',
585
+ stdio: ['pipe', 'pipe', 'ignore'],
586
+ })
587
+ .trim()
588
+ .split('\n')
589
+ .filter(Boolean);
590
+ const directions = entries
591
+ .map((f) => {
592
+ const match = f.match(/^(.+)_project_architect\.md$/);
593
+ return match ? match[1] : null;
594
+ })
595
+ .filter((d) => d !== null);
596
+ if (directions.length > 0) {
597
+ return dedupe(directions);
598
+ }
599
+ }
600
+ catch {
601
+ // Ignore
602
+ }
603
+ // Keep interactive prompt useful even when discovery fails.
604
+ return ['oms', 'ass'];
605
+ }
606
+ async promptForBusinessDirection() {
607
+ if (this.businessDirection) {
608
+ return;
609
+ }
610
+ const directions = this.fetchAvailableBusinessDirections();
611
+ // Check if interactive mode is available
612
+ if (!process.stdin.isTTY || process.env.OPEN_SPEC_INTERACTIVE === '0' || 'CI' in process.env) {
613
+ return;
614
+ }
615
+ const { select, input } = await import('@inquirer/prompts');
616
+ const MANUAL_VALUE = '__manual__';
617
+ const choices = [
618
+ ...directions.map((d) => ({
619
+ name: d,
620
+ value: d,
621
+ })),
622
+ {
623
+ name: '手动输入业务方向',
624
+ value: MANUAL_VALUE,
625
+ },
626
+ {
627
+ name: '跳过(使用默认模板)',
628
+ value: '__skip__',
629
+ },
630
+ ];
631
+ const answer = await select({
632
+ message: '请选择业务方向(用于获取项目架构文档)',
633
+ choices,
634
+ });
635
+ if (answer === MANUAL_VALUE) {
636
+ const manualDirection = (await input({
637
+ message: '请输入业务方向(例如 oms)',
638
+ })).trim();
639
+ if (manualDirection) {
640
+ this.businessDirection = manualDirection;
641
+ }
642
+ return;
643
+ }
644
+ if (answer !== '__skip__') {
645
+ this.businessDirection = answer;
646
+ }
647
+ }
648
+ async fetchArchitectureFile() {
649
+ if (!this.businessDirection) {
650
+ return null;
651
+ }
652
+ const fileName = `${this.businessDirection}_project_architect.md`;
653
+ const filePath = `specs/common/${fileName}`;
654
+ const remoteContent = this.fetchRemoteArchitectureContent(fileName);
655
+ if (remoteContent && remoteContent.trim()) {
656
+ return remoteContent.trim();
657
+ }
658
+ const repoCwd = this.getRepoCwd();
659
+ // Fallback: try git show from local repository branch/ref
660
+ try {
661
+ const content = execSync(`git show ${ARCH_REPO_BRANCH}:${filePath}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'], cwd: repoCwd });
662
+ if (content && content.trim()) {
663
+ return content.trim();
664
+ }
665
+ }
666
+ catch {
667
+ // Ignore
668
+ }
669
+ // Try reading from local file system
670
+ try {
671
+ const localPath = path.resolve(repoCwd, filePath);
672
+ if (await FileSystemUtils.fileExists(localPath)) {
673
+ return await FileSystemUtils.readFile(localPath);
674
+ }
675
+ }
676
+ catch {
677
+ // Ignore
678
+ }
679
+ return null;
680
+ }
681
+ fetchRemoteArchitectureFiles() {
682
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), 'zhuanspec-arch-'));
683
+ try {
684
+ execSync(`git clone --depth 1 --single-branch --branch ${ARCH_REPO_BRANCH} ${ARCH_REPO_URL} "${tempDir}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
685
+ const commonDir = path.join(tempDir, 'specs', 'common');
686
+ if (!existsSync(commonDir)) {
687
+ return [];
688
+ }
689
+ return readdirSync(commonDir).filter((name) => name.endsWith('_project_architect.md'));
690
+ }
691
+ catch {
692
+ return [];
693
+ }
694
+ finally {
695
+ rmSync(tempDir, { recursive: true, force: true });
696
+ }
697
+ }
698
+ fetchRemoteArchitectureContent(fileName) {
699
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), 'zhuanspec-arch-'));
700
+ try {
701
+ execSync(`git clone --depth 1 --single-branch --branch ${ARCH_REPO_BRANCH} ${ARCH_REPO_URL} "${tempDir}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
702
+ const fullPath = path.join(tempDir, 'specs', 'common', fileName);
703
+ if (!existsSync(fullPath)) {
704
+ return null;
705
+ }
706
+ return readFileSync(fullPath, 'utf-8');
707
+ }
708
+ catch {
709
+ return null;
710
+ }
711
+ finally {
712
+ rmSync(tempDir, { recursive: true, force: true });
713
+ }
714
+ }
715
+ async writeTemplateFiles(zhuanspecPath, config, skipExisting) {
716
+ const context = {};
717
+ const templates = TemplateManager.getTemplates(context);
718
+ for (const template of templates) {
719
+ const filePath = path.join(zhuanspecPath, template.path);
720
+ // Skip if file exists and we're in skipExisting mode
721
+ if (skipExisting && (await FileSystemUtils.fileExists(filePath))) {
722
+ continue;
723
+ }
724
+ const content = typeof template.content === 'function'
725
+ ? template.content(context)
726
+ : template.content;
727
+ await FileSystemUtils.writeFile(filePath, content);
728
+ }
729
+ }
730
+ async configureAITools(projectPath, zhuanspecDir, toolIds) {
731
+ const rootStubStatus = await this.configureRootAgentsStub(projectPath, zhuanspecDir);
732
+ for (const toolId of toolIds) {
733
+ const configurator = ToolRegistry.get(toolId);
734
+ if (configurator && configurator.isAvailable) {
735
+ await configurator.configure(projectPath, zhuanspecDir);
736
+ }
737
+ const slashConfigurator = SlashCommandRegistry.get(toolId);
738
+ if (slashConfigurator && slashConfigurator.isAvailable) {
739
+ await slashConfigurator.generateAll(projectPath, zhuanspecDir);
740
+ }
741
+ }
742
+ return rootStubStatus;
743
+ }
744
+ async configureRootAgentsStub(projectPath, zhuanspecDir) {
745
+ const configurator = ToolRegistry.get('agents');
746
+ if (!configurator || !configurator.isAvailable) {
747
+ return 'skipped';
748
+ }
749
+ const stubPath = path.join(projectPath, configurator.configFileName);
750
+ const existed = await FileSystemUtils.fileExists(stubPath);
751
+ await configurator.configure(projectPath, zhuanspecDir);
752
+ return existed ? 'updated' : 'created';
753
+ }
754
+ displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode, rootStubStatus) {
755
+ console.log(); // Empty line for spacing
756
+ const successHeadline = extendMode
757
+ ? 'ZhuanSpec 工具配置已更新!'
758
+ : 'ZhuanSpec 初始化成功!';
759
+ ora().succeed(PALETTE.white(successHeadline));
760
+ console.log();
761
+ console.log(PALETTE.lightGray('工具摘要:'));
762
+ const summaryLines = [
763
+ rootStubStatus === 'created'
764
+ ? `${PALETTE.white('▌')} ${PALETTE.white('为其他助手创建了根目录 AGENTS.md 存根')}`
765
+ : null,
766
+ rootStubStatus === 'updated'
767
+ ? `${PALETTE.lightGray('▌')} ${PALETTE.lightGray('为其他助手刷新了根目录 AGENTS.md 存根')}`
768
+ : null,
769
+ created.length
770
+ ? `${PALETTE.white('▌')} ${PALETTE.white('已创建:')} ${this.formatToolNames(created)}`
771
+ : null,
772
+ refreshed.length
773
+ ? `${PALETTE.lightGray('▌')} ${PALETTE.lightGray('已刷新:')} ${this.formatToolNames(refreshed)}`
774
+ : null,
775
+ skippedExisting.length
776
+ ? `${PALETTE.midGray('▌')} ${PALETTE.midGray('已跳过(已配置):')} ${this.formatToolNames(skippedExisting)}`
777
+ : null,
778
+ skipped.length
779
+ ? `${PALETTE.darkGray('▌')} ${PALETTE.darkGray('已跳过:')} ${this.formatToolNames(skipped)}`
780
+ : null,
781
+ ].filter((line) => Boolean(line));
782
+ for (const line of summaryLines) {
783
+ console.log(line);
784
+ }
785
+ console.log();
786
+ console.log(PALETTE.midGray('将来使用 `zhuanspec update` 刷新共享的 ZhuanSpec 说明。'));
787
+ // Show restart instruction if any tools were configured
788
+ if (created.length > 0 || refreshed.length > 0) {
789
+ console.log();
790
+ console.log(PALETTE.white('重要:请重启您的 IDE'));
791
+ console.log(PALETTE.midGray('斜杠命令在启动时加载。请重启您的编程助手'));
792
+ console.log(PALETTE.midGray('以确保新的 /zhuanspec 命令出现在命令面板中。'));
793
+ }
794
+ // Get the selected tool name(s) for display
795
+ const toolName = this.formatToolNames(selectedTools);
796
+ console.log();
797
+ console.log(`下一步 - 将这些提示复制到 ${toolName}:`);
798
+ console.log(chalk.gray('────────────────────────────────────────────────────────────'));
799
+ console.log(PALETTE.white('1. 填充您的项目上下文:'));
800
+ console.log(PALETTE.lightGray(' "请阅读 zhuanspec/project.md 并帮助我填写'));
801
+ console.log(PALETTE.lightGray(' 有关我的项目、技术栈和约定的详细信息"\n'));
802
+ console.log(PALETTE.white('2. 创建您的第一个变更提案:'));
803
+ console.log(PALETTE.lightGray(' "我想添加 [您的功能]。请为此功能'));
804
+ console.log(PALETTE.lightGray(' 创建一个 ZhuanSpec 变更提案"\n'));
805
+ console.log(PALETTE.white('3. 了解 ZhuanSpec 工作流:'));
806
+ console.log(PALETTE.lightGray(' "请解释 zhuanspec/AGENTS.md 中的 ZhuanSpec 工作流'));
807
+ console.log(PALETTE.lightGray(' 以及我应该如何在这个项目上与您合作"'));
808
+ console.log(PALETTE.darkGray('────────────────────────────────────────────────────────────\n'));
809
+ // Codex heads-up: prompts installed globally
810
+ const selectedToolIds = new Set(selectedTools.map((t) => t.value));
811
+ if (selectedToolIds.has('codex')) {
812
+ console.log(PALETTE.white('Codex 设置说明'));
813
+ console.log(PALETTE.midGray('提示已安装到 ~/.codex/prompts(或 $CODEX_HOME/prompts)。'));
814
+ console.log();
815
+ }
816
+ }
817
+ formatToolNames(tools) {
818
+ const names = tools
819
+ .map((tool) => tool.successLabel ?? tool.name)
820
+ .filter((name) => Boolean(name));
821
+ if (names.length === 0)
822
+ return PALETTE.lightGray('your AGENTS.md-compatible assistant');
823
+ if (names.length === 1)
824
+ return PALETTE.white(names[0]);
825
+ const base = names.slice(0, -1).map((name) => PALETTE.white(name));
826
+ const last = PALETTE.white(names[names.length - 1]);
827
+ return `${base.join(PALETTE.midGray(', '))}${base.length ? PALETTE.midGray(', and ') : ''}${last}`;
828
+ }
829
+ renderBanner(_extendMode) {
830
+ const rows = ['', '', '', '', ''];
831
+ for (const char of 'ZHUANSPEC') {
832
+ const glyph = LETTER_MAP[char] ?? LETTER_MAP[' '];
833
+ for (let i = 0; i < rows.length; i += 1) {
834
+ rows[i] += `${glyph[i]} `;
835
+ }
836
+ }
837
+ const rowStyles = [
838
+ PALETTE.white,
839
+ PALETTE.lightGray,
840
+ PALETTE.midGray,
841
+ PALETTE.lightGray,
842
+ PALETTE.white,
843
+ ];
844
+ console.log();
845
+ rows.forEach((row, index) => {
846
+ console.log(rowStyles[index](row.replace(/\s+$/u, '')));
847
+ });
848
+ console.log();
849
+ console.log(PALETTE.white('Welcome to ZhuanSpec!'));
850
+ console.log();
851
+ }
852
+ startSpinner(text) {
853
+ return ora({
854
+ text,
855
+ stream: process.stdout,
856
+ color: 'gray',
857
+ spinner: PROGRESS_SPINNER,
858
+ }).start();
859
+ }
860
+ }
861
+ //# sourceMappingURL=init.js.map