peaks-cli 1.0.7 → 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 +26 -11
|
@@ -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,8 +66,9 @@ const PROJECT_CONFIG_DEFAULTS = {
|
|
|
52
66
|
model: 'minimax-2.7'
|
|
53
67
|
}
|
|
54
68
|
},
|
|
55
|
-
|
|
56
|
-
};
|
|
69
|
+
proxy: {}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
57
72
|
|
|
58
73
|
function createConfigResult(overrides = {}) {
|
|
59
74
|
return { created: false, updated: false, skipped: false, ...overrides };
|
|
@@ -174,9 +189,9 @@ function resolveProjectRoot(options) {
|
|
|
174
189
|
return projectRoot ? resolve(projectRoot) : null;
|
|
175
190
|
}
|
|
176
191
|
|
|
177
|
-
function writeMergedConfig(configPath, label, writeConfig) {
|
|
192
|
+
function writeMergedConfig(configPath, label, defaults, writeConfig) {
|
|
178
193
|
const existing = readConfigFile(configPath, label);
|
|
179
|
-
const next = existing === null ?
|
|
194
|
+
const next = { ...(existing === null ? defaults : mergeMissingConfigValues(existing, defaults)), version: defaults.version };
|
|
180
195
|
const currentJson = existing === null ? null : `${JSON.stringify(existing, null, 2)}\n`;
|
|
181
196
|
const nextJson = `${JSON.stringify(next, null, 2)}\n`;
|
|
182
197
|
|
|
@@ -205,7 +220,7 @@ export function installUserConfig(options = {}) {
|
|
|
205
220
|
}
|
|
206
221
|
validateUserConfigPaths(userRoot, peaksRoot, configPath);
|
|
207
222
|
|
|
208
|
-
return writeMergedConfig(configPath, 'User', (content) => writeUserConfig(userRoot, peaksRoot, configPath, content));
|
|
223
|
+
return writeMergedConfig(configPath, 'User', createConfigDefaults(options.packageRoot), (content) => writeUserConfig(userRoot, peaksRoot, configPath, content));
|
|
209
224
|
}
|
|
210
225
|
|
|
211
226
|
export function installProjectConfig(options = {}) {
|
|
@@ -229,11 +244,11 @@ export function installProjectConfig(options = {}) {
|
|
|
229
244
|
}
|
|
230
245
|
validateProjectConfigPaths(projectRoot, peaksRoot, configPath);
|
|
231
246
|
|
|
232
|
-
return writeMergedConfig(configPath, 'Project', (content) => writeProjectConfig(projectRoot, peaksRoot, configPath, content));
|
|
247
|
+
return writeMergedConfig(configPath, 'Project', createConfigDefaults(options.packageRoot), (content) => writeProjectConfig(projectRoot, peaksRoot, configPath, content));
|
|
233
248
|
}
|
|
234
249
|
|
|
235
250
|
export function installBundledSkills(options = {}) {
|
|
236
|
-
const packageRoot =
|
|
251
|
+
const packageRoot = resolvePackageRoot(options);
|
|
237
252
|
const skillsRoot = join(packageRoot, 'skills');
|
|
238
253
|
const targetRoot = resolve(options.targetRoot ?? process.env.PEAKS_CLAUDE_SKILLS_DIR ?? join(homedir(), '.claude', 'skills'));
|
|
239
254
|
|
|
@@ -279,7 +294,7 @@ export function installBundledSkills(options = {}) {
|
|
|
279
294
|
}
|
|
280
295
|
|
|
281
296
|
export function installBundledOutputStyles(options = {}) {
|
|
282
|
-
const packageRoot =
|
|
297
|
+
const packageRoot = resolvePackageRoot(options);
|
|
283
298
|
const outputStylesRoot = join(packageRoot, 'output-styles');
|
|
284
299
|
const targetRoot = resolve(options.targetRoot ?? process.env.PEAKS_CLAUDE_OUTPUT_STYLES_DIR ?? join(homedir(), '.claude', 'output-styles'));
|
|
285
300
|
|