peaks-cli 1.4.0 → 1.4.2
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/core-artifact-commands.js +21 -0
- package/dist/src/cli/commands/memory-commands.d.ts +13 -0
- package/dist/src/cli/commands/memory-commands.js +60 -0
- package/dist/src/cli/commands/migrate-1-4-1-command.d.ts +11 -0
- package/dist/src/cli/commands/migrate-1-4-1-command.js +34 -0
- package/dist/src/cli/commands/retrospective-commands.d.ts +9 -0
- package/dist/src/cli/commands/retrospective-commands.js +58 -0
- package/dist/src/cli/commands/workspace-commands.js +8 -0
- package/dist/src/cli/program.js +16 -22
- package/dist/src/services/fuzzy-matching/fuzzy-match-service.d.ts +15 -0
- package/dist/src/services/fuzzy-matching/fuzzy-match-service.js +56 -0
- package/dist/src/services/fuzzy-matching/types.d.ts +20 -0
- package/dist/src/services/fuzzy-matching/types.js +1 -0
- package/dist/src/services/memory/memory-search-service.d.ts +61 -0
- package/dist/src/services/memory/memory-search-service.js +80 -0
- package/dist/src/services/recommendations/capability-seed-items.js +0 -1
- package/dist/src/services/recommendations/capability-seed-mappings.js +0 -1
- package/dist/src/services/recommendations/capability-seed-sources.js +0 -1
- package/dist/src/services/retrospective/retrospective-search-service.d.ts +37 -0
- package/dist/src/services/retrospective/retrospective-search-service.js +75 -0
- package/dist/src/services/standards/project-context.d.ts +1 -1
- package/dist/src/services/standards/project-context.js +0 -4
- package/dist/src/services/standards/project-standards-service.js +1 -3
- package/dist/src/services/workspace/migrate-1-4-1-service.d.ts +44 -0
- package/dist/src/services/workspace/migrate-1-4-1-service.js +195 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +3 -7
- package/skills/peaks-solo/SKILL.md +1 -1
- package/skills/peaks-solo/references/completion-handoff.md +3 -1
- package/dist/src/cli/commands/shadcn-commands.d.ts +0 -3
- package/dist/src/cli/commands/shadcn-commands.js +0 -35
- package/dist/src/cli/commands/skill-scope-commands.d.ts +0 -49
- package/dist/src/cli/commands/skill-scope-commands.js +0 -305
- package/dist/src/services/shadcn/shadcn-service.d.ts +0 -27
- package/dist/src/services/shadcn/shadcn-service.js +0 -128
- package/dist/src/services/skill-scope/adapters/_stub-helper.d.ts +0 -39
- package/dist/src/services/skill-scope/adapters/_stub-helper.js +0 -98
- package/dist/src/services/skill-scope/adapters/claude-code.d.ts +0 -59
- package/dist/src/services/skill-scope/adapters/claude-code.js +0 -304
- package/dist/src/services/skill-scope/adapters/codex.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/codex.js +0 -12
- package/dist/src/services/skill-scope/adapters/cursor.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/cursor.js +0 -13
- package/dist/src/services/skill-scope/adapters/qoder.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/qoder.js +0 -13
- package/dist/src/services/skill-scope/adapters/tongyi.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/tongyi.js +0 -13
- package/dist/src/services/skill-scope/adapters/trae.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/trae.js +0 -12
- package/dist/src/services/skill-scope/detect.d.ts +0 -75
- package/dist/src/services/skill-scope/detect.js +0 -480
- package/dist/src/services/skill-scope/registry.d.ts +0 -41
- package/dist/src/services/skill-scope/registry.js +0 -83
- package/dist/src/services/skill-scope/source-of-truth.d.ts +0 -44
- package/dist/src/services/skill-scope/source-of-truth.js +0 -118
- package/dist/src/services/skill-scope/types.d.ts +0 -176
- package/dist/src/services/skill-scope/types.js +0 -74
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `peaks skill scope` CLI surface (slice 025.1).
|
|
3
|
-
*
|
|
4
|
-
* Four subcommands (mutually exclusive):
|
|
5
|
-
* - `--detect` — dry-run; prints the relevance matrix, never touches files.
|
|
6
|
-
* - `--apply` — writes the source-of-truth + IDE-native config.
|
|
7
|
-
* - `--show` — reads the source-of-truth + native config back.
|
|
8
|
-
* - `--reset` — removes the source-of-truth + IDE-native config.
|
|
9
|
-
*
|
|
10
|
-
* Exit code matrix (tech-doc §6.3):
|
|
11
|
-
* 0 success
|
|
12
|
-
* 1 uncaught error
|
|
13
|
-
* 2 invalid usage (missing/incompatible flags)
|
|
14
|
-
* 3 source-of-truth written but adapter returned NOT_SUPPORTED
|
|
15
|
-
* 4 adapter failure other than NOT_SUPPORTED
|
|
16
|
-
*/
|
|
17
|
-
import { existsSync } from 'node:fs';
|
|
18
|
-
import { join } from 'node:path';
|
|
19
|
-
import { detectSkillScope, } from '../../services/skill-scope/detect.js';
|
|
20
|
-
import { resolveActiveAdapter, getScopeAdapter } from '../../services/skill-scope/registry.js';
|
|
21
|
-
import { ideCompanionFilePath, readIdeCompanion, readSourceOfTruth, removeIfExists, scopeFilePath, writeSourceOfTruth, } from '../../services/skill-scope/source-of-truth.js';
|
|
22
|
-
import { ALWAYS_RELEVANT_SKILLS } from '../../services/skill-scope/types.js';
|
|
23
|
-
import { fail, getErrorMessage, ok } from '../../shared/result.js';
|
|
24
|
-
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
25
|
-
const VALID_ACTIONS = ['detect', 'apply', 'show', 'reset'];
|
|
26
|
-
const VALID_IDES = ['claude-code', 'trae', 'codex', 'cursor', 'qoder', 'tongyi-lingma'];
|
|
27
|
-
function isValidIde(value) {
|
|
28
|
-
return VALID_IDES.includes(value);
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* G6: enforce the peaks-* allowlist. Re-adds any peak-* skill that is
|
|
32
|
-
* missing from the allowlist, and removes any peak-* skill from the
|
|
33
|
-
* denylist. The list is the same one declared in `types.ts`.
|
|
34
|
-
*/
|
|
35
|
-
function enforcePeaksAllowlist(allowlist) {
|
|
36
|
-
const set = new Set(allowlist);
|
|
37
|
-
for (const name of ALWAYS_RELEVANT_SKILLS) {
|
|
38
|
-
if (name.startsWith('peaks-'))
|
|
39
|
-
set.add(name);
|
|
40
|
-
}
|
|
41
|
-
return [...set];
|
|
42
|
-
}
|
|
43
|
-
function stripPeaksFromDenylist(denylist) {
|
|
44
|
-
return denylist.filter((name) => !name.startsWith('peaks-'));
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Determine the IDE. Caller-supplied `--ide` wins; otherwise the registry
|
|
48
|
-
* probes the project root.
|
|
49
|
-
*/
|
|
50
|
-
async function resolveIde(projectRoot, override) {
|
|
51
|
-
if (override !== undefined) {
|
|
52
|
-
if (!isValidIde(override)) {
|
|
53
|
-
throw new Error(`Unknown IDE: ${override}. Valid: ${VALID_IDES.join(', ')}`);
|
|
54
|
-
}
|
|
55
|
-
return { ide: override, isFallback: false };
|
|
56
|
-
}
|
|
57
|
-
const resolved = await resolveActiveAdapter(projectRoot);
|
|
58
|
-
return { ide: resolved.adapter.ide, isFallback: resolved.isFallback };
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Stable timestamp (no millisecond jitter) for the `generatedAt` field.
|
|
62
|
-
* `Date.now()` would still be deterministic per-run; we keep the natural
|
|
63
|
-
* one to ensure `generatedAt` matches what the user sees on disk.
|
|
64
|
-
*/
|
|
65
|
-
function nowIso() {
|
|
66
|
-
return new Date().toISOString();
|
|
67
|
-
}
|
|
68
|
-
/** Run the --detect subcommand. */
|
|
69
|
-
async function runDetect(input) {
|
|
70
|
-
try {
|
|
71
|
-
const result = await detectSkillScope({ projectRoot: input.project });
|
|
72
|
-
const envelope = ok('skill.scope.detect', result);
|
|
73
|
-
const stdout = input.json === true ? JSON.stringify(envelope, null, 2) : JSON.stringify(result, null, 2);
|
|
74
|
-
return { exitCode: 0, envelope, stdout, stderr: '' };
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
const envelope = fail('skill.scope.detect', 'DETECT_FAILED', getErrorMessage(error), null);
|
|
78
|
-
return { exitCode: 1, envelope, stdout: '', stderr: envelope.message ?? 'detect failed' };
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
/** Build the final ScopeConfig (applies G6 enforcement + override). */
|
|
82
|
-
function buildScopeConfig(args) {
|
|
83
|
-
const strict = args.strict;
|
|
84
|
-
const detected = args.detected;
|
|
85
|
-
// Build allowlist from detected.relevant + (in loose) borderline.
|
|
86
|
-
const allowFromDetect = detected.skills
|
|
87
|
-
.filter((s) => s.relevance === 'relevant' || (!strict && s.relevance === 'borderline'))
|
|
88
|
-
.map((s) => s.name);
|
|
89
|
-
const merged = args.allowOverride !== undefined ? [...args.allowOverride, ...allowFromDetect] : allowFromDetect;
|
|
90
|
-
const enforced = enforcePeaksAllowlist(merged);
|
|
91
|
-
// Denylist: irrelevant skills (strict + loose both), minus anything in allowlist.
|
|
92
|
-
const denyFromDetect = detected.skills
|
|
93
|
-
.filter((s) => s.relevance === 'irrelevant' && !enforced.includes(s.name))
|
|
94
|
-
.map((s) => s.name);
|
|
95
|
-
const finalDeny = stripPeaksFromDenylist(denyFromDetect);
|
|
96
|
-
return {
|
|
97
|
-
generatedAt: nowIso(),
|
|
98
|
-
ide: args.ide,
|
|
99
|
-
strict,
|
|
100
|
-
allowlist: enforced,
|
|
101
|
-
denylist: finalDeny,
|
|
102
|
-
skills: detected.skills,
|
|
103
|
-
signals: detected.projectSignals,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
/** Run the --apply subcommand. */
|
|
107
|
-
async function runApply(input) {
|
|
108
|
-
// 1. Detect the scope.
|
|
109
|
-
const detected = await detectSkillScope({ projectRoot: input.project });
|
|
110
|
-
// --strict wins when both flags are passed. Default is --loose per PRD.
|
|
111
|
-
const isStrict = input.strict === true && input.loose !== true;
|
|
112
|
-
const loose = !isStrict;
|
|
113
|
-
const { ide, isFallback } = await resolveIde(input.project, input.ide);
|
|
114
|
-
const adapter = getScopeAdapter(ide);
|
|
115
|
-
const config = buildScopeConfig({
|
|
116
|
-
ide,
|
|
117
|
-
strict: isStrict,
|
|
118
|
-
detected,
|
|
119
|
-
...(input.overrideAllowlist !== undefined ? { allowOverride: input.overrideAllowlist } : {}),
|
|
120
|
-
});
|
|
121
|
-
// 2. Write the source-of-truth first (atomic). Test seam: simulate failure.
|
|
122
|
-
let writtenFiles = [];
|
|
123
|
-
let sourceWritten = false;
|
|
124
|
-
try {
|
|
125
|
-
if (input.simulateSourceOfTruthWriteFailure) {
|
|
126
|
-
throw new Error('simulated source-of-truth write failure');
|
|
127
|
-
}
|
|
128
|
-
const file = await writeSourceOfTruth(input.project, config);
|
|
129
|
-
writtenFiles.push(file);
|
|
130
|
-
sourceWritten = true;
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
const envelope = fail('skill.scope.apply', 'WRITE_FAILED', getErrorMessage(error), { ide, sourceWritten: false }, ['Fix filesystem permissions on the project root and retry']);
|
|
134
|
-
return { exitCode: 4, envelope, stdout: '', stderr: envelope.message ?? 'write failed' };
|
|
135
|
-
}
|
|
136
|
-
// 3. Call the adapter. Stub adapters return notSupported=true; we surface it.
|
|
137
|
-
const adapterInput = {
|
|
138
|
-
allowlist: config.allowlist,
|
|
139
|
-
denylist: config.denylist,
|
|
140
|
-
strict: config.strict,
|
|
141
|
-
projectRoot: input.project,
|
|
142
|
-
sourceConfig: config,
|
|
143
|
-
shadowFallback: input.shadowFallback === true,
|
|
144
|
-
};
|
|
145
|
-
let result;
|
|
146
|
-
try {
|
|
147
|
-
result = await adapter.applyScope(adapterInput);
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
// Roll back the source-of-truth on adapter failure.
|
|
151
|
-
await removeIfExists(scopeFilePath(input.project));
|
|
152
|
-
const envelope = fail('skill.scope.apply', 'ADAPTER_FAILED', getErrorMessage(error), { ide, sourceWritten: false, writtenFiles: [] }, ['Inspect the adapter error and retry']);
|
|
153
|
-
return { exitCode: 4, envelope, stdout: '', stderr: envelope.message ?? 'adapter failed' };
|
|
154
|
-
}
|
|
155
|
-
// The stub adapter also writes the canonical skills.json — that's
|
|
156
|
-
// already on disk from step 2, so its second write is a no-op update.
|
|
157
|
-
const finalWrittenFiles = [...writtenFiles, ...result.writtenFiles];
|
|
158
|
-
const envelope = ok('skill.scope.apply', {
|
|
159
|
-
ide,
|
|
160
|
-
isFallback,
|
|
161
|
-
strict: isStrict,
|
|
162
|
-
loose,
|
|
163
|
-
allowlist: config.allowlist,
|
|
164
|
-
denylist: config.denylist,
|
|
165
|
-
signals: config.signals,
|
|
166
|
-
writtenFiles: finalWrittenFiles,
|
|
167
|
-
usedShadowStub: result.usedShadowStub,
|
|
168
|
-
notSupported: result.notSupported,
|
|
169
|
-
strippedFromDenylist: result.strippedFromDenylist ?? [],
|
|
170
|
-
error: result.error,
|
|
171
|
-
});
|
|
172
|
-
const stdout = input.json === true ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope.data, null, 2);
|
|
173
|
-
if (result.notSupported) {
|
|
174
|
-
// Stub adapter: NOT_SUPPORTED → exit 3, write error to stderr.
|
|
175
|
-
const stderr = `${result.error?.code ?? 'NOT_SUPPORTED'}: ${result.error?.message ?? 'not supported'}`;
|
|
176
|
-
return { exitCode: 3, envelope, stdout, stderr };
|
|
177
|
-
}
|
|
178
|
-
return { exitCode: 0, envelope, stdout, stderr: '' };
|
|
179
|
-
}
|
|
180
|
-
/** Run the --show subcommand. */
|
|
181
|
-
async function runShow(input) {
|
|
182
|
-
const source = await readSourceOfTruth(input.project);
|
|
183
|
-
const { ide } = await resolveIde(input.project, input.ide);
|
|
184
|
-
const companionPath = ideCompanionFilePath(input.project, ide);
|
|
185
|
-
const companion = await readIdeCompanion(input.project, ide);
|
|
186
|
-
// For Claude Code, the native config is `.claude/settings.local.json`.
|
|
187
|
-
const nativeSettingsPath = join(input.project, '.claude', 'settings.local.json');
|
|
188
|
-
const nativeExists = existsSync(nativeSettingsPath);
|
|
189
|
-
let native = companion;
|
|
190
|
-
if (nativeExists) {
|
|
191
|
-
try {
|
|
192
|
-
const { readFile } = await import('node:fs/promises');
|
|
193
|
-
native = JSON.parse(await readFile(nativeSettingsPath, 'utf8'));
|
|
194
|
-
}
|
|
195
|
-
catch {
|
|
196
|
-
native = null;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
const data = {
|
|
200
|
-
ide,
|
|
201
|
-
source,
|
|
202
|
-
native,
|
|
203
|
-
nativeSettingsPath: nativeExists ? '.claude/settings.local.json' : null,
|
|
204
|
-
companionPath: existsSync(companionPath) ? companionPath : null,
|
|
205
|
-
};
|
|
206
|
-
const envelope = ok('skill.scope.show', data);
|
|
207
|
-
const stdout = input.json === true ? JSON.stringify(envelope, null, 2) : JSON.stringify(data, null, 2);
|
|
208
|
-
return { exitCode: 0, envelope, stdout, stderr: '' };
|
|
209
|
-
}
|
|
210
|
-
/** Run the --reset subcommand. */
|
|
211
|
-
async function runReset(input) {
|
|
212
|
-
const { ide } = await resolveIde(input.project, input.ide);
|
|
213
|
-
const adapter = getScopeAdapter(ide);
|
|
214
|
-
const resetResult = await adapter.resetScope({ projectRoot: input.project });
|
|
215
|
-
const sourceFile = scopeFilePath(input.project);
|
|
216
|
-
const sourceRemoved = await removeIfExists(sourceFile);
|
|
217
|
-
const allRemoved = [...resetResult.removedFiles, ...(sourceRemoved ? [sourceFile] : [])];
|
|
218
|
-
const envelope = ok('skill.scope.reset', {
|
|
219
|
-
ide,
|
|
220
|
-
removedFiles: allRemoved,
|
|
221
|
-
});
|
|
222
|
-
// Always include the canonical source-of-truth path in the human-readable
|
|
223
|
-
// summary, even if it didn't exist (so the user knows what was targeted).
|
|
224
|
-
const displayFiles = allRemoved.length > 0 ? allRemoved : [sourceFile, join(input.project, '.claude', 'settings.local.json')];
|
|
225
|
-
const summary = `removed: ${displayFiles.join(', ')}`;
|
|
226
|
-
const stdout = input.json === true ? JSON.stringify(envelope, null, 2) : summary;
|
|
227
|
-
return { exitCode: 0, envelope, stdout, stderr: '' };
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Programmatic entry point for `peaks skill scope`. Used by the CLI shim
|
|
231
|
-
* AND by the unit tests.
|
|
232
|
-
*/
|
|
233
|
-
export async function runSkillScopeCommand(input) {
|
|
234
|
-
if (!VALID_ACTIONS.includes(input.subcommand)) {
|
|
235
|
-
const envelope = fail('skill.scope', 'INVALID_USAGE', `Unknown action: ${input.subcommand}`, null);
|
|
236
|
-
return { exitCode: 2, envelope, stdout: '', stderr: envelope.message ?? 'invalid usage' };
|
|
237
|
-
}
|
|
238
|
-
switch (input.subcommand) {
|
|
239
|
-
case 'detect': return runDetect(input);
|
|
240
|
-
case 'apply': return runApply(input);
|
|
241
|
-
case 'show': return runShow(input);
|
|
242
|
-
case 'reset': return runReset(input);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Register the `peaks skill scope` subcommand on the `skill` command group.
|
|
247
|
-
* Mutually-exclusive flags: exactly one of --detect / --apply / --show / --reset.
|
|
248
|
-
*/
|
|
249
|
-
export function registerSkillScopeCommands(program, io) {
|
|
250
|
-
// Find the existing 'skill' subcommand if any.
|
|
251
|
-
let skillCmd = program.commands.find((c) => c.name() === 'skill');
|
|
252
|
-
if (skillCmd === undefined) {
|
|
253
|
-
skillCmd = program.command('skill').description('Manage Peaks skills');
|
|
254
|
-
}
|
|
255
|
-
const scope = skillCmd
|
|
256
|
-
.command('scope')
|
|
257
|
-
.description('Per-project skill scoping: detect, apply, show, reset');
|
|
258
|
-
addJsonOption(scope
|
|
259
|
-
.option('--detect', 'dry-run: print the relevance matrix')
|
|
260
|
-
.option('--apply', 'apply the scope (writes source-of-truth + IDE config)')
|
|
261
|
-
.option('--show', 'show the currently applied scope')
|
|
262
|
-
.option('--reset', 'remove the scope config')
|
|
263
|
-
.option('--project <path>', 'target project root (defaults to cwd)', process.cwd())
|
|
264
|
-
.option('--strict', '--apply: only `relevant` skills in the allowlist')
|
|
265
|
-
.option('--loose', '--apply: `relevant` + `borderline` in the allowlist (default)')
|
|
266
|
-
.option('--ide <name>', 'force a specific IDE adapter (overrides auto-detect)')
|
|
267
|
-
.option('--shadow-fallback', '--apply: Claude Code uses shadow stubs for the denylist')).action(async (options) => {
|
|
268
|
-
const flags = [options.detect, options.apply, options.show, options.reset].filter(Boolean).length;
|
|
269
|
-
if (flags !== 1) {
|
|
270
|
-
const envelope = fail('skill.scope', 'INVALID_USAGE', 'Exactly one of --detect / --apply / --show / --reset is required', null, ['Pass exactly one action flag']);
|
|
271
|
-
printResult(io, envelope, options.json === true);
|
|
272
|
-
process.exitCode = 2;
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
const subcommand = options.detect
|
|
276
|
-
? 'detect'
|
|
277
|
-
: options.apply
|
|
278
|
-
? 'apply'
|
|
279
|
-
: options.show
|
|
280
|
-
? 'show'
|
|
281
|
-
: 'reset';
|
|
282
|
-
const result = await runSkillScopeCommand({
|
|
283
|
-
subcommand,
|
|
284
|
-
project: options.project ?? process.cwd(),
|
|
285
|
-
...(options.strict !== undefined ? { strict: options.strict } : {}),
|
|
286
|
-
...(options.loose !== undefined ? { loose: options.loose } : {}),
|
|
287
|
-
...(options.ide !== undefined ? { ide: options.ide } : {}),
|
|
288
|
-
...(options.shadowFallback !== undefined ? { shadowFallback: options.shadowFallback } : {}),
|
|
289
|
-
...(options.json !== undefined ? { json: options.json } : {}),
|
|
290
|
-
});
|
|
291
|
-
if (options.json === true) {
|
|
292
|
-
if (result.envelope !== null)
|
|
293
|
-
printResult(io, result.envelope, true);
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
if (result.stdout.length > 0)
|
|
297
|
-
io.stdout(result.stdout);
|
|
298
|
-
if (result.stderr.length > 0)
|
|
299
|
-
io.stderr(result.stderr);
|
|
300
|
-
}
|
|
301
|
-
if (result.exitCode !== 0) {
|
|
302
|
-
process.exitCode = result.exitCode;
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
declare const SHADCN_PACKAGE_NAME = "shadcn";
|
|
2
|
-
declare const SHADCN_PACKAGE_VERSION = "4.7.0";
|
|
3
|
-
declare const SHADCN_EXECUTABLE: string;
|
|
4
|
-
export type ShadcnInvocationOptions = {
|
|
5
|
-
args: string[];
|
|
6
|
-
cwd?: string;
|
|
7
|
-
};
|
|
8
|
-
export type ShadcnInvocation = {
|
|
9
|
-
executable: typeof SHADCN_EXECUTABLE;
|
|
10
|
-
args: string[];
|
|
11
|
-
cwd: string;
|
|
12
|
-
packageName: typeof SHADCN_PACKAGE_NAME;
|
|
13
|
-
packageVersion: typeof SHADCN_PACKAGE_VERSION;
|
|
14
|
-
};
|
|
15
|
-
export type ShadcnExecutionResult = {
|
|
16
|
-
exitCode: number | null;
|
|
17
|
-
stdout: string;
|
|
18
|
-
stderr: string;
|
|
19
|
-
};
|
|
20
|
-
export type ShadcnProcessRunner = (invocation: ShadcnInvocation) => Promise<ShadcnExecutionResult>;
|
|
21
|
-
declare function createShadcnEnvironment(sourceEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
22
|
-
export declare function createShadcnInvocation(options: ShadcnInvocationOptions): ShadcnInvocation;
|
|
23
|
-
export declare function executeShadcnInvocation(invocation: ShadcnInvocation, runner?: ShadcnProcessRunner): Promise<ShadcnExecutionResult>;
|
|
24
|
-
export declare const testInternals: {
|
|
25
|
-
createShadcnEnvironment: typeof createShadcnEnvironment;
|
|
26
|
-
};
|
|
27
|
-
export {};
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import { createRequire } from 'node:module';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
5
|
-
const SHADCN_PACKAGE_NAME = 'shadcn';
|
|
6
|
-
const SHADCN_PACKAGE_VERSION = '4.7.0';
|
|
7
|
-
const SHADCN_EXECUTABLE = process.execPath;
|
|
8
|
-
const SHADCN_BINARY_PATH = resolveShadcnBinaryPath();
|
|
9
|
-
const SHADCN_PROCESS_TIMEOUT_MS = 600_000;
|
|
10
|
-
const SHADCN_OUTPUT_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
11
|
-
const POSITIONAL_ARGUMENT_PREFIX = '-';
|
|
12
|
-
const PRESERVED_ENV_KEYS = ['PATH', 'Path', 'HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'TEMP', 'TMP', 'SystemRoot', 'WINDIR'];
|
|
13
|
-
function resolveShadcnBinaryPath() {
|
|
14
|
-
const require = createRequire(import.meta.url);
|
|
15
|
-
const binaryPath = require.resolve('shadcn');
|
|
16
|
-
if (!existsSync(binaryPath)) {
|
|
17
|
-
throw new Error('Unable to resolve local shadcn binary from shadcn');
|
|
18
|
-
}
|
|
19
|
-
return binaryPath;
|
|
20
|
-
}
|
|
21
|
-
function assertShadcnArgs(args) {
|
|
22
|
-
if (args.length === 0) {
|
|
23
|
-
throw new Error('shadcn arguments are required');
|
|
24
|
-
}
|
|
25
|
-
if (args[0]?.startsWith(POSITIONAL_ARGUMENT_PREFIX)) {
|
|
26
|
-
throw new Error('shadcn command must not start with -');
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function createShadcnEnvironment(sourceEnv = process.env) {
|
|
30
|
-
const environment = {};
|
|
31
|
-
for (const key of PRESERVED_ENV_KEYS) {
|
|
32
|
-
const value = sourceEnv[key];
|
|
33
|
-
if (value !== undefined) {
|
|
34
|
-
environment[key] = value;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return environment;
|
|
38
|
-
}
|
|
39
|
-
function assertOutputLimit(currentSize, chunkSize) {
|
|
40
|
-
const nextSize = currentSize + chunkSize;
|
|
41
|
-
if (nextSize > SHADCN_OUTPUT_LIMIT_BYTES) {
|
|
42
|
-
throw new Error(`shadcn output exceeded ${SHADCN_OUTPUT_LIMIT_BYTES} bytes`);
|
|
43
|
-
}
|
|
44
|
-
return nextSize;
|
|
45
|
-
}
|
|
46
|
-
function terminateShadcnProcess(childProcess) {
|
|
47
|
-
if (childProcess.pid === undefined) {
|
|
48
|
-
childProcess.kill();
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (process.platform === 'win32') {
|
|
52
|
-
const taskkillPath = process.env.SystemRoot ? resolve(process.env.SystemRoot, 'System32', 'taskkill.exe') : 'taskkill.exe';
|
|
53
|
-
spawn(taskkillPath, ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
try {
|
|
57
|
-
process.kill(-childProcess.pid, 'SIGTERM');
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
childProcess.kill('SIGTERM');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
function defaultShadcnProcessRunner(invocation) {
|
|
64
|
-
return new Promise((resolveResult, reject) => {
|
|
65
|
-
const childProcess = spawn(invocation.executable, invocation.args, {
|
|
66
|
-
cwd: invocation.cwd,
|
|
67
|
-
detached: process.platform !== 'win32',
|
|
68
|
-
env: createShadcnEnvironment(),
|
|
69
|
-
shell: false
|
|
70
|
-
});
|
|
71
|
-
const timeout = setTimeout(() => {
|
|
72
|
-
terminateShadcnProcess(childProcess);
|
|
73
|
-
reject(new Error(`shadcn process timed out after ${SHADCN_PROCESS_TIMEOUT_MS}ms`));
|
|
74
|
-
}, SHADCN_PROCESS_TIMEOUT_MS);
|
|
75
|
-
const stdoutChunks = [];
|
|
76
|
-
const stderrChunks = [];
|
|
77
|
-
let stdoutSize = 0;
|
|
78
|
-
let stderrSize = 0;
|
|
79
|
-
childProcess.stdout.on('data', (chunk) => {
|
|
80
|
-
try {
|
|
81
|
-
stdoutSize = assertOutputLimit(stdoutSize, chunk.length);
|
|
82
|
-
stdoutChunks.push(chunk);
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
terminateShadcnProcess(childProcess);
|
|
86
|
-
reject(error);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
childProcess.stderr.on('data', (chunk) => {
|
|
90
|
-
try {
|
|
91
|
-
stderrSize = assertOutputLimit(stderrSize, chunk.length);
|
|
92
|
-
stderrChunks.push(chunk);
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
terminateShadcnProcess(childProcess);
|
|
96
|
-
reject(error);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
childProcess.on('error', (error) => {
|
|
100
|
-
clearTimeout(timeout);
|
|
101
|
-
reject(error);
|
|
102
|
-
});
|
|
103
|
-
childProcess.on('close', (exitCode) => {
|
|
104
|
-
clearTimeout(timeout);
|
|
105
|
-
resolveResult({
|
|
106
|
-
exitCode,
|
|
107
|
-
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
|
|
108
|
-
stderr: Buffer.concat(stderrChunks).toString('utf8')
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
export function createShadcnInvocation(options) {
|
|
114
|
-
assertShadcnArgs(options.args);
|
|
115
|
-
return {
|
|
116
|
-
executable: SHADCN_EXECUTABLE,
|
|
117
|
-
args: [SHADCN_BINARY_PATH, ...options.args],
|
|
118
|
-
cwd: options.cwd ?? process.cwd(),
|
|
119
|
-
packageName: SHADCN_PACKAGE_NAME,
|
|
120
|
-
packageVersion: SHADCN_PACKAGE_VERSION
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
export async function executeShadcnInvocation(invocation, runner = defaultShadcnProcessRunner) {
|
|
124
|
-
return runner(invocation);
|
|
125
|
-
}
|
|
126
|
-
export const testInternals = {
|
|
127
|
-
createShadcnEnvironment
|
|
128
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared `makeStubAdapter` helper for the 5 non-shipped IDEs (Trae, Cursor,
|
|
3
|
-
* Codex, Qoder, Tongyi Lingma).
|
|
4
|
-
*
|
|
5
|
-
* Each stub adapter:
|
|
6
|
-
* 1. Implements `SkillScopeAdapter` with `supported: false`.
|
|
7
|
-
* 2. In `applyScope`, ALWAYS writes the companion source-of-truth
|
|
8
|
-
* `.peaks/scope/<ide>-skills.json` first, then returns a NOT_SUPPORTED
|
|
9
|
-
* ApplyResult (the test contract asserts the source-of-truth is on disk
|
|
10
|
-
* even when the adapter can't apply it natively).
|
|
11
|
-
* 3. In `showScope`, reads from the companion source-of-truth file.
|
|
12
|
-
* 4. In `resetScope`, removes the companion source-of-truth file.
|
|
13
|
-
* 5. In `detect`, returns 0.0 (the stub does not actually probe).
|
|
14
|
-
*
|
|
15
|
-
* The TODO comment in each stub file points at the follow-up slice (025.2+).
|
|
16
|
-
*/
|
|
17
|
-
import type { SkillScopeAdapter } from '../types.js';
|
|
18
|
-
/**
|
|
19
|
-
* IDE-id -> companion source-of-truth shape. The companion file is a
|
|
20
|
-
* parallel record so the user can see "this is what would have applied"
|
|
21
|
-
* even when the IDE doesn't support a real implementation.
|
|
22
|
-
*/
|
|
23
|
-
export interface StubSourceOfTruth {
|
|
24
|
-
readonly ide: string;
|
|
25
|
-
readonly generatedAt: string;
|
|
26
|
-
readonly strict: boolean;
|
|
27
|
-
readonly allowlist: readonly string[];
|
|
28
|
-
readonly denylist: readonly string[];
|
|
29
|
-
readonly todoRef: string;
|
|
30
|
-
readonly notes: string;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* The factory: every stub is a thin wrapper around this function. The
|
|
34
|
-
* `applyScope` implementation ALWAYS writes the source-of-truth, then
|
|
35
|
-
* returns a NOT_SUPPORTED ApplyResult (NOT a thrown error — the contract
|
|
36
|
-
* for stub adapters is "return ok:false, notSupported:true" so the CLI
|
|
37
|
-
* can keep going and surface the error to the user).
|
|
38
|
-
*/
|
|
39
|
-
export declare function makeStubAdapter(ide: SkillScopeAdapter['ide'], todoRef: string, displayName: string): SkillScopeAdapter;
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared `makeStubAdapter` helper for the 5 non-shipped IDEs (Trae, Cursor,
|
|
3
|
-
* Codex, Qoder, Tongyi Lingma).
|
|
4
|
-
*
|
|
5
|
-
* Each stub adapter:
|
|
6
|
-
* 1. Implements `SkillScopeAdapter` with `supported: false`.
|
|
7
|
-
* 2. In `applyScope`, ALWAYS writes the companion source-of-truth
|
|
8
|
-
* `.peaks/scope/<ide>-skills.json` first, then returns a NOT_SUPPORTED
|
|
9
|
-
* ApplyResult (the test contract asserts the source-of-truth is on disk
|
|
10
|
-
* even when the adapter can't apply it natively).
|
|
11
|
-
* 3. In `showScope`, reads from the companion source-of-truth file.
|
|
12
|
-
* 4. In `resetScope`, removes the companion source-of-truth file.
|
|
13
|
-
* 5. In `detect`, returns 0.0 (the stub does not actually probe).
|
|
14
|
-
*
|
|
15
|
-
* The TODO comment in each stub file points at the follow-up slice (025.2+).
|
|
16
|
-
*/
|
|
17
|
-
import { existsSync } from 'node:fs';
|
|
18
|
-
import { readFile } from 'node:fs/promises';
|
|
19
|
-
import { ideCompanionFilePath, removeIfExists, scopeFilePath, writeJsonAtomic } from '../source-of-truth.js';
|
|
20
|
-
async function writeStubCompanion(ide, input, todoRef) {
|
|
21
|
-
const file = ideCompanionFilePath(input.projectRoot, ide);
|
|
22
|
-
const data = {
|
|
23
|
-
ide,
|
|
24
|
-
generatedAt: input.sourceConfig.generatedAt,
|
|
25
|
-
strict: input.strict,
|
|
26
|
-
allowlist: input.allowlist,
|
|
27
|
-
denylist: input.denylist,
|
|
28
|
-
todoRef,
|
|
29
|
-
notes: `Stub source-of-truth for ${ide}. The real config format has not yet been researched. ` +
|
|
30
|
-
`This file is written so the user's intent is captured and can be ported when ` +
|
|
31
|
-
`the follow-up slice (${todoRef}) lands.`,
|
|
32
|
-
};
|
|
33
|
-
await writeJsonAtomic(file, data);
|
|
34
|
-
return file;
|
|
35
|
-
}
|
|
36
|
-
async function readStubCompanion(ide, projectRoot) {
|
|
37
|
-
const file = ideCompanionFilePath(projectRoot, ide);
|
|
38
|
-
if (!existsSync(file))
|
|
39
|
-
return null;
|
|
40
|
-
try {
|
|
41
|
-
return JSON.parse(await readFile(file, 'utf8'));
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* The factory: every stub is a thin wrapper around this function. The
|
|
49
|
-
* `applyScope` implementation ALWAYS writes the source-of-truth, then
|
|
50
|
-
* returns a NOT_SUPPORTED ApplyResult (NOT a thrown error — the contract
|
|
51
|
-
* for stub adapters is "return ok:false, notSupported:true" so the CLI
|
|
52
|
-
* can keep going and surface the error to the user).
|
|
53
|
-
*/
|
|
54
|
-
export function makeStubAdapter(ide, todoRef, displayName) {
|
|
55
|
-
const ideStr = String(ide);
|
|
56
|
-
return {
|
|
57
|
-
ide,
|
|
58
|
-
supported: false,
|
|
59
|
-
async detect() {
|
|
60
|
-
// Stubs never "win" detection; they return 0.0 so the registry falls
|
|
61
|
-
// back to the shipped adapter (Claude Code).
|
|
62
|
-
return 0.0;
|
|
63
|
-
},
|
|
64
|
-
async applyScope(input) {
|
|
65
|
-
// 1. Always write the source-of-truth first.
|
|
66
|
-
const companion = await writeStubCompanion(ideStr, input, todoRef);
|
|
67
|
-
// 2. Always write the canonical .peaks/scope/skills.json too.
|
|
68
|
-
const canonical = scopeFilePath(input.projectRoot);
|
|
69
|
-
await writeJsonAtomic(canonical, input.sourceConfig);
|
|
70
|
-
// 3. Surface NOT_SUPPORTED with a clear, IDE-named message.
|
|
71
|
-
const message = `${displayName} (${ideStr}) config format not yet researched — ${todoRef} follow-up. ` +
|
|
72
|
-
`Source-of-truth written to ${companion}.`;
|
|
73
|
-
return {
|
|
74
|
-
ide,
|
|
75
|
-
ok: false,
|
|
76
|
-
writtenFiles: [companion, canonical],
|
|
77
|
-
usedShadowStub: false,
|
|
78
|
-
notSupported: true,
|
|
79
|
-
error: { code: 'NOT_SUPPORTED', message },
|
|
80
|
-
};
|
|
81
|
-
},
|
|
82
|
-
async showScope(projectRoot) {
|
|
83
|
-
const native = await readStubCompanion(ideStr, projectRoot);
|
|
84
|
-
return { source: null, native, ide };
|
|
85
|
-
},
|
|
86
|
-
async resetScope(input) {
|
|
87
|
-
const removed = [];
|
|
88
|
-
const companion = ideCompanionFilePath(input.projectRoot, ideStr);
|
|
89
|
-
if (await removeIfExists(companion))
|
|
90
|
-
removed.push(companion);
|
|
91
|
-
// Also remove the canonical source-of-truth on reset.
|
|
92
|
-
const canonical = scopeFilePath(input.projectRoot);
|
|
93
|
-
if (await removeIfExists(canonical))
|
|
94
|
-
removed.push(canonical);
|
|
95
|
-
return { ide, removedFiles: removed };
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `peaks skill scope` — Claude Code adapter (full impl, slice 025.1).
|
|
3
|
-
*
|
|
4
|
-
* Strategy (tech-doc-025 §3):
|
|
5
|
-
* 1. PRIMARY: write `.claude/settings.local.json` with
|
|
6
|
-
* `permissions.allow: ["Skill(name)", ...]` + `permissions.deny: [...]`.
|
|
7
|
-
* 2. FALLBACK (R1, `--shadow-fallback`): when the runtime probe determines
|
|
8
|
-
* Claude Code rejects `Skill(name)` in `permissions.deny`, write a
|
|
9
|
-
* shadow stub at `.claude/skills/<name>/SKILL.md` for each denylisted
|
|
10
|
-
* skill. Tagged with `_peaks_scope_disabled: true` (R6).
|
|
11
|
-
*
|
|
12
|
-
* Idempotency: dedupe the allow/deny arrays; shadow-stub writes skip
|
|
13
|
-
* when the marker is already present. AC11.
|
|
14
|
-
*/
|
|
15
|
-
import type { ApplyResult, ApplyScopeInput, ResetScopeInput, ResetScopeResult, ShowScopeResult, SkillScopeAdapter } from '../types.js';
|
|
16
|
-
/** Format the `Skill(name)` string Claude Code's permission system uses. */
|
|
17
|
-
export declare function skillRef(name: string): string;
|
|
18
|
-
interface ClaudePermissions {
|
|
19
|
-
readonly allow: string[];
|
|
20
|
-
readonly deny: string[];
|
|
21
|
-
}
|
|
22
|
-
interface ClaudeSettings {
|
|
23
|
-
readonly permissions: ClaudePermissions;
|
|
24
|
-
readonly [key: string]: unknown;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Map allowlist/denylist → permissions.allow/permissions.deny. Never sorts;
|
|
28
|
-
* preserves input order. Always dedupes.
|
|
29
|
-
*/
|
|
30
|
-
export declare function toPermissions(allowlist: readonly string[], denylist: readonly string[]): ClaudeSettings;
|
|
31
|
-
/**
|
|
32
|
-
* Strip any peaks-* name from the denylist (G6 hard constraint). Returns
|
|
33
|
-
* the cleaned denylist + the list of stripped names for the audit log.
|
|
34
|
-
*/
|
|
35
|
-
export declare function stripPeaksFromDenylist(denylist: readonly string[]): {
|
|
36
|
-
readonly cleaned: readonly string[];
|
|
37
|
-
readonly stripped: readonly string[];
|
|
38
|
-
};
|
|
39
|
-
/**
|
|
40
|
-
* Runtime probe for whether Claude Code supports `Skill(name)` syntax in
|
|
41
|
-
* `permissions.deny` (R1). For slice 025.1 we return `unknown` and let
|
|
42
|
-
* the caller decide. Replace this with a real check when Claude Code's
|
|
43
|
-
* `permissions.deny` schema is documented.
|
|
44
|
-
*/
|
|
45
|
-
export declare function probeSkillDenySupport(): Promise<'support-allow-and-deny' | 'support-allow-only' | 'unknown'>;
|
|
46
|
-
export declare class ClaudeCodeSkillScope implements SkillScopeAdapter {
|
|
47
|
-
readonly ide: "claude-code";
|
|
48
|
-
readonly supported = true;
|
|
49
|
-
constructor(_opts?: {
|
|
50
|
-
readonly projectRoot?: string;
|
|
51
|
-
});
|
|
52
|
-
/** detect(): returns 1.0 when the project root has a .claude/ dir. */
|
|
53
|
-
detect(projectRoot: string): Promise<number>;
|
|
54
|
-
applyScope(input: ApplyScopeInput): Promise<ApplyResult>;
|
|
55
|
-
showScope(projectRoot: string): Promise<ShowScopeResult>;
|
|
56
|
-
resetScope(input: ResetScopeInput): Promise<ResetScopeResult>;
|
|
57
|
-
}
|
|
58
|
-
export declare const CLAUDE_CODE_SKILL_SCOPE: SkillScopeAdapter;
|
|
59
|
-
export {};
|