pumuki 6.3.13 → 6.3.14
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/README.md +95 -7
- package/VERSION +1 -1
- package/bin/pumuki-mcp-enterprise.js +5 -0
- package/bin/pumuki-pre-write.js +11 -0
- package/docs/API_REFERENCE.md +2 -1
- package/docs/INSTALLATION.md +101 -54
- package/docs/MCP_SERVERS.md +167 -74
- package/docs/PUMUKI_FULL_VALIDATION_CHECKLIST.md +46 -45
- package/docs/PUMUKI_OPENSPEC_SDD_ROADMAP.md +55 -0
- package/docs/README.md +5 -0
- package/docs/REFRACTOR_PROGRESS.md +102 -3
- package/docs/USAGE.md +115 -8
- package/docs/validation/README.md +2 -0
- package/docs/validation/phase12-go-no-go-report.md +73 -0
- package/docs/validation/post-phase12-next-lot-decision.md +75 -0
- package/integrations/config/skillsRuleSet.ts +53 -6
- package/integrations/evidence/buildEvidence.ts +42 -3
- package/integrations/evidence/generateEvidence.test.ts +59 -0
- package/integrations/evidence/readEvidence.test.ts +61 -0
- package/integrations/evidence/schema.test.ts +81 -0
- package/integrations/evidence/schema.ts +11 -0
- package/integrations/evidence/writeEvidence.test.ts +18 -0
- package/integrations/evidence/writeEvidence.ts +11 -0
- package/integrations/git/resolveGitRefs.ts +2 -2
- package/integrations/git/runPlatformGate.ts +64 -0
- package/integrations/git/runPlatformGateEvidence.ts +13 -0
- package/integrations/git/stageRunners.ts +10 -1
- package/integrations/lifecycle/artifacts.ts +57 -4
- package/integrations/lifecycle/cli.ts +248 -12
- package/integrations/lifecycle/constants.ts +1 -0
- package/integrations/lifecycle/gitService.ts +1 -0
- package/integrations/lifecycle/install.ts +24 -1
- package/integrations/lifecycle/openSpecBootstrap.ts +190 -0
- package/integrations/lifecycle/state.ts +57 -0
- package/integrations/lifecycle/uninstall.ts +3 -1
- package/integrations/lifecycle/update.ts +11 -0
- package/integrations/mcp/enterpriseServer.cli.ts +12 -0
- package/integrations/mcp/enterpriseServer.ts +762 -0
- package/integrations/mcp/index.ts +1 -0
- package/integrations/sdd/index.ts +11 -0
- package/integrations/sdd/openSpecCli.ts +180 -0
- package/integrations/sdd/policy.ts +190 -0
- package/integrations/sdd/sessionStore.ts +152 -0
- package/integrations/sdd/types.ts +69 -0
- package/package.json +10 -4
- package/scripts/framework-menu-runner-path-lib.ts +10 -3
- package/scripts/framework-menu.ts +86 -5
- package/scripts/package-install-smoke-gate-lib.ts +6 -1
- package/scripts/package-install-smoke-lifecycle-lib.ts +3 -0
|
@@ -4,6 +4,14 @@ import { runLifecycleRemove } from './remove';
|
|
|
4
4
|
import { readLifecycleStatus } from './status';
|
|
5
5
|
import { runLifecycleUninstall } from './uninstall';
|
|
6
6
|
import { runLifecycleUpdate } from './update';
|
|
7
|
+
import {
|
|
8
|
+
closeSddSession,
|
|
9
|
+
evaluateSddPolicy,
|
|
10
|
+
openSddSession,
|
|
11
|
+
readSddStatus,
|
|
12
|
+
refreshSddSession,
|
|
13
|
+
type SddStage,
|
|
14
|
+
} from '../sdd';
|
|
7
15
|
|
|
8
16
|
type LifecycleCommand =
|
|
9
17
|
| 'install'
|
|
@@ -11,12 +19,23 @@ type LifecycleCommand =
|
|
|
11
19
|
| 'remove'
|
|
12
20
|
| 'update'
|
|
13
21
|
| 'doctor'
|
|
14
|
-
| 'status'
|
|
22
|
+
| 'status'
|
|
23
|
+
| 'sdd';
|
|
24
|
+
|
|
25
|
+
type SddCommand = 'status' | 'validate' | 'session';
|
|
26
|
+
|
|
27
|
+
type SddSessionAction = 'open' | 'refresh' | 'close';
|
|
15
28
|
|
|
16
29
|
type ParsedArgs = {
|
|
17
30
|
command: LifecycleCommand;
|
|
18
31
|
purgeArtifacts: boolean;
|
|
19
32
|
updateSpec?: string;
|
|
33
|
+
json: boolean;
|
|
34
|
+
sddCommand?: SddCommand;
|
|
35
|
+
sddStage?: SddStage;
|
|
36
|
+
sddSessionAction?: SddSessionAction;
|
|
37
|
+
sddChangeId?: string;
|
|
38
|
+
sddTtlMinutes?: number;
|
|
20
39
|
};
|
|
21
40
|
|
|
22
41
|
const HELP_TEXT = `
|
|
@@ -27,6 +46,11 @@ Pumuki lifecycle commands:
|
|
|
27
46
|
pumuki update [--latest|--spec=<package-spec>]
|
|
28
47
|
pumuki doctor
|
|
29
48
|
pumuki status
|
|
49
|
+
pumuki sdd status [--json]
|
|
50
|
+
pumuki sdd validate [--stage=PRE_WRITE|PRE_COMMIT|PRE_PUSH|CI] [--json]
|
|
51
|
+
pumuki sdd session --open --change=<change-id> [--ttl-minutes=<n>] [--json]
|
|
52
|
+
pumuki sdd session --refresh [--ttl-minutes=<n>] [--json]
|
|
53
|
+
pumuki sdd session --close [--json]
|
|
30
54
|
`.trim();
|
|
31
55
|
|
|
32
56
|
const isLifecycleCommand = (value: string): value is LifecycleCommand =>
|
|
@@ -35,7 +59,21 @@ const isLifecycleCommand = (value: string): value is LifecycleCommand =>
|
|
|
35
59
|
value === 'remove' ||
|
|
36
60
|
value === 'update' ||
|
|
37
61
|
value === 'doctor' ||
|
|
38
|
-
value === 'status'
|
|
62
|
+
value === 'status' ||
|
|
63
|
+
value === 'sdd';
|
|
64
|
+
|
|
65
|
+
const parseSddStage = (value: string | undefined): SddStage => {
|
|
66
|
+
const normalized = (value ?? 'PRE_COMMIT').trim().toUpperCase();
|
|
67
|
+
if (
|
|
68
|
+
normalized === 'PRE_WRITE' ||
|
|
69
|
+
normalized === 'PRE_COMMIT' ||
|
|
70
|
+
normalized === 'PRE_PUSH' ||
|
|
71
|
+
normalized === 'CI'
|
|
72
|
+
) {
|
|
73
|
+
return normalized;
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`Unsupported SDD stage "${value}". Use PRE_WRITE, PRE_COMMIT, PRE_PUSH or CI.`);
|
|
76
|
+
};
|
|
39
77
|
|
|
40
78
|
export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs => {
|
|
41
79
|
const commandRaw = argv[0];
|
|
@@ -48,7 +86,105 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
48
86
|
|
|
49
87
|
let purgeArtifacts = false;
|
|
50
88
|
let updateSpec: string | undefined;
|
|
89
|
+
let json = false;
|
|
90
|
+
let sddCommand: SddCommand | undefined;
|
|
91
|
+
let sddStage: SddStage | undefined;
|
|
92
|
+
let sddSessionAction: SddSessionAction | undefined;
|
|
93
|
+
let sddChangeId: string | undefined;
|
|
94
|
+
let sddTtlMinutes: number | undefined;
|
|
95
|
+
|
|
96
|
+
if (commandRaw === 'sdd') {
|
|
97
|
+
const subcommandRaw = argv[1] ?? 'status';
|
|
98
|
+
if (
|
|
99
|
+
subcommandRaw !== 'status' &&
|
|
100
|
+
subcommandRaw !== 'validate' &&
|
|
101
|
+
subcommandRaw !== 'session'
|
|
102
|
+
) {
|
|
103
|
+
throw new Error(`Unsupported SDD subcommand "${subcommandRaw}".\n\n${HELP_TEXT}`);
|
|
104
|
+
}
|
|
105
|
+
sddCommand = subcommandRaw;
|
|
106
|
+
|
|
107
|
+
for (const arg of argv.slice(2)) {
|
|
108
|
+
if (arg === '--json') {
|
|
109
|
+
json = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (arg.startsWith('--stage=')) {
|
|
113
|
+
sddStage = parseSddStage(arg.slice('--stage='.length));
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (arg === '--open') {
|
|
117
|
+
sddSessionAction = 'open';
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (arg === '--refresh') {
|
|
121
|
+
sddSessionAction = 'refresh';
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (arg === '--close') {
|
|
125
|
+
sddSessionAction = 'close';
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (arg.startsWith('--change=')) {
|
|
129
|
+
sddChangeId = arg.slice('--change='.length).trim();
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (arg.startsWith('--ttl-minutes=')) {
|
|
133
|
+
const minutes = Number.parseInt(arg.slice('--ttl-minutes='.length), 10);
|
|
134
|
+
if (!Number.isFinite(minutes) || minutes <= 0) {
|
|
135
|
+
throw new Error(`Invalid --ttl-minutes value "${arg}".`);
|
|
136
|
+
}
|
|
137
|
+
sddTtlMinutes = minutes;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`Unsupported argument "${arg}".\n\n${HELP_TEXT}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (sddCommand === 'status') {
|
|
144
|
+
return {
|
|
145
|
+
command: commandRaw,
|
|
146
|
+
purgeArtifacts: false,
|
|
147
|
+
json,
|
|
148
|
+
sddCommand,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (sddCommand === 'validate') {
|
|
152
|
+
return {
|
|
153
|
+
command: commandRaw,
|
|
154
|
+
purgeArtifacts: false,
|
|
155
|
+
json,
|
|
156
|
+
sddCommand,
|
|
157
|
+
sddStage: sddStage ?? 'PRE_COMMIT',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!sddSessionAction) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Missing SDD session action. Use one of --open | --refresh | --close.\n\n${HELP_TEXT}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (sddSessionAction === 'open' && (!sddChangeId || sddChangeId.length === 0)) {
|
|
167
|
+
throw new Error(`Missing --change=<change-id> for "pumuki sdd session --open".\n\n${HELP_TEXT}`);
|
|
168
|
+
}
|
|
169
|
+
if (sddSessionAction !== 'open' && sddChangeId) {
|
|
170
|
+
throw new Error(`--change is only supported with "--open".\n\n${HELP_TEXT}`);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
command: commandRaw,
|
|
174
|
+
purgeArtifacts: false,
|
|
175
|
+
json,
|
|
176
|
+
sddCommand,
|
|
177
|
+
sddSessionAction,
|
|
178
|
+
sddChangeId,
|
|
179
|
+
sddTtlMinutes,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
51
183
|
for (const arg of argv.slice(1)) {
|
|
184
|
+
if (arg === '--json') {
|
|
185
|
+
json = true;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
52
188
|
if (arg === '--purge-artifacts') {
|
|
53
189
|
purgeArtifacts = true;
|
|
54
190
|
continue;
|
|
@@ -69,6 +205,7 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
69
205
|
command: commandRaw,
|
|
70
206
|
purgeArtifacts,
|
|
71
207
|
updateSpec,
|
|
208
|
+
json,
|
|
72
209
|
};
|
|
73
210
|
};
|
|
74
211
|
|
|
@@ -109,6 +246,14 @@ export const runLifecycleCli = async (
|
|
|
109
246
|
console.log(
|
|
110
247
|
`[pumuki] installed ${result.version} at ${result.repoRoot} (hooks changed: ${result.changedHooks.join(', ') || 'none'})`
|
|
111
248
|
);
|
|
249
|
+
if (result.openSpecBootstrap) {
|
|
250
|
+
console.log(
|
|
251
|
+
`[pumuki] openspec bootstrap: installed=${result.openSpecBootstrap.packageInstalled ? 'yes' : 'no'} project=${result.openSpecBootstrap.projectInitialized ? 'yes' : 'no'} actions=${result.openSpecBootstrap.actions.join(', ') || 'none'}`
|
|
252
|
+
);
|
|
253
|
+
if (result.openSpecBootstrap.skippedReason === 'NO_PACKAGE_JSON') {
|
|
254
|
+
console.log('[pumuki] openspec bootstrap skipped npm install (package.json not found)');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
112
257
|
return 0;
|
|
113
258
|
}
|
|
114
259
|
case 'uninstall': {
|
|
@@ -142,6 +287,9 @@ export const runLifecycleCli = async (
|
|
|
142
287
|
console.log(
|
|
143
288
|
`[pumuki] updated to ${result.targetSpec} at ${result.repoRoot} (hooks changed: ${result.reinstallHooksChanged.join(', ') || 'none'})`
|
|
144
289
|
);
|
|
290
|
+
console.log(
|
|
291
|
+
`[pumuki] openspec compatibility: migrated-legacy=${result.openSpecCompatibility.migratedLegacyPackage ? 'yes' : 'no'} actions=${result.openSpecCompatibility.actions.join(', ') || 'none'}`
|
|
292
|
+
);
|
|
145
293
|
return 0;
|
|
146
294
|
}
|
|
147
295
|
case 'doctor': {
|
|
@@ -151,16 +299,104 @@ export const runLifecycleCli = async (
|
|
|
151
299
|
}
|
|
152
300
|
case 'status': {
|
|
153
301
|
const status = readLifecycleStatus();
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
`[pumuki]
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
302
|
+
if (parsed.json) {
|
|
303
|
+
console.log(JSON.stringify(status, null, 2));
|
|
304
|
+
} else {
|
|
305
|
+
console.log(`[pumuki] repo: ${status.repoRoot}`);
|
|
306
|
+
console.log(`[pumuki] package version: ${status.packageVersion}`);
|
|
307
|
+
console.log(`[pumuki] lifecycle installed: ${status.lifecycleState.installed ?? 'false'}`);
|
|
308
|
+
console.log(`[pumuki] lifecycle version: ${status.lifecycleState.version ?? 'unknown'}`);
|
|
309
|
+
console.log(
|
|
310
|
+
`[pumuki] hooks: pre-commit=${status.hookStatus['pre-commit'].managedBlockPresent ? 'managed' : 'missing'}, pre-push=${status.hookStatus['pre-push'].managedBlockPresent ? 'managed' : 'missing'}`
|
|
311
|
+
);
|
|
312
|
+
console.log(
|
|
313
|
+
`[pumuki] tracked node_modules paths: ${status.trackedNodeModulesCount}`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
case 'sdd': {
|
|
319
|
+
if (parsed.sddCommand === 'status') {
|
|
320
|
+
const sddStatus = readSddStatus();
|
|
321
|
+
if (parsed.json) {
|
|
322
|
+
console.log(JSON.stringify(sddStatus, null, 2));
|
|
323
|
+
} else {
|
|
324
|
+
console.log(`[pumuki][sdd] repo: ${sddStatus.repoRoot}`);
|
|
325
|
+
console.log(
|
|
326
|
+
`[pumuki][sdd] openspec: installed=${sddStatus.openspec.installed ? 'yes' : 'no'} version=${sddStatus.openspec.version ?? 'unknown'}`
|
|
327
|
+
);
|
|
328
|
+
console.log(
|
|
329
|
+
`[pumuki][sdd] openspec compatibility: compatible=${sddStatus.openspec.compatible ? 'yes' : 'no'} minimum=${sddStatus.openspec.minimumVersion} recommended=${sddStatus.openspec.recommendedVersion} parsed=${sddStatus.openspec.parsedVersion ?? 'unknown'}`
|
|
330
|
+
);
|
|
331
|
+
console.log(
|
|
332
|
+
`[pumuki][sdd] openspec project initialized: ${sddStatus.openspec.projectInitialized ? 'yes' : 'no'}`
|
|
333
|
+
);
|
|
334
|
+
console.log(
|
|
335
|
+
`[pumuki][sdd] session: active=${sddStatus.session.active ? 'yes' : 'no'} valid=${sddStatus.session.valid ? 'yes' : 'no'} change=${sddStatus.session.changeId ?? 'none'}`
|
|
336
|
+
);
|
|
337
|
+
if (typeof sddStatus.session.remainingSeconds === 'number') {
|
|
338
|
+
console.log(
|
|
339
|
+
`[pumuki][sdd] session remaining seconds: ${sddStatus.session.remainingSeconds}`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
if (parsed.sddCommand === 'validate') {
|
|
346
|
+
const result = evaluateSddPolicy({
|
|
347
|
+
stage: parsed.sddStage ?? 'PRE_COMMIT',
|
|
348
|
+
});
|
|
349
|
+
if (parsed.json) {
|
|
350
|
+
console.log(JSON.stringify(result, null, 2));
|
|
351
|
+
} else {
|
|
352
|
+
console.log(
|
|
353
|
+
`[pumuki][sdd] stage=${result.stage} allowed=${result.decision.allowed ? 'yes' : 'no'} code=${result.decision.code}`
|
|
354
|
+
);
|
|
355
|
+
console.log(`[pumuki][sdd] ${result.decision.message}`);
|
|
356
|
+
if (result.validation) {
|
|
357
|
+
console.log(
|
|
358
|
+
`[pumuki][sdd] validation: ok=${result.validation.ok ? 'yes' : 'no'} failed=${result.validation.totals.failed} errors=${result.validation.issues.errors}`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return result.decision.allowed ? 0 : 1;
|
|
363
|
+
}
|
|
364
|
+
if (parsed.sddCommand === 'session') {
|
|
365
|
+
if (parsed.sddSessionAction === 'open') {
|
|
366
|
+
const session = openSddSession({
|
|
367
|
+
changeId: parsed.sddChangeId ?? '',
|
|
368
|
+
ttlMinutes: parsed.sddTtlMinutes,
|
|
369
|
+
});
|
|
370
|
+
if (parsed.json) {
|
|
371
|
+
console.log(JSON.stringify(session, null, 2));
|
|
372
|
+
} else {
|
|
373
|
+
console.log(
|
|
374
|
+
`[pumuki][sdd] session opened: change=${session.changeId} ttlMinutes=${session.ttlMinutes ?? 'unknown'} valid=${session.valid ? 'yes' : 'no'}`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
if (parsed.sddSessionAction === 'refresh') {
|
|
380
|
+
const session = refreshSddSession({
|
|
381
|
+
ttlMinutes: parsed.sddTtlMinutes,
|
|
382
|
+
});
|
|
383
|
+
if (parsed.json) {
|
|
384
|
+
console.log(JSON.stringify(session, null, 2));
|
|
385
|
+
} else {
|
|
386
|
+
console.log(
|
|
387
|
+
`[pumuki][sdd] session refreshed: change=${session.changeId ?? 'none'} ttlMinutes=${session.ttlMinutes ?? 'unknown'} valid=${session.valid ? 'yes' : 'no'}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
const session = closeSddSession();
|
|
393
|
+
if (parsed.json) {
|
|
394
|
+
console.log(JSON.stringify(session, null, 2));
|
|
395
|
+
} else {
|
|
396
|
+
console.log('[pumuki][sdd] session closed');
|
|
397
|
+
}
|
|
398
|
+
return 0;
|
|
399
|
+
}
|
|
164
400
|
return 0;
|
|
165
401
|
}
|
|
166
402
|
default:
|
|
@@ -6,6 +6,7 @@ export const PUMUKI_CONFIG_KEYS = {
|
|
|
6
6
|
version: 'pumuki.version',
|
|
7
7
|
hooks: 'pumuki.hooks',
|
|
8
8
|
installedAt: 'pumuki.installed-at',
|
|
9
|
+
openSpecManagedArtifacts: 'pumuki.openspec.managed-artifacts',
|
|
9
10
|
} as const;
|
|
10
11
|
|
|
11
12
|
export const PUMUKI_MANAGED_HOOKS = ['pre-commit', 'pre-push'] as const;
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { installPumukiHooks } from './hookManager';
|
|
2
2
|
import { LifecycleGitService, type ILifecycleGitService } from './gitService';
|
|
3
3
|
import { doctorHasBlockingIssues, runLifecycleDoctor } from './doctor';
|
|
4
|
+
import { runOpenSpecBootstrap, type OpenSpecBootstrapResult } from './openSpecBootstrap';
|
|
5
|
+
import { LifecycleNpmService, type ILifecycleNpmService } from './npmService';
|
|
4
6
|
import { getCurrentPumukiVersion } from './packageInfo';
|
|
5
|
-
import { writeLifecycleState } from './state';
|
|
7
|
+
import { readOpenSpecManagedArtifacts, writeLifecycleState } from './state';
|
|
6
8
|
|
|
7
9
|
export type LifecycleInstallResult = {
|
|
8
10
|
repoRoot: string;
|
|
9
11
|
version: string;
|
|
10
12
|
changedHooks: ReadonlyArray<string>;
|
|
13
|
+
openSpecBootstrap?: OpenSpecBootstrapResult;
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
export const runLifecycleInstall = (params?: {
|
|
14
17
|
cwd?: string;
|
|
15
18
|
git?: ILifecycleGitService;
|
|
19
|
+
npm?: ILifecycleNpmService;
|
|
20
|
+
bootstrapOpenSpec?: boolean;
|
|
16
21
|
}): LifecycleInstallResult => {
|
|
17
22
|
const git = params?.git ?? new LifecycleGitService();
|
|
18
23
|
const report = runLifecycleDoctor({
|
|
@@ -28,17 +33,35 @@ export const runLifecycleInstall = (params?: {
|
|
|
28
33
|
);
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
const shouldBootstrapOpenSpec =
|
|
37
|
+
params?.bootstrapOpenSpec ?? process.env.PUMUKI_SKIP_OPENSPEC_BOOTSTRAP !== '1';
|
|
38
|
+
|
|
39
|
+
const openSpecBootstrap = shouldBootstrapOpenSpec
|
|
40
|
+
? runOpenSpecBootstrap({
|
|
41
|
+
repoRoot: report.repoRoot,
|
|
42
|
+
npm: params?.npm ?? new LifecycleNpmService(),
|
|
43
|
+
})
|
|
44
|
+
: undefined;
|
|
45
|
+
|
|
31
46
|
const hookResult = installPumukiHooks(report.repoRoot);
|
|
32
47
|
const version = getCurrentPumukiVersion();
|
|
48
|
+
const mergedOpenSpecArtifacts = new Set(
|
|
49
|
+
readOpenSpecManagedArtifacts(git, report.repoRoot)
|
|
50
|
+
);
|
|
51
|
+
for (const artifact of openSpecBootstrap?.managedArtifacts ?? []) {
|
|
52
|
+
mergedOpenSpecArtifacts.add(artifact);
|
|
53
|
+
}
|
|
33
54
|
writeLifecycleState({
|
|
34
55
|
git,
|
|
35
56
|
repoRoot: report.repoRoot,
|
|
36
57
|
version,
|
|
58
|
+
openSpecManagedArtifacts: Array.from(mergedOpenSpecArtifacts),
|
|
37
59
|
});
|
|
38
60
|
|
|
39
61
|
return {
|
|
40
62
|
repoRoot: report.repoRoot,
|
|
41
63
|
version,
|
|
42
64
|
changedHooks: hookResult.changedHooks,
|
|
65
|
+
openSpecBootstrap,
|
|
43
66
|
};
|
|
44
67
|
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
detectOpenSpecInstallation,
|
|
5
|
+
evaluateOpenSpecCompatibility,
|
|
6
|
+
isOpenSpecProjectInitialized,
|
|
7
|
+
OPENSPEC_NPM_PACKAGE_NAME,
|
|
8
|
+
} from '../sdd/openSpecCli';
|
|
9
|
+
import { LifecycleNpmService, type ILifecycleNpmService } from './npmService';
|
|
10
|
+
|
|
11
|
+
export type OpenSpecBootstrapResult = {
|
|
12
|
+
repoRoot: string;
|
|
13
|
+
packageInstalled: boolean;
|
|
14
|
+
projectInitialized: boolean;
|
|
15
|
+
actions: ReadonlyArray<string>;
|
|
16
|
+
managedArtifacts: ReadonlyArray<string>;
|
|
17
|
+
skippedReason?: 'NO_PACKAGE_JSON';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type PackageDependencySource = 'dependencies' | 'devDependencies' | 'none';
|
|
21
|
+
|
|
22
|
+
type PackageJson = {
|
|
23
|
+
dependencies?: Record<string, string>;
|
|
24
|
+
devDependencies?: Record<string, string>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const readPackageJson = (repoRoot: string): PackageJson | undefined => {
|
|
28
|
+
const packageJsonPath = join(repoRoot, 'package.json');
|
|
29
|
+
if (!existsSync(packageJsonPath)) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return JSON.parse(readFileSync(packageJsonPath, 'utf8')) as PackageJson;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const resolveDependencySource = (
|
|
36
|
+
packageJson: PackageJson | undefined,
|
|
37
|
+
dependencyName: string
|
|
38
|
+
): PackageDependencySource => {
|
|
39
|
+
if (!packageJson) {
|
|
40
|
+
return 'none';
|
|
41
|
+
}
|
|
42
|
+
if (typeof packageJson.dependencies?.[dependencyName] === 'string') {
|
|
43
|
+
return 'dependencies';
|
|
44
|
+
}
|
|
45
|
+
if (typeof packageJson.devDependencies?.[dependencyName] === 'string') {
|
|
46
|
+
return 'devDependencies';
|
|
47
|
+
}
|
|
48
|
+
return 'none';
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const OPENSPEC_LEGACY_NPM_PACKAGE_NAME = 'openspec';
|
|
52
|
+
const OPENSPEC_PROJECT_MD = 'openspec/project.md';
|
|
53
|
+
const OPENSPEC_ARCHIVE_GITKEEP = 'openspec/changes/archive/.gitkeep';
|
|
54
|
+
const OPENSPEC_SPECS_GITKEEP = 'openspec/specs/.gitkeep';
|
|
55
|
+
|
|
56
|
+
export type OpenSpecCompatibilityMigrationResult = {
|
|
57
|
+
repoRoot: string;
|
|
58
|
+
migratedLegacyPackage: boolean;
|
|
59
|
+
migratedFrom?: Exclude<PackageDependencySource, 'none'>;
|
|
60
|
+
actions: ReadonlyArray<string>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const scaffoldOpenSpecProject = (repoRoot: string): ReadonlyArray<string> => {
|
|
64
|
+
const openspecRoot = join(repoRoot, 'openspec');
|
|
65
|
+
const changesRoot = join(openspecRoot, 'changes');
|
|
66
|
+
const archiveRoot = join(changesRoot, 'archive');
|
|
67
|
+
const specsRoot = join(openspecRoot, 'specs');
|
|
68
|
+
|
|
69
|
+
const projectDescriptionPath = join(repoRoot, OPENSPEC_PROJECT_MD);
|
|
70
|
+
const archiveKeepPath = join(repoRoot, OPENSPEC_ARCHIVE_GITKEEP);
|
|
71
|
+
const specsKeepPath = join(repoRoot, OPENSPEC_SPECS_GITKEEP);
|
|
72
|
+
|
|
73
|
+
const managedArtifacts: string[] = [];
|
|
74
|
+
|
|
75
|
+
const ensureDirectory = (directoryPath: string): void => {
|
|
76
|
+
if (!existsSync(directoryPath)) {
|
|
77
|
+
mkdirSync(directoryPath, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
ensureDirectory(openspecRoot);
|
|
82
|
+
ensureDirectory(changesRoot);
|
|
83
|
+
ensureDirectory(archiveRoot);
|
|
84
|
+
ensureDirectory(specsRoot);
|
|
85
|
+
|
|
86
|
+
if (!existsSync(projectDescriptionPath)) {
|
|
87
|
+
writeFileSync(
|
|
88
|
+
projectDescriptionPath,
|
|
89
|
+
[
|
|
90
|
+
'# OpenSpec Project',
|
|
91
|
+
'',
|
|
92
|
+
'This repository is bootstrapped by Pumuki for SDD/OpenSpec workflows.',
|
|
93
|
+
'Update this document with project context and design constraints.',
|
|
94
|
+
'',
|
|
95
|
+
].join('\n'),
|
|
96
|
+
'utf8'
|
|
97
|
+
);
|
|
98
|
+
managedArtifacts.push(OPENSPEC_PROJECT_MD);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!existsSync(archiveKeepPath)) {
|
|
102
|
+
writeFileSync(archiveKeepPath, '', 'utf8');
|
|
103
|
+
managedArtifacts.push(OPENSPEC_ARCHIVE_GITKEEP);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!existsSync(specsKeepPath)) {
|
|
107
|
+
writeFileSync(specsKeepPath, '', 'utf8');
|
|
108
|
+
managedArtifacts.push(OPENSPEC_SPECS_GITKEEP);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return managedArtifacts;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const runOpenSpecCompatibilityMigration = (params: {
|
|
115
|
+
repoRoot: string;
|
|
116
|
+
npm?: ILifecycleNpmService;
|
|
117
|
+
}): OpenSpecCompatibilityMigrationResult => {
|
|
118
|
+
const npm = params.npm ?? new LifecycleNpmService();
|
|
119
|
+
const packageJson = readPackageJson(params.repoRoot);
|
|
120
|
+
const legacySource = resolveDependencySource(
|
|
121
|
+
packageJson,
|
|
122
|
+
OPENSPEC_LEGACY_NPM_PACKAGE_NAME
|
|
123
|
+
);
|
|
124
|
+
if (legacySource === 'none') {
|
|
125
|
+
return {
|
|
126
|
+
repoRoot: params.repoRoot,
|
|
127
|
+
migratedLegacyPackage: false,
|
|
128
|
+
actions: [],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const actions: string[] = [];
|
|
133
|
+
npm.runNpm(['uninstall', OPENSPEC_LEGACY_NPM_PACKAGE_NAME], params.repoRoot);
|
|
134
|
+
actions.push(`npm-uninstall:${OPENSPEC_LEGACY_NPM_PACKAGE_NAME}`);
|
|
135
|
+
|
|
136
|
+
const installArgs =
|
|
137
|
+
legacySource === 'dependencies'
|
|
138
|
+
? ['install', '--save-exact', `${OPENSPEC_NPM_PACKAGE_NAME}@latest`]
|
|
139
|
+
: ['install', '--save-dev', '--save-exact', `${OPENSPEC_NPM_PACKAGE_NAME}@latest`];
|
|
140
|
+
|
|
141
|
+
npm.runNpm(installArgs, params.repoRoot);
|
|
142
|
+
actions.push(`npm-install:${OPENSPEC_NPM_PACKAGE_NAME}@latest`);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
repoRoot: params.repoRoot,
|
|
146
|
+
migratedLegacyPackage: true,
|
|
147
|
+
migratedFrom: legacySource,
|
|
148
|
+
actions,
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const runOpenSpecBootstrap = (params: {
|
|
153
|
+
repoRoot: string;
|
|
154
|
+
npm?: ILifecycleNpmService;
|
|
155
|
+
}): OpenSpecBootstrapResult => {
|
|
156
|
+
const npm = params.npm ?? new LifecycleNpmService();
|
|
157
|
+
const actions: string[] = [];
|
|
158
|
+
|
|
159
|
+
const installation = detectOpenSpecInstallation(params.repoRoot);
|
|
160
|
+
const compatibility = evaluateOpenSpecCompatibility(installation);
|
|
161
|
+
let packageInstalled = installation.installed;
|
|
162
|
+
const packageJsonPath = join(params.repoRoot, 'package.json');
|
|
163
|
+
const hasPackageJson = existsSync(packageJsonPath);
|
|
164
|
+
|
|
165
|
+
if ((!packageInstalled || !compatibility.compatible) && hasPackageJson) {
|
|
166
|
+
npm.runNpm(
|
|
167
|
+
['install', '--save-dev', '--save-exact', `${OPENSPEC_NPM_PACKAGE_NAME}@latest`],
|
|
168
|
+
params.repoRoot
|
|
169
|
+
);
|
|
170
|
+
packageInstalled = true;
|
|
171
|
+
actions.push(`npm-install:${OPENSPEC_NPM_PACKAGE_NAME}@latest`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const projectInitializedBefore = isOpenSpecProjectInitialized(params.repoRoot);
|
|
175
|
+
const managedArtifacts = !projectInitializedBefore
|
|
176
|
+
? scaffoldOpenSpecProject(params.repoRoot)
|
|
177
|
+
: [];
|
|
178
|
+
if (managedArtifacts.length > 0) {
|
|
179
|
+
actions.push('scaffold:openspec-project');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
repoRoot: params.repoRoot,
|
|
184
|
+
packageInstalled,
|
|
185
|
+
projectInitialized: isOpenSpecProjectInitialized(params.repoRoot),
|
|
186
|
+
actions,
|
|
187
|
+
managedArtifacts,
|
|
188
|
+
skippedReason: !packageInstalled && !hasPackageJson ? 'NO_PACKAGE_JSON' : undefined,
|
|
189
|
+
};
|
|
190
|
+
};
|
|
@@ -6,6 +6,26 @@ export type LifecycleState = {
|
|
|
6
6
|
version?: string;
|
|
7
7
|
hooks?: string;
|
|
8
8
|
installedAt?: string;
|
|
9
|
+
openSpecManagedArtifacts?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const parseManagedArtifacts = (raw: string | undefined): string[] => {
|
|
13
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
return raw
|
|
17
|
+
.split(',')
|
|
18
|
+
.map((value) => value.trim())
|
|
19
|
+
.filter((value) => value.length > 0);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const serializeManagedArtifacts = (artifacts: ReadonlyArray<string>): string | undefined => {
|
|
23
|
+
const unique = Array.from(new Set(artifacts.map((value) => value.trim()).filter((value) => value.length > 0)))
|
|
24
|
+
.sort();
|
|
25
|
+
if (unique.length === 0) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return unique.join(',');
|
|
9
29
|
};
|
|
10
30
|
|
|
11
31
|
export const readLifecycleState = (
|
|
@@ -16,18 +36,54 @@ export const readLifecycleState = (
|
|
|
16
36
|
version: git.getLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.version),
|
|
17
37
|
hooks: git.getLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.hooks),
|
|
18
38
|
installedAt: git.getLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.installedAt),
|
|
39
|
+
openSpecManagedArtifacts: git.getLocalConfig(
|
|
40
|
+
repoRoot,
|
|
41
|
+
PUMUKI_CONFIG_KEYS.openSpecManagedArtifacts
|
|
42
|
+
),
|
|
19
43
|
});
|
|
20
44
|
|
|
21
45
|
export const writeLifecycleState = (params: {
|
|
22
46
|
git: ILifecycleGitService;
|
|
23
47
|
repoRoot: string;
|
|
24
48
|
version: string;
|
|
49
|
+
openSpecManagedArtifacts?: ReadonlyArray<string>;
|
|
25
50
|
}): void => {
|
|
26
51
|
const { git, repoRoot, version } = params;
|
|
27
52
|
git.setLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.installed, 'true');
|
|
28
53
|
git.setLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.version, version);
|
|
29
54
|
git.setLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.hooks, PUMUKI_MANAGED_HOOKS.join(','));
|
|
30
55
|
git.setLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.installedAt, new Date().toISOString());
|
|
56
|
+
if (params.openSpecManagedArtifacts) {
|
|
57
|
+
const serialized = serializeManagedArtifacts(params.openSpecManagedArtifacts);
|
|
58
|
+
if (serialized) {
|
|
59
|
+
git.setLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.openSpecManagedArtifacts, serialized);
|
|
60
|
+
} else {
|
|
61
|
+
git.unsetLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.openSpecManagedArtifacts);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const readOpenSpecManagedArtifacts = (
|
|
67
|
+
git: ILifecycleGitService,
|
|
68
|
+
repoRoot: string
|
|
69
|
+
): ReadonlyArray<string> =>
|
|
70
|
+
parseManagedArtifacts(git.getLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.openSpecManagedArtifacts));
|
|
71
|
+
|
|
72
|
+
export const writeOpenSpecManagedArtifacts = (params: {
|
|
73
|
+
git: ILifecycleGitService;
|
|
74
|
+
repoRoot: string;
|
|
75
|
+
artifacts: ReadonlyArray<string>;
|
|
76
|
+
}): void => {
|
|
77
|
+
const serialized = serializeManagedArtifacts(params.artifacts);
|
|
78
|
+
if (serialized) {
|
|
79
|
+
params.git.setLocalConfig(
|
|
80
|
+
params.repoRoot,
|
|
81
|
+
PUMUKI_CONFIG_KEYS.openSpecManagedArtifacts,
|
|
82
|
+
serialized
|
|
83
|
+
);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
params.git.unsetLocalConfig(params.repoRoot, PUMUKI_CONFIG_KEYS.openSpecManagedArtifacts);
|
|
31
87
|
};
|
|
32
88
|
|
|
33
89
|
export const clearLifecycleState = (
|
|
@@ -38,4 +94,5 @@ export const clearLifecycleState = (
|
|
|
38
94
|
git.unsetLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.version);
|
|
39
95
|
git.unsetLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.hooks);
|
|
40
96
|
git.unsetLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.installedAt);
|
|
97
|
+
git.unsetLocalConfig(repoRoot, PUMUKI_CONFIG_KEYS.openSpecManagedArtifacts);
|
|
41
98
|
};
|