@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.
- package/LICENSE +22 -0
- package/README.md +461 -0
- package/README.zh.md +434 -0
- package/bin/zhuanspec.js +3 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +356 -0
- package/dist/commands/artifact-workflow.d.ts +13 -0
- package/dist/commands/artifact-workflow.js +916 -0
- package/dist/commands/change.d.ts +35 -0
- package/dist/commands/change.js +277 -0
- package/dist/commands/completion.d.ts +72 -0
- package/dist/commands/completion.js +221 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +198 -0
- package/dist/commands/show.d.ts +14 -0
- package/dist/commands/show.js +132 -0
- package/dist/commands/spec.d.ts +15 -0
- package/dist/commands/spec.js +225 -0
- package/dist/commands/validate.d.ts +24 -0
- package/dist/commands/validate.js +294 -0
- package/dist/core/archive.d.ts +30 -0
- package/dist/core/archive.js +438 -0
- package/dist/core/artifact-graph/graph.d.ts +56 -0
- package/dist/core/artifact-graph/graph.js +141 -0
- package/dist/core/artifact-graph/index.d.ts +7 -0
- package/dist/core/artifact-graph/index.js +13 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +134 -0
- package/dist/core/artifact-graph/instruction-loader.js +180 -0
- package/dist/core/artifact-graph/resolver.d.ts +61 -0
- package/dist/core/artifact-graph/resolver.js +187 -0
- package/dist/core/artifact-graph/schema.d.ts +13 -0
- package/dist/core/artifact-graph/schema.js +108 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +54 -0
- package/dist/core/artifact-graph/types.d.ts +45 -0
- package/dist/core/artifact-graph/types.js +43 -0
- package/dist/core/completions/command-registry.d.ts +7 -0
- package/dist/core/completions/command-registry.js +362 -0
- package/dist/core/completions/completion-provider.d.ts +60 -0
- package/dist/core/completions/completion-provider.js +102 -0
- package/dist/core/completions/factory.d.ts +51 -0
- package/dist/core/completions/factory.js +57 -0
- package/dist/core/completions/generators/zsh-generator.d.ts +58 -0
- package/dist/core/completions/generators/zsh-generator.js +319 -0
- package/dist/core/completions/installers/zsh-installer.d.ts +136 -0
- package/dist/core/completions/installers/zsh-installer.js +449 -0
- package/dist/core/completions/types.d.ts +78 -0
- package/dist/core/completions/types.js +2 -0
- package/dist/core/config-schema.d.ts +76 -0
- package/dist/core/config-schema.js +200 -0
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +29 -0
- package/dist/core/configurators/agents.d.ts +8 -0
- package/dist/core/configurators/agents.js +15 -0
- package/dist/core/configurators/base.d.ts +7 -0
- package/dist/core/configurators/base.js +2 -0
- package/dist/core/configurators/claude.d.ts +8 -0
- package/dist/core/configurators/claude.js +15 -0
- package/dist/core/configurators/cline.d.ts +8 -0
- package/dist/core/configurators/cline.js +15 -0
- package/dist/core/configurators/codebuddy.d.ts +8 -0
- package/dist/core/configurators/codebuddy.js +15 -0
- package/dist/core/configurators/costrict.d.ts +8 -0
- package/dist/core/configurators/costrict.js +15 -0
- package/dist/core/configurators/iflow.d.ts +8 -0
- package/dist/core/configurators/iflow.js +15 -0
- package/dist/core/configurators/qoder.d.ts +30 -0
- package/dist/core/configurators/qoder.js +42 -0
- package/dist/core/configurators/qwen.d.ts +24 -0
- package/dist/core/configurators/qwen.js +37 -0
- package/dist/core/configurators/registry.d.ts +9 -0
- package/dist/core/configurators/registry.js +43 -0
- package/dist/core/configurators/slash/amazon-q.d.ts +9 -0
- package/dist/core/configurators/slash/amazon-q.js +46 -0
- package/dist/core/configurators/slash/antigravity.d.ts +9 -0
- package/dist/core/configurators/slash/antigravity.js +23 -0
- package/dist/core/configurators/slash/auggie.d.ts +9 -0
- package/dist/core/configurators/slash/auggie.js +31 -0
- package/dist/core/configurators/slash/base.d.ts +19 -0
- package/dist/core/configurators/slash/base.js +69 -0
- package/dist/core/configurators/slash/claude.d.ts +9 -0
- package/dist/core/configurators/slash/claude.js +37 -0
- package/dist/core/configurators/slash/cline.d.ts +9 -0
- package/dist/core/configurators/slash/cline.js +23 -0
- package/dist/core/configurators/slash/codebuddy.d.ts +9 -0
- package/dist/core/configurators/slash/codebuddy.js +37 -0
- package/dist/core/configurators/slash/codex.d.ts +14 -0
- package/dist/core/configurators/slash/codex.js +109 -0
- package/dist/core/configurators/slash/costrict.d.ts +9 -0
- package/dist/core/configurators/slash/costrict.js +31 -0
- package/dist/core/configurators/slash/crush.d.ts +9 -0
- package/dist/core/configurators/slash/crush.js +37 -0
- package/dist/core/configurators/slash/cursor.d.ts +9 -0
- package/dist/core/configurators/slash/cursor.js +37 -0
- package/dist/core/configurators/slash/factory.d.ts +10 -0
- package/dist/core/configurators/slash/factory.js +35 -0
- package/dist/core/configurators/slash/gemini.d.ts +9 -0
- package/dist/core/configurators/slash/gemini.js +22 -0
- package/dist/core/configurators/slash/github-copilot.d.ts +9 -0
- package/dist/core/configurators/slash/github-copilot.js +34 -0
- package/dist/core/configurators/slash/iflow.d.ts +9 -0
- package/dist/core/configurators/slash/iflow.js +37 -0
- package/dist/core/configurators/slash/kilocode.d.ts +9 -0
- package/dist/core/configurators/slash/kilocode.js +17 -0
- package/dist/core/configurators/slash/opencode.d.ts +12 -0
- package/dist/core/configurators/slash/opencode.js +72 -0
- package/dist/core/configurators/slash/qoder.d.ts +35 -0
- package/dist/core/configurators/slash/qoder.js +76 -0
- package/dist/core/configurators/slash/qwen.d.ts +32 -0
- package/dist/core/configurators/slash/qwen.js +49 -0
- package/dist/core/configurators/slash/registry.d.ts +8 -0
- package/dist/core/configurators/slash/registry.js +75 -0
- package/dist/core/configurators/slash/roocode.d.ts +9 -0
- package/dist/core/configurators/slash/roocode.js +23 -0
- package/dist/core/configurators/slash/toml-base.d.ts +10 -0
- package/dist/core/configurators/slash/toml-base.js +53 -0
- package/dist/core/configurators/slash/windsurf.d.ts +9 -0
- package/dist/core/configurators/slash/windsurf.js +23 -0
- package/dist/core/converters/json-converter.d.ts +6 -0
- package/dist/core/converters/json-converter.js +51 -0
- package/dist/core/global-config.d.ts +39 -0
- package/dist/core/global-config.js +115 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -0
- package/dist/core/init.d.ts +60 -0
- package/dist/core/init.js +861 -0
- package/dist/core/list.d.ts +9 -0
- package/dist/core/list.js +171 -0
- package/dist/core/parsers/change-parser.d.ts +13 -0
- package/dist/core/parsers/change-parser.js +193 -0
- package/dist/core/parsers/markdown-parser.d.ts +22 -0
- package/dist/core/parsers/markdown-parser.js +187 -0
- package/dist/core/parsers/requirement-blocks.d.ts +37 -0
- package/dist/core/parsers/requirement-blocks.js +201 -0
- package/dist/core/project-config.d.ts +34 -0
- package/dist/core/project-config.js +79 -0
- package/dist/core/schemas/base.schema.d.ts +13 -0
- package/dist/core/schemas/base.schema.js +13 -0
- package/dist/core/schemas/change.schema.d.ts +73 -0
- package/dist/core/schemas/change.schema.js +31 -0
- package/dist/core/schemas/index.d.ts +4 -0
- package/dist/core/schemas/index.js +4 -0
- package/dist/core/schemas/spec.schema.d.ts +18 -0
- package/dist/core/schemas/spec.schema.js +15 -0
- package/dist/core/skill-discovery.d.ts +24 -0
- package/dist/core/skill-discovery.js +153 -0
- package/dist/core/specs-apply.d.ts +73 -0
- package/dist/core/specs-apply.js +384 -0
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/agents-root-stub.d.ts +2 -0
- package/dist/core/templates/agents-root-stub.js +17 -0
- package/dist/core/templates/agents-template.d.ts +2 -0
- package/dist/core/templates/agents-template.js +706 -0
- package/dist/core/templates/claude-template.d.ts +2 -0
- package/dist/core/templates/claude-template.js +2 -0
- package/dist/core/templates/cline-template.d.ts +2 -0
- package/dist/core/templates/cline-template.js +2 -0
- package/dist/core/templates/costrict-template.d.ts +2 -0
- package/dist/core/templates/costrict-template.js +2 -0
- package/dist/core/templates/index.d.ts +17 -0
- package/dist/core/templates/index.js +37 -0
- package/dist/core/templates/project-template.d.ts +8 -0
- package/dist/core/templates/project-template.js +32 -0
- package/dist/core/templates/skill-templates.d.ts +103 -0
- package/dist/core/templates/skill-templates.js +2131 -0
- package/dist/core/templates/slash-command-templates.d.ts +4 -0
- package/dist/core/templates/slash-command-templates.js +81 -0
- package/dist/core/update.d.ts +4 -0
- package/dist/core/update.js +88 -0
- package/dist/core/validation/constants.d.ts +34 -0
- package/dist/core/validation/constants.js +40 -0
- package/dist/core/validation/types.d.ts +18 -0
- package/dist/core/validation/types.js +2 -0
- package/dist/core/validation/validator.d.ts +33 -0
- package/dist/core/validation/validator.js +409 -0
- package/dist/core/view.d.ts +8 -0
- package/dist/core/view.js +168 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/utils/change-metadata.d.ts +47 -0
- package/dist/utils/change-metadata.js +130 -0
- package/dist/utils/change-utils.d.ts +51 -0
- package/dist/utils/change-utils.js +100 -0
- package/dist/utils/file-system.d.ts +19 -0
- package/dist/utils/file-system.js +177 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/interactive.d.ts +18 -0
- package/dist/utils/interactive.js +21 -0
- package/dist/utils/item-discovery.d.ts +4 -0
- package/dist/utils/item-discovery.js +72 -0
- package/dist/utils/match.d.ts +3 -0
- package/dist/utils/match.js +22 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +41 -0
- package/dist/utils/task-progress.d.ts +8 -0
- package/dist/utils/task-progress.js +36 -0
- package/package.json +81 -0
- package/schemas/spec-driven/schema.yaml +205 -0
- package/schemas/spec-driven/templates/design.md +19 -0
- package/schemas/spec-driven/templates/proposal.md +43 -0
- package/schemas/spec-driven/templates/spec.md +8 -0
- package/schemas/spec-driven/templates/tasks.md +25 -0
- package/schemas/tdd/schema.yaml +213 -0
- package/schemas/tdd/templates/docs.md +0 -0
- package/schemas/tdd/templates/implementation.md +11 -0
- package/schemas/tdd/templates/spec.md +11 -0
- package/schemas/tdd/templates/test.md +11 -0
- 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
|