peaks-cli 1.0.6 → 1.0.8
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/workflow-commands.js +18 -7
- package/dist/src/services/config/config-service.d.ts +4 -0
- package/dist/src/services/config/config-service.js +244 -24
- package/dist/src/shared/path-utils.d.ts +1 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/scripts/install-skills.mjs +102 -47
|
@@ -4,18 +4,29 @@ import { createWorkflowRouterPlan, isSoloMode, isWorkflowMode } from '../../serv
|
|
|
4
4
|
import { createAutonomousWorkflowPlan } from '../../services/workflow/workflow-autonomous-service.js';
|
|
5
5
|
import { createRecommendationPlan } from '../../services/recommendations/recommendation-service.js';
|
|
6
6
|
import { createRefactorDryRun } from '../../services/refactor/refactor-service.js';
|
|
7
|
-
import { getCurrentWorkspaceConfig, readConfig } from '../../services/config/config-service.js';
|
|
7
|
+
import { ensureWorkspaceConfigForCurrentPath, getCurrentWorkspaceConfig, readConfig } from '../../services/config/config-service.js';
|
|
8
8
|
import { validateChangeIdOrThrow } from '../../shared/change-id.js';
|
|
9
9
|
import { getEconomyAwareExecutionModelId } from '../../services/config/model-routing.js';
|
|
10
10
|
import { getLocalArtifactPath } from '../../services/artifacts/workspace-service.js';
|
|
11
11
|
import { fail, ok } from '../../shared/result.js';
|
|
12
12
|
import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isRecommendationWorkflow, printResult } from '../cli-helpers.js';
|
|
13
|
-
function
|
|
13
|
+
function getCurrentWorkspaceContext() {
|
|
14
14
|
const workspace = getCurrentWorkspaceConfig();
|
|
15
15
|
if (!workspace)
|
|
16
16
|
return {};
|
|
17
17
|
return { workspace, artifactWorkspacePath: getLocalArtifactPath(workspace) };
|
|
18
18
|
}
|
|
19
|
+
function getWorkflowWorkspaceContext() {
|
|
20
|
+
try {
|
|
21
|
+
const workspace = ensureWorkspaceConfigForCurrentPath() ?? getCurrentWorkspaceConfig();
|
|
22
|
+
if (!workspace)
|
|
23
|
+
return {};
|
|
24
|
+
return { workspace, artifactWorkspacePath: getLocalArtifactPath(workspace) };
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
19
30
|
function parseMaxWorkers(io, command, value, asJson) {
|
|
20
31
|
const maxWorkers = Number(value);
|
|
21
32
|
if (!Number.isInteger(maxWorkers) || maxWorkers < 1) {
|
|
@@ -54,7 +65,7 @@ function runTechPlan(io, options) {
|
|
|
54
65
|
}
|
|
55
66
|
try {
|
|
56
67
|
validatePlanningInput(options.changeId, options.goal);
|
|
57
|
-
const workspaceContext =
|
|
68
|
+
const workspaceContext = getCurrentWorkspaceContext();
|
|
58
69
|
const plan = createTechPlan({
|
|
59
70
|
changeId: options.changeId,
|
|
60
71
|
goal: options.goal,
|
|
@@ -71,7 +82,7 @@ function runTechPlan(io, options) {
|
|
|
71
82
|
}
|
|
72
83
|
function runTechStatus(io, options) {
|
|
73
84
|
try {
|
|
74
|
-
const workspaceContext =
|
|
85
|
+
const workspaceContext = getCurrentWorkspaceContext();
|
|
75
86
|
printResult(io, ok('tech.status', getTechStatus({ changeId: options.changeId, ...workspaceContext })), options.json);
|
|
76
87
|
}
|
|
77
88
|
catch (error) {
|
|
@@ -97,7 +108,7 @@ function runWorkflowRoute(io, options) {
|
|
|
97
108
|
return;
|
|
98
109
|
try {
|
|
99
110
|
validatePlanningInput(options.changeId, options.goal);
|
|
100
|
-
const workspaceContext =
|
|
111
|
+
const workspaceContext = getWorkflowWorkspaceContext();
|
|
101
112
|
const plan = createWorkflowRouterPlan({
|
|
102
113
|
changeId: options.changeId,
|
|
103
114
|
goal: options.goal,
|
|
@@ -133,7 +144,7 @@ function runAutonomousWorkflow(io, options) {
|
|
|
133
144
|
return;
|
|
134
145
|
try {
|
|
135
146
|
validatePlanningInput(options.changeId, options.goal);
|
|
136
|
-
const workspaceContext =
|
|
147
|
+
const workspaceContext = getWorkflowWorkspaceContext();
|
|
137
148
|
const plan = createAutonomousWorkflowPlan({
|
|
138
149
|
changeId: options.changeId,
|
|
139
150
|
goal: options.goal,
|
|
@@ -166,7 +177,7 @@ function runSwarmPlan(io, options) {
|
|
|
166
177
|
return;
|
|
167
178
|
try {
|
|
168
179
|
validatePlanningInput(options.changeId, options.goal);
|
|
169
|
-
const workspaceContext =
|
|
180
|
+
const workspaceContext = getWorkflowWorkspaceContext();
|
|
170
181
|
const config = readConfig();
|
|
171
182
|
const plan = createRdSwarmPlan({
|
|
172
183
|
skill: 'rd',
|
|
@@ -28,4 +28,8 @@ export declare function addWorkspace(workspace: WorkspaceConfig, layer?: ConfigL
|
|
|
28
28
|
export declare function removeWorkspace(workspaceId: string, layer?: ConfigLayer): boolean;
|
|
29
29
|
export declare function setCurrentWorkspace(workspaceId: string, layer?: ConfigLayer): boolean;
|
|
30
30
|
export declare function getCurrentWorkspaceConfig(): WorkspaceConfig | null;
|
|
31
|
+
export declare function getWorkspaceConfigForPath(path?: string): WorkspaceConfig | null;
|
|
32
|
+
export declare function ensureWorkspaceConfigForPath(path?: string): WorkspaceConfig | null;
|
|
33
|
+
export declare function getWorkspaceConfigForCurrentPath(): WorkspaceConfig | null;
|
|
34
|
+
export declare function ensureWorkspaceConfigForCurrentPath(): WorkspaceConfig | null;
|
|
31
35
|
export type { TokenRef, WorkspaceConfig, PeaksConfig, ConfigLayer };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
1
|
+
import { closeSync, constants, existsSync, fchmodSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { DEFAULT_CONFIG } from './config-types.js';
|
|
5
|
+
import { stablePath } from '../../shared/path-utils.js';
|
|
5
6
|
function getUserConfigPath() {
|
|
6
7
|
return resolve(homedir(), '.peaks', 'config.json');
|
|
7
8
|
}
|
|
@@ -26,10 +27,25 @@ function isSafeProjectConfigMarker(projectRoot) {
|
|
|
26
27
|
return false;
|
|
27
28
|
}
|
|
28
29
|
}
|
|
30
|
+
function normalizeBoundaryPath(path) {
|
|
31
|
+
const resolved = resolve(path);
|
|
32
|
+
let realPath = resolved;
|
|
33
|
+
try {
|
|
34
|
+
realPath = existsSync(resolved) ? realpathSync.native(resolved) : resolved;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
realPath = resolved;
|
|
38
|
+
}
|
|
39
|
+
return process.platform === 'win32' || process.platform === 'darwin' ? realPath.toLowerCase() : realPath;
|
|
40
|
+
}
|
|
41
|
+
function getHomeBoundaryPaths() {
|
|
42
|
+
return new Set([homedir(), process.env.HOME, process.env.USERPROFILE].filter((path) => typeof path === 'string' && path.length > 0).map(normalizeBoundaryPath));
|
|
43
|
+
}
|
|
29
44
|
function findProjectRoot(startPath) {
|
|
45
|
+
const homeBoundaryPaths = getHomeBoundaryPaths();
|
|
30
46
|
let current = resolve(startPath);
|
|
31
47
|
let parent = dirname(current);
|
|
32
|
-
while (current !== parent) {
|
|
48
|
+
while (current !== parent && !homeBoundaryPaths.has(normalizeBoundaryPath(current))) {
|
|
33
49
|
if (existsSync(resolve(current, '.peaks', 'config.json')) && isSafeProjectConfigMarker(current)) {
|
|
34
50
|
return current;
|
|
35
51
|
}
|
|
@@ -40,10 +56,10 @@ function findProjectRoot(startPath) {
|
|
|
40
56
|
}
|
|
41
57
|
export function resolveProjectRootForConfig(startPath) {
|
|
42
58
|
const start = resolve(startPath);
|
|
43
|
-
const
|
|
59
|
+
const homeBoundaryPaths = getHomeBoundaryPaths();
|
|
44
60
|
let current = start;
|
|
45
61
|
let parent = dirname(current);
|
|
46
|
-
while (current !== parent && current
|
|
62
|
+
while (current !== parent && !homeBoundaryPaths.has(normalizeBoundaryPath(current))) {
|
|
47
63
|
if (existsSync(resolve(current, '.peaks', 'config.json')) && isSafeProjectConfigMarker(current)) {
|
|
48
64
|
return current;
|
|
49
65
|
}
|
|
@@ -102,6 +118,105 @@ function validateProjectBootstrapConfigPathForWrite(projectRoot, configPath) {
|
|
|
102
118
|
const projectRootPath = resolve(projectRoot);
|
|
103
119
|
validateProjectBootstrapConfigPath(projectRootPath, resolve(projectRootPath, '.peaks'), configPath);
|
|
104
120
|
}
|
|
121
|
+
function validateUserConfigPathForWrite(configPath) {
|
|
122
|
+
const userRoot = resolve(homedir());
|
|
123
|
+
const peaksPath = resolve(userRoot, '.peaks');
|
|
124
|
+
const userRootReal = realpathSync(userRoot);
|
|
125
|
+
const peaksStats = lstatSync(peaksPath);
|
|
126
|
+
const peaksReal = realpathSync(peaksPath);
|
|
127
|
+
if (!peaksStats.isDirectory() || peaksStats.isSymbolicLink() || peaksReal !== resolve(userRootReal, '.peaks')) {
|
|
128
|
+
throw new Error('User config path must stay inside the user root');
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const markerStats = lstatSync(configPath);
|
|
132
|
+
if (!markerStats.isFile() || markerStats.isSymbolicLink()) {
|
|
133
|
+
throw new Error('User config path must stay inside the user root');
|
|
134
|
+
}
|
|
135
|
+
const markerReal = realpathSync(configPath);
|
|
136
|
+
if (!isInsidePath(markerReal, userRootReal) || !isInsidePath(markerReal, peaksReal)) {
|
|
137
|
+
throw new Error('User config path must stay inside the user root');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error.code !== 'ENOENT') {
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function validateArtifactWorkspaceRoot(artifactRoot, workspaceRoot) {
|
|
147
|
+
const artifactStats = lstatSync(artifactRoot);
|
|
148
|
+
if (!artifactStats.isDirectory() || artifactStats.isSymbolicLink()) {
|
|
149
|
+
throw new Error('Artifact workspace marker must stay inside the artifact workspace');
|
|
150
|
+
}
|
|
151
|
+
const artifactRootReal = realpathSync(artifactRoot);
|
|
152
|
+
const workspaceRootReal = realpathSync(workspaceRoot);
|
|
153
|
+
if (isInsidePath(artifactRootReal, workspaceRootReal)) {
|
|
154
|
+
throw new Error('Artifact workspace must stay outside the project root');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function validateArtifactWorkspaceMarkerPath(artifactRoot, peaksPath, markerPath) {
|
|
158
|
+
const artifactStats = lstatSync(artifactRoot);
|
|
159
|
+
if (!artifactStats.isDirectory() || artifactStats.isSymbolicLink()) {
|
|
160
|
+
throw new Error('Artifact workspace marker must stay inside the artifact workspace');
|
|
161
|
+
}
|
|
162
|
+
const artifactRootReal = realpathSync(artifactRoot);
|
|
163
|
+
const peaksStats = lstatSync(peaksPath);
|
|
164
|
+
const peaksReal = realpathSync(peaksPath);
|
|
165
|
+
if (!peaksStats.isDirectory() || peaksStats.isSymbolicLink() || peaksReal !== resolve(artifactRootReal, '.peaks')) {
|
|
166
|
+
throw new Error('Artifact workspace marker must stay inside the artifact workspace');
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const markerStats = lstatSync(markerPath);
|
|
170
|
+
if (!markerStats.isFile() || markerStats.isSymbolicLink()) {
|
|
171
|
+
throw new Error('Artifact workspace marker must stay inside the artifact workspace');
|
|
172
|
+
}
|
|
173
|
+
if (markerStats.nlink !== 1) {
|
|
174
|
+
throw new Error('Config path must not be hardlinked');
|
|
175
|
+
}
|
|
176
|
+
const markerReal = realpathSync(markerPath);
|
|
177
|
+
if (!isInsidePath(markerReal, artifactRootReal) || !isInsidePath(markerReal, peaksReal)) {
|
|
178
|
+
throw new Error('Artifact workspace marker must stay inside the artifact workspace');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error.code !== 'ENOENT') {
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function validateOpenConfigFile(fd, configPath, errorMessage) {
|
|
188
|
+
const fdStats = fstatSync(fd);
|
|
189
|
+
const pathStats = lstatSync(configPath);
|
|
190
|
+
if (!fdStats.isFile() || !pathStats.isFile() || fdStats.dev !== pathStats.dev || fdStats.ino !== pathStats.ino) {
|
|
191
|
+
throw new Error(errorMessage);
|
|
192
|
+
}
|
|
193
|
+
if (fdStats.nlink !== 1 || pathStats.nlink !== 1) {
|
|
194
|
+
throw new Error('Config path must not be hardlinked');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function writeConfigFileSafely(configPath, content, validateBeforeWrite, errorMessage) {
|
|
198
|
+
validateBeforeWrite();
|
|
199
|
+
if (typeof constants.O_NOFOLLOW !== 'number') {
|
|
200
|
+
throw new Error('Safe config writes require O_NOFOLLOW support');
|
|
201
|
+
}
|
|
202
|
+
const fd = openSync(configPath, constants.O_WRONLY | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
|
|
203
|
+
try {
|
|
204
|
+
validateBeforeWrite();
|
|
205
|
+
validateOpenConfigFile(fd, configPath, errorMessage);
|
|
206
|
+
fchmodSync(fd, 0o600);
|
|
207
|
+
ftruncateSync(fd, 0);
|
|
208
|
+
writeFileSync(fd, content, 'utf-8');
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
closeSync(fd);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function writeProjectConfigFile(projectRoot, configPath, content) {
|
|
215
|
+
writeConfigFileSafely(configPath, content, () => validateProjectBootstrapConfigPathForWrite(projectRoot, configPath), 'Project config path must stay inside the project root');
|
|
216
|
+
}
|
|
217
|
+
function writeUserConfigFile(configPath, content) {
|
|
218
|
+
writeConfigFileSafely(configPath, content, () => validateUserConfigPathForWrite(configPath), 'User config path must stay inside the user root');
|
|
219
|
+
}
|
|
105
220
|
function readJsonFile(path) {
|
|
106
221
|
if (!path || !existsSync(path))
|
|
107
222
|
return null;
|
|
@@ -284,6 +399,10 @@ function isRecord(value) {
|
|
|
284
399
|
function isSafeConfigSegment(value) {
|
|
285
400
|
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) && !value.includes('..') && !value.endsWith('.');
|
|
286
401
|
}
|
|
402
|
+
function toSafeConfigSegment(value) {
|
|
403
|
+
const normalized = value.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').replace(/\.+$/g, '');
|
|
404
|
+
return isSafeConfigSegment(normalized) ? normalized : 'workspace';
|
|
405
|
+
}
|
|
287
406
|
function toArtifactRemoteRepoConfig(value) {
|
|
288
407
|
if (!isRecord(value) || (value.provider !== 'github' && value.provider !== 'gitlab') || typeof value.owner !== 'string' || typeof value.name !== 'string') {
|
|
289
408
|
return null;
|
|
@@ -327,6 +446,15 @@ function toWorkspaceConfig(value) {
|
|
|
327
446
|
function toWorkspaceConfigs(value) {
|
|
328
447
|
return Array.isArray(value) ? value.map(toWorkspaceConfig).filter((workspace) => workspace !== null) : [];
|
|
329
448
|
}
|
|
449
|
+
function mergeWorkspaceConfigs(userWorkspaces, projectWorkspaces) {
|
|
450
|
+
const merged = new Map(userWorkspaces.map((workspace) => [workspace.workspaceId, workspace]));
|
|
451
|
+
for (const workspace of projectWorkspaces) {
|
|
452
|
+
if (!merged.has(workspace.workspaceId)) {
|
|
453
|
+
merged.set(workspace.workspaceId, workspace);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return [...merged.values()];
|
|
457
|
+
}
|
|
330
458
|
function toProviderModelConfig(value) {
|
|
331
459
|
if (!isRecord(value))
|
|
332
460
|
return {};
|
|
@@ -380,12 +508,16 @@ function toProxyConfig(value) {
|
|
|
380
508
|
return null;
|
|
381
509
|
return typeof value.httpProxy === 'string' && isValidProxyUrl(value.httpProxy) ? { httpProxy: value.httpProxy } : null;
|
|
382
510
|
}
|
|
383
|
-
function
|
|
384
|
-
const
|
|
385
|
-
|
|
511
|
+
function getProjectWriteTarget() {
|
|
512
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
513
|
+
const configPath = getProjectConfigPath(projectRoot);
|
|
514
|
+
if (!projectRoot || !configPath) {
|
|
386
515
|
throw new Error('Project config not found');
|
|
387
516
|
}
|
|
388
|
-
return
|
|
517
|
+
return { projectRoot, configPath };
|
|
518
|
+
}
|
|
519
|
+
function getProjectWritePath() {
|
|
520
|
+
return getProjectWriteTarget().configPath;
|
|
389
521
|
}
|
|
390
522
|
export function containsSensitiveConfigValue(value) {
|
|
391
523
|
if (Array.isArray(value)) {
|
|
@@ -489,8 +621,7 @@ export function bootstrapProjectLanguageConfig(projectRoot, language) {
|
|
|
489
621
|
if (typeof existing.language === 'string' && existing.language.trim().length > 0) {
|
|
490
622
|
return;
|
|
491
623
|
}
|
|
492
|
-
|
|
493
|
-
writeFileSync(projectPath, JSON.stringify({ ...existing, language: inferredLanguage }, null, 2), 'utf-8');
|
|
624
|
+
writeProjectConfigFile(projectRoot, projectPath, JSON.stringify({ ...existing, language: inferredLanguage }, null, 2));
|
|
494
625
|
}
|
|
495
626
|
export function readConfig(projectRoot) {
|
|
496
627
|
const detectedRoot = projectRoot ?? findProjectRoot(process.cwd());
|
|
@@ -498,11 +629,13 @@ export function readConfig(projectRoot) {
|
|
|
498
629
|
const projectPath = getProjectConfigPath(detectedRoot);
|
|
499
630
|
const userConfig = toPeaksConfig(readJsonFile(userPath));
|
|
500
631
|
const projectConfig = removeProjectSensitiveConfig(toPeaksConfig(readJsonFile(projectPath)));
|
|
501
|
-
const { proxy: projectProxy, ...projectConfigWithoutProxy } = projectConfig;
|
|
632
|
+
const { proxy: projectProxy, workspaces: projectWorkspaces, ...projectConfigWithoutProxy } = projectConfig;
|
|
633
|
+
const userWorkspaces = userConfig.workspaces ?? [];
|
|
502
634
|
return {
|
|
503
635
|
...DEFAULT_CONFIG,
|
|
504
636
|
...userConfig,
|
|
505
|
-
...projectConfigWithoutProxy
|
|
637
|
+
...projectConfigWithoutProxy,
|
|
638
|
+
workspaces: mergeWorkspaceConfigs(userWorkspaces, projectWorkspaces ?? [])
|
|
506
639
|
};
|
|
507
640
|
}
|
|
508
641
|
export function writeConfig(partial, layer = 'user') {
|
|
@@ -515,27 +648,33 @@ export function writeConfig(partial, layer = 'user') {
|
|
|
515
648
|
validateProviderConfig(partial);
|
|
516
649
|
validateProxyConfig(partial);
|
|
517
650
|
if (layer === 'project') {
|
|
518
|
-
const
|
|
519
|
-
ensureDir(dirname(
|
|
520
|
-
const existing = readJsonFile(
|
|
651
|
+
const { projectRoot, configPath } = getProjectWriteTarget();
|
|
652
|
+
ensureDir(dirname(configPath));
|
|
653
|
+
const existing = readJsonFile(configPath) ?? {};
|
|
521
654
|
const merged = { ...existing, ...partial };
|
|
522
|
-
|
|
655
|
+
writeProjectConfigFile(projectRoot, configPath, JSON.stringify(merged, null, 2));
|
|
523
656
|
return;
|
|
524
657
|
}
|
|
525
658
|
const userPath = getUserConfigPath();
|
|
526
659
|
ensureDir(dirname(userPath));
|
|
527
|
-
const userPathDir = dirname(userPath);
|
|
528
|
-
ensureDir(userPathDir);
|
|
529
660
|
const existing = readJsonFile(userPath) ?? {};
|
|
530
661
|
const merged = { ...existing, ...partial };
|
|
531
|
-
|
|
662
|
+
writeUserConfigFile(userPath, JSON.stringify(merged, null, 2));
|
|
532
663
|
}
|
|
533
664
|
export function getConfig(options = {}) {
|
|
534
665
|
const projectRoot = findProjectRoot(process.cwd());
|
|
535
666
|
const userConfig = readJsonFile(getUserConfigPath()) ?? {};
|
|
536
667
|
const projectConfig = removeProjectSensitiveConfig(readJsonFile(getProjectConfigPath(projectRoot)) ?? {});
|
|
537
|
-
const { proxy: projectProxy, ...projectConfigWithoutProxy } = projectConfig;
|
|
538
|
-
const source = options.layer === 'user'
|
|
668
|
+
const { proxy: projectProxy, workspaces: projectWorkspaces, ...projectConfigWithoutProxy } = projectConfig;
|
|
669
|
+
const source = options.layer === 'user'
|
|
670
|
+
? userConfig
|
|
671
|
+
: options.layer === 'project'
|
|
672
|
+
? projectConfig
|
|
673
|
+
: {
|
|
674
|
+
...userConfig,
|
|
675
|
+
...projectConfigWithoutProxy,
|
|
676
|
+
workspaces: mergeWorkspaceConfigs(toWorkspaceConfigs(userConfig.workspaces), toWorkspaceConfigs(projectWorkspaces))
|
|
677
|
+
};
|
|
539
678
|
const config = isRecord(source) ? { ...source, ...(source.tokens !== undefined ? { tokens: toTokenConfig(source.tokens) } : {}) } : source;
|
|
540
679
|
if (options.key !== undefined) {
|
|
541
680
|
return getNestedValue(config, options.key);
|
|
@@ -564,12 +703,19 @@ export function setConfig(options) {
|
|
|
564
703
|
}
|
|
565
704
|
}
|
|
566
705
|
validateProxyUrl(getProxyUrlCandidate(options.key, options.value));
|
|
567
|
-
const
|
|
706
|
+
const projectTarget = layer === 'project' ? getProjectWriteTarget() : null;
|
|
707
|
+
const targetPath = projectTarget?.configPath ?? getUserConfigPath();
|
|
568
708
|
ensureDir(dirname(targetPath));
|
|
569
709
|
const existing = readJsonFile(targetPath) ?? {};
|
|
570
710
|
const updated = { ...existing };
|
|
571
711
|
setNestedValue(updated, options.key, options.value);
|
|
572
|
-
|
|
712
|
+
const content = JSON.stringify(updated, null, 2);
|
|
713
|
+
if (projectTarget) {
|
|
714
|
+
writeProjectConfigFile(projectTarget.projectRoot, targetPath, content);
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
writeUserConfigFile(targetPath, content);
|
|
718
|
+
}
|
|
573
719
|
}
|
|
574
720
|
export function getWorkspaceConfig(workspaceId, projectRoot) {
|
|
575
721
|
const config = readConfig(projectRoot ?? findProjectRoot(process.cwd()));
|
|
@@ -626,3 +772,77 @@ export function getCurrentWorkspaceConfig() {
|
|
|
626
772
|
return null;
|
|
627
773
|
return getWorkspaceConfig(config.currentWorkspace);
|
|
628
774
|
}
|
|
775
|
+
export function getWorkspaceConfigForPath(path = process.cwd()) {
|
|
776
|
+
const config = readConfig(findProjectRoot(path));
|
|
777
|
+
return findWorkspaceForPath(config.workspaces, path);
|
|
778
|
+
}
|
|
779
|
+
function createWorkspaceId(projectRoot, existingIds) {
|
|
780
|
+
const base = toSafeConfigSegment(basename(projectRoot));
|
|
781
|
+
if (!existingIds.has(base))
|
|
782
|
+
return base;
|
|
783
|
+
let suffix = 2;
|
|
784
|
+
while (existingIds.has(`${base}-${suffix}`)) {
|
|
785
|
+
suffix += 1;
|
|
786
|
+
}
|
|
787
|
+
return `${base}-${suffix}`;
|
|
788
|
+
}
|
|
789
|
+
function findWorkspaceForPath(workspaces, path) {
|
|
790
|
+
const targetPath = stablePath(path);
|
|
791
|
+
const matches = workspaces.flatMap((workspace) => {
|
|
792
|
+
if (!isAbsolute(workspace.rootPath) || !existsSync(workspace.rootPath))
|
|
793
|
+
return [];
|
|
794
|
+
const rootPath = stablePath(workspace.rootPath);
|
|
795
|
+
return isInsidePath(targetPath, rootPath) ? [{ workspace, rootPath }] : [];
|
|
796
|
+
});
|
|
797
|
+
if (matches.length === 0)
|
|
798
|
+
return null;
|
|
799
|
+
return matches.reduce((best, match) => match.rootPath.length > best.rootPath.length ? match : best).workspace;
|
|
800
|
+
}
|
|
801
|
+
function getWorkspaceArtifactRoot(workspace) {
|
|
802
|
+
return workspace.artifactStorage?.localPath ? resolve(workspace.artifactStorage.localPath) : resolve(homedir(), '.peaks', 'workspaces', workspace.workspaceId, 'artifacts');
|
|
803
|
+
}
|
|
804
|
+
function ensureArtifactWorkspaceMarker(workspace) {
|
|
805
|
+
const artifactRoot = getWorkspaceArtifactRoot(workspace);
|
|
806
|
+
const peaksPath = resolve(artifactRoot, '.peaks');
|
|
807
|
+
const markerPath = resolve(peaksPath, 'config.json');
|
|
808
|
+
ensureDir(artifactRoot);
|
|
809
|
+
validateArtifactWorkspaceRoot(artifactRoot, workspace.rootPath);
|
|
810
|
+
ensureDir(peaksPath);
|
|
811
|
+
validateArtifactWorkspaceMarkerPath(artifactRoot, peaksPath, markerPath);
|
|
812
|
+
if (!existsSync(markerPath)) {
|
|
813
|
+
writeConfigFileSafely(markerPath, '{}\n', () => validateArtifactWorkspaceMarkerPath(artifactRoot, peaksPath, markerPath), 'Artifact workspace marker must stay inside the artifact workspace');
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
export function ensureWorkspaceConfigForPath(path = process.cwd()) {
|
|
817
|
+
const projectRoot = resolveProjectRootForConfig(path);
|
|
818
|
+
if (!isAbsolute(projectRoot) || !existsSync(projectRoot))
|
|
819
|
+
return null;
|
|
820
|
+
const config = readLayerConfig('user');
|
|
821
|
+
const existingWorkspace = findWorkspaceForPath(config.workspaces, path);
|
|
822
|
+
if (existingWorkspace) {
|
|
823
|
+
ensureArtifactWorkspaceMarker(existingWorkspace);
|
|
824
|
+
if (!config.currentWorkspace) {
|
|
825
|
+
writeConfig({ currentWorkspace: existingWorkspace.workspaceId }, 'user');
|
|
826
|
+
}
|
|
827
|
+
return existingWorkspace;
|
|
828
|
+
}
|
|
829
|
+
const existingIds = new Set(config.workspaces.map((workspace) => workspace.workspaceId));
|
|
830
|
+
const workspaceId = createWorkspaceId(projectRoot, existingIds);
|
|
831
|
+
const workspace = {
|
|
832
|
+
workspaceId,
|
|
833
|
+
name: basename(projectRoot) || 'Workspace',
|
|
834
|
+
rootPath: stablePath(projectRoot),
|
|
835
|
+
artifactStorage: { mode: 'local', localPath: resolve(homedir(), '.peaks', 'workspaces', workspaceId, 'artifacts') },
|
|
836
|
+
installedCapabilityIds: []
|
|
837
|
+
};
|
|
838
|
+
ensureArtifactWorkspaceMarker(workspace);
|
|
839
|
+
const updatedWorkspaces = [...config.workspaces, workspace];
|
|
840
|
+
writeConfig({ workspaces: updatedWorkspaces, ...(!config.currentWorkspace ? { currentWorkspace: workspace.workspaceId } : {}) }, 'user');
|
|
841
|
+
return workspace;
|
|
842
|
+
}
|
|
843
|
+
export function getWorkspaceConfigForCurrentPath() {
|
|
844
|
+
return getWorkspaceConfigForPath(process.cwd());
|
|
845
|
+
}
|
|
846
|
+
export function ensureWorkspaceConfigForCurrentPath() {
|
|
847
|
+
return ensureWorkspaceConfigForPath(process.cwd());
|
|
848
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Platform } from './platform.js';
|
|
2
|
-
export declare const SEP: "
|
|
2
|
+
export declare const SEP: "\\" | "/";
|
|
3
3
|
export declare function normalizePath(p: string): string;
|
|
4
4
|
export declare function pathsEqual(a: string, b: string): boolean;
|
|
5
5
|
export declare function localPath(p: string, targetPlatform?: Platform): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.0.
|
|
1
|
+
export declare const CLI_VERSION = "1.0.8";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.0.
|
|
1
|
+
export const CLI_VERSION = "1.0.8";
|
package/package.json
CHANGED
|
@@ -38,9 +38,23 @@ function createInstallResult() {
|
|
|
38
38
|
return { installed: [], skipped: [] };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
function resolvePackageRoot(options = {}) {
|
|
42
|
+
return resolve(options.packageRoot ?? join(dirname(fileURLToPath(import.meta.url)), '..'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readPackageVersion(packageRoot = resolvePackageRoot()) {
|
|
46
|
+
const packageJson = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
|
|
47
|
+
if (typeof packageJson.version !== 'string' || packageJson.version.length === 0) {
|
|
48
|
+
throw new Error('package.json version must be a non-empty string');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return packageJson.version;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createConfigDefaults(packageRoot) {
|
|
55
|
+
return {
|
|
56
|
+
version: readPackageVersion(packageRoot),
|
|
57
|
+
currentWorkspace: null,
|
|
44
58
|
workspaces: [],
|
|
45
59
|
language: 'en',
|
|
46
60
|
model: 'sonnet',
|
|
@@ -52,10 +66,11 @@ const PROJECT_CONFIG_DEFAULTS = {
|
|
|
52
66
|
model: 'minimax-2.7'
|
|
53
67
|
}
|
|
54
68
|
},
|
|
55
|
-
|
|
56
|
-
};
|
|
69
|
+
proxy: {}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
57
72
|
|
|
58
|
-
function
|
|
73
|
+
function createConfigResult(overrides = {}) {
|
|
59
74
|
return { created: false, updated: false, skipped: false, ...overrides };
|
|
60
75
|
}
|
|
61
76
|
|
|
@@ -83,7 +98,7 @@ function mergeMissingConfigValues(existing, defaults) {
|
|
|
83
98
|
}, { ...existing });
|
|
84
99
|
}
|
|
85
100
|
|
|
86
|
-
function
|
|
101
|
+
function readConfigFile(configPath, label) {
|
|
87
102
|
if (!existsSync(configPath)) {
|
|
88
103
|
return null;
|
|
89
104
|
}
|
|
@@ -91,60 +106,68 @@ function readProjectConfig(configPath) {
|
|
|
91
106
|
try {
|
|
92
107
|
const parsed = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
93
108
|
if (!isPlainObject(parsed)) {
|
|
94
|
-
throw new Error(
|
|
109
|
+
throw new Error(`${label} config must contain a JSON object`);
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
return parsed;
|
|
98
113
|
} catch (error) {
|
|
99
|
-
const message = error instanceof SyntaxError ?
|
|
114
|
+
const message = error instanceof SyntaxError ? `${label} config must contain valid JSON` : error instanceof Error ? error.message : String(error);
|
|
100
115
|
throw new Error(message);
|
|
101
116
|
}
|
|
102
117
|
}
|
|
103
118
|
|
|
104
|
-
function
|
|
105
|
-
const
|
|
119
|
+
function validateConfigPath(root, peaksRoot, configPath, label) {
|
|
120
|
+
const rootReal = realpathSync(root);
|
|
106
121
|
const peaksStats = lstatSync(peaksRoot);
|
|
107
122
|
const peaksReal = realpathSync(peaksRoot);
|
|
108
|
-
if (!peaksStats.isDirectory() || peaksStats.isSymbolicLink() || peaksReal !== resolve(
|
|
109
|
-
throw new Error(
|
|
123
|
+
if (!peaksStats.isDirectory() || peaksStats.isSymbolicLink() || peaksReal !== resolve(rootReal, '.peaks')) {
|
|
124
|
+
throw new Error(`${label} config path must stay inside the ${label.toLowerCase()} root`);
|
|
110
125
|
}
|
|
111
126
|
|
|
112
127
|
const configStats = getPathStats(configPath);
|
|
113
128
|
if (configStats?.isSymbolicLink()) {
|
|
114
|
-
throw new Error(
|
|
129
|
+
throw new Error(`${label} config path must not be a symlink`);
|
|
115
130
|
}
|
|
116
131
|
if (configStats && !configStats.isFile()) {
|
|
117
|
-
throw new Error(
|
|
132
|
+
throw new Error(`${label} config path must be a file`);
|
|
118
133
|
}
|
|
119
134
|
if (configStats) {
|
|
120
135
|
const configReal = realpathSync(configPath);
|
|
121
|
-
if (!isInsidePath(configReal,
|
|
122
|
-
throw new Error(
|
|
136
|
+
if (!isInsidePath(configReal, rootReal) || !isInsidePath(configReal, peaksReal)) {
|
|
137
|
+
throw new Error(`${label} config path must stay inside the ${label.toLowerCase()} root`);
|
|
123
138
|
}
|
|
124
139
|
}
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
function
|
|
142
|
+
function validateProjectConfigPaths(projectRoot, peaksRoot, configPath) {
|
|
143
|
+
validateConfigPath(projectRoot, peaksRoot, configPath, 'Project');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validateUserConfigPaths(userRoot, peaksRoot, configPath) {
|
|
147
|
+
validateConfigPath(userRoot, peaksRoot, configPath, 'User');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function validateOpenConfigFile(fd, configPath, label) {
|
|
128
151
|
const fdStats = fstatSync(fd);
|
|
129
152
|
const pathStats = lstatSync(configPath);
|
|
130
153
|
if (!fdStats.isFile() || !pathStats.isFile() || fdStats.dev !== pathStats.dev || fdStats.ino !== pathStats.ino) {
|
|
131
|
-
throw new Error(
|
|
154
|
+
throw new Error(`${label} config path changed during write`);
|
|
132
155
|
}
|
|
133
156
|
if (fdStats.nlink !== 1 || pathStats.nlink !== 1) {
|
|
134
|
-
throw new Error(
|
|
157
|
+
throw new Error(`${label} config path must not be hardlinked`);
|
|
135
158
|
}
|
|
136
159
|
}
|
|
137
160
|
|
|
138
|
-
function
|
|
139
|
-
|
|
161
|
+
function writeConfigFile(configPath, content, label, validateBeforeWrite) {
|
|
162
|
+
validateBeforeWrite();
|
|
140
163
|
if (typeof constants.O_NOFOLLOW !== 'number') {
|
|
141
|
-
throw new Error('Safe
|
|
164
|
+
throw new Error('Safe config writes require O_NOFOLLOW support');
|
|
142
165
|
}
|
|
143
166
|
|
|
144
167
|
const fd = openSync(configPath, constants.O_WRONLY | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
|
|
145
168
|
try {
|
|
146
|
-
|
|
147
|
-
validateOpenConfigFile(fd, configPath);
|
|
169
|
+
validateBeforeWrite();
|
|
170
|
+
validateOpenConfigFile(fd, configPath, label);
|
|
148
171
|
fchmodSync(fd, 0o600);
|
|
149
172
|
ftruncateSync(fd, 0);
|
|
150
173
|
writeFileSync(fd, content, 'utf8');
|
|
@@ -153,19 +176,61 @@ function writeProjectConfig(projectRoot, peaksRoot, configPath, content) {
|
|
|
153
176
|
}
|
|
154
177
|
}
|
|
155
178
|
|
|
179
|
+
function writeProjectConfig(projectRoot, peaksRoot, configPath, content) {
|
|
180
|
+
writeConfigFile(configPath, content, 'Project', () => validateProjectConfigPaths(projectRoot, peaksRoot, configPath));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function writeUserConfig(userRoot, peaksRoot, configPath, content) {
|
|
184
|
+
writeConfigFile(configPath, content, 'User', () => validateUserConfigPaths(userRoot, peaksRoot, configPath));
|
|
185
|
+
}
|
|
186
|
+
|
|
156
187
|
function resolveProjectRoot(options) {
|
|
157
188
|
const projectRoot = options.projectRoot ?? process.env.PEAKS_PROJECT_ROOT ?? process.env.INIT_CWD;
|
|
158
189
|
return projectRoot ? resolve(projectRoot) : null;
|
|
159
190
|
}
|
|
160
191
|
|
|
192
|
+
function writeMergedConfig(configPath, label, defaults, writeConfig) {
|
|
193
|
+
const existing = readConfigFile(configPath, label);
|
|
194
|
+
const next = { ...(existing === null ? defaults : mergeMissingConfigValues(existing, defaults)), version: defaults.version };
|
|
195
|
+
const currentJson = existing === null ? null : `${JSON.stringify(existing, null, 2)}\n`;
|
|
196
|
+
const nextJson = `${JSON.stringify(next, null, 2)}\n`;
|
|
197
|
+
|
|
198
|
+
if (currentJson === nextJson) {
|
|
199
|
+
return createConfigResult();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
writeConfig(nextJson);
|
|
203
|
+
return createConfigResult(existing === null ? { created: true } : { updated: true });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function installUserConfig(options = {}) {
|
|
207
|
+
if (process.env.PEAKS_SKIP_SKILL_INSTALL === '1' || process.env.PEAKS_SKIP_USER_CONFIG_INSTALL === '1') {
|
|
208
|
+
return createConfigResult({ skipped: true });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const userRoot = resolve(options.userRoot ?? homedir());
|
|
212
|
+
const peaksRoot = resolve(userRoot, '.peaks');
|
|
213
|
+
const configPath = resolve(peaksRoot, 'config.json');
|
|
214
|
+
if (!isInsidePath(configPath, userRoot)) {
|
|
215
|
+
throw new Error('User config path must stay inside the user root');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!existsSync(peaksRoot)) {
|
|
219
|
+
mkdirSync(peaksRoot, { recursive: true });
|
|
220
|
+
}
|
|
221
|
+
validateUserConfigPaths(userRoot, peaksRoot, configPath);
|
|
222
|
+
|
|
223
|
+
return writeMergedConfig(configPath, 'User', createConfigDefaults(options.packageRoot), (content) => writeUserConfig(userRoot, peaksRoot, configPath, content));
|
|
224
|
+
}
|
|
225
|
+
|
|
161
226
|
export function installProjectConfig(options = {}) {
|
|
162
227
|
if (process.env.PEAKS_SKIP_SKILL_INSTALL === '1' || process.env.PEAKS_SKIP_PROJECT_CONFIG_INSTALL === '1') {
|
|
163
|
-
return
|
|
228
|
+
return createConfigResult({ skipped: true });
|
|
164
229
|
}
|
|
165
230
|
|
|
166
231
|
const projectRoot = resolveProjectRoot(options);
|
|
167
232
|
if (!projectRoot) {
|
|
168
|
-
return
|
|
233
|
+
return createConfigResult({ skipped: true });
|
|
169
234
|
}
|
|
170
235
|
|
|
171
236
|
const peaksRoot = resolve(projectRoot, '.peaks');
|
|
@@ -179,21 +244,11 @@ export function installProjectConfig(options = {}) {
|
|
|
179
244
|
}
|
|
180
245
|
validateProjectConfigPaths(projectRoot, peaksRoot, configPath);
|
|
181
246
|
|
|
182
|
-
|
|
183
|
-
const next = existing === null ? PROJECT_CONFIG_DEFAULTS : mergeMissingConfigValues(existing, PROJECT_CONFIG_DEFAULTS);
|
|
184
|
-
const currentJson = existing === null ? null : `${JSON.stringify(existing, null, 2)}\n`;
|
|
185
|
-
const nextJson = `${JSON.stringify(next, null, 2)}\n`;
|
|
186
|
-
|
|
187
|
-
if (currentJson === nextJson) {
|
|
188
|
-
return createProjectConfigResult();
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
writeProjectConfig(projectRoot, peaksRoot, configPath, nextJson);
|
|
192
|
-
return createProjectConfigResult(existing === null ? { created: true } : { updated: true });
|
|
247
|
+
return writeMergedConfig(configPath, 'Project', createConfigDefaults(options.packageRoot), (content) => writeProjectConfig(projectRoot, peaksRoot, configPath, content));
|
|
193
248
|
}
|
|
194
249
|
|
|
195
250
|
export function installBundledSkills(options = {}) {
|
|
196
|
-
const packageRoot =
|
|
251
|
+
const packageRoot = resolvePackageRoot(options);
|
|
197
252
|
const skillsRoot = join(packageRoot, 'skills');
|
|
198
253
|
const targetRoot = resolve(options.targetRoot ?? process.env.PEAKS_CLAUDE_SKILLS_DIR ?? join(homedir(), '.claude', 'skills'));
|
|
199
254
|
|
|
@@ -239,7 +294,7 @@ export function installBundledSkills(options = {}) {
|
|
|
239
294
|
}
|
|
240
295
|
|
|
241
296
|
export function installBundledOutputStyles(options = {}) {
|
|
242
|
-
const packageRoot =
|
|
297
|
+
const packageRoot = resolvePackageRoot(options);
|
|
243
298
|
const outputStylesRoot = join(packageRoot, 'output-styles');
|
|
244
299
|
const targetRoot = resolve(options.targetRoot ?? process.env.PEAKS_CLAUDE_OUTPUT_STYLES_DIR ?? join(homedir(), '.claude', 'output-styles'));
|
|
245
300
|
|
|
@@ -283,12 +338,12 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(p
|
|
|
283
338
|
try {
|
|
284
339
|
const skillsResult = installBundledSkills();
|
|
285
340
|
const outputStylesResult = installBundledOutputStyles();
|
|
286
|
-
let
|
|
341
|
+
let userConfigResult = createConfigResult({ skipped: true });
|
|
287
342
|
try {
|
|
288
|
-
|
|
343
|
+
userConfigResult = installUserConfig();
|
|
289
344
|
} catch (error) {
|
|
290
345
|
const message = error instanceof Error ? error.message : String(error);
|
|
291
|
-
process.stderr.write(`Peaks
|
|
346
|
+
process.stderr.write(`Peaks user config was not installed: ${message}\n`);
|
|
292
347
|
}
|
|
293
348
|
if (skillsResult.installed.length > 0) {
|
|
294
349
|
process.stdout.write(`Peaks skills linked: ${skillsResult.installed.join(', ')}\n`);
|
|
@@ -302,11 +357,11 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(p
|
|
|
302
357
|
if (outputStylesResult.skipped.length > 0) {
|
|
303
358
|
process.stderr.write(`Peaks output styles skipped because local files already exist: ${outputStylesResult.skipped.join(', ')}\n`);
|
|
304
359
|
}
|
|
305
|
-
if (
|
|
306
|
-
process.stdout.write('Peaks
|
|
360
|
+
if (userConfigResult.created) {
|
|
361
|
+
process.stdout.write('Peaks user config created: ~/.peaks/config.json\n');
|
|
307
362
|
}
|
|
308
|
-
if (
|
|
309
|
-
process.stdout.write('Peaks
|
|
363
|
+
if (userConfigResult.updated) {
|
|
364
|
+
process.stdout.write('Peaks user config updated: ~/.peaks/config.json\n');
|
|
310
365
|
}
|
|
311
366
|
} catch (error) {
|
|
312
367
|
const message = error instanceof Error ? error.message : String(error);
|