peaks-cli 1.0.6 → 1.0.7

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.
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.0.6";
1
+ export declare const CLI_VERSION = "1.0.7";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.0.6";
1
+ export const CLI_VERSION = "1.0.7";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Peaks CLI and short skill family for Claude Code automation.",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -55,7 +55,7 @@ const PROJECT_CONFIG_DEFAULTS = {
55
55
  proxy: {}
56
56
  };
57
57
 
58
- function createProjectConfigResult(overrides = {}) {
58
+ function createConfigResult(overrides = {}) {
59
59
  return { created: false, updated: false, skipped: false, ...overrides };
60
60
  }
61
61
 
@@ -83,7 +83,7 @@ function mergeMissingConfigValues(existing, defaults) {
83
83
  }, { ...existing });
84
84
  }
85
85
 
86
- function readProjectConfig(configPath) {
86
+ function readConfigFile(configPath, label) {
87
87
  if (!existsSync(configPath)) {
88
88
  return null;
89
89
  }
@@ -91,60 +91,68 @@ function readProjectConfig(configPath) {
91
91
  try {
92
92
  const parsed = JSON.parse(readFileSync(configPath, 'utf8'));
93
93
  if (!isPlainObject(parsed)) {
94
- throw new Error('Project config must contain a JSON object');
94
+ throw new Error(`${label} config must contain a JSON object`);
95
95
  }
96
96
 
97
97
  return parsed;
98
98
  } catch (error) {
99
- const message = error instanceof SyntaxError ? 'Project config must contain valid JSON' : error instanceof Error ? error.message : String(error);
99
+ const message = error instanceof SyntaxError ? `${label} config must contain valid JSON` : error instanceof Error ? error.message : String(error);
100
100
  throw new Error(message);
101
101
  }
102
102
  }
103
103
 
104
- function validateProjectConfigPaths(projectRoot, peaksRoot, configPath) {
105
- const projectRootReal = realpathSync(projectRoot);
104
+ function validateConfigPath(root, peaksRoot, configPath, label) {
105
+ const rootReal = realpathSync(root);
106
106
  const peaksStats = lstatSync(peaksRoot);
107
107
  const peaksReal = realpathSync(peaksRoot);
108
- if (!peaksStats.isDirectory() || peaksStats.isSymbolicLink() || peaksReal !== resolve(projectRootReal, '.peaks')) {
109
- throw new Error('Project config path must stay inside the project root');
108
+ if (!peaksStats.isDirectory() || peaksStats.isSymbolicLink() || peaksReal !== resolve(rootReal, '.peaks')) {
109
+ throw new Error(`${label} config path must stay inside the ${label.toLowerCase()} root`);
110
110
  }
111
111
 
112
112
  const configStats = getPathStats(configPath);
113
113
  if (configStats?.isSymbolicLink()) {
114
- throw new Error('Project config path must not be a symlink');
114
+ throw new Error(`${label} config path must not be a symlink`);
115
115
  }
116
116
  if (configStats && !configStats.isFile()) {
117
- throw new Error('Project config path must be a file');
117
+ throw new Error(`${label} config path must be a file`);
118
118
  }
119
119
  if (configStats) {
120
120
  const configReal = realpathSync(configPath);
121
- if (!isInsidePath(configReal, projectRootReal) || !isInsidePath(configReal, peaksReal)) {
122
- throw new Error('Project config path must stay inside the project root');
121
+ if (!isInsidePath(configReal, rootReal) || !isInsidePath(configReal, peaksReal)) {
122
+ throw new Error(`${label} config path must stay inside the ${label.toLowerCase()} root`);
123
123
  }
124
124
  }
125
125
  }
126
126
 
127
- function validateOpenConfigFile(fd, configPath) {
127
+ function validateProjectConfigPaths(projectRoot, peaksRoot, configPath) {
128
+ validateConfigPath(projectRoot, peaksRoot, configPath, 'Project');
129
+ }
130
+
131
+ function validateUserConfigPaths(userRoot, peaksRoot, configPath) {
132
+ validateConfigPath(userRoot, peaksRoot, configPath, 'User');
133
+ }
134
+
135
+ function validateOpenConfigFile(fd, configPath, label) {
128
136
  const fdStats = fstatSync(fd);
129
137
  const pathStats = lstatSync(configPath);
130
138
  if (!fdStats.isFile() || !pathStats.isFile() || fdStats.dev !== pathStats.dev || fdStats.ino !== pathStats.ino) {
131
- throw new Error('Project config path changed during write');
139
+ throw new Error(`${label} config path changed during write`);
132
140
  }
133
141
  if (fdStats.nlink !== 1 || pathStats.nlink !== 1) {
134
- throw new Error('Project config path must not be hardlinked');
142
+ throw new Error(`${label} config path must not be hardlinked`);
135
143
  }
136
144
  }
137
145
 
138
- function writeProjectConfig(projectRoot, peaksRoot, configPath, content) {
139
- validateProjectConfigPaths(projectRoot, peaksRoot, configPath);
146
+ function writeConfigFile(configPath, content, label, validateBeforeWrite) {
147
+ validateBeforeWrite();
140
148
  if (typeof constants.O_NOFOLLOW !== 'number') {
141
- throw new Error('Safe project config writes require O_NOFOLLOW support');
149
+ throw new Error('Safe config writes require O_NOFOLLOW support');
142
150
  }
143
151
 
144
152
  const fd = openSync(configPath, constants.O_WRONLY | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
145
153
  try {
146
- validateProjectConfigPaths(projectRoot, peaksRoot, configPath);
147
- validateOpenConfigFile(fd, configPath);
154
+ validateBeforeWrite();
155
+ validateOpenConfigFile(fd, configPath, label);
148
156
  fchmodSync(fd, 0o600);
149
157
  ftruncateSync(fd, 0);
150
158
  writeFileSync(fd, content, 'utf8');
@@ -153,19 +161,61 @@ function writeProjectConfig(projectRoot, peaksRoot, configPath, content) {
153
161
  }
154
162
  }
155
163
 
164
+ function writeProjectConfig(projectRoot, peaksRoot, configPath, content) {
165
+ writeConfigFile(configPath, content, 'Project', () => validateProjectConfigPaths(projectRoot, peaksRoot, configPath));
166
+ }
167
+
168
+ function writeUserConfig(userRoot, peaksRoot, configPath, content) {
169
+ writeConfigFile(configPath, content, 'User', () => validateUserConfigPaths(userRoot, peaksRoot, configPath));
170
+ }
171
+
156
172
  function resolveProjectRoot(options) {
157
173
  const projectRoot = options.projectRoot ?? process.env.PEAKS_PROJECT_ROOT ?? process.env.INIT_CWD;
158
174
  return projectRoot ? resolve(projectRoot) : null;
159
175
  }
160
176
 
177
+ function writeMergedConfig(configPath, label, writeConfig) {
178
+ const existing = readConfigFile(configPath, label);
179
+ const next = existing === null ? PROJECT_CONFIG_DEFAULTS : mergeMissingConfigValues(existing, PROJECT_CONFIG_DEFAULTS);
180
+ const currentJson = existing === null ? null : `${JSON.stringify(existing, null, 2)}\n`;
181
+ const nextJson = `${JSON.stringify(next, null, 2)}\n`;
182
+
183
+ if (currentJson === nextJson) {
184
+ return createConfigResult();
185
+ }
186
+
187
+ writeConfig(nextJson);
188
+ return createConfigResult(existing === null ? { created: true } : { updated: true });
189
+ }
190
+
191
+ export function installUserConfig(options = {}) {
192
+ if (process.env.PEAKS_SKIP_SKILL_INSTALL === '1' || process.env.PEAKS_SKIP_USER_CONFIG_INSTALL === '1') {
193
+ return createConfigResult({ skipped: true });
194
+ }
195
+
196
+ const userRoot = resolve(options.userRoot ?? homedir());
197
+ const peaksRoot = resolve(userRoot, '.peaks');
198
+ const configPath = resolve(peaksRoot, 'config.json');
199
+ if (!isInsidePath(configPath, userRoot)) {
200
+ throw new Error('User config path must stay inside the user root');
201
+ }
202
+
203
+ if (!existsSync(peaksRoot)) {
204
+ mkdirSync(peaksRoot, { recursive: true });
205
+ }
206
+ validateUserConfigPaths(userRoot, peaksRoot, configPath);
207
+
208
+ return writeMergedConfig(configPath, 'User', (content) => writeUserConfig(userRoot, peaksRoot, configPath, content));
209
+ }
210
+
161
211
  export function installProjectConfig(options = {}) {
162
212
  if (process.env.PEAKS_SKIP_SKILL_INSTALL === '1' || process.env.PEAKS_SKIP_PROJECT_CONFIG_INSTALL === '1') {
163
- return createProjectConfigResult({ skipped: true });
213
+ return createConfigResult({ skipped: true });
164
214
  }
165
215
 
166
216
  const projectRoot = resolveProjectRoot(options);
167
217
  if (!projectRoot) {
168
- return createProjectConfigResult({ skipped: true });
218
+ return createConfigResult({ skipped: true });
169
219
  }
170
220
 
171
221
  const peaksRoot = resolve(projectRoot, '.peaks');
@@ -179,17 +229,7 @@ export function installProjectConfig(options = {}) {
179
229
  }
180
230
  validateProjectConfigPaths(projectRoot, peaksRoot, configPath);
181
231
 
182
- const existing = readProjectConfig(configPath);
183
- const next = existing === null ? PROJECT_CONFIG_DEFAULTS : mergeMissingConfigValues(existing, PROJECT_CONFIG_DEFAULTS);
184
- const currentJson = existing === null ? null : `${JSON.stringify(existing, null, 2)}\n`;
185
- const nextJson = `${JSON.stringify(next, null, 2)}\n`;
186
-
187
- if (currentJson === nextJson) {
188
- return createProjectConfigResult();
189
- }
190
-
191
- writeProjectConfig(projectRoot, peaksRoot, configPath, nextJson);
192
- return createProjectConfigResult(existing === null ? { created: true } : { updated: true });
232
+ return writeMergedConfig(configPath, 'Project', (content) => writeProjectConfig(projectRoot, peaksRoot, configPath, content));
193
233
  }
194
234
 
195
235
  export function installBundledSkills(options = {}) {
@@ -283,12 +323,12 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(p
283
323
  try {
284
324
  const skillsResult = installBundledSkills();
285
325
  const outputStylesResult = installBundledOutputStyles();
286
- let projectConfigResult = createProjectConfigResult({ skipped: true });
326
+ let userConfigResult = createConfigResult({ skipped: true });
287
327
  try {
288
- projectConfigResult = installProjectConfig();
328
+ userConfigResult = installUserConfig();
289
329
  } catch (error) {
290
330
  const message = error instanceof Error ? error.message : String(error);
291
- process.stderr.write(`Peaks project config was not installed: ${message}\n`);
331
+ process.stderr.write(`Peaks user config was not installed: ${message}\n`);
292
332
  }
293
333
  if (skillsResult.installed.length > 0) {
294
334
  process.stdout.write(`Peaks skills linked: ${skillsResult.installed.join(', ')}\n`);
@@ -302,11 +342,11 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(p
302
342
  if (outputStylesResult.skipped.length > 0) {
303
343
  process.stderr.write(`Peaks output styles skipped because local files already exist: ${outputStylesResult.skipped.join(', ')}\n`);
304
344
  }
305
- if (projectConfigResult.created) {
306
- process.stdout.write('Peaks project config created: .peaks/config.json\n');
345
+ if (userConfigResult.created) {
346
+ process.stdout.write('Peaks user config created: ~/.peaks/config.json\n');
307
347
  }
308
- if (projectConfigResult.updated) {
309
- process.stdout.write('Peaks project config updated: .peaks/config.json\n');
348
+ if (userConfigResult.updated) {
349
+ process.stdout.write('Peaks user config updated: ~/.peaks/config.json\n');
310
350
  }
311
351
  } catch (error) {
312
352
  const message = error instanceof Error ? error.message : String(error);