peaks-cli 1.0.21 → 1.0.22
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/capability-commands.d.ts +1 -1
- package/dist/src/cli/commands/capability-commands.js +2 -5
- package/dist/src/cli/commands/config-commands.js +2 -85
- package/dist/src/cli/commands/core-artifact-commands.js +6 -1
- package/dist/src/cli/commands/request-commands.js +82 -2
- package/dist/src/cli/commands/scan-commands.js +30 -0
- package/dist/src/cli/commands/workflow-commands.js +9 -5
- package/dist/src/services/artifacts/artifact-prerequisites.js +53 -13
- package/dist/src/services/artifacts/artifact-service.js +2 -2
- package/dist/src/services/artifacts/request-artifact-service.d.ts +32 -0
- package/dist/src/services/artifacts/request-artifact-service.js +148 -16
- package/dist/src/services/artifacts/workspace-service.js +8 -9
- package/dist/src/services/config/config-service.js +54 -69
- package/dist/src/services/config/config-types.d.ts +0 -2
- package/dist/src/services/config/config-types.js +0 -2
- package/dist/src/services/mode/bypass-tracker.d.ts +4 -0
- package/dist/src/services/mode/bypass-tracker.js +31 -0
- package/dist/src/services/mode/mode-enforcement.d.ts +14 -0
- package/dist/src/services/mode/mode-enforcement.js +81 -0
- package/dist/src/services/sc/sc-service.js +5 -5
- package/dist/src/services/scan/file-size-scan.d.ts +19 -0
- package/dist/src/services/scan/file-size-scan.js +44 -0
- package/dist/src/services/session/index.d.ts +1 -0
- package/dist/src/services/session/index.js +1 -0
- package/dist/src/services/session/session-manager.d.ts +60 -0
- package/dist/src/services/session/session-manager.js +150 -0
- package/dist/src/services/skills/skill-presence-service.d.ts +4 -1
- package/dist/src/services/skills/skill-presence-service.js +11 -1
- package/dist/src/services/workspace/workspace-service.js +6 -0
- package/dist/src/shared/change-id.d.ts +13 -0
- package/dist/src/shared/change-id.js +32 -1
- package/dist/src/shared/incrementing-number.d.ts +31 -0
- package/dist/src/shared/incrementing-number.js +58 -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-rd/SKILL.md +3 -0
- package/skills/peaks-solo/SKILL.md +9 -11
- package/skills/peaks-ui/SKILL.md +3 -0
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
3
|
-
import { isDirectory, listDirectories
|
|
4
|
+
import { isDirectory, listDirectories } from '../../shared/fs.js';
|
|
4
5
|
import { checkPrerequisites, DEFAULT_REQUEST_TYPE, isRequestType, VALID_REQUEST_TYPES } from './artifact-prerequisites.js';
|
|
6
|
+
import { ensureSession } from '../session/session-manager.js';
|
|
7
|
+
import { getNextNumber, buildNumberedFilename } from '../../shared/incrementing-number.js';
|
|
8
|
+
import { lintRequestArtifact } from './artifact-lint-service.js';
|
|
9
|
+
import { checkTypeSanity } from '../scan/type-sanity-service.js';
|
|
10
|
+
import { requireUserConfirmation } from '../mode/mode-enforcement.js';
|
|
11
|
+
import { scanFileSize } from '../scan/file-size-scan.js';
|
|
5
12
|
export { VALID_REQUEST_TYPES, DEFAULT_REQUEST_TYPE, isRequestType };
|
|
6
13
|
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
7
14
|
const VALID_ROLES = new Set(['prd', 'ui', 'rd', 'qa', 'sc']);
|
|
@@ -299,17 +306,42 @@ export async function createRequestArtifact(options) {
|
|
|
299
306
|
const requestType = options.requestType ?? DEFAULT_REQUEST_TYPE;
|
|
300
307
|
const clock = options.clock ?? defaultClock;
|
|
301
308
|
const timestamp = clock();
|
|
302
|
-
|
|
303
|
-
const
|
|
309
|
+
// Use provided session ID or get/create current session
|
|
310
|
+
const sessionId = options.sessionId ?? await ensureSession(options.projectRoot);
|
|
311
|
+
// Build numbered path in session directory
|
|
312
|
+
const requestsDir = join(options.projectRoot, '.peaks', sessionId, options.role, 'requests');
|
|
313
|
+
// Check if a file with this requestId already exists (regardless of number prefix)
|
|
314
|
+
if (await isDirectory(requestsDir)) {
|
|
315
|
+
const existingFiles = await listMarkdownFiles(requestsDir);
|
|
316
|
+
const alreadyExists = existingFiles.some((file) => {
|
|
317
|
+
if (file === `${options.requestId}.md`)
|
|
318
|
+
return true;
|
|
319
|
+
if (/^\d+-/.test(file) && file.endsWith(`-${options.requestId}.md`))
|
|
320
|
+
return true;
|
|
321
|
+
return false;
|
|
322
|
+
});
|
|
323
|
+
if (alreadyExists) {
|
|
324
|
+
throw new Error(`A request artifact with id "${options.requestId}" already exists in ${requestsDir}. Remove it before re-running peaks request init.`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const number = getNextNumber(requestsDir);
|
|
328
|
+
const filename = buildNumberedFilename(number, options.requestId);
|
|
329
|
+
const path = join(requestsDir, filename);
|
|
304
330
|
const content = renderTemplate(options.role, options.requestId, sessionId, timestamp, requestType);
|
|
305
331
|
if (options.apply !== true) {
|
|
306
332
|
return { role: options.role, requestId: options.requestId, sessionId, path, content, applied: false };
|
|
307
333
|
}
|
|
308
|
-
if (await pathExists(path)) {
|
|
309
|
-
throw new Error(`Refusing to write: ${path} already exists. Update it in place or remove it before re-running peaks request init.`);
|
|
310
|
-
}
|
|
311
334
|
await mkdir(dirname(path), { recursive: true });
|
|
312
335
|
await writeFile(path, content, 'utf8');
|
|
336
|
+
// Create QA initiated marker so rd:qa-handoff gate can verify QA was invoked
|
|
337
|
+
if (options.role === 'qa') {
|
|
338
|
+
const qaDir = join(options.projectRoot, '.peaks', sessionId, 'qa');
|
|
339
|
+
const initiatedPath = join(qaDir, '.initiated');
|
|
340
|
+
if (!existsSync(initiatedPath)) {
|
|
341
|
+
await mkdir(qaDir, { recursive: true });
|
|
342
|
+
await writeFile(initiatedPath, '', 'utf8');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
313
345
|
return { role: options.role, requestId: options.requestId, sessionId, path, content, applied: true };
|
|
314
346
|
}
|
|
315
347
|
function extractMetadata(markdown) {
|
|
@@ -346,7 +378,9 @@ async function readSummary(projectRoot, sessionId, role, fileName) {
|
|
|
346
378
|
const path = join(projectRoot, '.peaks', sessionId, role, 'requests', fileName);
|
|
347
379
|
const body = await readFile(path, 'utf8');
|
|
348
380
|
const { state, createdAt, requestType } = extractMetadata(body);
|
|
349
|
-
|
|
381
|
+
// Strip numbered prefix (e.g., "001-requestId.md" -> "requestId")
|
|
382
|
+
// Only strip 3-digit zero-padded prefixes (our incrementing number format)
|
|
383
|
+
const requestId = fileName.replace(/^0\d{2}-/, '').replace(/\.md$/, '');
|
|
350
384
|
const summary = { role, sessionId, requestId, path, state, requestType };
|
|
351
385
|
if (createdAt !== undefined) {
|
|
352
386
|
summary.createdAt = createdAt;
|
|
@@ -389,14 +423,29 @@ export async function showRequestArtifact(options) {
|
|
|
389
423
|
if (!REQUEST_ID_PATTERN.test(options.requestId)) {
|
|
390
424
|
throw new Error(`Invalid request id: ${options.requestId} (expected letters, digits, dots, underscores, or dashes)`);
|
|
391
425
|
}
|
|
392
|
-
|
|
426
|
+
// Search for files matching the requestId (supports both legacy and numbered formats)
|
|
427
|
+
const findFileInDir = async (dir) => {
|
|
428
|
+
const files = await listMarkdownFiles(dir);
|
|
429
|
+
for (const file of files) {
|
|
430
|
+
// Match legacy format: ${requestId}.md
|
|
431
|
+
if (file === `${options.requestId}.md`) {
|
|
432
|
+
return { fileName: file, path: join(dir, file) };
|
|
433
|
+
}
|
|
434
|
+
// Match numbered format: ${number}-${requestId}.md
|
|
435
|
+
if (/^\d+-/.test(file) && file.endsWith(`-${options.requestId}.md`)) {
|
|
436
|
+
return { fileName: file, path: join(dir, file) };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
};
|
|
393
441
|
if (options.sessionId !== undefined) {
|
|
394
|
-
const
|
|
395
|
-
|
|
442
|
+
const dir = join(options.projectRoot, '.peaks', options.sessionId, options.role, 'requests');
|
|
443
|
+
const found = await findFileInDir(dir);
|
|
444
|
+
if (found === null) {
|
|
396
445
|
return null;
|
|
397
446
|
}
|
|
398
|
-
const summary = await readSummary(options.projectRoot, options.sessionId, options.role, fileName);
|
|
399
|
-
const content = await readFile(path, 'utf8');
|
|
447
|
+
const summary = await readSummary(options.projectRoot, options.sessionId, options.role, found.fileName);
|
|
448
|
+
const content = await readFile(found.path, 'utf8');
|
|
400
449
|
return { ...summary, content };
|
|
401
450
|
}
|
|
402
451
|
const peaksRoot = join(options.projectRoot, '.peaks');
|
|
@@ -405,10 +454,11 @@ export async function showRequestArtifact(options) {
|
|
|
405
454
|
}
|
|
406
455
|
const sessions = await listDirectories(peaksRoot);
|
|
407
456
|
for (const sessionId of sessions) {
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
457
|
+
const dir = join(peaksRoot, sessionId, options.role, 'requests');
|
|
458
|
+
const found = await findFileInDir(dir);
|
|
459
|
+
if (found !== null) {
|
|
460
|
+
const summary = await readSummary(options.projectRoot, sessionId, options.role, found.fileName);
|
|
461
|
+
const content = await readFile(found.path, 'utf8');
|
|
412
462
|
return { ...summary, content };
|
|
413
463
|
}
|
|
414
464
|
}
|
|
@@ -439,6 +489,48 @@ export class PrerequisitesNotSatisfiedError extends Error {
|
|
|
439
489
|
this.missing = missing;
|
|
440
490
|
}
|
|
441
491
|
}
|
|
492
|
+
export class LintGateError extends Error {
|
|
493
|
+
code = 'LINT_GATE_FAILED';
|
|
494
|
+
role;
|
|
495
|
+
newState;
|
|
496
|
+
errorCount;
|
|
497
|
+
constructor(role, newState, errorCount) {
|
|
498
|
+
super(`Cannot transition ${role} to ${newState}: ${errorCount} lint error(s) found in artifact. ` +
|
|
499
|
+
'Fix lint errors or use --allow-incomplete to bypass.');
|
|
500
|
+
this.name = 'LintGateError';
|
|
501
|
+
this.role = role;
|
|
502
|
+
this.newState = newState;
|
|
503
|
+
this.errorCount = errorCount;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
export class TypeSanityViolationError extends Error {
|
|
507
|
+
code = 'TYPE_SANITY_VIOLATION';
|
|
508
|
+
declaredType;
|
|
509
|
+
suggestedTypes;
|
|
510
|
+
rationale;
|
|
511
|
+
constructor(declaredType, suggestedTypes, rationale) {
|
|
512
|
+
super(`Type sanity violation: declared --type=${declaredType} disagrees with changed files. ` +
|
|
513
|
+
`Suggested types: ${suggestedTypes.join(' | ')}. ` +
|
|
514
|
+
`Rationale: ${rationale}`);
|
|
515
|
+
this.name = 'TypeSanityViolationError';
|
|
516
|
+
this.declaredType = declaredType;
|
|
517
|
+
this.suggestedTypes = suggestedTypes;
|
|
518
|
+
this.rationale = rationale;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
export class FileSizeViolationError extends Error {
|
|
522
|
+
code = 'FILE_SIZE_VIOLATION';
|
|
523
|
+
violations;
|
|
524
|
+
threshold;
|
|
525
|
+
constructor(violations, threshold) {
|
|
526
|
+
const summary = violations.map((v) => `${v.file} (${v.lines} lines)`).join(', ');
|
|
527
|
+
super(`File size violation: ${violations.length} file(s) exceed ${threshold} lines: ${summary}. ` +
|
|
528
|
+
'Split into smaller modules or use --allow-incomplete to bypass.');
|
|
529
|
+
this.name = 'FileSizeViolationError';
|
|
530
|
+
this.violations = violations;
|
|
531
|
+
this.threshold = threshold;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
442
534
|
function updateStatusBlock(markdown, newState, timestamp, reason) {
|
|
443
535
|
const lines = markdown.split(/\r?\n/);
|
|
444
536
|
let previousState = 'unknown';
|
|
@@ -499,6 +591,14 @@ export async function transitionRequestArtifact(options) {
|
|
|
499
591
|
if (existing === null) {
|
|
500
592
|
return null;
|
|
501
593
|
}
|
|
594
|
+
// Mode enforcement: require user confirmation in assisted/strict modes
|
|
595
|
+
const transitionKey = `${options.role}:${options.newState}`;
|
|
596
|
+
await requireUserConfirmation({
|
|
597
|
+
projectRoot: options.projectRoot,
|
|
598
|
+
transitionKey,
|
|
599
|
+
confirmed: options.confirmed,
|
|
600
|
+
forceConfirm: options.forceConfirm
|
|
601
|
+
});
|
|
502
602
|
const prerequisiteResult = await checkPrerequisites({
|
|
503
603
|
projectRoot: options.projectRoot,
|
|
504
604
|
sessionId: existing.sessionId,
|
|
@@ -510,6 +610,38 @@ export async function transitionRequestArtifact(options) {
|
|
|
510
610
|
if (!prerequisiteResult.ok && options.allowIncomplete !== true) {
|
|
511
611
|
throw new PrerequisitesNotSatisfiedError(options.role, options.newState, existing.sessionId, prerequisiteResult.missing);
|
|
512
612
|
}
|
|
613
|
+
// Type sanity check for PRD handoff
|
|
614
|
+
if (options.typeSanityCheck !== undefined && options.role === 'prd' && options.newState === 'handed-off') {
|
|
615
|
+
const sanityReport = checkTypeSanity({
|
|
616
|
+
projectRoot: options.typeSanityCheck.projectRoot,
|
|
617
|
+
declaredType: options.typeSanityCheck.declaredType
|
|
618
|
+
});
|
|
619
|
+
if (!sanityReport.consistent) {
|
|
620
|
+
throw new TypeSanityViolationError(options.typeSanityCheck.declaredType, sanityReport.suggestedTypes, sanityReport.rationale);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
// Lint gate: when transitioning OUT of draft, lint must pass (unless --allow-incomplete)
|
|
624
|
+
if (existing.state === 'draft' && options.allowIncomplete !== true) {
|
|
625
|
+
const lintReport = await lintRequestArtifact({
|
|
626
|
+
projectRoot: options.projectRoot,
|
|
627
|
+
role: options.role,
|
|
628
|
+
requestId: options.requestId,
|
|
629
|
+
sessionId: existing.sessionId
|
|
630
|
+
});
|
|
631
|
+
if (lintReport !== null && !lintReport.ok) {
|
|
632
|
+
const errorCount = lintReport.findings.filter((f) => f.severity === 'error').length;
|
|
633
|
+
if (errorCount > 0) {
|
|
634
|
+
throw new LintGateError(options.role, options.newState, errorCount);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// File size gate: when RD declares implemented, scan for oversized files (karpathy-skills "Simplicity First")
|
|
639
|
+
if (options.role === 'rd' && options.newState === 'implemented' && options.allowIncomplete !== true) {
|
|
640
|
+
const sizeResult = scanFileSize({ projectRoot: options.projectRoot });
|
|
641
|
+
if (!sizeResult.ok) {
|
|
642
|
+
throw new FileSizeViolationError(sizeResult.violations, sizeResult.threshold);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
513
645
|
const clock = options.clock ?? defaultClock;
|
|
514
646
|
const timestamp = clock();
|
|
515
647
|
const bypassNote = !prerequisiteResult.ok && options.allowIncomplete === true
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import { resolve } from 'node:path';
|
|
5
4
|
import { isInsidePath, stablePath } from '../../shared/path-utils.js';
|
|
6
|
-
import {
|
|
5
|
+
import { getWorkspaceConfig, getWorkspaceConfigForCurrentPath } from '../config/config-service.js';
|
|
7
6
|
import { pathExists } from '../../shared/fs.js';
|
|
8
7
|
import { execCommand } from '../../shared/process.js';
|
|
9
8
|
function canonicalPath(path) {
|
|
@@ -16,7 +15,7 @@ export function getLocalArtifactPath(workspace) {
|
|
|
16
15
|
if (workspace.artifactStorage?.localPath) {
|
|
17
16
|
return resolve(workspace.artifactStorage.localPath);
|
|
18
17
|
}
|
|
19
|
-
return resolve(
|
|
18
|
+
return resolve(workspace.rootPath, '.peaks', 'artifacts');
|
|
20
19
|
}
|
|
21
20
|
export function isArtifactWorkspaceOutsideTarget(workspace, artifactWorkspacePath = getLocalArtifactPath(workspace)) {
|
|
22
21
|
const targetRoot = canonicalPath(workspace.rootPath);
|
|
@@ -88,8 +87,8 @@ function redactSecrets(message) {
|
|
|
88
87
|
}
|
|
89
88
|
export async function executeArtifactSync(workspaceId) {
|
|
90
89
|
const workspace = workspaceId
|
|
91
|
-
?
|
|
92
|
-
:
|
|
90
|
+
? getWorkspaceConfig(workspaceId)
|
|
91
|
+
: getWorkspaceConfigForCurrentPath();
|
|
93
92
|
if (!workspace) {
|
|
94
93
|
return {
|
|
95
94
|
workspaceId: workspaceId ?? 'unknown',
|
|
@@ -190,8 +189,8 @@ export async function executeArtifactSync(workspaceId) {
|
|
|
190
189
|
}
|
|
191
190
|
export function getArtifactWorkspaceStatus(workspaceId) {
|
|
192
191
|
const workspace = workspaceId
|
|
193
|
-
?
|
|
194
|
-
:
|
|
192
|
+
? getWorkspaceConfig(workspaceId)
|
|
193
|
+
: getWorkspaceConfigForCurrentPath();
|
|
195
194
|
if (!workspace) {
|
|
196
195
|
return {
|
|
197
196
|
workspaceId: workspaceId ?? 'unknown',
|
|
@@ -230,8 +229,8 @@ export function getArtifactWorkspaceStatus(workspaceId) {
|
|
|
230
229
|
}
|
|
231
230
|
export function planArtifactSync(workspaceId, dryRun = true) {
|
|
232
231
|
const workspace = workspaceId
|
|
233
|
-
?
|
|
234
|
-
:
|
|
232
|
+
? getWorkspaceConfig(workspaceId)
|
|
233
|
+
: getWorkspaceConfigForCurrentPath();
|
|
235
234
|
if (!workspace) {
|
|
236
235
|
return {
|
|
237
236
|
workspaceId: workspaceId ?? 'unknown',
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import {
|
|
3
|
-
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
4
3
|
import { DEFAULT_CONFIG } from './config-types.js';
|
|
5
4
|
import { stablePath } from '../../shared/path-utils.js';
|
|
6
5
|
import { findProjectRoot, getProjectBootstrapConfigPath, getProjectConfigPath, getUserConfigPath, isInsidePath, readConfigFileSafely, resolveProjectRootForConfig, validateArtifactWorkspaceMarkerPath, validateArtifactWorkspaceRoot, validateProjectBootstrapConfigPathForWrite, validateUserConfigPathForWrite, writeConfigFileSafely, writeProjectConfigFile, writeUserConfigFile } from './config-safety.js';
|
|
@@ -199,10 +198,6 @@ function isRecord(value) {
|
|
|
199
198
|
function isSafeConfigSegment(value) {
|
|
200
199
|
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) && !value.includes('..') && !value.endsWith('.');
|
|
201
200
|
}
|
|
202
|
-
function toSafeConfigSegment(value) {
|
|
203
|
-
const normalized = value.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').replace(/\.+$/g, '');
|
|
204
|
-
return isSafeConfigSegment(normalized) ? normalized : 'workspace';
|
|
205
|
-
}
|
|
206
201
|
function toArtifactRemoteRepoConfig(value) {
|
|
207
202
|
if (!isRecord(value) || (value.provider !== 'github' && value.provider !== 'gitlab') || typeof value.owner !== 'string' || typeof value.name !== 'string') {
|
|
208
203
|
return null;
|
|
@@ -246,15 +241,6 @@ function toWorkspaceConfig(value) {
|
|
|
246
241
|
function toWorkspaceConfigs(value) {
|
|
247
242
|
return Array.isArray(value) ? value.map(toWorkspaceConfig).filter((workspace) => workspace !== null) : [];
|
|
248
243
|
}
|
|
249
|
-
function mergeWorkspaceConfigs(userWorkspaces, projectWorkspaces) {
|
|
250
|
-
const merged = new Map(userWorkspaces.map((workspace) => [workspace.workspaceId, workspace]));
|
|
251
|
-
for (const workspace of projectWorkspaces) {
|
|
252
|
-
if (!merged.has(workspace.workspaceId)) {
|
|
253
|
-
merged.set(workspace.workspaceId, workspace);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return [...merged.values()];
|
|
257
|
-
}
|
|
258
244
|
function toProviderModelConfig(value) {
|
|
259
245
|
if (!isRecord(value))
|
|
260
246
|
return {};
|
|
@@ -400,8 +386,6 @@ function toPeaksConfig(value) {
|
|
|
400
386
|
const proxy = toProxyConfig(value.proxy);
|
|
401
387
|
return {
|
|
402
388
|
...(typeof value.version === 'string' ? { version: value.version } : {}),
|
|
403
|
-
...(typeof value.currentWorkspace === 'string' ? { currentWorkspace: value.currentWorkspace } : {}),
|
|
404
|
-
...(Array.isArray(value.workspaces) ? { workspaces: toWorkspaceConfigs(value.workspaces) } : {}),
|
|
405
389
|
...(typeof value.language === 'string' ? { language: value.language } : {}),
|
|
406
390
|
...(typeof value.model === 'string' && ['haiku', 'sonnet', 'opus', 'minimax'].includes(value.model) ? { model: value.model } : {}),
|
|
407
391
|
...(typeof value.economyMode === 'boolean' ? { economyMode: value.economyMode } : {}),
|
|
@@ -424,15 +408,23 @@ export function readConfig(projectRoot) {
|
|
|
424
408
|
const detectedRoot = projectRoot ?? findProjectRoot(process.cwd());
|
|
425
409
|
const userConfig = toPeaksConfig(readUserJsonFile());
|
|
426
410
|
const projectConfig = removeProjectSensitiveConfig(toPeaksConfig(readProjectJsonFile(detectedRoot)));
|
|
427
|
-
const { proxy: projectProxy,
|
|
428
|
-
const userWorkspaces = userConfig.workspaces ?? [];
|
|
411
|
+
const { proxy: projectProxy, ...projectConfigWithoutProxy } = projectConfig;
|
|
429
412
|
return {
|
|
430
413
|
...DEFAULT_CONFIG,
|
|
431
414
|
...userConfig,
|
|
432
|
-
...projectConfigWithoutProxy
|
|
433
|
-
workspaces: mergeWorkspaceConfigs(userWorkspaces, projectWorkspaces ?? [])
|
|
415
|
+
...projectConfigWithoutProxy
|
|
434
416
|
};
|
|
435
417
|
}
|
|
418
|
+
function sanitizeWorkspacePartial(partial) {
|
|
419
|
+
const result = { ...partial };
|
|
420
|
+
if (Array.isArray(result.workspaces)) {
|
|
421
|
+
result.workspaces = toWorkspaceConfigs(result.workspaces);
|
|
422
|
+
}
|
|
423
|
+
if (typeof result.currentWorkspace !== 'string' && result.currentWorkspace !== null && result.currentWorkspace !== undefined) {
|
|
424
|
+
delete result.currentWorkspace;
|
|
425
|
+
}
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
436
428
|
export function writeConfig(partial, layer = 'user') {
|
|
437
429
|
if (!isConfigLayer(layer)) {
|
|
438
430
|
throw new Error('Invalid config layer');
|
|
@@ -446,29 +438,28 @@ export function writeConfig(partial, layer = 'user') {
|
|
|
446
438
|
const { projectRoot, configPath } = getProjectWriteTarget();
|
|
447
439
|
ensureDir(dirname(configPath));
|
|
448
440
|
const existing = readJsonFile(configPath, () => validateProjectBootstrapConfigPathForWrite(projectRoot, configPath)) ?? {};
|
|
449
|
-
const merged = { ...existing, ...partial };
|
|
441
|
+
const merged = sanitizeWorkspacePartial({ ...existing, ...partial });
|
|
450
442
|
writeProjectConfigFile(projectRoot, configPath, JSON.stringify(merged, null, 2));
|
|
451
443
|
return;
|
|
452
444
|
}
|
|
453
445
|
const userPath = getUserConfigPath();
|
|
454
446
|
ensureDir(dirname(userPath));
|
|
455
447
|
const existing = readJsonFile(userPath, () => validateUserConfigPathForWrite(userPath)) ?? {};
|
|
456
|
-
const merged = { ...existing, ...partial };
|
|
448
|
+
const merged = sanitizeWorkspacePartial({ ...existing, ...partial });
|
|
457
449
|
writeUserConfigFile(userPath, JSON.stringify(merged, null, 2));
|
|
458
450
|
}
|
|
459
451
|
export function getConfig(options = {}) {
|
|
460
452
|
const projectRoot = findProjectRoot(process.cwd());
|
|
461
453
|
const userConfig = readUserJsonFile() ?? {};
|
|
462
454
|
const projectConfig = removeProjectSensitiveConfig(readProjectJsonFile(projectRoot) ?? {});
|
|
463
|
-
const { proxy: projectProxy,
|
|
455
|
+
const { proxy: projectProxy, ...projectConfigWithoutProxy } = projectConfig;
|
|
464
456
|
const source = options.layer === 'user'
|
|
465
457
|
? userConfig
|
|
466
458
|
: options.layer === 'project'
|
|
467
459
|
? projectConfig
|
|
468
460
|
: {
|
|
469
461
|
...userConfig,
|
|
470
|
-
...projectConfigWithoutProxy
|
|
471
|
-
workspaces: mergeWorkspaceConfigs(toWorkspaceConfigs(userConfig.workspaces), toWorkspaceConfigs(projectWorkspaces))
|
|
462
|
+
...projectConfigWithoutProxy
|
|
472
463
|
};
|
|
473
464
|
const config = isRecord(source) ? { ...source, ...(source.tokens !== undefined ? { tokens: toTokenConfig(source.tokens) } : {}) } : source;
|
|
474
465
|
if (options.key !== undefined) {
|
|
@@ -514,11 +505,7 @@ export function setConfig(options) {
|
|
|
514
505
|
writeUserConfigFile(targetPath, content);
|
|
515
506
|
}
|
|
516
507
|
}
|
|
517
|
-
|
|
518
|
-
const config = readConfig(projectRoot ?? findProjectRoot(process.cwd()));
|
|
519
|
-
return config.workspaces.find((w) => w.workspaceId === workspaceId) ?? null;
|
|
520
|
-
}
|
|
521
|
-
function readLayerConfig(layer) {
|
|
508
|
+
function readRawWorkspaceData(layer) {
|
|
522
509
|
const config = getConfig({ layer });
|
|
523
510
|
return isRecord(config)
|
|
524
511
|
? {
|
|
@@ -527,61 +514,74 @@ function readLayerConfig(layer) {
|
|
|
527
514
|
}
|
|
528
515
|
: { currentWorkspace: null, workspaces: [] };
|
|
529
516
|
}
|
|
517
|
+
function writeRawWorkspaceData(data, layer) {
|
|
518
|
+
const projectTarget = layer === 'project' ? getProjectWriteTarget() : null;
|
|
519
|
+
const targetPath = projectTarget?.configPath ?? getUserConfigPath();
|
|
520
|
+
ensureDir(dirname(targetPath));
|
|
521
|
+
const existing = projectTarget
|
|
522
|
+
? readJsonFile(targetPath, () => validateProjectBootstrapConfigPathForWrite(projectTarget.projectRoot, targetPath)) ?? {}
|
|
523
|
+
: readJsonFile(targetPath, () => validateUserConfigPathForWrite(targetPath)) ?? {};
|
|
524
|
+
const merged = { ...existing, ...data };
|
|
525
|
+
const content = JSON.stringify(merged, null, 2);
|
|
526
|
+
if (projectTarget) {
|
|
527
|
+
writeProjectConfigFile(projectTarget.projectRoot, targetPath, content);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
writeUserConfigFile(targetPath, content);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
export function getWorkspaceConfig(workspaceId, projectRoot) {
|
|
534
|
+
const { workspaces } = readRawWorkspaceData('user');
|
|
535
|
+
return workspaces.find((w) => w.workspaceId === workspaceId) ?? null;
|
|
536
|
+
}
|
|
537
|
+
function readLayerConfig(layer) {
|
|
538
|
+
return readRawWorkspaceData(layer);
|
|
539
|
+
}
|
|
530
540
|
export function addWorkspace(workspace, layer = 'user') {
|
|
531
541
|
if (!isSafeConfigSegment(workspace.workspaceId)) {
|
|
532
542
|
throw new Error('Workspace id must only contain letters, numbers, dots, underscores, or hyphens and must not contain path traversal');
|
|
533
543
|
}
|
|
534
|
-
const config =
|
|
544
|
+
const config = readRawWorkspaceData(layer);
|
|
535
545
|
const workspaces = config.workspaces;
|
|
536
546
|
const existing = workspaces.findIndex((w) => w.workspaceId === workspace.workspaceId);
|
|
537
547
|
const updatedWorkspaces = existing >= 0
|
|
538
548
|
? workspaces.map((existingWorkspace) => existingWorkspace.workspaceId === workspace.workspaceId ? workspace : existingWorkspace)
|
|
539
549
|
: [...workspaces, workspace];
|
|
540
|
-
|
|
550
|
+
writeRawWorkspaceData({ workspaces: updatedWorkspaces }, layer);
|
|
541
551
|
}
|
|
542
552
|
export function removeWorkspace(workspaceId, layer = 'user') {
|
|
543
553
|
if (!isSafeConfigSegment(workspaceId))
|
|
544
554
|
return false;
|
|
545
|
-
const config =
|
|
555
|
+
const config = readRawWorkspaceData(layer);
|
|
546
556
|
const workspaces = config.workspaces;
|
|
547
557
|
const idx = workspaces.findIndex((w) => w.workspaceId === workspaceId);
|
|
548
558
|
if (idx < 0)
|
|
549
559
|
return false;
|
|
550
560
|
const updatedWorkspaces = workspaces.filter((w) => w.workspaceId !== workspaceId);
|
|
551
561
|
const currentWorkspace = config.currentWorkspace === workspaceId ? updatedWorkspaces[0]?.workspaceId ?? null : config.currentWorkspace ?? null;
|
|
552
|
-
|
|
562
|
+
writeRawWorkspaceData({ workspaces: updatedWorkspaces, currentWorkspace }, layer);
|
|
553
563
|
return true;
|
|
554
564
|
}
|
|
555
565
|
export function setCurrentWorkspace(workspaceId, layer = 'user') {
|
|
556
566
|
if (!isSafeConfigSegment(workspaceId))
|
|
557
567
|
return false;
|
|
558
|
-
const config =
|
|
568
|
+
const config = readRawWorkspaceData(layer);
|
|
559
569
|
const workspaces = config.workspaces;
|
|
560
570
|
const exists = workspaces.some((w) => w.workspaceId === workspaceId);
|
|
561
571
|
if (!exists)
|
|
562
572
|
return false;
|
|
563
|
-
|
|
573
|
+
writeRawWorkspaceData({ currentWorkspace: workspaceId }, layer);
|
|
564
574
|
return true;
|
|
565
575
|
}
|
|
566
576
|
export function getCurrentWorkspaceConfig() {
|
|
567
|
-
const
|
|
568
|
-
if (!
|
|
577
|
+
const { currentWorkspace, workspaces } = readRawWorkspaceData('user');
|
|
578
|
+
if (!currentWorkspace)
|
|
569
579
|
return null;
|
|
570
|
-
return
|
|
580
|
+
return workspaces.find((w) => w.workspaceId === currentWorkspace) ?? null;
|
|
571
581
|
}
|
|
572
582
|
export function getWorkspaceConfigForPath(path = process.cwd()) {
|
|
573
|
-
const
|
|
574
|
-
return findWorkspaceForPath(
|
|
575
|
-
}
|
|
576
|
-
function createWorkspaceId(projectRoot, existingIds) {
|
|
577
|
-
const base = toSafeConfigSegment(basename(projectRoot));
|
|
578
|
-
if (!existingIds.has(base))
|
|
579
|
-
return base;
|
|
580
|
-
let suffix = 2;
|
|
581
|
-
while (existingIds.has(`${base}-${suffix}`)) {
|
|
582
|
-
suffix += 1;
|
|
583
|
-
}
|
|
584
|
-
return `${base}-${suffix}`;
|
|
583
|
+
const { workspaces } = readRawWorkspaceData('user');
|
|
584
|
+
return findWorkspaceForPath(workspaces, path);
|
|
585
585
|
}
|
|
586
586
|
function findWorkspaceForPath(workspaces, path) {
|
|
587
587
|
const targetPath = stablePath(path);
|
|
@@ -596,7 +596,7 @@ function findWorkspaceForPath(workspaces, path) {
|
|
|
596
596
|
return matches.reduce((best, match) => match.rootPath.length > best.rootPath.length ? match : best).workspace;
|
|
597
597
|
}
|
|
598
598
|
function getWorkspaceArtifactRoot(workspace) {
|
|
599
|
-
return workspace.artifactStorage?.localPath ? resolve(workspace.artifactStorage.localPath) : resolve(
|
|
599
|
+
return workspace.artifactStorage?.localPath ? resolve(workspace.artifactStorage.localPath) : resolve(workspace.rootPath, '.peaks', 'artifacts');
|
|
600
600
|
}
|
|
601
601
|
function ensureArtifactWorkspaceMarker(workspace) {
|
|
602
602
|
const artifactRoot = getWorkspaceArtifactRoot(workspace);
|
|
@@ -618,24 +618,9 @@ export function ensureWorkspaceConfigForPath(path = process.cwd()) {
|
|
|
618
618
|
const existingWorkspace = findWorkspaceForPath(config.workspaces, path);
|
|
619
619
|
if (existingWorkspace) {
|
|
620
620
|
ensureArtifactWorkspaceMarker(existingWorkspace);
|
|
621
|
-
if (!config.currentWorkspace) {
|
|
622
|
-
writeConfig({ currentWorkspace: existingWorkspace.workspaceId }, 'user');
|
|
623
|
-
}
|
|
624
621
|
return existingWorkspace;
|
|
625
622
|
}
|
|
626
|
-
|
|
627
|
-
const workspaceId = createWorkspaceId(projectRoot, existingIds);
|
|
628
|
-
const workspace = {
|
|
629
|
-
workspaceId,
|
|
630
|
-
name: basename(projectRoot) || 'Workspace',
|
|
631
|
-
rootPath: stablePath(projectRoot),
|
|
632
|
-
artifactStorage: { mode: 'local', localPath: resolve(homedir(), '.peaks', 'workspaces', workspaceId, 'artifacts') },
|
|
633
|
-
installedCapabilityIds: []
|
|
634
|
-
};
|
|
635
|
-
ensureArtifactWorkspaceMarker(workspace);
|
|
636
|
-
const updatedWorkspaces = [...config.workspaces, workspace];
|
|
637
|
-
writeConfig({ workspaces: updatedWorkspaces, ...(!config.currentWorkspace ? { currentWorkspace: workspace.workspaceId } : {}) }, 'user');
|
|
638
|
-
return workspace;
|
|
623
|
+
return null;
|
|
639
624
|
}
|
|
640
625
|
export function getWorkspaceConfigForCurrentPath() {
|
|
641
626
|
return getWorkspaceConfigForPath(process.cwd());
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const MAX_BYPASSES_PER_SESSION = 3;
|
|
4
|
+
const BYPASS_FILE = '.bypass-count.json';
|
|
5
|
+
function bypassFilePath(sessionRoot) {
|
|
6
|
+
return join(sessionRoot, BYPASS_FILE);
|
|
7
|
+
}
|
|
8
|
+
export function getBypassCount(sessionRoot) {
|
|
9
|
+
const filePath = bypassFilePath(sessionRoot);
|
|
10
|
+
if (!existsSync(filePath)) {
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
return typeof parsed.count === 'number' ? parsed.count : 0;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function recordBypass(sessionRoot) {
|
|
23
|
+
const current = getBypassCount(sessionRoot);
|
|
24
|
+
const next = current + 1;
|
|
25
|
+
const filePath = bypassFilePath(sessionRoot);
|
|
26
|
+
writeFileSync(filePath, JSON.stringify({ count: next }, null, 2), 'utf8');
|
|
27
|
+
return next;
|
|
28
|
+
}
|
|
29
|
+
export function isBypassLimitReached(sessionRoot) {
|
|
30
|
+
return getBypassCount(sessionRoot) >= MAX_BYPASSES_PER_SESSION;
|
|
31
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type SkillPresenceMode } from '../skills/skill-presence-service.js';
|
|
2
|
+
type TransitionKey = `${string}:${string}`;
|
|
3
|
+
export declare function requiresConfirmation(mode: SkillPresenceMode, transitionKey: TransitionKey): boolean;
|
|
4
|
+
export type ConfirmationOptions = {
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
transitionKey: TransitionKey;
|
|
7
|
+
confirmed?: boolean | undefined;
|
|
8
|
+
forceConfirm?: boolean | undefined;
|
|
9
|
+
};
|
|
10
|
+
export declare class ConfirmationRequiredError extends Error {
|
|
11
|
+
constructor(transitionKey: TransitionKey);
|
|
12
|
+
}
|
|
13
|
+
export declare function requireUserConfirmation(options: ConfirmationOptions): Promise<void>;
|
|
14
|
+
export {};
|