peaks-cli 1.0.2 → 1.0.4
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/config-commands.js +3 -2
- package/dist/src/cli/commands/sc-commands.js +1 -1
- package/dist/src/cli/commands/workflow-commands.js +13 -2
- package/dist/src/services/artifacts/artifact-service.js +5 -4
- package/dist/src/services/artifacts/workspace-service.d.ts +1 -0
- package/dist/src/services/artifacts/workspace-service.js +56 -28
- package/dist/src/services/config/config-service.d.ts +2 -0
- package/dist/src/services/config/config-service.js +139 -12
- package/dist/src/services/config/config-types.d.ts +16 -5
- package/dist/src/services/rd/rd-service.js +16 -14
- package/dist/src/services/refactor/refactor-service.js +7 -4
- package/dist/src/services/sc/sc-service.d.ts +2 -1
- package/dist/src/services/sc/sc-service.js +65 -36
- package/dist/src/services/tech/tech-service.js +59 -15
- package/dist/src/services/workflow/workflow-autonomous-service.js +31 -8
- package/dist/src/shared/change-id.js +1 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/output-styles/peaks-skill-swarm.md +132 -0
- package/package.json +4 -1
- package/schemas/artifact-retention-report.schema.json +31 -10
- package/scripts/install-skills.mjs +65 -8
- package/skills/peaks-prd/SKILL.md +59 -0
- package/skills/peaks-prd/references/artifact-contracts.md +4 -0
- package/skills/peaks-prd/references/workflow.md +29 -0
- package/skills/peaks-qa/SKILL.md +44 -2
- package/skills/peaks-qa/references/artifact-contracts.md +4 -0
- package/skills/peaks-qa/references/regression-gates.md +9 -1
- package/skills/peaks-rd/SKILL.md +78 -2
- package/skills/peaks-rd/references/artifact-contracts.md +4 -0
- package/skills/peaks-rd/references/refactor-workflow.md +11 -3
- package/skills/peaks-sc/SKILL.md +11 -3
- package/skills/peaks-sc/references/artifact-retention.md +3 -3
- package/skills/peaks-solo/SKILL.md +54 -1
- package/skills/peaks-solo/references/artifact-contracts.md +4 -0
- package/skills/peaks-solo/references/refactor-mode.md +2 -2
- package/skills/peaks-solo/references/workflow.md +18 -0
- package/skills/peaks-txt/SKILL.md +28 -1
- package/skills/peaks-txt/references/artifact-contracts.md +4 -0
- package/skills/peaks-txt/references/context-capsule.md +2 -1
- package/skills/peaks-ui/SKILL.md +25 -0
- package/skills/peaks-ui/references/workflow.md +17 -1
|
@@ -144,7 +144,8 @@ function registerWorkspaceCommands(config, io) {
|
|
|
144
144
|
const artifactRepo = parseArtifactRepoInput(io, options, options.json);
|
|
145
145
|
if (artifactRepo === null)
|
|
146
146
|
return;
|
|
147
|
-
const
|
|
147
|
+
const artifactStorage = artifactRepo ? { mode: 'local-with-remote-sync', remote: artifactRepo } : { mode: 'local' };
|
|
148
|
+
const workspace = { workspaceId: options.id, name: options.name, rootPath: options.path, installedCapabilityIds: [], artifactStorage };
|
|
148
149
|
const configLayer = layer ?? 'user';
|
|
149
150
|
if (artifactRepo) {
|
|
150
151
|
addWorkspace({ ...workspace, artifactRepo }, configLayer);
|
|
@@ -152,7 +153,7 @@ function registerWorkspaceCommands(config, io) {
|
|
|
152
153
|
else {
|
|
153
154
|
addWorkspace(workspace, configLayer);
|
|
154
155
|
}
|
|
155
|
-
printResult(io, ok('config.workspace.add', { workspaceId: options.id, name: options.name, rootPath: options.path, artifactRepo }), options.json);
|
|
156
|
+
printResult(io, ok('config.workspace.add', { workspaceId: options.id, name: options.name, rootPath: options.path, artifactRepo, artifactStorage }), options.json);
|
|
156
157
|
});
|
|
157
158
|
addJsonOption(configWorkspace.command('remove').description('Remove a workspace').requiredOption('--id <id>', 'workspace identifier').option('--layer <layer>', 'user or project')).action((options) => {
|
|
158
159
|
const layer = parseConfigLayer(options.layer);
|
|
@@ -31,7 +31,7 @@ function registerSCArtifactCommands(sc, io) {
|
|
|
31
31
|
addJsonOption(sc.command('validate').description('Validate artifact retention for a slice').requiredOption('--slice-id <id>', 'slice identifier')).action((options) => {
|
|
32
32
|
printResult(io, ok('sc.validate', validateArtifactRetention(options.sliceId)), options.json);
|
|
33
33
|
});
|
|
34
|
-
addJsonOption(sc.command('boundary').description('Record
|
|
34
|
+
addJsonOption(sc.command('boundary').description('Record retention boundary for a slice').requiredOption('--slice-id <id>', 'slice identifier').option('--artifact <path>', 'artifact path', multipleOption).option('--code <file>', 'code file path', multipleOption)).action((options) => {
|
|
35
35
|
printResult(io, ok('sc.boundary', recordCommitBoundary({ sliceId: options.sliceId, ...(options.artifact ? { artifacts: options.artifact } : {}), ...(options.code ? { codeFiles: options.code } : {}) })), options.json);
|
|
36
36
|
});
|
|
37
37
|
}
|
|
@@ -5,6 +5,7 @@ import { createAutonomousWorkflowPlan } from '../../services/workflow/workflow-a
|
|
|
5
5
|
import { createRecommendationPlan } from '../../services/recommendations/recommendation-service.js';
|
|
6
6
|
import { createRefactorDryRun } from '../../services/refactor/refactor-service.js';
|
|
7
7
|
import { getCurrentWorkspaceConfig, readConfig } from '../../services/config/config-service.js';
|
|
8
|
+
import { validateChangeIdOrThrow } from '../../shared/change-id.js';
|
|
8
9
|
import { getEconomyAwareExecutionModelId } from '../../services/config/model-routing.js';
|
|
9
10
|
import { getLocalArtifactPath } from '../../services/artifacts/workspace-service.js';
|
|
10
11
|
import { fail, ok } from '../../shared/result.js';
|
|
@@ -24,6 +25,12 @@ function parseMaxWorkers(io, command, value, asJson) {
|
|
|
24
25
|
}
|
|
25
26
|
return maxWorkers;
|
|
26
27
|
}
|
|
28
|
+
function validatePlanningInput(changeId, goal) {
|
|
29
|
+
validateChangeIdOrThrow(changeId);
|
|
30
|
+
if (!goal.trim()) {
|
|
31
|
+
throw new Error('Goal must be non-empty');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
27
34
|
function parseSoloMode(io, command, mode, soloMode, asJson) {
|
|
28
35
|
if (mode !== 'solo' && soloMode) {
|
|
29
36
|
printResult(io, fail(command, 'SOLO_MODE_REQUIRES_SOLO_WORKFLOW', '--solo-mode can only be used with --mode solo', {}, ['Remove --solo-mode or use --mode solo']), asJson);
|
|
@@ -46,6 +53,7 @@ function runTechPlan(io, options) {
|
|
|
46
53
|
return;
|
|
47
54
|
}
|
|
48
55
|
try {
|
|
56
|
+
validatePlanningInput(options.changeId, options.goal);
|
|
49
57
|
const workspaceContext = getWorkspaceContext();
|
|
50
58
|
const plan = createTechPlan({
|
|
51
59
|
changeId: options.changeId,
|
|
@@ -88,6 +96,7 @@ function runWorkflowRoute(io, options) {
|
|
|
88
96
|
if (soloMode === null)
|
|
89
97
|
return;
|
|
90
98
|
try {
|
|
99
|
+
validatePlanningInput(options.changeId, options.goal);
|
|
91
100
|
const workspaceContext = getWorkspaceContext();
|
|
92
101
|
const plan = createWorkflowRouterPlan({
|
|
93
102
|
changeId: options.changeId,
|
|
@@ -123,6 +132,7 @@ function runAutonomousWorkflow(io, options) {
|
|
|
123
132
|
if (soloMode === null)
|
|
124
133
|
return;
|
|
125
134
|
try {
|
|
135
|
+
validatePlanningInput(options.changeId, options.goal);
|
|
126
136
|
const workspaceContext = getWorkspaceContext();
|
|
127
137
|
const plan = createAutonomousWorkflowPlan({
|
|
128
138
|
changeId: options.changeId,
|
|
@@ -155,6 +165,7 @@ function runSwarmPlan(io, options) {
|
|
|
155
165
|
if (maxWorkers === null)
|
|
156
166
|
return;
|
|
157
167
|
try {
|
|
168
|
+
validatePlanningInput(options.changeId, options.goal);
|
|
158
169
|
const workspaceContext = getWorkspaceContext();
|
|
159
170
|
const config = readConfig();
|
|
160
171
|
const plan = createRdSwarmPlan({
|
|
@@ -246,12 +257,12 @@ export function registerWorkflowCommands(program, io) {
|
|
|
246
257
|
.command('recommend')
|
|
247
258
|
.description('Create a dry-run recommendation plan for a workflow')
|
|
248
259
|
.requiredOption('--workflow <workflow>', 'workflow: code-refactor, product-refactor, or frontend-design')
|
|
249
|
-
.option('--language <language>', 'human presentation language'
|
|
260
|
+
.option('--language <language>', 'human presentation language')).action((options) => {
|
|
250
261
|
if (!isRecommendationWorkflow(options.workflow)) {
|
|
251
262
|
printResult(io, fail('recommend', 'UNSUPPORTED_RECOMMENDATION_WORKFLOW', `Unsupported recommendation workflow ${options.workflow}`, {}, ['Use --workflow code-refactor, product-refactor, or frontend-design']), options.json);
|
|
252
263
|
process.exitCode = 1;
|
|
253
264
|
return;
|
|
254
265
|
}
|
|
255
|
-
printResult(io, ok('recommend', createRecommendationPlan({ workflow: options.workflow, language: options.language })), options.json);
|
|
266
|
+
printResult(io, ok('recommend', createRecommendationPlan({ workflow: options.workflow, language: options.language ?? readConfig().language })), options.json);
|
|
256
267
|
});
|
|
257
268
|
}
|
|
@@ -3,7 +3,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { getCurrentWorkspaceConfig } from '../config/config-service.js';
|
|
6
|
-
import { getLocalArtifactPath } from './workspace-service.js';
|
|
6
|
+
import { getArtifactRemoteRepo, getLocalArtifactPath } from './workspace-service.js';
|
|
7
7
|
function getRemoteUrl(artifactRepo) {
|
|
8
8
|
if (!artifactRepo)
|
|
9
9
|
return null;
|
|
@@ -46,7 +46,7 @@ export function createArtifactInitPlan(options) {
|
|
|
46
46
|
}
|
|
47
47
|
export function createGuidedArtifactSetup() {
|
|
48
48
|
const workspace = getCurrentWorkspaceConfig();
|
|
49
|
-
const artifactRepo = workspace
|
|
49
|
+
const artifactRepo = workspace ? getArtifactRemoteRepo(workspace) : null;
|
|
50
50
|
const validationResult = {
|
|
51
51
|
workspaceExists: workspace !== null,
|
|
52
52
|
gitAvailable: hasGit(),
|
|
@@ -65,7 +65,7 @@ export function createGuidedArtifactSetup() {
|
|
|
65
65
|
localPath,
|
|
66
66
|
remoteUrl,
|
|
67
67
|
validationResult,
|
|
68
|
-
nextStep: workspace ? (artifactRepo ? 'validate' : '
|
|
68
|
+
nextStep: workspace ? (artifactRepo ? 'validate' : 'complete') : 'configure',
|
|
69
69
|
guidance: [
|
|
70
70
|
'Step 1: Detect current workspace and environment',
|
|
71
71
|
` - Workspace: ${workspace?.workspaceId ?? 'not configured'}`,
|
|
@@ -74,6 +74,7 @@ export function createGuidedArtifactSetup() {
|
|
|
74
74
|
` - SSH key for code push: ${validationResult.sshKeyAvailable ? 'available' : 'not found'}`,
|
|
75
75
|
'',
|
|
76
76
|
'Step 2: Configure artifact repository',
|
|
77
|
+
' - Optional for local-only storage',
|
|
77
78
|
' - Run: peaks artifacts init --provider github --name <repo> --dry-run',
|
|
78
79
|
' - Or add to workspace: peaks config workspace add --id <id> --provider github --repo-owner <owner> --repo-name <name>',
|
|
79
80
|
'',
|
|
@@ -82,7 +83,7 @@ export function createGuidedArtifactSetup() {
|
|
|
82
83
|
' - Run: peaks artifacts workspace',
|
|
83
84
|
'',
|
|
84
85
|
'Step 4: Complete',
|
|
85
|
-
' - Artifact sync is ready when workspace has
|
|
86
|
+
artifactRepo ? ' - Artifact sync is ready when workspace has remote artifact storage configured' : ' - Local artifact storage is ready'
|
|
86
87
|
]
|
|
87
88
|
};
|
|
88
89
|
}
|
|
@@ -22,6 +22,7 @@ export type SyncResult = {
|
|
|
22
22
|
export declare function getLocalArtifactPath(workspace: WorkspaceConfig): string;
|
|
23
23
|
export declare function isArtifactWorkspaceOutsideTarget(workspace: WorkspaceConfig, artifactWorkspacePath?: string): boolean;
|
|
24
24
|
export declare function hasValidArtifactWorkspace(workspace: WorkspaceConfig, artifactWorkspacePath?: string): boolean;
|
|
25
|
+
export declare function getArtifactRemoteRepo(workspace: WorkspaceConfig): WorkspaceConfig['artifactRepo'] | null;
|
|
25
26
|
export declare function executeArtifactSync(workspaceId?: string): Promise<SyncResult>;
|
|
26
27
|
export declare function getArtifactWorkspaceStatus(workspaceId?: string): ArtifactWorkspaceStatus;
|
|
27
28
|
export declare function planArtifactSync(workspaceId?: string, dryRun?: boolean): {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
-
import {
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
4
5
|
import { isInsidePath, stablePath } from '../../shared/path-utils.js';
|
|
5
6
|
import { readConfig, getCurrentWorkspaceConfig } from '../config/config-service.js';
|
|
6
7
|
import { pathExists } from '../../shared/fs.js';
|
|
@@ -12,8 +13,10 @@ function canonicalChildPath(parentPath, ...segments) {
|
|
|
12
13
|
return stablePath(resolve(parentPath, ...segments));
|
|
13
14
|
}
|
|
14
15
|
export function getLocalArtifactPath(workspace) {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
if (workspace.artifactStorage?.localPath) {
|
|
17
|
+
return resolve(workspace.artifactStorage.localPath);
|
|
18
|
+
}
|
|
19
|
+
return resolve(homedir(), '.peaks', 'workspaces', workspace.workspaceId, 'artifacts');
|
|
17
20
|
}
|
|
18
21
|
export function isArtifactWorkspaceOutsideTarget(workspace, artifactWorkspacePath = getLocalArtifactPath(workspace)) {
|
|
19
22
|
const targetRoot = canonicalPath(workspace.rootPath);
|
|
@@ -44,6 +47,15 @@ export function hasValidArtifactWorkspace(workspace, artifactWorkspacePath = get
|
|
|
44
47
|
return false;
|
|
45
48
|
return true;
|
|
46
49
|
}
|
|
50
|
+
export function getArtifactRemoteRepo(workspace) {
|
|
51
|
+
if (workspace.artifactStorage?.mode === 'local-with-remote-sync') {
|
|
52
|
+
return workspace.artifactStorage.remote;
|
|
53
|
+
}
|
|
54
|
+
if (workspace.artifactStorage?.mode === 'local') {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return workspace.artifactRepo ?? null;
|
|
58
|
+
}
|
|
47
59
|
function getPublicRemoteUrl(artifactRepo) {
|
|
48
60
|
if (!artifactRepo)
|
|
49
61
|
return null;
|
|
@@ -78,7 +90,7 @@ export async function executeArtifactSync(workspaceId) {
|
|
|
78
90
|
const workspace = workspaceId
|
|
79
91
|
? readConfig().workspaces.find((w) => w.workspaceId === workspaceId) ?? null
|
|
80
92
|
: getCurrentWorkspaceConfig();
|
|
81
|
-
if (!workspace
|
|
93
|
+
if (!workspace) {
|
|
82
94
|
return {
|
|
83
95
|
workspaceId: workspaceId ?? 'unknown',
|
|
84
96
|
success: false,
|
|
@@ -86,7 +98,7 @@ export async function executeArtifactSync(workspaceId) {
|
|
|
86
98
|
remoteUrl: null,
|
|
87
99
|
commands: [],
|
|
88
100
|
output: [],
|
|
89
|
-
error: '
|
|
101
|
+
error: 'Workspace not found'
|
|
90
102
|
};
|
|
91
103
|
}
|
|
92
104
|
const localPath = getLocalArtifactPath(workspace);
|
|
@@ -101,8 +113,19 @@ export async function executeArtifactSync(workspaceId) {
|
|
|
101
113
|
error: 'Artifact workspace must be outside the target repository'
|
|
102
114
|
};
|
|
103
115
|
}
|
|
104
|
-
const
|
|
105
|
-
|
|
116
|
+
const artifactRepo = getArtifactRemoteRepo(workspace);
|
|
117
|
+
if (!artifactRepo) {
|
|
118
|
+
return {
|
|
119
|
+
workspaceId: workspace.workspaceId,
|
|
120
|
+
success: true,
|
|
121
|
+
localPath,
|
|
122
|
+
remoteUrl: null,
|
|
123
|
+
commands: [],
|
|
124
|
+
output: ['Local artifact storage is configured']
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const remoteUrl = getPublicRemoteUrl(artifactRepo);
|
|
128
|
+
const gitAuthEnv = getGitAuthEnv(artifactRepo);
|
|
106
129
|
if (!remoteUrl) {
|
|
107
130
|
return {
|
|
108
131
|
workspaceId: workspace.workspaceId,
|
|
@@ -183,9 +206,9 @@ export function getArtifactWorkspaceStatus(workspaceId) {
|
|
|
183
206
|
}
|
|
184
207
|
const localPath = getLocalArtifactPath(workspace);
|
|
185
208
|
const hasLocalDir = existsSync(localPath);
|
|
186
|
-
const
|
|
209
|
+
const artifactRepo = getArtifactRemoteRepo(workspace);
|
|
187
210
|
const hasSafeBoundary = isArtifactWorkspaceOutsideTarget(workspace, localPath);
|
|
188
|
-
const syncStatus = !
|
|
211
|
+
const syncStatus = !hasSafeBoundary
|
|
189
212
|
? 'unknown'
|
|
190
213
|
: !hasLocalDir
|
|
191
214
|
? 'pending'
|
|
@@ -193,23 +216,23 @@ export function getArtifactWorkspaceStatus(workspaceId) {
|
|
|
193
216
|
return {
|
|
194
217
|
workspaceId: workspace.workspaceId,
|
|
195
218
|
localPath,
|
|
196
|
-
configured:
|
|
219
|
+
configured: hasSafeBoundary,
|
|
197
220
|
syncStatus,
|
|
198
221
|
lastSync: null,
|
|
199
222
|
hasLocalChanges: false,
|
|
200
|
-
artifactRepo
|
|
223
|
+
artifactRepo,
|
|
201
224
|
nextActions: !hasSafeBoundary
|
|
202
225
|
? ['Configure artifact workspace outside the target repository.']
|
|
203
|
-
:
|
|
226
|
+
: artifactRepo
|
|
204
227
|
? [`Run peaks artifacts sync --workspace ${workspace.workspaceId} --dry-run`]
|
|
205
|
-
: [`
|
|
228
|
+
: [`Local artifact storage ready at ${localPath}`]
|
|
206
229
|
};
|
|
207
230
|
}
|
|
208
231
|
export function planArtifactSync(workspaceId, dryRun = true) {
|
|
209
232
|
const workspace = workspaceId
|
|
210
233
|
? readConfig().workspaces.find((w) => w.workspaceId === workspaceId) ?? null
|
|
211
234
|
: getCurrentWorkspaceConfig();
|
|
212
|
-
if (!workspace
|
|
235
|
+
if (!workspace) {
|
|
213
236
|
return {
|
|
214
237
|
workspaceId: workspaceId ?? 'unknown',
|
|
215
238
|
dryRun,
|
|
@@ -228,21 +251,26 @@ export function planArtifactSync(workspaceId, dryRun = true) {
|
|
|
228
251
|
plannedCommands: ['Artifact workspace must be outside the target repository']
|
|
229
252
|
};
|
|
230
253
|
}
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
254
|
+
const artifactRepo = getArtifactRemoteRepo(workspace);
|
|
255
|
+
const remoteUrl = getPublicRemoteUrl(artifactRepo);
|
|
256
|
+
const plannedCommands = artifactRepo
|
|
257
|
+
? dryRun
|
|
258
|
+
? [
|
|
259
|
+
`# Sync plan for workspace ${workspace.workspaceId}`,
|
|
260
|
+
`# Local: ${localPath}`,
|
|
261
|
+
`# Remote: ${remoteUrl}`,
|
|
262
|
+
'# peaks artifacts sync --workspace ' + workspace.workspaceId,
|
|
263
|
+
'# (dry-run only — no changes made)'
|
|
264
|
+
]
|
|
265
|
+
: [
|
|
266
|
+
`# Sync execution for workspace ${workspace.workspaceId}`,
|
|
267
|
+
`# Confirm: will sync ${localPath} with ${remoteUrl}`,
|
|
268
|
+
'# Exit 1 if not confirmed'
|
|
269
|
+
]
|
|
242
270
|
: [
|
|
243
|
-
`#
|
|
244
|
-
`#
|
|
245
|
-
'#
|
|
271
|
+
`# Local artifact storage for workspace ${workspace.workspaceId}`,
|
|
272
|
+
`# Local: ${localPath}`,
|
|
273
|
+
'# No remote repository is configured or required'
|
|
246
274
|
];
|
|
247
275
|
return {
|
|
248
276
|
workspaceId: workspace.workspaceId,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConfigGetOptions, ConfigLayer, ConfigSetOptions, MiniMaxProviderConfig, PeaksConfig, TokenRef, WorkspaceConfig } from './config-types.js';
|
|
2
|
+
export declare function resolveProjectRootForConfig(startPath: string): string;
|
|
2
3
|
export declare function isConfigLayer(value: string): value is ConfigLayer;
|
|
3
4
|
export declare function isSensitiveConfigPath(path: string): boolean;
|
|
4
5
|
export declare function containsSensitiveConfigValue(value: unknown): boolean;
|
|
@@ -17,6 +18,7 @@ export type MiniMaxProviderStatus = {
|
|
|
17
18
|
export declare function getMiniMaxProviderConfig(): MiniMaxProviderConfig;
|
|
18
19
|
export declare function getMiniMaxProviderStatus(): MiniMaxProviderStatus;
|
|
19
20
|
export declare function setMiniMaxProviderConfig(input: MiniMaxProviderConfig): MiniMaxProviderStatus;
|
|
21
|
+
export declare function bootstrapProjectLanguageConfig(projectRoot: string, language: string): void;
|
|
20
22
|
export declare function readConfig(projectRoot?: string | null): PeaksConfig;
|
|
21
23
|
export declare function writeConfig(partial: Partial<PeaksConfig>, layer?: ConfigLayer): void;
|
|
22
24
|
export declare function getConfig(options?: ConfigGetOptions): unknown;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { DEFAULT_CONFIG } from './config-types.js';
|
|
@@ -38,6 +38,23 @@ function findProjectRoot(startPath) {
|
|
|
38
38
|
}
|
|
39
39
|
return null;
|
|
40
40
|
}
|
|
41
|
+
export function resolveProjectRootForConfig(startPath) {
|
|
42
|
+
const start = resolve(startPath);
|
|
43
|
+
const homePath = resolve(homedir());
|
|
44
|
+
let current = start;
|
|
45
|
+
let parent = dirname(current);
|
|
46
|
+
while (current !== parent && current !== homePath) {
|
|
47
|
+
if (existsSync(resolve(current, '.peaks', 'config.json')) && isSafeProjectConfigMarker(current)) {
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
50
|
+
if (existsSync(resolve(current, 'package.json')) || existsSync(resolve(current, '.git'))) {
|
|
51
|
+
return current;
|
|
52
|
+
}
|
|
53
|
+
parent = current;
|
|
54
|
+
current = dirname(parent);
|
|
55
|
+
}
|
|
56
|
+
return start;
|
|
57
|
+
}
|
|
41
58
|
function getProjectConfigPath(projectRoot) {
|
|
42
59
|
if (!projectRoot)
|
|
43
60
|
return null;
|
|
@@ -45,6 +62,46 @@ function getProjectConfigPath(projectRoot) {
|
|
|
45
62
|
return null;
|
|
46
63
|
return resolve(projectRoot, '.peaks', 'config.json');
|
|
47
64
|
}
|
|
65
|
+
function getProjectBootstrapConfigPath(projectRoot) {
|
|
66
|
+
const projectRootPath = resolve(projectRoot);
|
|
67
|
+
const peaksPath = resolve(projectRootPath, '.peaks');
|
|
68
|
+
const configPath = resolve(peaksPath, 'config.json');
|
|
69
|
+
if (!isInsidePath(configPath, projectRootPath)) {
|
|
70
|
+
throw new Error('Project config path must stay inside the project root');
|
|
71
|
+
}
|
|
72
|
+
if (!existsSync(peaksPath)) {
|
|
73
|
+
mkdirSync(peaksPath, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
validateProjectBootstrapConfigPath(projectRootPath, peaksPath, configPath);
|
|
76
|
+
return configPath;
|
|
77
|
+
}
|
|
78
|
+
function validateProjectBootstrapConfigPath(projectRootPath, peaksPath, configPath) {
|
|
79
|
+
const projectRootReal = realpathSync(projectRootPath);
|
|
80
|
+
const peaksStats = lstatSync(peaksPath);
|
|
81
|
+
const peaksReal = realpathSync(peaksPath);
|
|
82
|
+
if (!peaksStats.isDirectory() || peaksStats.isSymbolicLink() || peaksReal !== resolve(projectRootReal, '.peaks')) {
|
|
83
|
+
throw new Error('Project config path must stay inside the project root');
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const markerStats = lstatSync(configPath);
|
|
87
|
+
if (!markerStats.isFile() || markerStats.isSymbolicLink()) {
|
|
88
|
+
throw new Error('Project config path must stay inside the project root');
|
|
89
|
+
}
|
|
90
|
+
const markerReal = realpathSync(configPath);
|
|
91
|
+
if (!isInsidePath(markerReal, projectRootReal) || !isInsidePath(markerReal, peaksReal)) {
|
|
92
|
+
throw new Error('Project config path must stay inside the project root');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error.code !== 'ENOENT') {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function validateProjectBootstrapConfigPathForWrite(projectRoot, configPath) {
|
|
102
|
+
const projectRootPath = resolve(projectRoot);
|
|
103
|
+
validateProjectBootstrapConfigPath(projectRootPath, resolve(projectRootPath, '.peaks'), configPath);
|
|
104
|
+
}
|
|
48
105
|
function readJsonFile(path) {
|
|
49
106
|
if (!path || !existsSync(path))
|
|
50
107
|
return null;
|
|
@@ -55,6 +112,16 @@ function readJsonFile(path) {
|
|
|
55
112
|
return null;
|
|
56
113
|
}
|
|
57
114
|
}
|
|
115
|
+
function readExistingJsonFile(path, errorMessage) {
|
|
116
|
+
if (!existsSync(path))
|
|
117
|
+
return null;
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new Error(errorMessage);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
58
125
|
function ensureDir(dirPath) {
|
|
59
126
|
if (!existsSync(dirPath)) {
|
|
60
127
|
mkdirSync(dirPath, { recursive: true });
|
|
@@ -97,9 +164,9 @@ function setNestedValue(obj, path, value) {
|
|
|
97
164
|
const last = parts[parts.length - 1];
|
|
98
165
|
current[last] = value;
|
|
99
166
|
}
|
|
100
|
-
function
|
|
101
|
-
const { providers, ...safeConfig } = config;
|
|
102
|
-
return safeConfig;
|
|
167
|
+
function removeProjectSensitiveConfig(config) {
|
|
168
|
+
const { providers, proxy, tokens, ...safeConfig } = config;
|
|
169
|
+
return Object.fromEntries(Object.entries(safeConfig).filter(([key, value]) => !isSecretKey(key) && !containsSensitiveConfigValue(value)));
|
|
103
170
|
}
|
|
104
171
|
export function isConfigLayer(value) {
|
|
105
172
|
return value === 'user' || value === 'project';
|
|
@@ -214,18 +281,48 @@ function validateProxyConfig(partial) {
|
|
|
214
281
|
function isRecord(value) {
|
|
215
282
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
216
283
|
}
|
|
284
|
+
function isSafeConfigSegment(value) {
|
|
285
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) && !value.includes('..') && !value.endsWith('.');
|
|
286
|
+
}
|
|
287
|
+
function toArtifactRemoteRepoConfig(value) {
|
|
288
|
+
if (!isRecord(value) || (value.provider !== 'github' && value.provider !== 'gitlab') || typeof value.owner !== 'string' || typeof value.name !== 'string') {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
if (!isSafeConfigSegment(value.owner) || !isSafeConfigSegment(value.name)) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return { provider: value.provider, owner: value.owner, name: value.name };
|
|
295
|
+
}
|
|
296
|
+
function toArtifactStorageConfig(value) {
|
|
297
|
+
if (!isRecord(value))
|
|
298
|
+
return null;
|
|
299
|
+
const localPath = typeof value.localPath === 'string' ? { localPath: value.localPath } : {};
|
|
300
|
+
if (value.mode === 'local') {
|
|
301
|
+
return { mode: 'local', ...localPath };
|
|
302
|
+
}
|
|
303
|
+
const remote = toArtifactRemoteRepoConfig(value.remote);
|
|
304
|
+
if (value.mode === 'local-with-remote-sync' && remote) {
|
|
305
|
+
return { mode: 'local-with-remote-sync', ...localPath, remote };
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
217
309
|
function toWorkspaceConfig(value) {
|
|
218
310
|
if (!isRecord(value))
|
|
219
311
|
return null;
|
|
220
312
|
const { workspaceId, name, rootPath, installedCapabilityIds } = value;
|
|
221
|
-
if (typeof workspaceId !== 'string' || typeof name !== 'string' || typeof rootPath !== 'string' || !Array.isArray(installedCapabilityIds) || !installedCapabilityIds.every((id) => typeof id === 'string')) {
|
|
313
|
+
if (typeof workspaceId !== 'string' || !isSafeConfigSegment(workspaceId) || typeof name !== 'string' || typeof rootPath !== 'string' || !Array.isArray(installedCapabilityIds) || !installedCapabilityIds.every((id) => typeof id === 'string')) {
|
|
222
314
|
return null;
|
|
223
315
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
316
|
+
const artifactRepo = toArtifactRemoteRepoConfig(value.artifactRepo);
|
|
317
|
+
const artifactStorage = toArtifactStorageConfig(value.artifactStorage);
|
|
318
|
+
return {
|
|
319
|
+
workspaceId,
|
|
320
|
+
name,
|
|
321
|
+
rootPath,
|
|
322
|
+
installedCapabilityIds,
|
|
323
|
+
...(artifactRepo ? { artifactRepo } : {}),
|
|
324
|
+
...(artifactStorage ? { artifactStorage } : {})
|
|
325
|
+
};
|
|
229
326
|
}
|
|
230
327
|
function toWorkspaceConfigs(value) {
|
|
231
328
|
return Array.isArray(value) ? value.map(toWorkspaceConfig).filter((workspace) => workspace !== null) : [];
|
|
@@ -355,6 +452,19 @@ export function setMiniMaxProviderConfig(input) {
|
|
|
355
452
|
writeConfig({ providers }, 'user');
|
|
356
453
|
return createMiniMaxProviderStatus(providers.minimax ?? {});
|
|
357
454
|
}
|
|
455
|
+
function inferHumanLanguage(value) {
|
|
456
|
+
const normalized = value.trim();
|
|
457
|
+
if (!normalized) {
|
|
458
|
+
throw new Error('Language must be non-empty');
|
|
459
|
+
}
|
|
460
|
+
if (/^zh(?:-|$)/i.test(normalized) || /[㐀-鿿]/u.test(normalized)) {
|
|
461
|
+
return 'zh-CN';
|
|
462
|
+
}
|
|
463
|
+
if (/^en(?:-|$)/i.test(normalized)) {
|
|
464
|
+
return 'en';
|
|
465
|
+
}
|
|
466
|
+
return 'en';
|
|
467
|
+
}
|
|
358
468
|
function toPeaksConfig(value) {
|
|
359
469
|
if (!isRecord(value))
|
|
360
470
|
return {};
|
|
@@ -372,12 +482,22 @@ function toPeaksConfig(value) {
|
|
|
372
482
|
...(proxy ? { proxy } : {})
|
|
373
483
|
};
|
|
374
484
|
}
|
|
485
|
+
export function bootstrapProjectLanguageConfig(projectRoot, language) {
|
|
486
|
+
const inferredLanguage = inferHumanLanguage(language);
|
|
487
|
+
const projectPath = getProjectBootstrapConfigPath(projectRoot);
|
|
488
|
+
const existing = readExistingJsonFile(projectPath, 'Project config must contain valid JSON') ?? {};
|
|
489
|
+
if (typeof existing.language === 'string' && existing.language.trim().length > 0) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
validateProjectBootstrapConfigPathForWrite(projectRoot, projectPath);
|
|
493
|
+
writeFileSync(projectPath, JSON.stringify({ ...existing, language: inferredLanguage }, null, 2), 'utf-8');
|
|
494
|
+
}
|
|
375
495
|
export function readConfig(projectRoot) {
|
|
376
496
|
const detectedRoot = projectRoot ?? findProjectRoot(process.cwd());
|
|
377
497
|
const userPath = getUserConfigPath();
|
|
378
498
|
const projectPath = getProjectConfigPath(detectedRoot);
|
|
379
499
|
const userConfig = toPeaksConfig(readJsonFile(userPath));
|
|
380
|
-
const projectConfig =
|
|
500
|
+
const projectConfig = removeProjectSensitiveConfig(toPeaksConfig(readJsonFile(projectPath)));
|
|
381
501
|
const { proxy: projectProxy, ...projectConfigWithoutProxy } = projectConfig;
|
|
382
502
|
return {
|
|
383
503
|
...DEFAULT_CONFIG,
|
|
@@ -413,7 +533,7 @@ export function writeConfig(partial, layer = 'user') {
|
|
|
413
533
|
export function getConfig(options = {}) {
|
|
414
534
|
const projectRoot = findProjectRoot(process.cwd());
|
|
415
535
|
const userConfig = readJsonFile(getUserConfigPath()) ?? {};
|
|
416
|
-
const projectConfig =
|
|
536
|
+
const projectConfig = removeProjectSensitiveConfig(readJsonFile(getProjectConfigPath(projectRoot)) ?? {});
|
|
417
537
|
const { proxy: projectProxy, ...projectConfigWithoutProxy } = projectConfig;
|
|
418
538
|
const source = options.layer === 'user' ? userConfig : options.layer === 'project' ? projectConfig : { ...userConfig, ...projectConfigWithoutProxy };
|
|
419
539
|
const config = isRecord(source) ? { ...source, ...(source.tokens !== undefined ? { tokens: toTokenConfig(source.tokens) } : {}) } : source;
|
|
@@ -465,6 +585,9 @@ function readLayerConfig(layer) {
|
|
|
465
585
|
: { currentWorkspace: null, workspaces: [] };
|
|
466
586
|
}
|
|
467
587
|
export function addWorkspace(workspace, layer = 'user') {
|
|
588
|
+
if (!isSafeConfigSegment(workspace.workspaceId)) {
|
|
589
|
+
throw new Error('Workspace id must only contain letters, numbers, dots, underscores, or hyphens and must not contain path traversal');
|
|
590
|
+
}
|
|
468
591
|
const config = readLayerConfig(layer);
|
|
469
592
|
const workspaces = config.workspaces;
|
|
470
593
|
const existing = workspaces.findIndex((w) => w.workspaceId === workspace.workspaceId);
|
|
@@ -474,6 +597,8 @@ export function addWorkspace(workspace, layer = 'user') {
|
|
|
474
597
|
writeConfig({ workspaces: updatedWorkspaces }, layer);
|
|
475
598
|
}
|
|
476
599
|
export function removeWorkspace(workspaceId, layer = 'user') {
|
|
600
|
+
if (!isSafeConfigSegment(workspaceId))
|
|
601
|
+
return false;
|
|
477
602
|
const config = readLayerConfig(layer);
|
|
478
603
|
const workspaces = config.workspaces;
|
|
479
604
|
const idx = workspaces.findIndex((w) => w.workspaceId === workspaceId);
|
|
@@ -485,6 +610,8 @@ export function removeWorkspace(workspaceId, layer = 'user') {
|
|
|
485
610
|
return true;
|
|
486
611
|
}
|
|
487
612
|
export function setCurrentWorkspace(workspaceId, layer = 'user') {
|
|
613
|
+
if (!isSafeConfigSegment(workspaceId))
|
|
614
|
+
return false;
|
|
488
615
|
const config = readLayerConfig(layer);
|
|
489
616
|
const workspaces = config.workspaces;
|
|
490
617
|
const exists = workspaces.some((w) => w.workspaceId === workspaceId);
|
|
@@ -27,15 +27,26 @@ export type ModelProviderConfig = {
|
|
|
27
27
|
export type ProxyConfig = {
|
|
28
28
|
httpProxy?: string;
|
|
29
29
|
};
|
|
30
|
+
export type ArtifactProvider = 'github' | 'gitlab';
|
|
31
|
+
export type ArtifactRemoteRepoConfig = {
|
|
32
|
+
provider: ArtifactProvider;
|
|
33
|
+
owner: string;
|
|
34
|
+
name: string;
|
|
35
|
+
};
|
|
36
|
+
export type ArtifactStorageConfig = {
|
|
37
|
+
mode: 'local';
|
|
38
|
+
localPath?: string;
|
|
39
|
+
} | {
|
|
40
|
+
mode: 'local-with-remote-sync';
|
|
41
|
+
localPath?: string;
|
|
42
|
+
remote: ArtifactRemoteRepoConfig;
|
|
43
|
+
};
|
|
30
44
|
export type WorkspaceConfig = {
|
|
31
45
|
workspaceId: string;
|
|
32
46
|
name: string;
|
|
33
47
|
rootPath: string;
|
|
34
|
-
artifactRepo?:
|
|
35
|
-
|
|
36
|
-
owner: string;
|
|
37
|
-
name: string;
|
|
38
|
-
};
|
|
48
|
+
artifactRepo?: ArtifactRemoteRepoConfig;
|
|
49
|
+
artifactStorage?: ArtifactStorageConfig;
|
|
39
50
|
installedCapabilityIds: string[];
|
|
40
51
|
};
|
|
41
52
|
export type PeaksConfig = {
|