peaks-loop 4.0.0-beta.5 → 4.0.0-beta.6
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/adapter-commands-s2a.d.ts +34 -0
- package/dist/src/cli/commands/adapter-commands-s2a.js +92 -0
- package/dist/src/cli/commands/polyrepo-commands.d.ts +37 -0
- package/dist/src/cli/commands/polyrepo-commands.js +155 -0
- package/dist/src/cli/commands/runtime-commands.d.ts +52 -0
- package/dist/src/cli/commands/runtime-commands.js +150 -0
- package/dist/src/cli/program.js +16 -0
- package/dist/src/services/adapter/adapter-registry.d.ts +48 -0
- package/dist/src/services/adapter/adapter-registry.js +186 -0
- package/dist/src/services/polyrepo/polyrepo-dispatcher.d.ts +34 -0
- package/dist/src/services/polyrepo/polyrepo-dispatcher.js +119 -0
- package/dist/src/services/polyrepo/polyrepo-scanner.d.ts +9 -0
- package/dist/src/services/polyrepo/polyrepo-scanner.js +94 -0
- package/dist/src/services/polyrepo/polyrepo-service.d.ts +34 -0
- package/dist/src/services/polyrepo/polyrepo-service.js +49 -0
- package/dist/src/services/polyrepo/polyrepo-types.d.ts +55 -0
- package/dist/src/services/polyrepo/polyrepo-types.js +1 -0
- package/dist/src/services/runtime/runtime-detector.d.ts +15 -0
- package/dist/src/services/runtime/runtime-detector.js +62 -0
- package/dist/src/services/runtime/runtime-service.d.ts +59 -0
- package/dist/src/services/runtime/runtime-service.js +51 -0
- package/dist/src/services/runtime/vendor-adapter.d.ts +59 -0
- package/dist/src/services/runtime/vendor-adapter.js +1 -0
- package/dist/src/services/runtime/vendors/claude-code.d.ts +7 -0
- package/dist/src/services/runtime/vendors/claude-code.js +39 -0
- package/dist/src/services/runtime/vendors/codex.d.ts +7 -0
- package/dist/src/services/runtime/vendors/codex.js +43 -0
- package/dist/src/services/runtime/vendors/copilot.d.ts +7 -0
- package/dist/src/services/runtime/vendors/copilot.js +40 -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-code/SKILL.md +1 -8
- package/skills/peaks-code/references/external-references.md +1 -3
- package/skills/peaks-code/references/external-skill-invocation.md +1 -1
- package/skills/peaks-code/references/runbook.md +1 -3
- package/skills/peaks-code/references/step-0-55-1x-detection.md +2 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
- package/skills/peaks-code/references/openspec-workflow.md +0 -81
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks adapter *` — vendor adapter registry CLI surface.
|
|
3
|
+
*
|
|
4
|
+
* Slice S2-a of RD-2 (2026-07-08 session). Two subcommands:
|
|
5
|
+
*
|
|
6
|
+
* list — list registered user adapters (persisted at
|
|
7
|
+
* `.peaks/runtime/adapters.json`).
|
|
8
|
+
* register — register a new adapter. Vendor verb strings live in
|
|
9
|
+
* the binary the user picks, NOT in peaks-loop source.
|
|
10
|
+
*
|
|
11
|
+
* This file coexists with the older `adapter-commands.ts` (slice
|
|
12
|
+
* 2026-07-04-cli-15a) which exposes `peaks skill adapter <verb>` —
|
|
13
|
+
* a different surface for skill materialization (resolveScratchDir /
|
|
14
|
+
* materialize / publish). The two surfaces do NOT collide because
|
|
15
|
+
* one is `peaks skill adapter` and the other is `peaks adapter`.
|
|
16
|
+
*/
|
|
17
|
+
import type { Command } from 'commander';
|
|
18
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
19
|
+
export interface AdapterListOptions {
|
|
20
|
+
json?: boolean;
|
|
21
|
+
project?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface AdapterRegisterOptions {
|
|
24
|
+
json?: boolean;
|
|
25
|
+
id?: string;
|
|
26
|
+
/** Display name override (defaults to the id). */
|
|
27
|
+
name?: string;
|
|
28
|
+
binary?: string;
|
|
29
|
+
/** Repeated --arg values. */
|
|
30
|
+
arg?: string[];
|
|
31
|
+
force?: boolean;
|
|
32
|
+
project?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function registerAdapterS2ACommands(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
3
|
+
import { fail, ok, getErrorMessage } from '../../shared/result.js';
|
|
4
|
+
import { AdapterRegistry } from '../../services/adapter/adapter-registry.js';
|
|
5
|
+
import { registryFileFor } from './runtime-commands.js';
|
|
6
|
+
function resolveProjectRoot(explicit) {
|
|
7
|
+
if (explicit !== undefined && explicit.length > 0)
|
|
8
|
+
return explicit;
|
|
9
|
+
return process.cwd();
|
|
10
|
+
}
|
|
11
|
+
export function registerAdapterS2ACommands(program, io) {
|
|
12
|
+
const adapter = program
|
|
13
|
+
.command('adapter')
|
|
14
|
+
.description('Vendor adapter registry (user-registered, persisted to .peaks/runtime/adapters.json)');
|
|
15
|
+
// -----------------------------------------------------------------
|
|
16
|
+
// 1. peaks adapter list [--json] [--project <root>]
|
|
17
|
+
// -----------------------------------------------------------------
|
|
18
|
+
addJsonOption(adapter
|
|
19
|
+
.command('list')
|
|
20
|
+
.description('List user-registered vendor adapters')
|
|
21
|
+
.option('--project <path>', 'project root (defaults to cwd)')).action((options) => {
|
|
22
|
+
try {
|
|
23
|
+
const projectRoot = resolveProjectRoot(options.project);
|
|
24
|
+
const file = registryFileFor(projectRoot);
|
|
25
|
+
const reg = new AdapterRegistry();
|
|
26
|
+
if (existsSync(file))
|
|
27
|
+
reg.load(file);
|
|
28
|
+
const records = reg.list();
|
|
29
|
+
printResult(io, ok('adapter.list', { projectRoot, file, records, count: records.length }, [], [
|
|
30
|
+
records.length === 0
|
|
31
|
+
? 'No adapters registered yet. Run `peaks adapter register --id <vendor> --binary <cmd>` to add one.'
|
|
32
|
+
: `Run \`peaks runtime compact --via <id>\` to invoke a registered adapter.`
|
|
33
|
+
]), options.json);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
printResult(io, fail('adapter.list', 'ADAPTER_LIST_FAILED', getErrorMessage(error), {}, []), options.json);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// -----------------------------------------------------------------
|
|
41
|
+
// 2. peaks adapter register --id <id> --binary <cmd> [--arg <a>]... [--force]
|
|
42
|
+
// -----------------------------------------------------------------
|
|
43
|
+
addJsonOption(adapter
|
|
44
|
+
.command('register')
|
|
45
|
+
.description('Register a new vendor adapter; persists to .peaks/runtime/adapters.json')
|
|
46
|
+
.requiredOption('--id <id>', 'vendor adapter id (lowercase, e.g. my-cli)')
|
|
47
|
+
.option('--name <name>', 'human-readable display name (defaults to <id>)')
|
|
48
|
+
.requiredOption('--binary <cmd>', 'binary name to invoke for compact (e.g. my-cli)')
|
|
49
|
+
.option('--arg <value>', 'extra arg appended before --force (repeatable)', (value, previous = []) => [...previous, value])
|
|
50
|
+
.option('--force', 'overwrite an existing adapter with the same id')
|
|
51
|
+
.option('--project <path>', 'project root (defaults to cwd)')).action((options) => {
|
|
52
|
+
try {
|
|
53
|
+
const projectRoot = resolveProjectRoot(options.project);
|
|
54
|
+
const file = registryFileFor(projectRoot);
|
|
55
|
+
const reg = new AdapterRegistry();
|
|
56
|
+
if (existsSync(file))
|
|
57
|
+
reg.load(file);
|
|
58
|
+
const id = options.id ?? '';
|
|
59
|
+
const record = options.arg !== undefined
|
|
60
|
+
? { id, displayName: options.name ?? id, binary: options.binary ?? '', args: options.arg }
|
|
61
|
+
: { id, displayName: options.name ?? id, binary: options.binary ?? '' };
|
|
62
|
+
let result;
|
|
63
|
+
try {
|
|
64
|
+
result = reg.register(record, { force: options.force === true });
|
|
65
|
+
}
|
|
66
|
+
catch (validationError) {
|
|
67
|
+
printResult(io, fail('adapter.register', 'INVALID_ADAPTER_RECORD', getErrorMessage(validationError), { id }, [
|
|
68
|
+
'Use --id with /^[a-z0-9][a-z0-9._-]*$/, --binary as a binary name (no path separators).'
|
|
69
|
+
]), options.json);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
reg.persist(file);
|
|
74
|
+
const warnings = [];
|
|
75
|
+
if (!result.created && options.force !== true) {
|
|
76
|
+
warnings.push(`adapter "${id}" already registered; pass --force to overwrite`);
|
|
77
|
+
}
|
|
78
|
+
printResult(io, ok('adapter.register', {
|
|
79
|
+
projectRoot,
|
|
80
|
+
file,
|
|
81
|
+
adapter: result.record,
|
|
82
|
+
created: result.created
|
|
83
|
+
}, warnings, [
|
|
84
|
+
`Run \`peaks runtime compact --via ${id}\` to invoke it.`
|
|
85
|
+
]), options.json);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
printResult(io, fail('adapter.register', 'ADAPTER_REGISTER_FAILED', getErrorMessage(error), {}, []), options.json);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks polyrepo *` — polyrepo workflow CLI surface.
|
|
3
|
+
*
|
|
4
|
+
* Slice S2-b of RD-2 (2026-07-08 session). Three subcommands:
|
|
5
|
+
*
|
|
6
|
+
* init — scan the parent dir for child git repos + write a
|
|
7
|
+
* polyrepo manifest to `.peaks/polyrepo.json`.
|
|
8
|
+
* status — read the persisted manifest + report children.
|
|
9
|
+
* dispatch — mirror a PRD/RD/QA artifact from the parent into
|
|
10
|
+
* each target child's
|
|
11
|
+
* `.peaks/_runtime/<sid>/<role>/` directory.
|
|
12
|
+
*
|
|
13
|
+
* Per the project's two-axis convention, all reviewable artifacts
|
|
14
|
+
* live under `.peaks/_runtime/<sid>/<role>/` — never as top-level
|
|
15
|
+
* siblings of `.peaks/_runtime/`.
|
|
16
|
+
*/
|
|
17
|
+
import type { Command } from 'commander';
|
|
18
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
19
|
+
export interface PolyrepoInitOptions {
|
|
20
|
+
json?: boolean;
|
|
21
|
+
root?: string;
|
|
22
|
+
children?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface PolyrepoStatusOptions {
|
|
25
|
+
json?: boolean;
|
|
26
|
+
root?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface PolyrepoDispatchOptions {
|
|
29
|
+
json?: boolean;
|
|
30
|
+
root?: string;
|
|
31
|
+
rid?: string;
|
|
32
|
+
to?: string;
|
|
33
|
+
role?: 'prd' | 'rd' | 'qa';
|
|
34
|
+
artifact?: string;
|
|
35
|
+
sid?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function registerPolyrepoCommands(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
3
|
+
import { fail, ok, getErrorMessage } from '../../shared/result.js';
|
|
4
|
+
import { PolyrepoService } from '../../services/polyrepo/polyrepo-service.js';
|
|
5
|
+
function parseChildren(raw) {
|
|
6
|
+
if (raw === undefined || raw.length === 0)
|
|
7
|
+
return undefined;
|
|
8
|
+
return raw.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
9
|
+
}
|
|
10
|
+
function resolveRoot(explicit) {
|
|
11
|
+
if (explicit !== undefined && explicit.length > 0)
|
|
12
|
+
return explicit;
|
|
13
|
+
return process.cwd();
|
|
14
|
+
}
|
|
15
|
+
export function registerPolyrepoCommands(program, io) {
|
|
16
|
+
const polyrepo = program
|
|
17
|
+
.command('polyrepo')
|
|
18
|
+
.description('Polyrepo workflow (parent dir + child git repos with their own .peaks/ subsets)');
|
|
19
|
+
// -----------------------------------------------------------------
|
|
20
|
+
// 1. peaks polyrepo init [--children <dir1,dir2>] [--root <dir>]
|
|
21
|
+
// -----------------------------------------------------------------
|
|
22
|
+
addJsonOption(polyrepo
|
|
23
|
+
.command('init')
|
|
24
|
+
.description('Scan the parent dir for child git repos and write .peaks/polyrepo.json')
|
|
25
|
+
.option('--children <list>', 'comma-separated child directory names (defaults to auto-discovery)')
|
|
26
|
+
.option('--root <path>', 'parent root (defaults to cwd)')).action((options) => {
|
|
27
|
+
try {
|
|
28
|
+
const root = resolveRoot(options.root);
|
|
29
|
+
const svc = new PolyrepoService();
|
|
30
|
+
const children = parseChildren(options.children);
|
|
31
|
+
const result = children === undefined
|
|
32
|
+
? svc.init({ root })
|
|
33
|
+
: svc.init({ root, children });
|
|
34
|
+
printResult(io, ok('polyrepo.init', {
|
|
35
|
+
root: result.manifest.root,
|
|
36
|
+
detectedAt: result.manifest.detectedAt,
|
|
37
|
+
children: result.manifest.children,
|
|
38
|
+
manifestPath: `${result.manifest.root}/.peaks/polyrepo.json`,
|
|
39
|
+
created: result.created
|
|
40
|
+
}, [], [
|
|
41
|
+
result.manifest.children.length === 0
|
|
42
|
+
? 'No child git repos detected. Pass --children <dir1,dir2> to override.'
|
|
43
|
+
: `Run \`peaks polyrepo dispatch --rid <rid> --to <child-id>\` to mirror an artifact into one or more children.`
|
|
44
|
+
]), options.json);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
printResult(io, fail('polyrepo.init', 'POLYREPO_INIT_FAILED', getErrorMessage(error), {}, ['Verify --root exists and is a directory']), options.json);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
// -----------------------------------------------------------------
|
|
52
|
+
// 2. peaks polyrepo status [--root <dir>]
|
|
53
|
+
// -----------------------------------------------------------------
|
|
54
|
+
addJsonOption(polyrepo
|
|
55
|
+
.command('status')
|
|
56
|
+
.description('Report the persisted polyrepo manifest + children')
|
|
57
|
+
.option('--root <path>', 'parent root (defaults to cwd)')).action((options) => {
|
|
58
|
+
try {
|
|
59
|
+
const root = resolveRoot(options.root);
|
|
60
|
+
const svc = new PolyrepoService();
|
|
61
|
+
const result = svc.status(root);
|
|
62
|
+
if (!result.manifestExists) {
|
|
63
|
+
printResult(io, ok('polyrepo.status', { ...result, initialized: false }, [], [
|
|
64
|
+
'No polyrepo manifest found. Run `peaks polyrepo init [--children <list>]` first.'
|
|
65
|
+
]), options.json);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
printResult(io, ok('polyrepo.status', { ...result, initialized: true }, [], [
|
|
69
|
+
result.children.length === 0
|
|
70
|
+
? 'Manifest exists but has no children. Re-run `peaks polyrepo init --children <list>`.'
|
|
71
|
+
: `Run \`peaks polyrepo dispatch --rid <rid> --to <child-id>\` to mirror an artifact.`
|
|
72
|
+
]), options.json);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
printResult(io, fail('polyrepo.status', 'POLYREPO_STATUS_FAILED', getErrorMessage(error), {}, []), options.json);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// -----------------------------------------------------------------
|
|
80
|
+
// 3. peaks polyrepo dispatch --rid <rid> [--to <child-id>] --role <prd|rd|qa> --artifact <path>
|
|
81
|
+
// -----------------------------------------------------------------
|
|
82
|
+
addJsonOption(polyrepo
|
|
83
|
+
.command('dispatch')
|
|
84
|
+
.description('Mirror a PRD/RD/QA artifact from the parent into one or more children')
|
|
85
|
+
.requiredOption('--rid <rid>', 'request id (used in the on-disk filename)')
|
|
86
|
+
.option('--to <list>', 'comma-separated child ids (defaults to all children in the manifest)')
|
|
87
|
+
.requiredOption('--role <role>', 'artifact role: prd | rd | qa')
|
|
88
|
+
.requiredOption('--artifact <path>', 'source artifact path (relative to --root or absolute)')
|
|
89
|
+
.option('--sid <sid>', 'session id override (defaults to the canonical binding)')
|
|
90
|
+
.option('--root <path>', 'parent root (defaults to cwd)')).action((options) => {
|
|
91
|
+
try {
|
|
92
|
+
const root = resolveRoot(options.root);
|
|
93
|
+
const sid = options.sid ?? 'default';
|
|
94
|
+
const rid = options.rid ?? '';
|
|
95
|
+
const role = options.role;
|
|
96
|
+
if (role !== 'prd' && role !== 'rd' && role !== 'qa') {
|
|
97
|
+
printResult(io, fail('polyrepo.dispatch', 'INVALID_ROLE', `--role must be prd | rd | qa (got "${role}")`, {}, [
|
|
98
|
+
'Re-run with --role prd (or rd / qa).'
|
|
99
|
+
]), options.json);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const artifact = options.artifact ?? '';
|
|
104
|
+
if (!existsSync(artifact)) {
|
|
105
|
+
printResult(io, fail('polyrepo.dispatch', 'ARTIFACT_NOT_FOUND', `source artifact does not exist: ${artifact}`, {}, [
|
|
106
|
+
'Verify the path or pass an absolute path.'
|
|
107
|
+
]), options.json);
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Resolve targets from the manifest first so we can default to
|
|
112
|
+
// "all children" when --to is omitted.
|
|
113
|
+
const svc = new PolyrepoService();
|
|
114
|
+
const status = svc.status(root);
|
|
115
|
+
if (!status.manifestExists) {
|
|
116
|
+
printResult(io, fail('polyrepo.dispatch', 'NO_MANIFEST', `no polyrepo manifest at ${root}/.peaks/polyrepo.json`, {}, [
|
|
117
|
+
'Run `peaks polyrepo init` first.'
|
|
118
|
+
]), options.json);
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let targets;
|
|
123
|
+
if (options.to !== undefined && options.to.length > 0) {
|
|
124
|
+
targets = options.to.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
targets = status.children.map((c) => c.id);
|
|
128
|
+
}
|
|
129
|
+
const result = svc.dispatch(root, {
|
|
130
|
+
sid, rid, targets, role, artifactPath: artifact
|
|
131
|
+
});
|
|
132
|
+
const allOk = result.perChild.every((c) => c.ok);
|
|
133
|
+
printResult(io, ok('polyrepo.dispatch', {
|
|
134
|
+
root,
|
|
135
|
+
sid,
|
|
136
|
+
rid,
|
|
137
|
+
role,
|
|
138
|
+
artifact,
|
|
139
|
+
dispatch: result.dispatch,
|
|
140
|
+
perChild: result.perChild,
|
|
141
|
+
allOk
|
|
142
|
+
}, result.warnings, [
|
|
143
|
+
allOk
|
|
144
|
+
? `Mirrored artifact to ${result.perChild.length} child(ren).`
|
|
145
|
+
: `One or more child mirrors failed; inspect perChild[].ok.`
|
|
146
|
+
]), options.json);
|
|
147
|
+
if (!allOk)
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
printResult(io, fail('polyrepo.dispatch', 'POLYREPO_DISPATCH_FAILED', getErrorMessage(error), {}, []), options.json);
|
|
152
|
+
process.exitCode = 1;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks runtime *` — vendor-runtime detection + compact CLI surface.
|
|
3
|
+
*
|
|
4
|
+
* Slice S2-a of RD-2 (2026-07-08 session). Four subcommands:
|
|
5
|
+
*
|
|
6
|
+
* detect — print which vendor is the active AI runtime
|
|
7
|
+
* (claude-code / codex / copilot / unknown).
|
|
8
|
+
* list — list the built-in vendor adapters registered by
|
|
9
|
+
* RuntimeService (Claude Code / Codex / Copilot).
|
|
10
|
+
* compact — invoke a vendor's compact verb via the adapter. The
|
|
11
|
+
* adapter (NOT this file) chooses the vendor verb;
|
|
12
|
+
* see src/services/runtime/vendors/<vendor>.ts.
|
|
13
|
+
*
|
|
14
|
+
* Vendor verb strings (`claude --compact`, `codex --compact`,
|
|
15
|
+
* `copilot compact`) MUST live ONLY in `src/services/runtime/vendors/`
|
|
16
|
+
* — verified by AC-1: `rg -n "claude --compact|codex --compact|copilot
|
|
17
|
+
* compact" src/services/code/` returns 0 matches.
|
|
18
|
+
*
|
|
19
|
+
* `peaks runtime compact --via <id>` first consults the user
|
|
20
|
+
* adapter registry (`.peaks/runtime/adapters.json`) for the id, then
|
|
21
|
+
* falls back to the built-in adapter list. Missing vendor CLI
|
|
22
|
+
* surfaces as exitCode=127 + warning, NOT as a fatal error — vendor
|
|
23
|
+
* neutrality demands peaks-loop stay alive.
|
|
24
|
+
*/
|
|
25
|
+
import type { Command } from 'commander';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
28
|
+
export interface RuntimeDetectOptions {
|
|
29
|
+
json?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface RuntimeListOptions {
|
|
32
|
+
json?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface RuntimeCompactOptions {
|
|
35
|
+
json?: boolean;
|
|
36
|
+
/** Vendor adapter id (built-in or registered). */
|
|
37
|
+
via?: string;
|
|
38
|
+
force?: boolean;
|
|
39
|
+
/** Override the project root (defaults to .peaks runtime lookup). */
|
|
40
|
+
project?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function registerRuntimeCommands(program: Command, io: ProgramIO): void;
|
|
43
|
+
/** Internal helper exposed for tests + future programmatic callers:
|
|
44
|
+
* resolve the registry file location, ensuring the parent dir
|
|
45
|
+
* exists. Idempotent. */
|
|
46
|
+
export declare function ensureRegistryDir(registryFile: string): void;
|
|
47
|
+
/** Internal helper for tests: locate the registry file under the
|
|
48
|
+
* given project root. Thin wrapper so tests can stub the resolution. */
|
|
49
|
+
export declare function registryFileFor(projectRoot: string): string;
|
|
50
|
+
/** Re-export the path join helper for completeness; not strictly
|
|
51
|
+
* needed by callers but keeps the module self-contained. */
|
|
52
|
+
export { join };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
4
|
+
import { fail, ok } from '../../shared/result.js';
|
|
5
|
+
import { getErrorMessage } from '../../shared/result.js';
|
|
6
|
+
import { detectRuntime } from '../../services/runtime/runtime-detector.js';
|
|
7
|
+
import { RuntimeService } from '../../services/runtime/runtime-service.js';
|
|
8
|
+
import { AdapterRegistry } from '../../services/adapter/adapter-registry.js';
|
|
9
|
+
function resolveProjectRoot(explicit) {
|
|
10
|
+
if (explicit !== undefined && explicit.length > 0)
|
|
11
|
+
return explicit;
|
|
12
|
+
return process.cwd();
|
|
13
|
+
}
|
|
14
|
+
export function registerRuntimeCommands(program, io) {
|
|
15
|
+
const runtime = program
|
|
16
|
+
.command('runtime')
|
|
17
|
+
.description('Vendor runtime detection + compact via adapter (--via <vendor-id>)');
|
|
18
|
+
// -----------------------------------------------------------------
|
|
19
|
+
// 1. peaks runtime detect [--json]
|
|
20
|
+
// -----------------------------------------------------------------
|
|
21
|
+
addJsonOption(runtime
|
|
22
|
+
.command('detect')
|
|
23
|
+
.description('Print the active AI runtime vendor (claude-code / codex / copilot / unknown)')).action((options) => {
|
|
24
|
+
try {
|
|
25
|
+
const result = detectRuntime();
|
|
26
|
+
printResult(io, ok('runtime.detect', result, [], [
|
|
27
|
+
result.vendor === 'unknown'
|
|
28
|
+
? 'No vendor sentinel detected. Use `peaks adapter register --id <vendor> --binary <cmd>` to wire a custom one.'
|
|
29
|
+
: `Run \`peaks runtime compact --via ${result.vendor}\` to invoke its compact verb.`
|
|
30
|
+
]), options.json);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
printResult(io, fail('runtime.detect', 'RUNTIME_DETECT_FAILED', getErrorMessage(error), {}, ['Retry with a clean env']), options.json);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// -----------------------------------------------------------------
|
|
38
|
+
// 2. peaks runtime list [--json]
|
|
39
|
+
// -----------------------------------------------------------------
|
|
40
|
+
addJsonOption(runtime
|
|
41
|
+
.command('list')
|
|
42
|
+
.description('List the built-in vendor adapters registered by RuntimeService')).action((options) => {
|
|
43
|
+
try {
|
|
44
|
+
const svc = new RuntimeService();
|
|
45
|
+
const adapters = svc.listBuiltInAdapters().map((a) => ({ id: a.id, displayName: a.displayName }));
|
|
46
|
+
printResult(io, ok('runtime.list', { builtIn: adapters }, [], [
|
|
47
|
+
`Run \`peaks adapter register --id <vendor> --binary <cmd>\` to add a custom adapter (persists to .peaks/runtime/adapters.json).`
|
|
48
|
+
]), options.json);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
printResult(io, fail('runtime.list', 'RUNTIME_LIST_FAILED', getErrorMessage(error), {}, []), options.json);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// -----------------------------------------------------------------
|
|
56
|
+
// 3. peaks runtime compact [--via <id>] [--force] [--json] [--project <root>]
|
|
57
|
+
// -----------------------------------------------------------------
|
|
58
|
+
addJsonOption(runtime
|
|
59
|
+
.command('compact')
|
|
60
|
+
.description('Compact the active vendor via the chosen adapter (--via <vendor-id>); vendor verbs live ONLY in adapter files')
|
|
61
|
+
.option('--via <id>', 'vendor adapter id (built-in or user-registered)')
|
|
62
|
+
.option('--force', 'ask the vendor to compact unconditionally')
|
|
63
|
+
.option('--project <path>', 'project root (defaults to cwd)')).action(async (options) => {
|
|
64
|
+
try {
|
|
65
|
+
const projectRoot = resolveProjectRoot(options.project);
|
|
66
|
+
const requested = options.via;
|
|
67
|
+
if (requested === undefined || requested.length === 0) {
|
|
68
|
+
// No explicit --via: try to detect first, then fall back.
|
|
69
|
+
const detected = detectRuntime();
|
|
70
|
+
if (detected.vendor === 'unknown') {
|
|
71
|
+
printResult(io, fail('runtime.compact', 'NO_VENDOR_SPECIFIED', 'No --via <id> provided and no vendor detected.', { detected }, [
|
|
72
|
+
'Pass --via <vendor-id>, or run `peaks runtime detect` to see what was detected.'
|
|
73
|
+
]), options.json);
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await runCompact(projectRoot, detected.vendor, options.force === true, io, options.json);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await runCompact(projectRoot, requested, options.force === true, io, options.json);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
printResult(io, fail('runtime.compact', 'RUNTIME_COMPACT_FAILED', getErrorMessage(error), {}, []), options.json);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function runCompact(projectRoot, viaId, force, io, asJson) {
|
|
89
|
+
// Layer 1: user-registered adapter (via AdapterRegistry).
|
|
90
|
+
const registryFile = AdapterRegistry.defaultFile(projectRoot);
|
|
91
|
+
const registry = new AdapterRegistry();
|
|
92
|
+
if (existsSync(registryFile)) {
|
|
93
|
+
try {
|
|
94
|
+
registry.load(registryFile);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Treat corrupt registry as empty — vendor-neutrality: a corrupt
|
|
98
|
+
// .peaks/runtime/adapters.json must NOT block peaks runtime.
|
|
99
|
+
// We still try the built-in path below.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const registered = registry.resolve(viaId);
|
|
103
|
+
if (registered !== undefined) {
|
|
104
|
+
const r = await registered.compact({ force });
|
|
105
|
+
const warnings = [];
|
|
106
|
+
if (r.exitCode === 127) {
|
|
107
|
+
warnings.push(`binary for adapter "${viaId}" not found on PATH; compact is a no-op`);
|
|
108
|
+
}
|
|
109
|
+
printResult(io, ok('runtime.compact', { via: viaId, source: 'registry', compact: r }, warnings, [
|
|
110
|
+
r.exitCode === 0
|
|
111
|
+
? 'Compact completed.'
|
|
112
|
+
: `Compact exited with code ${r.exitCode}. Check stderr above.`
|
|
113
|
+
]), asJson);
|
|
114
|
+
if (r.exitCode !== 0)
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Layer 2: built-in adapter (via RuntimeService).
|
|
119
|
+
const svc = new RuntimeService();
|
|
120
|
+
const r = await svc.compactVia(viaId, force);
|
|
121
|
+
const warnings = [];
|
|
122
|
+
if (r.warning !== undefined)
|
|
123
|
+
warnings.push(r.warning);
|
|
124
|
+
if (r.exitCode === 127) {
|
|
125
|
+
warnings.push(`built-in adapter "${viaId}" binary not found on PATH; compact is a no-op`);
|
|
126
|
+
}
|
|
127
|
+
printResult(io, ok('runtime.compact', { via: viaId, source: 'built-in', compact: { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr } }, warnings, [
|
|
128
|
+
r.exitCode === 0
|
|
129
|
+
? 'Compact completed.'
|
|
130
|
+
: `Compact exited with code ${r.exitCode}. Check stderr above.`
|
|
131
|
+
]), asJson);
|
|
132
|
+
if (r.exitCode !== 0)
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
}
|
|
135
|
+
/** Internal helper exposed for tests + future programmatic callers:
|
|
136
|
+
* resolve the registry file location, ensuring the parent dir
|
|
137
|
+
* exists. Idempotent. */
|
|
138
|
+
export function ensureRegistryDir(registryFile) {
|
|
139
|
+
mkdirSync(dirname(registryFile), { recursive: true });
|
|
140
|
+
}
|
|
141
|
+
/** Internal helper for tests: locate the registry file under the
|
|
142
|
+
* given project root. Thin wrapper so tests can stub the resolution. */
|
|
143
|
+
export function registryFileFor(projectRoot) {
|
|
144
|
+
const f = AdapterRegistry.defaultFile(projectRoot);
|
|
145
|
+
ensureRegistryDir(f);
|
|
146
|
+
return f;
|
|
147
|
+
}
|
|
148
|
+
/** Re-export the path join helper for completeness; not strictly
|
|
149
|
+
* needed by callers but keeps the module self-contained. */
|
|
150
|
+
export { join };
|
package/dist/src/cli/program.js
CHANGED
|
@@ -85,6 +85,15 @@ import { registerJobCommands } from './commands/job-commands.js';
|
|
|
85
85
|
// 15b / 15c / 15d will extend sediment-commands.ts with the remaining 14 verbs.
|
|
86
86
|
import { registerSedimentCommands } from './commands/sediment-commands.js';
|
|
87
87
|
import { registerAdapterCommands } from './commands/adapter-commands.js';
|
|
88
|
+
// Slice RD-2 S2-a: `peaks runtime *` (detect / list / compact) +
|
|
89
|
+
// `peaks adapter *` (list / register). See src/services/runtime/ and
|
|
90
|
+
// src/services/adapter/adapter-registry.ts. Coexists with the older
|
|
91
|
+
// `peaks skill adapter` surface (slice 2026-07-04-cli-15a) which is
|
|
92
|
+
// for skill materialization, not vendor-adapter registry.
|
|
93
|
+
import { registerRuntimeCommands } from './commands/runtime-commands.js';
|
|
94
|
+
import { registerAdapterS2ACommands } from './commands/adapter-commands-s2a.js';
|
|
95
|
+
// Slice RD-2 S2-b: `peaks polyrepo *` (init / status / dispatch).
|
|
96
|
+
import { registerPolyrepoCommands } from './commands/polyrepo-commands.js';
|
|
88
97
|
import { registerSkillVisibilityCommand } from './commands/skill-visibility.js';
|
|
89
98
|
import { applyRetention } from '../services/log/retention.js';
|
|
90
99
|
import { writeLogEntry, maybeWriteStderr } from '../services/log/logger.js';
|
|
@@ -375,6 +384,13 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
|
|
|
375
384
|
registerSedimentCommands(program, io);
|
|
376
385
|
// Slice 2026-07-04-cli-15a: `peaks skill adapter <verb>` — adapter list / set-active.
|
|
377
386
|
registerAdapterCommands(program, io);
|
|
387
|
+
// Slice RD-2 S2-a: `peaks runtime *` (detect / list / compact) +
|
|
388
|
+
// `peaks adapter *` (list / register). Vendor verbs live ONLY in
|
|
389
|
+
// src/services/runtime/vendors/<vendor>.ts (AC-1 enforcement).
|
|
390
|
+
registerRuntimeCommands(program, io);
|
|
391
|
+
registerAdapterS2ACommands(program, io);
|
|
392
|
+
// Slice RD-2 S2-b: `peaks polyrepo *` (init / status / dispatch).
|
|
393
|
+
registerPolyrepoCommands(program, io);
|
|
378
394
|
// Slice 2026-07-05-peaks-code-to-peaks-code (Task 1): `peaks skill:visibility --list --json`
|
|
379
395
|
// — read .claude-plugin/marketplace.json and report each skill's
|
|
380
396
|
// visibility (public vs internal). Used to surface which skills are
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { VendorAdapter } from '../runtime/vendor-adapter.js';
|
|
2
|
+
/** Serializable adapter record (the on-disk shape). */
|
|
3
|
+
export interface AdapterRecord {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly displayName: string;
|
|
6
|
+
/** Binary name or absolute path to invoke for compact. */
|
|
7
|
+
readonly binary: string;
|
|
8
|
+
/** Extra args appended after the compact verb. */
|
|
9
|
+
readonly args?: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface PersistedAdapterRegistry {
|
|
12
|
+
readonly version: 1;
|
|
13
|
+
readonly adapters: AdapterRecord[];
|
|
14
|
+
}
|
|
15
|
+
export interface RegisterOptions {
|
|
16
|
+
/** Overwrite an existing record with the same id. Defaults to false
|
|
17
|
+
* to prevent silent overwrites of user customizations. */
|
|
18
|
+
readonly force?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** In-memory + on-disk adapter registry. The registry holds USER
|
|
21
|
+
* adapters (those registered via `peaks adapter register`); built-in
|
|
22
|
+
* adapters live in RuntimeService and are NOT registered here. */
|
|
23
|
+
export declare class AdapterRegistry {
|
|
24
|
+
private readonly records;
|
|
25
|
+
/** Register a new adapter. When an adapter with the same id already
|
|
26
|
+
* exists, returns the existing record unless `force: true`. */
|
|
27
|
+
register(record: AdapterRecord, opts?: RegisterOptions): {
|
|
28
|
+
record: AdapterRecord;
|
|
29
|
+
created: boolean;
|
|
30
|
+
};
|
|
31
|
+
/** Resolve a registered adapter by id, materializing it as a
|
|
32
|
+
* runnable VendorAdapter. Returns undefined when not registered. */
|
|
33
|
+
resolve(id: string): VendorAdapter | undefined;
|
|
34
|
+
/** List registered adapter records (serializable form). */
|
|
35
|
+
list(): AdapterRecord[];
|
|
36
|
+
/** Remove a registered adapter. Returns true if it existed. */
|
|
37
|
+
unregister(id: string): boolean;
|
|
38
|
+
/** Persist the current registry to disk as JSON. Writes atomically
|
|
39
|
+
* (write to `<file>.tmp` then rename) so a crash mid-write does
|
|
40
|
+
* not corrupt the registry file. */
|
|
41
|
+
persist(file: string): void;
|
|
42
|
+
/** Load a registry from disk. Missing file = empty registry.
|
|
43
|
+
* Corrupt file = throw (caller decides whether to bail or
|
|
44
|
+
* fall back to empty). */
|
|
45
|
+
load(file: string): void;
|
|
46
|
+
/** Default location for the on-disk registry file. */
|
|
47
|
+
static defaultFile(projectRoot: string): string;
|
|
48
|
+
}
|