peaks-cli 1.0.17 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/commands/request-commands.js +109 -3
- package/dist/src/cli/commands/scan-commands.d.ts +3 -0
- package/dist/src/cli/commands/scan-commands.js +194 -0
- package/dist/src/cli/commands/workspace-commands.d.ts +3 -0
- package/dist/src/cli/commands/workspace-commands.js +32 -0
- package/dist/src/cli/program.js +4 -0
- package/dist/src/services/artifacts/artifact-lint-service.d.ts +23 -0
- package/dist/src/services/artifacts/artifact-lint-service.js +80 -0
- package/dist/src/services/artifacts/artifact-prerequisites.d.ts +28 -0
- package/dist/src/services/artifacts/artifact-prerequisites.js +77 -0
- package/dist/src/services/artifacts/repair-cycle-service.d.ts +23 -0
- package/dist/src/services/artifacts/repair-cycle-service.js +52 -0
- package/dist/src/services/artifacts/request-artifact-service.d.ts +14 -0
- package/dist/src/services/artifacts/request-artifact-service.js +73 -21
- package/dist/src/services/scan/acceptance-coverage-service.d.ts +42 -0
- package/dist/src/services/scan/acceptance-coverage-service.js +135 -0
- package/dist/src/services/scan/archetype-service.d.ts +5 -0
- package/dist/src/services/scan/archetype-service.js +253 -0
- package/dist/src/services/scan/diff-scope-service.d.ts +40 -0
- package/dist/src/services/scan/diff-scope-service.js +198 -0
- package/dist/src/services/scan/existing-system-service.d.ts +7 -0
- package/dist/src/services/scan/existing-system-service.js +300 -0
- package/dist/src/services/scan/scan-types.d.ts +59 -0
- package/dist/src/services/scan/scan-types.js +1 -0
- package/dist/src/services/scan/type-sanity-service.d.ts +23 -0
- package/dist/src/services/scan/type-sanity-service.js +108 -0
- package/dist/src/services/standards/project-context.d.ts +24 -0
- package/dist/src/services/standards/project-context.js +318 -0
- package/dist/src/services/standards/project-standards-service.js +145 -40
- package/dist/src/services/workspace/workspace-service.d.ts +16 -0
- package/dist/src/services/workspace/workspace-service.js +66 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/peaks-qa/SKILL.md +56 -0
- package/skills/peaks-rd/SKILL.md +65 -2
- package/skills/peaks-solo/SKILL.md +307 -61
- package/skills/peaks-solo/references/existing-system-extraction.md +78 -0
- package/skills/peaks-ui/SKILL.md +9 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
function readPackageJson(projectRoot) {
|
|
4
|
+
const pkgPath = join(projectRoot, 'package.json');
|
|
5
|
+
if (!existsSync(pkgPath))
|
|
6
|
+
return { exists: false, deps: {} };
|
|
7
|
+
try {
|
|
8
|
+
const raw = readFileSync(pkgPath, 'utf8');
|
|
9
|
+
const parsed = JSON.parse(raw);
|
|
10
|
+
return {
|
|
11
|
+
exists: true,
|
|
12
|
+
deps: {
|
|
13
|
+
...(parsed.dependencies ?? {}),
|
|
14
|
+
...(parsed.devDependencies ?? {}),
|
|
15
|
+
...(parsed.peerDependencies ?? {})
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return { exists: true, deps: {} };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function configExists(projectRoot, candidates) {
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
if (existsSync(join(projectRoot, candidate)))
|
|
26
|
+
return candidate;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
function detectBuildTool(projectRoot) {
|
|
31
|
+
const map = [
|
|
32
|
+
['umi', ['.umirc.ts', '.umirc.js', 'config/config.ts', 'config/config.js']],
|
|
33
|
+
['next', ['next.config.js', 'next.config.ts', 'next.config.mjs']],
|
|
34
|
+
['vite', ['vite.config.ts', 'vite.config.js', 'vite.config.mjs']],
|
|
35
|
+
['rsbuild', ['rsbuild.config.ts', 'rsbuild.config.js']],
|
|
36
|
+
['rspack', ['rspack.config.ts', 'rspack.config.js']],
|
|
37
|
+
['farm', ['farm.config.ts', 'farm.config.js']],
|
|
38
|
+
['craco', ['craco.config.js', 'craco.config.ts']],
|
|
39
|
+
['webpack', ['webpack.config.js', 'webpack.config.ts']],
|
|
40
|
+
['gulp', ['gulpfile.js', 'gulpfile.ts']],
|
|
41
|
+
['angular', ['angular.json']]
|
|
42
|
+
];
|
|
43
|
+
for (const [tool, candidates] of map) {
|
|
44
|
+
const found = configExists(projectRoot, candidates);
|
|
45
|
+
if (found !== undefined)
|
|
46
|
+
return { tool, path: found };
|
|
47
|
+
}
|
|
48
|
+
return { tool: 'unknown' };
|
|
49
|
+
}
|
|
50
|
+
function majorOf(version) {
|
|
51
|
+
if (version === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
const cleaned = version.replace(/^[\^~>=<]+/, '').trim();
|
|
54
|
+
const major = cleaned.split('.')[0];
|
|
55
|
+
return major !== undefined && /^\d+$/.test(major) ? major : undefined;
|
|
56
|
+
}
|
|
57
|
+
function detectComponentLibrary(deps) {
|
|
58
|
+
if ('antd' in deps) {
|
|
59
|
+
const major = majorOf(deps['antd']);
|
|
60
|
+
const hasProSuite = '@ant-design/pro-components' in deps || Object.keys(deps).some((d) => d.startsWith('@ant-design/pro-'));
|
|
61
|
+
return hasProSuite
|
|
62
|
+
? { name: 'antd-pro', ...(major !== undefined ? { majorVersion: major } : {}), hasProSuite: true }
|
|
63
|
+
: { name: 'antd', ...(major !== undefined ? { majorVersion: major } : {}) };
|
|
64
|
+
}
|
|
65
|
+
if ('@mui/material' in deps)
|
|
66
|
+
return { name: 'mui', ...(majorOf(deps['@mui/material']) !== undefined ? { majorVersion: majorOf(deps['@mui/material']) } : {}) };
|
|
67
|
+
if ('tailwindcss' in deps && Object.keys(deps).some((d) => d.startsWith('@radix-ui/')))
|
|
68
|
+
return { name: 'shadcn' };
|
|
69
|
+
if ('element-plus' in deps)
|
|
70
|
+
return { name: 'element-plus' };
|
|
71
|
+
if ('element-ui' in deps)
|
|
72
|
+
return { name: 'element-ui' };
|
|
73
|
+
if ('@arco-design/web-react' in deps)
|
|
74
|
+
return { name: 'arco' };
|
|
75
|
+
if ('tdesign-react' in deps || 'tdesign-vue-next' in deps)
|
|
76
|
+
return { name: 'tdesign' };
|
|
77
|
+
if ('@douyinfe/semi-ui' in deps)
|
|
78
|
+
return { name: 'semi' };
|
|
79
|
+
if ('@nextui-org/react' in deps)
|
|
80
|
+
return { name: 'nextui' };
|
|
81
|
+
if ('@chakra-ui/react' in deps)
|
|
82
|
+
return { name: 'chakra' };
|
|
83
|
+
if ('vant' in deps)
|
|
84
|
+
return { name: 'vant' };
|
|
85
|
+
return { name: 'none' };
|
|
86
|
+
}
|
|
87
|
+
function detectCssFrameworks(projectRoot, deps) {
|
|
88
|
+
const frameworks = [];
|
|
89
|
+
if ('tailwindcss' in deps || existsSync(join(projectRoot, 'tailwind.config.js')) || existsSync(join(projectRoot, 'tailwind.config.ts'))) {
|
|
90
|
+
frameworks.push('tailwind');
|
|
91
|
+
}
|
|
92
|
+
if ('less' in deps || 'less-loader' in deps)
|
|
93
|
+
frameworks.push('less');
|
|
94
|
+
if ('sass' in deps || 'node-sass' in deps || 'sass-loader' in deps)
|
|
95
|
+
frameworks.push('sass');
|
|
96
|
+
if ('styled-components' in deps)
|
|
97
|
+
frameworks.push('styled-components');
|
|
98
|
+
if ('@emotion/react' in deps || '@emotion/styled' in deps)
|
|
99
|
+
frameworks.push('emotion');
|
|
100
|
+
// css-modules detection is heuristic (Umi/Next default); skip unless we see *.module.* in src
|
|
101
|
+
return frameworks;
|
|
102
|
+
}
|
|
103
|
+
function detectCssConflicts(library, frameworks) {
|
|
104
|
+
const conflicts = [];
|
|
105
|
+
const hasTailwind = frameworks.includes('tailwind');
|
|
106
|
+
if (hasTailwind && (library.name === 'antd' || library.name === 'antd-pro')) {
|
|
107
|
+
conflicts.push("Tailwind preflight reset can break antd component base styles; set `corePlugins.preflight: false` in tailwind.config or scope Tailwind via `important: '#root'`.");
|
|
108
|
+
}
|
|
109
|
+
if (hasTailwind && library.name === 'mui') {
|
|
110
|
+
conflicts.push('Tailwind preflight overrides MUI base styles; disable Tailwind preflight or scope it away from MUI roots.');
|
|
111
|
+
}
|
|
112
|
+
const cssInJsCount = [frameworks.includes('styled-components'), frameworks.includes('emotion')].filter(Boolean).length;
|
|
113
|
+
if (cssInJsCount >= 2) {
|
|
114
|
+
conflicts.push('Multiple CSS-in-JS libraries detected (styled-components + emotion); pick one and remove the other.');
|
|
115
|
+
}
|
|
116
|
+
return conflicts;
|
|
117
|
+
}
|
|
118
|
+
function detectStateManagement(deps) {
|
|
119
|
+
const map = [
|
|
120
|
+
['zustand', 'zustand'],
|
|
121
|
+
['jotai', 'jotai'],
|
|
122
|
+
['@reduxjs/toolkit', 'Redux Toolkit'],
|
|
123
|
+
['redux', 'redux'],
|
|
124
|
+
['valtio', 'valtio'],
|
|
125
|
+
['mobx', 'mobx'],
|
|
126
|
+
['hox', 'hox']
|
|
127
|
+
];
|
|
128
|
+
return map.filter(([dep]) => dep in deps).map(([, label]) => label);
|
|
129
|
+
}
|
|
130
|
+
function detectRouting(deps) {
|
|
131
|
+
const out = [];
|
|
132
|
+
if ('react-router-dom' in deps)
|
|
133
|
+
out.push('react-router-dom');
|
|
134
|
+
if ('@umijs/max' in deps || '@umijs/preset-react' in deps)
|
|
135
|
+
out.push('Umi router');
|
|
136
|
+
if ('next' in deps)
|
|
137
|
+
out.push('Next.js file-based');
|
|
138
|
+
if ('vue-router' in deps)
|
|
139
|
+
out.push('vue-router');
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
function detectDataFetching(deps) {
|
|
143
|
+
const out = [];
|
|
144
|
+
if ('@tanstack/react-query' in deps)
|
|
145
|
+
out.push('@tanstack/react-query');
|
|
146
|
+
if ('swr' in deps)
|
|
147
|
+
out.push('swr');
|
|
148
|
+
if ('ahooks' in deps)
|
|
149
|
+
out.push('ahooks (useRequest)');
|
|
150
|
+
if ('umi-request' in deps)
|
|
151
|
+
out.push('umi-request');
|
|
152
|
+
if ('axios' in deps)
|
|
153
|
+
out.push('axios');
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
function detectLegacySignals(projectRoot, deps) {
|
|
157
|
+
const signals = [];
|
|
158
|
+
if ('moment' in deps)
|
|
159
|
+
signals.push('`moment` in deps — prefer `dayjs` or `date-fns` for new code');
|
|
160
|
+
if (Object.keys(deps).some((d) => d.startsWith('enzyme')))
|
|
161
|
+
signals.push('Enzyme test suite — write new tests with React Testing Library');
|
|
162
|
+
if ('redux-saga' in deps)
|
|
163
|
+
signals.push('redux-saga — keep saga patterns for existing flows; use Redux Toolkit thunks/RTK Query for new code');
|
|
164
|
+
if ('redux-thunk' in deps && !('@reduxjs/toolkit' in deps))
|
|
165
|
+
signals.push('Plain redux-thunk — prefer Redux Toolkit createAsyncThunk for new code');
|
|
166
|
+
if ('jquery' in deps)
|
|
167
|
+
signals.push('jQuery — do not add new jQuery usage');
|
|
168
|
+
if ('backbone' in deps)
|
|
169
|
+
signals.push('Backbone — legacy; do not add new Backbone code');
|
|
170
|
+
if (deps['vue']?.startsWith('2') === true)
|
|
171
|
+
signals.push('Vue 2 — preserve Options API for existing components');
|
|
172
|
+
// Lightweight heuristic for class components and inline styles in src/
|
|
173
|
+
const srcRoot = join(projectRoot, 'src');
|
|
174
|
+
if (existsSync(srcRoot)) {
|
|
175
|
+
const sample = sampleSourceFiles(srcRoot, 80);
|
|
176
|
+
let classComponentHits = 0;
|
|
177
|
+
let inlineStyleHits = 0;
|
|
178
|
+
for (const filePath of sample) {
|
|
179
|
+
try {
|
|
180
|
+
const content = readFileSync(filePath, 'utf8');
|
|
181
|
+
if (/extends\s+(?:React\.)?Component\b/.test(content))
|
|
182
|
+
classComponentHits += 1;
|
|
183
|
+
const matches = content.match(/style=\{\{/g);
|
|
184
|
+
if (matches !== null)
|
|
185
|
+
inlineStyleHits += matches.length;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// ignore unreadable files
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (classComponentHits >= 1)
|
|
192
|
+
signals.push(`React class components detected (${classComponentHits}+ files) — keep class style for existing modules, use function components + hooks for new code`);
|
|
193
|
+
if (inlineStyleHits >= 50)
|
|
194
|
+
signals.push(`Inline styles dominant (${inlineStyleHits}+ occurrences) — match existing styling for new code in same modules`);
|
|
195
|
+
}
|
|
196
|
+
return signals;
|
|
197
|
+
}
|
|
198
|
+
function sampleSourceFiles(root, limit) {
|
|
199
|
+
const out = [];
|
|
200
|
+
const queue = [root];
|
|
201
|
+
while (queue.length > 0 && out.length < limit) {
|
|
202
|
+
const dir = queue.shift();
|
|
203
|
+
let entries;
|
|
204
|
+
try {
|
|
205
|
+
entries = readdirSync(dir);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
for (const entry of entries) {
|
|
211
|
+
if (entry.startsWith('.') || entry === 'node_modules' || entry === 'dist' || entry === 'build')
|
|
212
|
+
continue;
|
|
213
|
+
const full = join(dir, entry);
|
|
214
|
+
let s;
|
|
215
|
+
try {
|
|
216
|
+
s = statSync(full);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (s.isDirectory()) {
|
|
222
|
+
queue.push(full);
|
|
223
|
+
}
|
|
224
|
+
else if (/\.(tsx|jsx)$/.test(entry)) {
|
|
225
|
+
out.push(full);
|
|
226
|
+
if (out.length >= limit)
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
function notableDepsList(deps) {
|
|
234
|
+
const interesting = ['monaco-editor', '@monaco-editor/react', 'react-querybuilder', '@dnd-kit/core', 'react-dnd', 'echarts', 'recharts', '@ant-design/charts', 'antd-style', 'lodash', 'lodash-es', 'rxjs', 'socket.io-client'];
|
|
235
|
+
return interesting.filter((d) => d in deps);
|
|
236
|
+
}
|
|
237
|
+
export function detectProjectContext(projectRoot) {
|
|
238
|
+
const { exists, deps } = readPackageJson(projectRoot);
|
|
239
|
+
const { tool, path } = detectBuildTool(projectRoot);
|
|
240
|
+
const componentLibrary = detectComponentLibrary(deps);
|
|
241
|
+
const cssFrameworks = detectCssFrameworks(projectRoot, deps);
|
|
242
|
+
return {
|
|
243
|
+
hasPackageJson: exists,
|
|
244
|
+
buildTool: tool,
|
|
245
|
+
...(path !== undefined ? { buildConfigPath: path } : {}),
|
|
246
|
+
componentLibrary,
|
|
247
|
+
cssFrameworks,
|
|
248
|
+
cssConflicts: detectCssConflicts(componentLibrary, cssFrameworks),
|
|
249
|
+
stateManagement: detectStateManagement(deps),
|
|
250
|
+
routing: detectRouting(deps),
|
|
251
|
+
dataFetching: detectDataFetching(deps),
|
|
252
|
+
legacySignals: detectLegacySignals(projectRoot, deps),
|
|
253
|
+
notableDeps: notableDepsList(deps)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
export function buildToolLabel(tool) {
|
|
257
|
+
const labels = {
|
|
258
|
+
umi: 'Umi',
|
|
259
|
+
next: 'Next.js',
|
|
260
|
+
vite: 'Vite',
|
|
261
|
+
rsbuild: 'Rsbuild',
|
|
262
|
+
rspack: 'Rspack',
|
|
263
|
+
farm: 'Farm',
|
|
264
|
+
craco: 'CRA + craco',
|
|
265
|
+
webpack: 'Webpack',
|
|
266
|
+
gulp: 'Gulp (legacy)',
|
|
267
|
+
angular: 'Angular',
|
|
268
|
+
custom: 'Custom build pipeline',
|
|
269
|
+
unknown: 'unknown'
|
|
270
|
+
};
|
|
271
|
+
return labels[tool];
|
|
272
|
+
}
|
|
273
|
+
export function componentLibraryLabel(lib) {
|
|
274
|
+
const base = (() => {
|
|
275
|
+
switch (lib.name) {
|
|
276
|
+
case 'antd':
|
|
277
|
+
return 'Ant Design';
|
|
278
|
+
case 'antd-pro':
|
|
279
|
+
return 'Ant Design + Ant Design Pro';
|
|
280
|
+
case 'mui':
|
|
281
|
+
return 'Material UI';
|
|
282
|
+
case 'shadcn':
|
|
283
|
+
return 'shadcn/ui (Tailwind + Radix)';
|
|
284
|
+
case 'element-plus':
|
|
285
|
+
return 'Element Plus';
|
|
286
|
+
case 'element-ui':
|
|
287
|
+
return 'Element UI';
|
|
288
|
+
case 'arco':
|
|
289
|
+
return 'Arco Design';
|
|
290
|
+
case 'tdesign':
|
|
291
|
+
return 'TDesign';
|
|
292
|
+
case 'semi':
|
|
293
|
+
return 'Semi Design';
|
|
294
|
+
case 'nextui':
|
|
295
|
+
return 'NextUI';
|
|
296
|
+
case 'chakra':
|
|
297
|
+
return 'Chakra UI';
|
|
298
|
+
case 'vant':
|
|
299
|
+
return 'Vant (mobile)';
|
|
300
|
+
case 'none':
|
|
301
|
+
return 'no component library detected';
|
|
302
|
+
}
|
|
303
|
+
})();
|
|
304
|
+
return lib.majorVersion !== undefined ? `${base} v${lib.majorVersion}` : base;
|
|
305
|
+
}
|
|
306
|
+
export function cssFrameworkLabel(framework) {
|
|
307
|
+
const labels = {
|
|
308
|
+
less: 'Less',
|
|
309
|
+
sass: 'Sass/SCSS',
|
|
310
|
+
tailwind: 'TailwindCSS',
|
|
311
|
+
'css-modules': 'CSS Modules',
|
|
312
|
+
'styled-components': 'styled-components',
|
|
313
|
+
emotion: 'Emotion',
|
|
314
|
+
'plain-css': 'plain CSS',
|
|
315
|
+
unknown: 'unknown'
|
|
316
|
+
};
|
|
317
|
+
return labels[framework];
|
|
318
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, realpathSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { buildToolLabel, componentLibraryLabel, cssFrameworkLabel, detectProjectContext } from './project-context.js';
|
|
3
4
|
const SOURCE = {
|
|
4
5
|
sourceId: 'everything-claude-code',
|
|
5
6
|
url: 'https://github.com/affaan-m/everything-claude-code',
|
|
@@ -82,8 +83,40 @@ function renderHeader(title) {
|
|
|
82
83
|
''
|
|
83
84
|
].join('\n');
|
|
84
85
|
}
|
|
85
|
-
function
|
|
86
|
-
|
|
86
|
+
function renderProjectStackSection(ctx) {
|
|
87
|
+
if (!ctx.hasPackageJson)
|
|
88
|
+
return '';
|
|
89
|
+
const lines = ['## Detected project stack', ''];
|
|
90
|
+
lines.push(`- Build tool: ${buildToolLabel(ctx.buildTool)}${ctx.buildConfigPath !== undefined ? ` (\`${ctx.buildConfigPath}\`)` : ''}`);
|
|
91
|
+
lines.push(`- Component library: ${componentLibraryLabel(ctx.componentLibrary)}`);
|
|
92
|
+
if (ctx.cssFrameworks.length > 0) {
|
|
93
|
+
lines.push(`- CSS: ${ctx.cssFrameworks.map(cssFrameworkLabel).join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
if (ctx.stateManagement.length > 0)
|
|
96
|
+
lines.push(`- State management: ${ctx.stateManagement.join(', ')}`);
|
|
97
|
+
if (ctx.routing.length > 0)
|
|
98
|
+
lines.push(`- Routing: ${ctx.routing.join(', ')}`);
|
|
99
|
+
if (ctx.dataFetching.length > 0)
|
|
100
|
+
lines.push(`- Data fetching: ${ctx.dataFetching.join(', ')}`);
|
|
101
|
+
if (ctx.notableDeps.length > 0)
|
|
102
|
+
lines.push(`- Notable deps: ${ctx.notableDeps.join(', ')}`);
|
|
103
|
+
lines.push('');
|
|
104
|
+
if (ctx.cssConflicts.length > 0) {
|
|
105
|
+
lines.push('## CSS framework conflicts', '');
|
|
106
|
+
for (const conflict of ctx.cssConflicts)
|
|
107
|
+
lines.push(`- ${conflict}`);
|
|
108
|
+
lines.push('');
|
|
109
|
+
}
|
|
110
|
+
if (ctx.legacySignals.length > 0) {
|
|
111
|
+
lines.push('## Legacy constraints (preserve for new code in the same modules)', '');
|
|
112
|
+
for (const signal of ctx.legacySignals)
|
|
113
|
+
lines.push(`- ${signal}`);
|
|
114
|
+
lines.push('');
|
|
115
|
+
}
|
|
116
|
+
return lines.join('\n');
|
|
117
|
+
}
|
|
118
|
+
function renderClaudeMd(language, ctx) {
|
|
119
|
+
const head = [
|
|
87
120
|
'# Project Instructions',
|
|
88
121
|
'',
|
|
89
122
|
'> 🤖 AI 生成,请审阅',
|
|
@@ -104,39 +137,109 @@ function renderClaudeMd(language) {
|
|
|
104
137
|
'External reference: https://github.com/affaan-m/everything-claude-code is used as a curated reference only. Do not execute or install external content without explicit approval.',
|
|
105
138
|
''
|
|
106
139
|
].join('\n');
|
|
140
|
+
const stack = renderProjectStackSection(ctx);
|
|
141
|
+
return stack === '' ? head : `${head}\n${stack}`;
|
|
142
|
+
}
|
|
143
|
+
function renderCommonCodingStyle(ctx) {
|
|
144
|
+
const baseRules = [
|
|
145
|
+
'- Prefer simple, readable code over clever abstractions.',
|
|
146
|
+
'- Keep functions focused and files cohesive.',
|
|
147
|
+
'- Use immutable updates unless a language-specific convention explicitly favors mutation.',
|
|
148
|
+
'- Validate user input, external data, file paths, and configuration at system boundaries.',
|
|
149
|
+
'- Preserve existing project conventions when they are stricter than this baseline.'
|
|
150
|
+
];
|
|
151
|
+
const stackRules = [];
|
|
152
|
+
const lib = ctx.componentLibrary.name;
|
|
153
|
+
if (lib === 'antd' || lib === 'antd-pro') {
|
|
154
|
+
const major = ctx.componentLibrary.majorVersion ?? '5';
|
|
155
|
+
stackRules.push(`- Use existing antd v${major} components (\`Button\`, \`Form\`, \`Table\`, \`Modal\`, \`Select\`). Never mix antd v3/v4/v5 APIs.`);
|
|
156
|
+
stackRules.push(`- Customize antd via \`theme.token\` / \`ConfigProvider\` / \`className\` / \`styles\`. Do NOT apply TailwindCSS utility classes directly to antd components.`);
|
|
157
|
+
if (ctx.componentLibrary.hasProSuite === true) {
|
|
158
|
+
stackRules.push('- Use `@ant-design/pro-components` (`ProTable`, `ProForm`, `ProLayout`) where the page is already pro-based — do not introduce a parallel non-pro table/form.');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (lib === 'mui')
|
|
162
|
+
stackRules.push('- Style MUI via `sx`, `styled()`, and `theme`. Do NOT apply TailwindCSS utility classes directly to MUI components.');
|
|
163
|
+
if (lib === 'shadcn')
|
|
164
|
+
stackRules.push('- Use existing shadcn component variants and Tailwind utility classes. Do not introduce a competing component library.');
|
|
165
|
+
if (ctx.cssFrameworks.includes('tailwind') && (lib === 'antd' || lib === 'antd-pro' || lib === 'mui')) {
|
|
166
|
+
stackRules.push('- TailwindCSS is for layout/utility only; component-library tokens own component styling.');
|
|
167
|
+
}
|
|
168
|
+
if (ctx.cssFrameworks.includes('less'))
|
|
169
|
+
stackRules.push('- Less variables in `src/theme/*.less` (or equivalent) are the canonical design tokens — extend them, do not hardcode colors/spacing.');
|
|
170
|
+
if (ctx.stateManagement.length > 0)
|
|
171
|
+
stackRules.push(`- Follow the existing state library (${ctx.stateManagement.join(', ')}); do not introduce a competing state library.`);
|
|
172
|
+
if (ctx.dataFetching.length > 0)
|
|
173
|
+
stackRules.push(`- Reuse the existing data-fetching pattern (${ctx.dataFetching.join(', ')}) for new API calls.`);
|
|
174
|
+
for (const signal of ctx.legacySignals)
|
|
175
|
+
stackRules.push(`- ${signal}`);
|
|
176
|
+
const rules = stackRules.length > 0 ? [...baseRules, '', '## Project-specific rules', ...stackRules] : baseRules;
|
|
177
|
+
return `${renderHeader('Common Coding Standards')}${rules.join('\n')}\n`;
|
|
178
|
+
}
|
|
179
|
+
function renderCodeReview(ctx) {
|
|
180
|
+
const baseRules = [
|
|
181
|
+
'- Review diffs for correctness, maintainability, test coverage, and regression risk.',
|
|
182
|
+
'- Treat missing tests for changed behavior as a blocker unless the change is documentation-only.',
|
|
183
|
+
'- Verify code paths that handle filesystem, external APIs, credentials, user input, or generated artifacts.',
|
|
184
|
+
'- peaks-qa must use this guidance as part of code workflow preflight and final verification.'
|
|
185
|
+
];
|
|
186
|
+
const extra = [];
|
|
187
|
+
const lib = ctx.componentLibrary.name;
|
|
188
|
+
if (lib === 'antd' || lib === 'antd-pro') {
|
|
189
|
+
extra.push('- Block PRs that introduce a second component library (MUI/shadcn/Chakra) alongside antd.');
|
|
190
|
+
extra.push('- Block PRs that import antd v3/v4 APIs in this v5 project, or vice versa.');
|
|
191
|
+
}
|
|
192
|
+
if (ctx.cssFrameworks.includes('tailwind') && (lib === 'antd' || lib === 'antd-pro' || lib === 'mui')) {
|
|
193
|
+
extra.push('- Flag Tailwind utility classes applied directly to component-library primitives; require component-library APIs instead.');
|
|
194
|
+
}
|
|
195
|
+
if (ctx.legacySignals.length > 0) {
|
|
196
|
+
extra.push('- Verify new code in legacy modules preserves the existing patterns (see `.claude/rules/common/coding-style.md` "Project-specific rules").');
|
|
197
|
+
}
|
|
198
|
+
const rules = extra.length > 0 ? [...baseRules, '', '## Project-specific review focus', ...extra] : baseRules;
|
|
199
|
+
return `${renderHeader('Code Review Standards')}${rules.join('\n')}\n`;
|
|
200
|
+
}
|
|
201
|
+
function renderSecurity(ctx) {
|
|
202
|
+
const baseRules = [
|
|
203
|
+
'- Never hardcode secrets, API keys, passwords, tokens, or credentials.',
|
|
204
|
+
'- Do not send private code or secrets to external services without explicit user authorization.',
|
|
205
|
+
'- Guard filesystem writes against path traversal, symlink, and junction escapes.',
|
|
206
|
+
'- Require explicit confirmation for destructive actions, external state changes, and credential use.'
|
|
207
|
+
];
|
|
208
|
+
const extra = [];
|
|
209
|
+
if (ctx.buildTool === 'next')
|
|
210
|
+
extra.push('- Validate request body / query / params at every API route boundary (`pages/api/**` or `app/api/**`).');
|
|
211
|
+
if (ctx.dataFetching.length > 0)
|
|
212
|
+
extra.push(`- Sanitize and validate API responses before rendering or persisting (current fetchers: ${ctx.dataFetching.join(', ')}).`);
|
|
213
|
+
if (ctx.notableDeps.includes('monaco-editor') || ctx.notableDeps.includes('@monaco-editor/react')) {
|
|
214
|
+
extra.push('- Monaco editor content is untrusted; never `eval` or `Function`-construct user-authored code without an explicit, reviewed sandbox.');
|
|
215
|
+
}
|
|
216
|
+
const rules = extra.length > 0 ? [...baseRules, '', '## Project-specific security focus', ...extra] : baseRules;
|
|
217
|
+
return `${renderHeader('Security Review Standards')}${rules.join('\n')}\n`;
|
|
107
218
|
}
|
|
108
|
-
function
|
|
109
|
-
return `${renderHeader('Common Coding Standards')}- Prefer simple, readable code over clever abstractions.
|
|
110
|
-
- Keep functions focused and files cohesive.
|
|
111
|
-
- Use immutable updates unless a language-specific convention explicitly favors mutation.
|
|
112
|
-
- Validate user input, external data, file paths, and configuration at system boundaries.
|
|
113
|
-
- Preserve existing project conventions when they are stricter than this baseline.
|
|
114
|
-
`;
|
|
115
|
-
}
|
|
116
|
-
function renderCodeReview() {
|
|
117
|
-
return `${renderHeader('Code Review Standards')}- Review diffs for correctness, maintainability, test coverage, and regression risk.
|
|
118
|
-
- Treat missing tests for changed behavior as a blocker unless the change is documentation-only.
|
|
119
|
-
- Verify code paths that handle filesystem, external APIs, credentials, user input, or generated artifacts.
|
|
120
|
-
- peaks-qa must use this guidance as part of code workflow preflight and final verification.
|
|
121
|
-
`;
|
|
122
|
-
}
|
|
123
|
-
function renderSecurity() {
|
|
124
|
-
return `${renderHeader('Security Review Standards')}- Never hardcode secrets, API keys, passwords, tokens, or credentials.
|
|
125
|
-
- Do not send private code or secrets to external services without explicit user authorization.
|
|
126
|
-
- Guard filesystem writes against path traversal, symlink, and junction escapes.
|
|
127
|
-
- Require explicit confirmation for destructive actions, external state changes, and credential use.
|
|
128
|
-
`;
|
|
129
|
-
}
|
|
130
|
-
function renderLanguageCodingStyle(language) {
|
|
219
|
+
function renderLanguageCodingStyle(language, ctx) {
|
|
131
220
|
const languageName = language === 'generic' ? 'Generic' : language[0].toUpperCase() + language.slice(1);
|
|
132
221
|
const typeSafetyRule = language === 'typescript' || language === 'javascript'
|
|
133
222
|
? '- Do not add new `any` types; use explicit domain types, generics, or `unknown` with narrowing.\n'
|
|
134
223
|
: '';
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
224
|
+
const baseRules = [
|
|
225
|
+
`- Apply project-local conventions before generic ${language} guidance.`,
|
|
226
|
+
`- Keep public APIs typed or documented according to ${language} ecosystem norms.`,
|
|
227
|
+
typeSafetyRule.trim() !== '' ? typeSafetyRule.trim() : null,
|
|
228
|
+
'- Prefer standard tooling and existing project scripts for formatting, linting, tests, and coverage.',
|
|
229
|
+
`- peaks-rd must check this file before planning code changes in ${language} projects.`
|
|
230
|
+
].filter((line) => line !== null);
|
|
231
|
+
const extra = [];
|
|
232
|
+
if ((language === 'typescript' || language === 'javascript') && (ctx.componentLibrary.name === 'antd' || ctx.componentLibrary.name === 'antd-pro')) {
|
|
233
|
+
extra.push('- Type form values, table records, and API responses with named interfaces; do not rely on `Form.useForm()` inference for shared shapes.');
|
|
234
|
+
}
|
|
235
|
+
if ((language === 'typescript' || language === 'javascript') && ctx.dataFetching.includes('@tanstack/react-query')) {
|
|
236
|
+
extra.push('- Declare query/mutation generics (`useQuery<TData, TError>`) so consumers get typed data.');
|
|
237
|
+
}
|
|
238
|
+
if ((language === 'typescript' || language === 'javascript') && ctx.buildTool === 'umi') {
|
|
239
|
+
extra.push('- Use the project\'s existing service-layer pattern (`src/services/**`) for API calls; do not hand-roll `fetch` in components.');
|
|
240
|
+
}
|
|
241
|
+
const rules = extra.length > 0 ? [...baseRules, '', '## Project-specific rules', ...extra] : baseRules;
|
|
242
|
+
return `${renderHeader(`${languageName} Coding Standards`)}${rules.join('\n')}\n`;
|
|
140
243
|
}
|
|
141
244
|
function renderManagedClaudeMdIndex(language) {
|
|
142
245
|
return [
|
|
@@ -189,19 +292,19 @@ function writeMissingStandardsRules(plan, writes = getPendingStandardsRuleWrites
|
|
|
189
292
|
}
|
|
190
293
|
return writtenFiles;
|
|
191
294
|
}
|
|
192
|
-
function createTemplates(language) {
|
|
295
|
+
function createTemplates(language, ctx) {
|
|
193
296
|
return [
|
|
194
|
-
{ relativePath: 'CLAUDE.md', content: renderClaudeMd(language) },
|
|
195
|
-
{ relativePath: '.claude/rules/common/code-review.md', content: renderCodeReview() },
|
|
196
|
-
{ relativePath: '.claude/rules/common/coding-style.md', content: renderCommonCodingStyle() },
|
|
197
|
-
{ relativePath: '.claude/rules/common/security.md', content: renderSecurity() },
|
|
198
|
-
{ relativePath: `.claude/rules/${language}/coding-style.md`, content: renderLanguageCodingStyle(language) }
|
|
297
|
+
{ relativePath: 'CLAUDE.md', content: renderClaudeMd(language, ctx) },
|
|
298
|
+
{ relativePath: '.claude/rules/common/code-review.md', content: renderCodeReview(ctx) },
|
|
299
|
+
{ relativePath: '.claude/rules/common/coding-style.md', content: renderCommonCodingStyle(ctx) },
|
|
300
|
+
{ relativePath: '.claude/rules/common/security.md', content: renderSecurity(ctx) },
|
|
301
|
+
{ relativePath: `.claude/rules/${language}/coding-style.md`, content: renderLanguageCodingStyle(language, ctx) }
|
|
199
302
|
];
|
|
200
303
|
}
|
|
201
304
|
function createManagedClaudeBlock(language) {
|
|
202
305
|
return renderManagedClaudeMdIndex(language);
|
|
203
306
|
}
|
|
204
|
-
function buildClaudeUpdate(projectRoot, language) {
|
|
307
|
+
function buildClaudeUpdate(projectRoot, language, ctx) {
|
|
205
308
|
const filePath = resolve(projectRoot, 'CLAUDE.md');
|
|
206
309
|
assertSafeClaudeMdPath(filePath, projectRoot);
|
|
207
310
|
const existingContent = readFileIfExists(filePath);
|
|
@@ -211,7 +314,7 @@ function buildClaudeUpdate(projectRoot, language) {
|
|
|
211
314
|
relativePath: 'CLAUDE.md',
|
|
212
315
|
filePath,
|
|
213
316
|
status: 'planned',
|
|
214
|
-
content: `${renderClaudeMd(language).trimEnd()}\n\n${managedBlock}`,
|
|
317
|
+
content: `${renderClaudeMd(language, ctx).trimEnd()}\n\n${managedBlock}`,
|
|
215
318
|
appendBlock: '',
|
|
216
319
|
reviewSuggestions: []
|
|
217
320
|
};
|
|
@@ -281,7 +384,8 @@ export function createProjectStandardsInitPlan(options) {
|
|
|
281
384
|
const projectRoot = normalizeRoot(options.projectRoot);
|
|
282
385
|
assertSafeStandardsRoot(projectRoot);
|
|
283
386
|
const language = options.language === undefined ? detectLanguage(projectRoot) : parseLanguage(options.language);
|
|
284
|
-
const
|
|
387
|
+
const ctx = detectProjectContext(projectRoot);
|
|
388
|
+
const plannedWrites = createTemplates(language, ctx).map((template) => buildWrite(projectRoot, template));
|
|
285
389
|
return {
|
|
286
390
|
apply: options.apply ?? false,
|
|
287
391
|
projectRoot,
|
|
@@ -293,7 +397,8 @@ export function createProjectStandardsInitPlan(options) {
|
|
|
293
397
|
}
|
|
294
398
|
export function createProjectStandardsUpdatePlan(options) {
|
|
295
399
|
const basePlan = createProjectStandardsInitPlan(options);
|
|
296
|
-
const
|
|
400
|
+
const ctx = detectProjectContext(basePlan.projectRoot);
|
|
401
|
+
const claudeMd = buildClaudeUpdate(basePlan.projectRoot, basePlan.language, ctx);
|
|
297
402
|
return {
|
|
298
403
|
...basePlan,
|
|
299
404
|
claudeMd
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type WorkspaceInitOptions = {
|
|
2
|
+
projectRoot: string;
|
|
3
|
+
sessionId: string;
|
|
4
|
+
};
|
|
5
|
+
export type WorkspaceInitReport = {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
sessionRoot: string;
|
|
8
|
+
created: string[];
|
|
9
|
+
alreadyExisted: string[];
|
|
10
|
+
};
|
|
11
|
+
export declare class InvalidSessionIdError extends Error {
|
|
12
|
+
readonly code = "INVALID_SESSION_ID";
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
export declare function validateSessionId(sessionId: string): void;
|
|
16
|
+
export declare function initWorkspace(options: WorkspaceInitOptions): Promise<WorkspaceInitReport>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { isDirectory } from '../../shared/fs.js';
|
|
4
|
+
const SUBDIRECTORIES = [
|
|
5
|
+
'prd/source',
|
|
6
|
+
'prd/requests',
|
|
7
|
+
'ui/requests',
|
|
8
|
+
'rd/requests',
|
|
9
|
+
'qa/test-cases',
|
|
10
|
+
'qa/test-reports',
|
|
11
|
+
'qa/requests',
|
|
12
|
+
'sc',
|
|
13
|
+
'txt',
|
|
14
|
+
'system'
|
|
15
|
+
];
|
|
16
|
+
const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z][a-z0-9-]*[a-z0-9]$/;
|
|
17
|
+
const PROHIBITED_SUFFIXES = ['session', 'work', 'task', 'test', 'temp', 'tmp'];
|
|
18
|
+
export class InvalidSessionIdError extends Error {
|
|
19
|
+
code = 'INVALID_SESSION_ID';
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'InvalidSessionIdError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function validateSessionId(sessionId) {
|
|
26
|
+
if (/^\d+$/.test(sessionId)) {
|
|
27
|
+
throw new InvalidSessionIdError(`Session id "${sessionId}" is numeric-only. Use the format YYYY-MM-DD-<kebab-slug> with a 2-5 word topic description.`);
|
|
28
|
+
}
|
|
29
|
+
if (/^\d{8}T\d{6}$/.test(sessionId) || /^\d{8}$/.test(sessionId)) {
|
|
30
|
+
throw new InvalidSessionIdError(`Session id "${sessionId}" looks like a bare timestamp. Use YYYY-MM-DD-<kebab-slug>.`);
|
|
31
|
+
}
|
|
32
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(sessionId)) {
|
|
33
|
+
throw new InvalidSessionIdError(`Session id "${sessionId}" is a bare date. Append a 2-5 word topic slug (e.g. "${sessionId}-add-user-auth").`);
|
|
34
|
+
}
|
|
35
|
+
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
36
|
+
throw new InvalidSessionIdError(`Session id "${sessionId}" must match YYYY-MM-DD-<kebab-slug>, all lowercase, dashes only.`);
|
|
37
|
+
}
|
|
38
|
+
const suffix = sessionId.slice(11); // strip "YYYY-MM-DD-"
|
|
39
|
+
if (PROHIBITED_SUFFIXES.includes(suffix)) {
|
|
40
|
+
throw new InvalidSessionIdError(`Session id suffix "${suffix}" is a generic placeholder. Use a real topic slug (e.g. "add-user-auth", "v3-indicator-model").`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function initWorkspace(options) {
|
|
44
|
+
validateSessionId(options.sessionId);
|
|
45
|
+
const sessionRoot = join(options.projectRoot, '.peaks', options.sessionId);
|
|
46
|
+
const created = [];
|
|
47
|
+
const alreadyExisted = [];
|
|
48
|
+
if (await isDirectory(sessionRoot)) {
|
|
49
|
+
alreadyExisted.push('.');
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
await mkdir(sessionRoot, { recursive: true });
|
|
53
|
+
created.push('.');
|
|
54
|
+
}
|
|
55
|
+
for (const sub of SUBDIRECTORIES) {
|
|
56
|
+
const full = join(sessionRoot, sub);
|
|
57
|
+
if (await isDirectory(full)) {
|
|
58
|
+
alreadyExisted.push(sub);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await mkdir(full, { recursive: true });
|
|
62
|
+
created.push(sub);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { sessionId: options.sessionId, sessionRoot, created, alreadyExisted };
|
|
66
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.0.
|
|
1
|
+
export declare const CLI_VERSION = "1.0.19";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.0.
|
|
1
|
+
export const CLI_VERSION = "1.0.19";
|