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.
@@ -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 getWorkspaceContext() {
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 = getWorkspaceContext();
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 = getWorkspaceContext();
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 = getWorkspaceContext();
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 = getWorkspaceContext();
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 = getWorkspaceContext();
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 homePath = resolve(homedir());
59
+ const homeBoundaryPaths = getHomeBoundaryPaths();
44
60
  let current = start;
45
61
  let parent = dirname(current);
46
- while (current !== parent && current !== homePath) {
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 getProjectWritePath() {
384
- const projectPath = getProjectConfigPath(findProjectRoot(process.cwd()));
385
- if (!projectPath) {
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 projectPath;
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
- validateProjectBootstrapConfigPathForWrite(projectRoot, projectPath);
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 projectPath = getProjectWritePath();
519
- ensureDir(dirname(projectPath));
520
- const existing = readJsonFile(projectPath) ?? {};
651
+ const { projectRoot, configPath } = getProjectWriteTarget();
652
+ ensureDir(dirname(configPath));
653
+ const existing = readJsonFile(configPath) ?? {};
521
654
  const merged = { ...existing, ...partial };
522
- writeFileSync(projectPath, JSON.stringify(merged, null, 2), 'utf-8');
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
- writeFileSync(userPath, JSON.stringify(merged, null, 2), 'utf-8');
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' ? userConfig : options.layer === 'project' ? projectConfig : { ...userConfig, ...projectConfigWithoutProxy };
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 targetPath = layer === 'project' ? getProjectWritePath() : getUserConfigPath();
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
- writeFileSync(targetPath, JSON.stringify(updated, null, 2), 'utf-8');
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.7";
1
+ export declare const CLI_VERSION = "1.0.8";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.0.7";
1
+ export const CLI_VERSION = "1.0.8";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Peaks CLI and short skill family for Claude Code automation.",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -38,9 +38,23 @@ function createInstallResult() {
38
38
  return { installed: [], skipped: [] };
39
39
  }
40
40
 
41
- const PROJECT_CONFIG_DEFAULTS = {
42
- version: '0.1.0',
43
- currentWorkspace: null,
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
- proxy: {}
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 ? PROJECT_CONFIG_DEFAULTS : mergeMissingConfigValues(existing, PROJECT_CONFIG_DEFAULTS);
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 = resolve(options.packageRoot ?? join(dirname(fileURLToPath(import.meta.url)), '..'));
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 = resolve(options.packageRoot ?? join(dirname(fileURLToPath(import.meta.url)), '..'));
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