@house365/comate 0.10.3 → 1.0.2-next.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 +21 -21
- package/README.md +104 -64
- package/bin/cli.mjs +197 -190
- package/bin/doctor.mjs +29 -41
- package/bin/init-run.mjs +310 -368
- package/bin/init.mjs +53 -71
- package/bin/postinstall.mjs +56 -56
- package/docs/CHANGELOG-fix-simplification.md +145 -0
- package/docs/COMATE_BETA.md +90 -0
- package/docs/COMATE_MANUAL.md +239 -665
- package/docs/superpowers/plans/2026-10-14-review-fixes.md +48 -0
- package/docs/superpowers/plans/2026-10-14-workflow-quality.md +48 -0
- package/package.json +54 -55
- package/skills/comate/SKILL.md +42 -325
- package/skills/comate/reference/archive.md +50 -0
- package/skills/comate/reference/build.md +67 -0
- package/skills/comate/reference/fix.md +150 -0
- package/skills/comate/reference/open.md +162 -0
- package/skills/comate/reference/verify.md +123 -0
- package/skills/comate/scaffold/comate/README.md +18 -0
- package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/architecture/README.md +5 -6
- package/skills/comate/scaffold/comate/comate/config.json +10 -0
- package/skills/comate/scaffold/comate/knowledge-base/README.md +42 -0
- package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/knowledge-base/integrations/_TEMPLATE/README.md +2 -2
- package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/knowledge-base/integrations/_TEMPLATE/api-snapshot.md +47 -47
- package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/knowledge-base/requirements/_templates/PRD.md +4 -4
- package/skills/comate/scripts/comate.mjs +147 -0
- package/skills/comate/scripts/lib/acceptance.mjs +112 -0
- package/skills/comate/scripts/lib/brief.mjs +50 -0
- package/skills/comate/scripts/lib/checks.mjs +178 -0
- package/skills/comate/scripts/lib/cmd-archive.mjs +152 -0
- package/skills/comate/scripts/lib/cmd-decide.mjs +116 -0
- package/skills/comate/scripts/lib/cmd-init.mjs +75 -0
- package/skills/comate/scripts/lib/cmd-loop.mjs +273 -0
- package/skills/comate/scripts/lib/cmd-open.mjs +161 -0
- package/skills/comate/scripts/lib/cmd-status.mjs +121 -0
- package/skills/comate/scripts/lib/common.mjs +85 -0
- package/skills/comate/scripts/lib/docs.mjs +45 -0
- package/skills/comate/scripts/lib/finish.mjs +108 -0
- package/skills/comate/scripts/lib/git.mjs +112 -0
- package/skills/comate/scripts/lib/md.mjs +85 -0
- package/skills/comate/scripts/lib/next.mjs +217 -0
- package/skills/comate/scripts/lib/paths.mjs +96 -0
- package/skills/comate/scripts/lib/render.mjs +46 -0
- package/skills/comate/scripts/lib/spec.mjs +106 -0
- package/skills/comate/scripts/lib/state.mjs +128 -0
- package/skills/comate/scripts/lib/tasks.mjs +53 -0
- package/skills/comate/scripts/lib/templates.mjs +58 -0
- package/skills/comate/scripts/lib/util.mjs +108 -0
- package/skills/comate-kb-integration/SKILL.md +128 -128
- package/skills/comate-kb-integration/scripts/fetch_rendered_doc.py +84 -84
- package/bin/cleanup-duplicate-superpowers.mjs +0 -119
- package/docs/COMATE_BEGINNER_GUIDE.md +0 -188
- package/docs/COMATE_INIT_DIRECTORY_GUIDE.md +0 -201
- package/docs/superpowers/plans/2026-09-13-fast-workflow.md +0 -62
- package/skills/comate/reference/decision-points.md +0 -103
- package/skills/comate/reference/dirty-worktree.md +0 -58
- package/skills/comate/reference/prerequisites.md +0 -89
- package/skills/comate/scripts/comate-lib.sh +0 -559
- package/skills/comate/scripts/comate.sh +0 -2147
- package/skills/comate/tests/comate.bats +0 -1019
- package/skills/comate/tests/run.sh +0 -101
- package/skills/comate-archive/SKILL.md +0 -63
- package/skills/comate-build/SKILL.md +0 -278
- package/skills/comate-design/SKILL.md +0 -178
- package/skills/comate-fix/SKILL.md +0 -195
- package/skills/comate-gate/SKILL.md +0 -319
- package/skills/comate-gate/scaffold/docs/README.md +0 -36
- package/skills/comate-gate/scaffold/docs/knowledge-base/README.md +0 -47
- package/skills/comate-grill/SKILL.md +0 -185
- package/skills/comate-open/SKILL.md +0 -240
- package/skills/comate-verify/SKILL.md +0 -212
- /package/skills/{comate-gate/scaffold/docs → comate/scaffold/comate}/knowledge-base/integrations/README.md +0 -0
package/bin/init-run.mjs
CHANGED
|
@@ -1,368 +1,310 @@
|
|
|
1
|
-
// The `init` flow: install whatever the current project is missing.
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
-
import { join, resolve } from 'node:path';
|
|
5
|
-
import { createInterface } from 'node:readline';
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
capture,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
'.
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
[[233, 150, 255], [200, 76, 255]],
|
|
312
|
-
[[255, 150, 224], [255, 72, 178]],
|
|
313
|
-
];
|
|
314
|
-
|
|
315
|
-
// Blocks are the lit face, box-drawing pieces are the bevelled edge. Two
|
|
316
|
-
// shades per row is the whole trick: it gives every glyph a rim, which is
|
|
317
|
-
// what separates brushed metal from a flat decal.
|
|
318
|
-
const emboss = (line, [face, edge]) => {
|
|
319
|
-
let out = '';
|
|
320
|
-
let cur = null;
|
|
321
|
-
for (const ch of line) {
|
|
322
|
-
const want = ch === ' ' ? null : ch === '█' ? face : edge;
|
|
323
|
-
if (want !== cur) {
|
|
324
|
-
out += want ? sgr(want) : RESET;
|
|
325
|
-
cur = want;
|
|
326
|
-
}
|
|
327
|
-
out += ch;
|
|
328
|
-
}
|
|
329
|
-
return out + RESET;
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
// The tagline picks up the ramp instead of going dim grey: the name in the
|
|
333
|
-
// white highlight so it stays the thing you read first, the strapline in the
|
|
334
|
-
// magenta rim so the line closes the gradient the art opened. Dropping \x1B[2m
|
|
335
|
-
// matters — dim on a saturated colour is what made this read as washed out.
|
|
336
|
-
const nameLit = `${sgr(ramp[0][0])}${tag}${RESET}`;
|
|
337
|
-
const strap = `${sgr(ramp[5][1])} - AI coding almost there!${RESET}`;
|
|
338
|
-
|
|
339
|
-
return [
|
|
340
|
-
'',
|
|
341
|
-
...art.map((line, i) => ` ${emboss(trim(line), ramp[i])}`),
|
|
342
|
-
'',
|
|
343
|
-
`${indent}${nameLit}${strap}`,
|
|
344
|
-
'',
|
|
345
|
-
].join('\n');
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Major version of the bash that comate's scripts will actually run under, or
|
|
349
|
-
// null when there is no bash at all — that is doctor's diagnosis to deliver, not
|
|
350
|
-
// a reason to block the install. Windows resolves through findBash so a Git Bash
|
|
351
|
-
// install is not mistaken for the WSL launcher.
|
|
352
|
-
// Parsed from --version rather than `-c 'echo $BASH_VERSINFO'`: capture() runs
|
|
353
|
-
// through a shell on Windows, which swallows the variable before bash sees it.
|
|
354
|
-
export function bashVersion(probe = (bin) => capture(bin, ['--version'])) {
|
|
355
|
-
const bin = process.platform === 'win32' ? findBash() : 'bash';
|
|
356
|
-
if (!bin) return null;
|
|
357
|
-
const out = probe(bin);
|
|
358
|
-
if (out === null) return null;
|
|
359
|
-
// "GNU bash, version 5.2.26(1)-release ..." / macOS: "... version 3.2.57(1)"
|
|
360
|
-
const major = out.match(/version (\d+)\./)?.[1];
|
|
361
|
-
return major === undefined ? 0 : Number(major);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Every major CI sets CI=true; GITHUB_ACTIONS covers the Actions runner even in
|
|
365
|
-
// the odd config that doesn't.
|
|
366
|
-
function isCI() {
|
|
367
|
-
return Boolean(process.env.CI || process.env.GITHUB_ACTIONS);
|
|
368
|
-
}
|
|
1
|
+
// The `init` flow: install whatever the current project is missing.
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
LAYOUT_MOVES,
|
|
9
|
+
SKILLS_TIMEOUT_MS,
|
|
10
|
+
capture,
|
|
11
|
+
migrateLayout,
|
|
12
|
+
pruneDeprecatedSkills,
|
|
13
|
+
run,
|
|
14
|
+
scaffoldComate,
|
|
15
|
+
skillsInvocation,
|
|
16
|
+
userSkillsRoot,
|
|
17
|
+
} from './init.mjs';
|
|
18
|
+
|
|
19
|
+
// `comate init [--migrate [--dry-run]]`. Skills go user-level (~/.agents/skills),
|
|
20
|
+
// matching how the CLI itself is installed (npm i -g): one copy, every project
|
|
21
|
+
// sees it.
|
|
22
|
+
//
|
|
23
|
+
// comate/ is per-project — it holds the project's own specs and changes — so
|
|
24
|
+
// cwd must be a project. Refuse the two cases that are almost certainly a mistake:
|
|
25
|
+
// the home directory, and a directory with no project marker at all.
|
|
26
|
+
const PROJECT_MARKERS = [
|
|
27
|
+
'.git',
|
|
28
|
+
'package.json',
|
|
29
|
+
'pyproject.toml',
|
|
30
|
+
'Cargo.toml',
|
|
31
|
+
'go.mod',
|
|
32
|
+
'pom.xml',
|
|
33
|
+
'build.gradle',
|
|
34
|
+
'composer.json',
|
|
35
|
+
'Gemfile',
|
|
36
|
+
'openspec',
|
|
37
|
+
'.agents',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export function inspectTarget(dir, home) {
|
|
41
|
+
const normalise = (p) => resolve(p).replace(/[\\/]+$/, '').toLowerCase();
|
|
42
|
+
if (home && normalise(dir) === normalise(home)) return { kind: 'home' };
|
|
43
|
+
const marker = PROJECT_MARKERS.find((m) => existsSync(join(dir, m)));
|
|
44
|
+
return marker ? { kind: 'project', marker } : { kind: 'bare' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function init(pkgRoot, argv = []) {
|
|
48
|
+
const results = [];
|
|
49
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
50
|
+
const cwd = process.cwd();
|
|
51
|
+
|
|
52
|
+
const target = inspectTarget(cwd, home);
|
|
53
|
+
if (target.kind !== 'project') {
|
|
54
|
+
// Home directory is always refused
|
|
55
|
+
if (target.kind === 'home') {
|
|
56
|
+
console.error('\n这是主目录,请在项目目录下运行 comate init。');
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Bare directory: ask user if they want to create a project here
|
|
61
|
+
console.log('\n当前目录不像项目(无 .git / package.json 等)。');
|
|
62
|
+
const confirmed = await askConfirm('要在这里初始化新项目吗?');
|
|
63
|
+
if (!confirmed) {
|
|
64
|
+
console.log('已取消。');
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --migrate: move 0.x docs/knowledge-base and docs/architecture into comate/.
|
|
70
|
+
// --dry-run lists the moves and stops; a conflict stops before moving anything.
|
|
71
|
+
if (argv.includes('--migrate')) {
|
|
72
|
+
const dryRun = argv.includes('--dry-run');
|
|
73
|
+
const { moves, ok } = migrateLayout(cwd, { dryRun });
|
|
74
|
+
if (!moves.length) console.log('\n没有需要迁移的目录(docs/knowledge-base、docs/architecture 都不存在)。');
|
|
75
|
+
for (const m of moves) {
|
|
76
|
+
const mark = { move: '→', moved: '✓', conflict: '✗' }[m.status];
|
|
77
|
+
console.log(` ${mark} ${m.from} → ${m.to}${m.status === 'conflict' ? '(目标已存在,未移动)' : ''}`);
|
|
78
|
+
}
|
|
79
|
+
if (!ok) {
|
|
80
|
+
console.error('\n目标目录已存在,没有移动任何文件。手动合并后重跑 comate init --migrate。');
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
if (dryRun) {
|
|
84
|
+
console.log('\n以上为预览(--dry-run)。确认后运行 comate init --migrate 执行(使用 git mv 保留历史)。');
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const stopSpinner = startSpinner('Initializing…');
|
|
90
|
+
|
|
91
|
+
// 1. git — comate 1.0 keeps candidates, archives and finish steps in git.
|
|
92
|
+
const gitVersion = capture('git', ['--version']);
|
|
93
|
+
if (gitVersion === null) {
|
|
94
|
+
results.push(['git', 'failed', '找不到 git:comate 1.0 需要 git']);
|
|
95
|
+
} else if (spawnSync('git', ['rev-parse', '--git-dir'], { cwd, stdio: 'ignore' }).status !== 0) {
|
|
96
|
+
results.push(['git', 'warn', '当前目录还不是 git 仓库:先 git init 并做一次初始提交,再用 /comate']);
|
|
97
|
+
} else {
|
|
98
|
+
results.push(['git', 'skipped', gitVersion]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. comate itself. The skills CLI reports per-agent failures without ever
|
|
102
|
+
// setting a non-zero exit code, and we swallow its output — so a zero status
|
|
103
|
+
// is not evidence the skill landed. Check the file.
|
|
104
|
+
const comateArgs = ['add', pkgRoot, '--skill', '*', '-g', '-y'];
|
|
105
|
+
const r = run(skillsInvocation(pkgRoot, comateArgs), SKILLS_TIMEOUT_MS, { quiet: true });
|
|
106
|
+
const landed = existsSync(join(home, '.agents', 'skills', 'comate', 'SKILL.md'));
|
|
107
|
+
if (r.ok && !landed) {
|
|
108
|
+
results.push(['comate', 'failed', `未装到 ${join(home, '.agents', 'skills')}`]);
|
|
109
|
+
} else {
|
|
110
|
+
results.push(['comate', r.ok ? 'installed' : 'failed', r.ok ? '' : r.reason, r.output]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Prune comate skill dirs renamed/dropped in past releases. `skills add` above
|
|
114
|
+
// only adds/overwrites, so /comate-spec etc. would otherwise linger as dead
|
|
115
|
+
// slash commands. The postinstall hook does this too, but --ignore-scripts
|
|
116
|
+
// users never run it — init is their only path, so it must clean up here.
|
|
117
|
+
const pruned = pruneDeprecatedSkills(userSkillsRoot(home));
|
|
118
|
+
if (pruned.length) results.push(['清理废弃 skill', 'installed', pruned.join('、')]);
|
|
119
|
+
|
|
120
|
+
// 3. comate/ project scaffold: config.json, knowledge-base (PRD template),
|
|
121
|
+
// architecture placeholder. Idempotent — existing files are left alone.
|
|
122
|
+
const sc = scaffoldComate(pkgRoot);
|
|
123
|
+
if (!sc.ok) {
|
|
124
|
+
results.push(['comate/', 'failed', sc.reason]);
|
|
125
|
+
} else if (sc.created === 0) {
|
|
126
|
+
results.push(['comate/', 'skipped', '已存在']);
|
|
127
|
+
} else {
|
|
128
|
+
const note = sc.skipped > 0 ? `新建 ${sc.created} 个文件,保留 ${sc.skipped} 个已有` : '';
|
|
129
|
+
results.push(['comate/', 'installed', note]);
|
|
130
|
+
}
|
|
131
|
+
if (!argv.includes('--migrate') && LAYOUT_MOVES.some(([from]) => existsSync(join(cwd, from)))) {
|
|
132
|
+
results.push(['docs/', 'warn', '发现 0.x 的 docs/knowledge-base 或 docs/architecture:comate init --migrate --dry-run 查看迁移清单']);
|
|
133
|
+
}
|
|
134
|
+
if (existsSync(join(cwd, 'openspec', 'specs'))) {
|
|
135
|
+
results.push(['openspec/', 'warn', '发现 0.x 主规格:comate import-specs --dry-run 查看迁移清单']);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
stopSpinner();
|
|
139
|
+
return report(results);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// A yes/no prompt. Defaults to "no" so a piped/non-interactive run (CI, an
|
|
143
|
+
// unattended npx) never silently scaffolds a project into whatever directory it
|
|
144
|
+
// happened to land in — those callers should point at a real project instead.
|
|
145
|
+
// [y/N]: empty input, EOF (Ctrl-D / closed stdin), or anything but y/yes = no.
|
|
146
|
+
export function askConfirm(question, { input = process.stdin, output = process.stdout } = {}) {
|
|
147
|
+
if (!input.isTTY) return Promise.resolve(false);
|
|
148
|
+
const rl = createInterface({ input, output });
|
|
149
|
+
return new Promise((done) => {
|
|
150
|
+
rl.question(`${question} [y/N] `, (answer) => {
|
|
151
|
+
rl.close();
|
|
152
|
+
done(/^\s*y(es)?\s*$/i.test(answer));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The spinner runs in its own process because every step above is a synchronous
|
|
158
|
+
// spawnSync — an interval on this thread would never fire. In a pipe \r does not
|
|
159
|
+
// overwrite, so animating there would leave every frame on screen; print one
|
|
160
|
+
// static line instead.
|
|
161
|
+
function startSpinner(label) {
|
|
162
|
+
if (!process.stdout.isTTY) {
|
|
163
|
+
console.log(`\n${label}`);
|
|
164
|
+
return () => {};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const child = spawn(process.execPath, [join(import.meta.dirname, 'spinner.mjs'), label], {
|
|
168
|
+
stdio: ['ignore', 'inherit', 'ignore'],
|
|
169
|
+
});
|
|
170
|
+
let stopped = false;
|
|
171
|
+
return () => {
|
|
172
|
+
if (stopped) return;
|
|
173
|
+
stopped = true;
|
|
174
|
+
child.kill();
|
|
175
|
+
// The parent erases the line, not the child: on Windows SIGTERM is not a
|
|
176
|
+
// real signal — Node terminates the process outright, so a handler in the
|
|
177
|
+
// child never runs and its last frame would sit under the logo.
|
|
178
|
+
process.stdout.write(`\r${' '.repeat(label.length + 6)}\r`);
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Success is one screen: the wordmark and where things landed. The per-item
|
|
183
|
+
// breakdown only earns its space when something failed, and then it carries the
|
|
184
|
+
// reason — a bare "失败" would leave the user with nowhere to go.
|
|
185
|
+
function report(results) {
|
|
186
|
+
const failed = results.filter(([, s]) => s === 'failed');
|
|
187
|
+
if (failed.length > 0) {
|
|
188
|
+
console.error('\n安装失败\n');
|
|
189
|
+
for (const [name, status, note, output] of results) {
|
|
190
|
+
const mark = { installed: '✓', skipped: '·', warn: '⚠', failed: '✗' }[status];
|
|
191
|
+
console.error(` ${mark} ${name.padEnd(14)}${note}`);
|
|
192
|
+
// The child's own output, indented under the item it belongs to, so a
|
|
193
|
+
// knock-on failure doesn't look like a second unrelated problem.
|
|
194
|
+
if (status === 'failed' && output) {
|
|
195
|
+
for (const line of output.split('\n')) console.error(` ${line.trim()}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
console.error('\n修好后重跑 comate init。');
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log(banner(pkgVersion()));
|
|
203
|
+
for (const [name, , note] of results.filter(([, s]) => s === 'warn')) console.log(` ⚠ ${name.padEnd(12)}${note}`);
|
|
204
|
+
console.log(' 在 agent 中输入 /comate 开始;配置检查命令:comate/comate/config.json\n');
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ponytail: read package.json rather than hardcoding — npm version bumps the
|
|
209
|
+
// file, and a stale literal in the banner is worse than no version at all.
|
|
210
|
+
export function pkgVersion(root = resolve(import.meta.dirname, '..')) {
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version;
|
|
213
|
+
} catch {
|
|
214
|
+
return '';
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Only CI gates the wordmark. Two earlier guards were both wrong and are worth
|
|
219
|
+
// naming so they don't come back:
|
|
220
|
+
//
|
|
221
|
+
// isTTY — npm's Windows global install resolves `comate` to a comate.ps1
|
|
222
|
+
// shim, and PowerShell calling node through a .ps1 wrapper takes over stdout,
|
|
223
|
+
// so isTTY is false in a real terminal — exactly where the logo belongs.
|
|
224
|
+
//
|
|
225
|
+
// chcp — reports the codepage the child process inherited, which the shim
|
|
226
|
+
// does not propagate (65001 outside, 936 inside). It also answers the wrong
|
|
227
|
+
// question: writing to a real Windows console goes through WriteConsoleW,
|
|
228
|
+
// which converts to UTF-16 and bypasses the codepage entirely.
|
|
229
|
+
export function banner(version, { ci = isCI() } = {}) {
|
|
230
|
+
const tag = `comate${version ? ` v${version}` : ''}`;
|
|
231
|
+
const plain = `\n ${tag} - AI coding almost there!\n`;
|
|
232
|
+
if (ci) return plain;
|
|
233
|
+
|
|
234
|
+
const art = [
|
|
235
|
+
' ██████╗ ██████╗ ███╗ ███╗ █████╗ ████████╗███████╗',
|
|
236
|
+
'██╔════╝██╔═══██╗████╗ ████║██╔══██╗╚══██╔══╝██╔════╝',
|
|
237
|
+
'██║ ██║ ██║██╔████╔██║███████║ ██║ █████╗ ',
|
|
238
|
+
'██║ ██║ ██║██║╚██╔╝██║██╔══██║ ██║ ██╔══╝ ',
|
|
239
|
+
'╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ██║ ██║ ███████╗',
|
|
240
|
+
' ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝',
|
|
241
|
+
];
|
|
242
|
+
// Trailing padding keeps the source rows aligned; strip it so a colour run
|
|
243
|
+
// doesn't sit behind spaces the terminal may highlight on select.
|
|
244
|
+
const trim = (line) => line.replace(/\s+$/, '');
|
|
245
|
+
// Centred under the art, not under the terminal: the wordmark is fixed-width,
|
|
246
|
+
// so centring against art[0] holds at any window size. Computed rather than
|
|
247
|
+
// hardcoded because the version string changes length on every release.
|
|
248
|
+
const tagline = `${tag} - AI coding almost there!`;
|
|
249
|
+
const indent = ' '.repeat(2 + Math.max(0, Math.round((art[0].length - tagline.length) / 2)));
|
|
250
|
+
if (process.env.NO_COLOR) {
|
|
251
|
+
return ['', ...art.map((l) => ` ${trim(l)}`), '', `${indent}${tagline}`, ''].join('\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 24-bit SGR. Truecolor is not universal, but the two terminals that matter
|
|
255
|
+
// here (Windows Terminal, VS Code) both support it, and one that doesn't
|
|
256
|
+
// degrades to its nearest palette entry rather than breaking.
|
|
257
|
+
const sgr = ([r, g, b]) => `\x1B[38;2;${r};${g};${b}m`;
|
|
258
|
+
const RESET = '\x1B[0m';
|
|
259
|
+
|
|
260
|
+
// Polished chrome under a cyan→magenta rim light: [face, edge] per row. The
|
|
261
|
+
// ramp shifts hue rather than dropping brightness — every row stays high
|
|
262
|
+
// luminance, which is what keeps it reading as lit metal instead of fading
|
|
263
|
+
// out. An earlier darkening ramp went muddy across the bottom half.
|
|
264
|
+
const ramp = [
|
|
265
|
+
[[255, 255, 255], [150, 240, 255]],
|
|
266
|
+
[[214, 248, 255], [ 56, 214, 255]],
|
|
267
|
+
[[168, 214, 255], [ 92, 148, 255]],
|
|
268
|
+
[[186, 160, 255], [148, 100, 255]],
|
|
269
|
+
[[233, 150, 255], [200, 76, 255]],
|
|
270
|
+
[[255, 150, 224], [255, 72, 178]],
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
// Blocks are the lit face, box-drawing pieces are the bevelled edge. Two
|
|
274
|
+
// shades per row is the whole trick: it gives every glyph a rim, which is
|
|
275
|
+
// what separates brushed metal from a flat decal.
|
|
276
|
+
const emboss = (line, [face, edge]) => {
|
|
277
|
+
let out = '';
|
|
278
|
+
let cur = null;
|
|
279
|
+
for (const ch of line) {
|
|
280
|
+
const want = ch === ' ' ? null : ch === '█' ? face : edge;
|
|
281
|
+
if (want !== cur) {
|
|
282
|
+
out += want ? sgr(want) : RESET;
|
|
283
|
+
cur = want;
|
|
284
|
+
}
|
|
285
|
+
out += ch;
|
|
286
|
+
}
|
|
287
|
+
return out + RESET;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// The tagline picks up the ramp instead of going dim grey: the name in the
|
|
291
|
+
// white highlight so it stays the thing you read first, the strapline in the
|
|
292
|
+
// magenta rim so the line closes the gradient the art opened. Dropping \x1B[2m
|
|
293
|
+
// matters — dim on a saturated colour is what made this read as washed out.
|
|
294
|
+
const nameLit = `${sgr(ramp[0][0])}${tag}${RESET}`;
|
|
295
|
+
const strap = `${sgr(ramp[5][1])} - AI coding almost there!${RESET}`;
|
|
296
|
+
|
|
297
|
+
return [
|
|
298
|
+
'',
|
|
299
|
+
...art.map((line, i) => ` ${emboss(trim(line), ramp[i])}`),
|
|
300
|
+
'',
|
|
301
|
+
`${indent}${nameLit}${strap}`,
|
|
302
|
+
'',
|
|
303
|
+
].join('\n');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Every major CI sets CI=true; GITHUB_ACTIONS covers the Actions runner even in
|
|
307
|
+
// the odd config that doesn't.
|
|
308
|
+
function isCI() {
|
|
309
|
+
return Boolean(process.env.CI || process.env.GITHUB_ACTIONS);
|
|
310
|
+
}
|