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.
Files changed (42) hide show
  1. package/dist/src/cli/commands/config-commands.js +3 -2
  2. package/dist/src/cli/commands/sc-commands.js +1 -1
  3. package/dist/src/cli/commands/workflow-commands.js +13 -2
  4. package/dist/src/services/artifacts/artifact-service.js +5 -4
  5. package/dist/src/services/artifacts/workspace-service.d.ts +1 -0
  6. package/dist/src/services/artifacts/workspace-service.js +56 -28
  7. package/dist/src/services/config/config-service.d.ts +2 -0
  8. package/dist/src/services/config/config-service.js +139 -12
  9. package/dist/src/services/config/config-types.d.ts +16 -5
  10. package/dist/src/services/rd/rd-service.js +16 -14
  11. package/dist/src/services/refactor/refactor-service.js +7 -4
  12. package/dist/src/services/sc/sc-service.d.ts +2 -1
  13. package/dist/src/services/sc/sc-service.js +65 -36
  14. package/dist/src/services/tech/tech-service.js +59 -15
  15. package/dist/src/services/workflow/workflow-autonomous-service.js +31 -8
  16. package/dist/src/shared/change-id.js +1 -1
  17. package/dist/src/shared/version.d.ts +1 -1
  18. package/dist/src/shared/version.js +1 -1
  19. package/output-styles/peaks-skill-swarm.md +132 -0
  20. package/package.json +4 -1
  21. package/schemas/artifact-retention-report.schema.json +31 -10
  22. package/scripts/install-skills.mjs +65 -8
  23. package/skills/peaks-prd/SKILL.md +59 -0
  24. package/skills/peaks-prd/references/artifact-contracts.md +4 -0
  25. package/skills/peaks-prd/references/workflow.md +29 -0
  26. package/skills/peaks-qa/SKILL.md +44 -2
  27. package/skills/peaks-qa/references/artifact-contracts.md +4 -0
  28. package/skills/peaks-qa/references/regression-gates.md +9 -1
  29. package/skills/peaks-rd/SKILL.md +78 -2
  30. package/skills/peaks-rd/references/artifact-contracts.md +4 -0
  31. package/skills/peaks-rd/references/refactor-workflow.md +11 -3
  32. package/skills/peaks-sc/SKILL.md +11 -3
  33. package/skills/peaks-sc/references/artifact-retention.md +3 -3
  34. package/skills/peaks-solo/SKILL.md +54 -1
  35. package/skills/peaks-solo/references/artifact-contracts.md +4 -0
  36. package/skills/peaks-solo/references/refactor-mode.md +2 -2
  37. package/skills/peaks-solo/references/workflow.md +18 -0
  38. package/skills/peaks-txt/SKILL.md +28 -1
  39. package/skills/peaks-txt/references/artifact-contracts.md +4 -0
  40. package/skills/peaks-txt/references/context-capsule.md +2 -1
  41. package/skills/peaks-ui/SKILL.md +25 -0
  42. 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 workspace = { workspaceId: options.id, name: options.name, rootPath: options.path, installedCapabilityIds: [] };
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 commit 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) => {
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', 'en')).action((options) => {
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?.artifactRepo ?? null;
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' : 'configure') : 'configure',
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 artifactRepo configured'
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 { basename, dirname, resolve } from 'node:path';
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
- const rootPath = resolve(workspace.rootPath);
16
- return resolve(dirname(rootPath), `${basename(rootPath)}.peaks-artifacts`);
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 || !workspace.artifactRepo) {
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: 'No artifact repository configured for this workspace'
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 remoteUrl = getPublicRemoteUrl(workspace.artifactRepo);
105
- const gitAuthEnv = getGitAuthEnv(workspace.artifactRepo);
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 hasArtifactRepo = !!workspace.artifactRepo;
209
+ const artifactRepo = getArtifactRemoteRepo(workspace);
187
210
  const hasSafeBoundary = isArtifactWorkspaceOutsideTarget(workspace, localPath);
188
- const syncStatus = !hasArtifactRepo || !hasSafeBoundary
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: hasArtifactRepo && hasSafeBoundary,
219
+ configured: hasSafeBoundary,
197
220
  syncStatus,
198
221
  lastSync: null,
199
222
  hasLocalChanges: false,
200
- artifactRepo: workspace.artifactRepo ?? null,
223
+ artifactRepo,
201
224
  nextActions: !hasSafeBoundary
202
225
  ? ['Configure artifact workspace outside the target repository.']
203
- : hasArtifactRepo
226
+ : artifactRepo
204
227
  ? [`Run peaks artifacts sync --workspace ${workspace.workspaceId} --dry-run`]
205
- : [`Configure artifact repo: peaks config workspace add --id ${workspace.workspaceId} --provider github --repo-owner <owner> --repo-name <name>`]
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 || !workspace.artifactRepo) {
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 remoteUrl = workspace.artifactRepo.provider === 'github'
232
- ? `https://github.com/${workspace.artifactRepo.owner}/${workspace.artifactRepo.name}.git`
233
- : `https://gitlab.com/${workspace.artifactRepo.owner}/${workspace.artifactRepo.name}.git`;
234
- const plannedCommands = dryRun
235
- ? [
236
- `# Sync plan for workspace ${workspace.workspaceId}`,
237
- `# Local: ${localPath}`,
238
- `# Remote: ${remoteUrl}`,
239
- '# peaks artifacts sync --workspace ' + workspace.workspaceId,
240
- '# (dry-run only — no changes made)'
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
- `# Sync execution for workspace ${workspace.workspaceId}`,
244
- `# Confirm: will sync ${localPath} with ${remoteUrl}`,
245
- '# Exit 1 if not confirmed'
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 removeProjectProviderSecrets(config) {
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
- let artifactRepo;
225
- if (isRecord(value.artifactRepo) && (value.artifactRepo.provider === 'github' || value.artifactRepo.provider === 'gitlab') && typeof value.artifactRepo.owner === 'string' && typeof value.artifactRepo.name === 'string') {
226
- artifactRepo = { provider: value.artifactRepo.provider, owner: value.artifactRepo.owner, name: value.artifactRepo.name };
227
- }
228
- return artifactRepo ? { workspaceId, name, rootPath, installedCapabilityIds, artifactRepo } : { workspaceId, name, rootPath, installedCapabilityIds };
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 = removeProjectProviderSecrets(toPeaksConfig(readJsonFile(projectPath)));
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 = removeProjectProviderSecrets(readJsonFile(getProjectConfigPath(projectRoot)) ?? {});
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
- provider: 'github' | 'gitlab';
36
- owner: string;
37
- name: string;
38
- };
48
+ artifactRepo?: ArtifactRemoteRepoConfig;
49
+ artifactStorage?: ArtifactStorageConfig;
39
50
  installedCapabilityIds: string[];
40
51
  };
41
52
  export type PeaksConfig = {