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.
|
|
1
|
+
export declare const CLI_VERSION = "1.0.7";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.0.
|
|
1
|
+
export const CLI_VERSION = "1.0.7";
|
package/package.json
CHANGED
|
@@ -55,7 +55,7 @@ const PROJECT_CONFIG_DEFAULTS = {
|
|
|
55
55
|
proxy: {}
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
function
|
|
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
|
|
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(
|
|
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 ?
|
|
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
|
|
105
|
-
const
|
|
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(
|
|
109
|
-
throw new Error(
|
|
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(
|
|
114
|
+
throw new Error(`${label} config path must not be a symlink`);
|
|
115
115
|
}
|
|
116
116
|
if (configStats && !configStats.isFile()) {
|
|
117
|
-
throw new Error(
|
|
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,
|
|
122
|
-
throw new Error(
|
|
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
|
|
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(
|
|
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(
|
|
142
|
+
throw new Error(`${label} config path must not be hardlinked`);
|
|
135
143
|
}
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
function
|
|
139
|
-
|
|
146
|
+
function writeConfigFile(configPath, content, label, validateBeforeWrite) {
|
|
147
|
+
validateBeforeWrite();
|
|
140
148
|
if (typeof constants.O_NOFOLLOW !== 'number') {
|
|
141
|
-
throw new Error('Safe
|
|
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
|
-
|
|
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
|
|
213
|
+
return createConfigResult({ skipped: true });
|
|
164
214
|
}
|
|
165
215
|
|
|
166
216
|
const projectRoot = resolveProjectRoot(options);
|
|
167
217
|
if (!projectRoot) {
|
|
168
|
-
return
|
|
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
|
-
|
|
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
|
|
326
|
+
let userConfigResult = createConfigResult({ skipped: true });
|
|
287
327
|
try {
|
|
288
|
-
|
|
328
|
+
userConfigResult = installUserConfig();
|
|
289
329
|
} catch (error) {
|
|
290
330
|
const message = error instanceof Error ? error.message : String(error);
|
|
291
|
-
process.stderr.write(`Peaks
|
|
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 (
|
|
306
|
-
process.stdout.write('Peaks
|
|
345
|
+
if (userConfigResult.created) {
|
|
346
|
+
process.stdout.write('Peaks user config created: ~/.peaks/config.json\n');
|
|
307
347
|
}
|
|
308
|
-
if (
|
|
309
|
-
process.stdout.write('Peaks
|
|
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);
|