peaks-cli 1.0.8 → 1.0.9
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/bin/peaks.js
CHANGED
|
File without changes
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { closeSync, constants, existsSync, fchmodSync, fstatSync,
|
|
1
|
+
import { closeSync, constants, existsSync, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
import { DEFAULT_CONFIG } from './config-types.js';
|
|
@@ -15,7 +16,13 @@ function isSafeProjectConfigMarker(projectRoot) {
|
|
|
15
16
|
const markerPath = resolve(peaksPath, 'config.json');
|
|
16
17
|
try {
|
|
17
18
|
const projectRootReal = realpathSync(projectRoot);
|
|
19
|
+
const peaksStats = lstatSync(peaksPath);
|
|
18
20
|
const peaksReal = realpathSync(peaksPath);
|
|
21
|
+
const markerStats = lstatSync(markerPath);
|
|
22
|
+
if (!peaksStats.isDirectory() || peaksStats.isSymbolicLink())
|
|
23
|
+
return false;
|
|
24
|
+
if (!markerStats.isFile() || markerStats.isSymbolicLink() || markerStats.nlink !== 1)
|
|
25
|
+
return false;
|
|
19
26
|
const markerReal = realpathSync(markerPath);
|
|
20
27
|
if (!isInsidePath(peaksReal, projectRootReal))
|
|
21
28
|
return false;
|
|
@@ -103,6 +110,9 @@ function validateProjectBootstrapConfigPath(projectRootPath, peaksPath, configPa
|
|
|
103
110
|
if (!markerStats.isFile() || markerStats.isSymbolicLink()) {
|
|
104
111
|
throw new Error('Project config path must stay inside the project root');
|
|
105
112
|
}
|
|
113
|
+
if (markerStats.nlink !== 1) {
|
|
114
|
+
throw new Error('Config path must not be hardlinked');
|
|
115
|
+
}
|
|
106
116
|
const markerReal = realpathSync(configPath);
|
|
107
117
|
if (!isInsidePath(markerReal, projectRootReal) || !isInsidePath(markerReal, peaksReal)) {
|
|
108
118
|
throw new Error('Project config path must stay inside the project root');
|
|
@@ -132,6 +142,9 @@ function validateUserConfigPathForWrite(configPath) {
|
|
|
132
142
|
if (!markerStats.isFile() || markerStats.isSymbolicLink()) {
|
|
133
143
|
throw new Error('User config path must stay inside the user root');
|
|
134
144
|
}
|
|
145
|
+
if (markerStats.nlink !== 1) {
|
|
146
|
+
throw new Error('Config path must not be hardlinked');
|
|
147
|
+
}
|
|
135
148
|
const markerReal = realpathSync(configPath);
|
|
136
149
|
if (!isInsidePath(markerReal, userRootReal) || !isInsidePath(markerReal, peaksReal)) {
|
|
137
150
|
throw new Error('User config path must stay inside the user root');
|
|
@@ -184,9 +197,9 @@ function validateArtifactWorkspaceMarkerPath(artifactRoot, peaksPath, markerPath
|
|
|
184
197
|
}
|
|
185
198
|
}
|
|
186
199
|
}
|
|
187
|
-
function validateOpenConfigFile(fd,
|
|
200
|
+
function validateOpenConfigFile(fd, tempPath, errorMessage) {
|
|
188
201
|
const fdStats = fstatSync(fd);
|
|
189
|
-
const pathStats = lstatSync(
|
|
202
|
+
const pathStats = lstatSync(tempPath);
|
|
190
203
|
if (!fdStats.isFile() || !pathStats.isFile() || fdStats.dev !== pathStats.dev || fdStats.ino !== pathStats.ino) {
|
|
191
204
|
throw new Error(errorMessage);
|
|
192
205
|
}
|
|
@@ -194,21 +207,66 @@ function validateOpenConfigFile(fd, configPath, errorMessage) {
|
|
|
194
207
|
throw new Error('Config path must not be hardlinked');
|
|
195
208
|
}
|
|
196
209
|
}
|
|
210
|
+
function getSafeTempOpenFlags() {
|
|
211
|
+
const baseFlags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL;
|
|
212
|
+
return typeof constants.O_NOFOLLOW === 'number' ? baseFlags | constants.O_NOFOLLOW : baseFlags;
|
|
213
|
+
}
|
|
214
|
+
function getSafeReadOpenFlags() {
|
|
215
|
+
return typeof constants.O_NOFOLLOW === 'number' ? constants.O_RDONLY | constants.O_NOFOLLOW : constants.O_RDONLY;
|
|
216
|
+
}
|
|
217
|
+
function readConfigFileSafely(configPath, errorMessage) {
|
|
218
|
+
const fd = openSync(configPath, getSafeReadOpenFlags());
|
|
219
|
+
try {
|
|
220
|
+
validateOpenConfigFile(fd, configPath, errorMessage);
|
|
221
|
+
return readFileSync(fd, 'utf-8');
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
closeSync(fd);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
197
227
|
function writeConfigFileSafely(configPath, content, validateBeforeWrite, errorMessage) {
|
|
198
228
|
validateBeforeWrite();
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
229
|
+
const tempPath = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
230
|
+
let fd = openSync(tempPath, getSafeTempOpenFlags(), 0o600);
|
|
231
|
+
let renamed = false;
|
|
232
|
+
let closeError;
|
|
203
233
|
try {
|
|
204
|
-
|
|
205
|
-
validateOpenConfigFile(fd, configPath, errorMessage);
|
|
234
|
+
validateOpenConfigFile(fd, tempPath, errorMessage);
|
|
206
235
|
fchmodSync(fd, 0o600);
|
|
207
|
-
ftruncateSync(fd, 0);
|
|
208
236
|
writeFileSync(fd, content, 'utf-8');
|
|
237
|
+
const writeFd = fd;
|
|
238
|
+
fd = null;
|
|
239
|
+
closeSync(writeFd);
|
|
240
|
+
validateBeforeWrite();
|
|
241
|
+
const readFd = openSync(tempPath, getSafeReadOpenFlags());
|
|
242
|
+
try {
|
|
243
|
+
validateOpenConfigFile(readFd, tempPath, errorMessage);
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
closeSync(readFd);
|
|
247
|
+
}
|
|
248
|
+
renameSync(tempPath, configPath);
|
|
249
|
+
renamed = true;
|
|
209
250
|
}
|
|
210
251
|
finally {
|
|
211
|
-
|
|
252
|
+
if (fd !== null) {
|
|
253
|
+
try {
|
|
254
|
+
closeSync(fd);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
closeError = error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
if (!renamed && existsSync(tempPath)) {
|
|
262
|
+
unlinkSync(tempPath);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
if (closeError) {
|
|
267
|
+
throw closeError;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
212
270
|
}
|
|
213
271
|
}
|
|
214
272
|
function writeProjectConfigFile(projectRoot, configPath, content) {
|
|
@@ -217,26 +275,37 @@ function writeProjectConfigFile(projectRoot, configPath, content) {
|
|
|
217
275
|
function writeUserConfigFile(configPath, content) {
|
|
218
276
|
writeConfigFileSafely(configPath, content, () => validateUserConfigPathForWrite(configPath), 'User config path must stay inside the user root');
|
|
219
277
|
}
|
|
220
|
-
function readJsonFile(path) {
|
|
278
|
+
function readJsonFile(path, validateBeforeRead, errorMessage = 'Config path must stay inside the config root') {
|
|
221
279
|
if (!path || !existsSync(path))
|
|
222
280
|
return null;
|
|
281
|
+
validateBeforeRead?.();
|
|
282
|
+
const content = readConfigFileSafely(path, errorMessage);
|
|
223
283
|
try {
|
|
224
|
-
return JSON.parse(
|
|
284
|
+
return JSON.parse(content);
|
|
225
285
|
}
|
|
226
286
|
catch {
|
|
227
287
|
return null;
|
|
228
288
|
}
|
|
229
289
|
}
|
|
230
|
-
function readExistingJsonFile(path, errorMessage) {
|
|
290
|
+
function readExistingJsonFile(path, errorMessage, validateBeforeRead) {
|
|
231
291
|
if (!existsSync(path))
|
|
232
292
|
return null;
|
|
293
|
+
validateBeforeRead?.();
|
|
233
294
|
try {
|
|
234
|
-
return JSON.parse(
|
|
295
|
+
return JSON.parse(readConfigFileSafely(path, errorMessage));
|
|
235
296
|
}
|
|
236
297
|
catch {
|
|
237
298
|
throw new Error(errorMessage);
|
|
238
299
|
}
|
|
239
300
|
}
|
|
301
|
+
function readUserJsonFile() {
|
|
302
|
+
const userPath = getUserConfigPath();
|
|
303
|
+
return readJsonFile(userPath, () => validateUserConfigPathForWrite(userPath), 'User config path must stay inside the user root');
|
|
304
|
+
}
|
|
305
|
+
function readProjectJsonFile(projectRoot) {
|
|
306
|
+
const projectPath = getProjectConfigPath(projectRoot);
|
|
307
|
+
return readJsonFile(projectPath, projectRoot && projectPath ? () => validateProjectBootstrapConfigPathForWrite(projectRoot, projectPath) : undefined, 'Project config path must stay inside the project root');
|
|
308
|
+
}
|
|
240
309
|
function ensureDir(dirPath) {
|
|
241
310
|
if (!existsSync(dirPath)) {
|
|
242
311
|
mkdirSync(dirPath, { recursive: true });
|
|
@@ -516,9 +585,6 @@ function getProjectWriteTarget() {
|
|
|
516
585
|
}
|
|
517
586
|
return { projectRoot, configPath };
|
|
518
587
|
}
|
|
519
|
-
function getProjectWritePath() {
|
|
520
|
-
return getProjectWriteTarget().configPath;
|
|
521
|
-
}
|
|
522
588
|
export function containsSensitiveConfigValue(value) {
|
|
523
589
|
if (Array.isArray(value)) {
|
|
524
590
|
return value.some(containsSensitiveConfigValue);
|
|
@@ -564,14 +630,14 @@ function createMiniMaxProviderStatus(config) {
|
|
|
564
630
|
};
|
|
565
631
|
}
|
|
566
632
|
export function getMiniMaxProviderConfig() {
|
|
567
|
-
return toMiniMaxProviderConfig(
|
|
633
|
+
return toMiniMaxProviderConfig(readUserJsonFile()?.providers?.minimax);
|
|
568
634
|
}
|
|
569
635
|
export function getMiniMaxProviderStatus() {
|
|
570
636
|
return createMiniMaxProviderStatus(getMiniMaxProviderConfig());
|
|
571
637
|
}
|
|
572
638
|
export function setMiniMaxProviderConfig(input) {
|
|
573
639
|
validateMiniMaxBaseUrl(input.baseUrl);
|
|
574
|
-
const userConfig =
|
|
640
|
+
const userConfig = readUserJsonFile() ?? {};
|
|
575
641
|
const existingProviders = toModelProviderConfig(userConfig.providers);
|
|
576
642
|
const providers = {
|
|
577
643
|
...existingProviders,
|
|
@@ -617,7 +683,7 @@ function toPeaksConfig(value) {
|
|
|
617
683
|
export function bootstrapProjectLanguageConfig(projectRoot, language) {
|
|
618
684
|
const inferredLanguage = inferHumanLanguage(language);
|
|
619
685
|
const projectPath = getProjectBootstrapConfigPath(projectRoot);
|
|
620
|
-
const existing = readExistingJsonFile(projectPath, 'Project config must contain valid JSON') ?? {};
|
|
686
|
+
const existing = readExistingJsonFile(projectPath, 'Project config must contain valid JSON', () => validateProjectBootstrapConfigPathForWrite(projectRoot, projectPath)) ?? {};
|
|
621
687
|
if (typeof existing.language === 'string' && existing.language.trim().length > 0) {
|
|
622
688
|
return;
|
|
623
689
|
}
|
|
@@ -625,10 +691,8 @@ export function bootstrapProjectLanguageConfig(projectRoot, language) {
|
|
|
625
691
|
}
|
|
626
692
|
export function readConfig(projectRoot) {
|
|
627
693
|
const detectedRoot = projectRoot ?? findProjectRoot(process.cwd());
|
|
628
|
-
const
|
|
629
|
-
const
|
|
630
|
-
const userConfig = toPeaksConfig(readJsonFile(userPath));
|
|
631
|
-
const projectConfig = removeProjectSensitiveConfig(toPeaksConfig(readJsonFile(projectPath)));
|
|
694
|
+
const userConfig = toPeaksConfig(readUserJsonFile());
|
|
695
|
+
const projectConfig = removeProjectSensitiveConfig(toPeaksConfig(readProjectJsonFile(detectedRoot)));
|
|
632
696
|
const { proxy: projectProxy, workspaces: projectWorkspaces, ...projectConfigWithoutProxy } = projectConfig;
|
|
633
697
|
const userWorkspaces = userConfig.workspaces ?? [];
|
|
634
698
|
return {
|
|
@@ -650,21 +714,21 @@ export function writeConfig(partial, layer = 'user') {
|
|
|
650
714
|
if (layer === 'project') {
|
|
651
715
|
const { projectRoot, configPath } = getProjectWriteTarget();
|
|
652
716
|
ensureDir(dirname(configPath));
|
|
653
|
-
const existing = readJsonFile(configPath) ?? {};
|
|
717
|
+
const existing = readJsonFile(configPath, () => validateProjectBootstrapConfigPathForWrite(projectRoot, configPath)) ?? {};
|
|
654
718
|
const merged = { ...existing, ...partial };
|
|
655
719
|
writeProjectConfigFile(projectRoot, configPath, JSON.stringify(merged, null, 2));
|
|
656
720
|
return;
|
|
657
721
|
}
|
|
658
722
|
const userPath = getUserConfigPath();
|
|
659
723
|
ensureDir(dirname(userPath));
|
|
660
|
-
const existing = readJsonFile(userPath) ?? {};
|
|
724
|
+
const existing = readJsonFile(userPath, () => validateUserConfigPathForWrite(userPath)) ?? {};
|
|
661
725
|
const merged = { ...existing, ...partial };
|
|
662
726
|
writeUserConfigFile(userPath, JSON.stringify(merged, null, 2));
|
|
663
727
|
}
|
|
664
728
|
export function getConfig(options = {}) {
|
|
665
729
|
const projectRoot = findProjectRoot(process.cwd());
|
|
666
|
-
const userConfig =
|
|
667
|
-
const projectConfig = removeProjectSensitiveConfig(
|
|
730
|
+
const userConfig = readUserJsonFile() ?? {};
|
|
731
|
+
const projectConfig = removeProjectSensitiveConfig(readProjectJsonFile(projectRoot) ?? {});
|
|
668
732
|
const { proxy: projectProxy, workspaces: projectWorkspaces, ...projectConfigWithoutProxy } = projectConfig;
|
|
669
733
|
const source = options.layer === 'user'
|
|
670
734
|
? userConfig
|
|
@@ -706,7 +770,9 @@ export function setConfig(options) {
|
|
|
706
770
|
const projectTarget = layer === 'project' ? getProjectWriteTarget() : null;
|
|
707
771
|
const targetPath = projectTarget?.configPath ?? getUserConfigPath();
|
|
708
772
|
ensureDir(dirname(targetPath));
|
|
709
|
-
const existing =
|
|
773
|
+
const existing = projectTarget
|
|
774
|
+
? readJsonFile(targetPath, () => validateProjectBootstrapConfigPathForWrite(projectTarget.projectRoot, targetPath)) ?? {}
|
|
775
|
+
: readJsonFile(targetPath, () => validateUserConfigPathForWrite(targetPath)) ?? {};
|
|
710
776
|
const updated = { ...existing };
|
|
711
777
|
setNestedValue(updated, options.key, options.value);
|
|
712
778
|
const content = JSON.stringify(updated, null, 2);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.0.
|
|
1
|
+
export declare const CLI_VERSION = "1.0.9";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.0.
|
|
1
|
+
export const CLI_VERSION = "1.0.9";
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { closeSync, constants,
|
|
2
|
+
import { closeSync, constants, existsSync, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, realpathSync, readdirSync, renameSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
|
-
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
6
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
7
|
|
|
7
8
|
function getPathStats(path) {
|
|
@@ -16,22 +17,140 @@ function isBrokenSymlink(stats, targetPath) {
|
|
|
16
17
|
return stats.isSymbolicLink() && !existsSync(targetPath);
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
function validateManagedMarkerPath(markerPath) {
|
|
21
|
+
const markerStats = getPathStats(markerPath);
|
|
22
|
+
if (!markerStats) return;
|
|
23
|
+
if (markerStats.isSymbolicLink()) {
|
|
24
|
+
throw new Error('Peaks managed marker path must not be a symlink');
|
|
25
|
+
}
|
|
26
|
+
if (!markerStats.isFile()) {
|
|
27
|
+
throw new Error('Peaks managed marker path must be a file');
|
|
28
|
+
}
|
|
29
|
+
if (markerStats.nlink !== 1) {
|
|
30
|
+
throw new Error('Peaks managed marker path must not be hardlinked');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validateOpenFile(fd, path, errorMessage) {
|
|
35
|
+
const fdStats = fstatSync(fd);
|
|
36
|
+
const pathStats = lstatSync(path);
|
|
37
|
+
if (!fdStats.isFile() || !pathStats.isFile() || fdStats.dev !== pathStats.dev || fdStats.ino !== pathStats.ino) {
|
|
38
|
+
throw new Error(errorMessage);
|
|
39
|
+
}
|
|
40
|
+
if (fdStats.nlink !== 1 || pathStats.nlink !== 1) {
|
|
41
|
+
throw new Error(`${errorMessage}: hardlinked file`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createFileIdentity(path) {
|
|
46
|
+
const stats = lstatSync(path);
|
|
47
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return { dev: stats.dev, ino: stats.ino };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isSameFileIdentity(path, identity) {
|
|
54
|
+
if (identity === null) return false;
|
|
55
|
+
const stats = getPathStats(path);
|
|
56
|
+
return Boolean(stats?.isFile() && !stats.isSymbolicLink() && stats.nlink === 1 && stats.dev === identity.dev && stats.ino === identity.ino);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getSafeReadOpenFlags() {
|
|
60
|
+
return typeof constants.O_NOFOLLOW === 'number' ? constants.O_RDONLY | constants.O_NOFOLLOW : constants.O_RDONLY;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readFileSafely(path, errorMessage) {
|
|
64
|
+
const fd = openSync(path, getSafeReadOpenFlags());
|
|
65
|
+
try {
|
|
66
|
+
validateOpenFile(fd, path, errorMessage);
|
|
67
|
+
return readFileSync(fd, 'utf8');
|
|
68
|
+
} finally {
|
|
69
|
+
closeSync(fd);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
19
73
|
function getManagedTarget(targetPath) {
|
|
20
74
|
const markerPath = `${targetPath}.peaks-managed`;
|
|
75
|
+
validateManagedMarkerPath(markerPath);
|
|
21
76
|
if (!existsSync(markerPath)) {
|
|
22
77
|
return null;
|
|
23
78
|
}
|
|
24
|
-
return
|
|
79
|
+
return readFileSafely(markerPath, 'Peaks managed marker path changed during read').trim();
|
|
25
80
|
}
|
|
26
81
|
|
|
27
82
|
function markManagedPeaksLink(targetPath, sourcePath) {
|
|
28
83
|
const markerPath = `${targetPath}.peaks-managed`;
|
|
29
|
-
|
|
84
|
+
validateManagedMarkerPath(markerPath);
|
|
85
|
+
writeFileAtomically(markerPath, `${sourcePath}\n`, 'Peaks managed marker path changed during write', () => validateManagedMarkerPath(markerPath));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readPackageSourceFile(path) {
|
|
89
|
+
const stats = lstatSync(path);
|
|
90
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
91
|
+
throw new Error('Peaks package source path must be a file');
|
|
92
|
+
}
|
|
93
|
+
return readFileSync(path, 'utf8');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hashContent(content) {
|
|
97
|
+
return createHash('sha256').update(content).digest('hex');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function hashFileContent(path) {
|
|
101
|
+
return hashContent(readFileSafely(path, 'Peaks managed file path changed during read'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createManagedOutputStyleMarker(sourcePath, outputStyleName) {
|
|
105
|
+
const content = readPackageSourceFile(sourcePath);
|
|
106
|
+
return `${JSON.stringify({ version: 1, kind: 'output-style', outputStyleName, sourcePath, contentSha256: hashContent(content) })}\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseManagedOutputStyleMarker(managedTarget) {
|
|
110
|
+
if (managedTarget === null) return null;
|
|
111
|
+
try {
|
|
112
|
+
const marker = JSON.parse(managedTarget);
|
|
113
|
+
if (marker?.version !== 1 || marker?.kind !== 'output-style' || typeof marker.outputStyleName !== 'string' || typeof marker.sourcePath !== 'string' || typeof marker.contentSha256 !== 'string') {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return marker;
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isTrustedOutputStyleSource(marker, sourcePath, outputStyleName) {
|
|
123
|
+
return marker.outputStyleName === outputStyleName && resolve(marker.sourcePath) === resolve(sourcePath) && basename(resolve(marker.sourcePath)) === outputStyleName;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getManagedPeaksOutputStyleIdentity(managedTarget, targetPath, sourcePath, outputStyleName) {
|
|
127
|
+
const marker = parseManagedOutputStyleMarker(managedTarget);
|
|
128
|
+
const sourceHash = hashContent(readPackageSourceFile(sourcePath));
|
|
129
|
+
if (marker === null || !isTrustedOutputStyleSource(marker, sourcePath, outputStyleName) || !existsSync(targetPath) || hashFileContent(targetPath) !== sourceHash || marker.contentSha256 !== sourceHash) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return createFileIdentity(targetPath);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateInstallRoot(targetRoot, label) {
|
|
136
|
+
const rootStats = lstatSync(targetRoot);
|
|
137
|
+
if (rootStats.isSymbolicLink()) {
|
|
138
|
+
throw new Error(`${label} install root must not be a symlink`);
|
|
139
|
+
}
|
|
140
|
+
if (!rootStats.isDirectory()) {
|
|
141
|
+
throw new Error(`${label} install root must be a directory`);
|
|
142
|
+
}
|
|
143
|
+
return rootStats;
|
|
30
144
|
}
|
|
31
145
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
return
|
|
146
|
+
function createInstallRootValidator(targetRoot, label) {
|
|
147
|
+
const expectedStats = validateInstallRoot(targetRoot, label);
|
|
148
|
+
return () => {
|
|
149
|
+
const rootStats = validateInstallRoot(targetRoot, label);
|
|
150
|
+
if (rootStats.dev !== expectedStats.dev || rootStats.ino !== expectedStats.ino) {
|
|
151
|
+
throw new Error(`${label} install root changed during write`);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
35
154
|
}
|
|
36
155
|
|
|
37
156
|
function createInstallResult() {
|
|
@@ -104,7 +223,7 @@ function readConfigFile(configPath, label) {
|
|
|
104
223
|
}
|
|
105
224
|
|
|
106
225
|
try {
|
|
107
|
-
const parsed = JSON.parse(
|
|
226
|
+
const parsed = JSON.parse(readFileSafely(configPath, `${label} config path changed during read`));
|
|
108
227
|
if (!isPlainObject(parsed)) {
|
|
109
228
|
throw new Error(`${label} config must contain a JSON object`);
|
|
110
229
|
}
|
|
@@ -132,6 +251,9 @@ function validateConfigPath(root, peaksRoot, configPath, label) {
|
|
|
132
251
|
throw new Error(`${label} config path must be a file`);
|
|
133
252
|
}
|
|
134
253
|
if (configStats) {
|
|
254
|
+
if (configStats.nlink !== 1) {
|
|
255
|
+
throw new Error(`${label} config path must not be hardlinked`);
|
|
256
|
+
}
|
|
135
257
|
const configReal = realpathSync(configPath);
|
|
136
258
|
if (!isInsidePath(configReal, rootReal) || !isInsidePath(configReal, peaksReal)) {
|
|
137
259
|
throw new Error(`${label} config path must stay inside the ${label.toLowerCase()} root`);
|
|
@@ -147,41 +269,93 @@ function validateUserConfigPaths(userRoot, peaksRoot, configPath) {
|
|
|
147
269
|
validateConfigPath(userRoot, peaksRoot, configPath, 'User');
|
|
148
270
|
}
|
|
149
271
|
|
|
150
|
-
function
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
if (!fdStats.isFile() || !pathStats.isFile() || fdStats.dev !== pathStats.dev || fdStats.ino !== pathStats.ino) {
|
|
154
|
-
throw new Error(`${label} config path changed during write`);
|
|
155
|
-
}
|
|
156
|
-
if (fdStats.nlink !== 1 || pathStats.nlink !== 1) {
|
|
157
|
-
throw new Error(`${label} config path must not be hardlinked`);
|
|
158
|
-
}
|
|
272
|
+
function getSafeTempOpenFlags() {
|
|
273
|
+
const baseFlags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL;
|
|
274
|
+
return typeof constants.O_NOFOLLOW === 'number' ? baseFlags | constants.O_NOFOLLOW : baseFlags;
|
|
159
275
|
}
|
|
160
276
|
|
|
161
|
-
function
|
|
277
|
+
function writeFileExclusively(path, content, errorMessage, validateBeforeWrite) {
|
|
162
278
|
validateBeforeWrite();
|
|
163
|
-
|
|
164
|
-
|
|
279
|
+
let fd = openSync(path, getSafeTempOpenFlags(), 0o600);
|
|
280
|
+
let closeError = null;
|
|
281
|
+
let identity = null;
|
|
282
|
+
try {
|
|
283
|
+
validateOpenFile(fd, path, errorMessage);
|
|
284
|
+
validateBeforeWrite();
|
|
285
|
+
validateOpenFile(fd, path, errorMessage);
|
|
286
|
+
identity = createFileIdentity(path);
|
|
287
|
+
if (identity === null) {
|
|
288
|
+
throw new Error(errorMessage);
|
|
289
|
+
}
|
|
290
|
+
fchmodSync(fd, 0o600);
|
|
291
|
+
writeFileSync(fd, content, 'utf8');
|
|
292
|
+
const writeFd = fd;
|
|
293
|
+
fd = null;
|
|
294
|
+
closeSync(writeFd);
|
|
295
|
+
return identity;
|
|
296
|
+
} finally {
|
|
297
|
+
if (fd !== null) {
|
|
298
|
+
try {
|
|
299
|
+
closeSync(fd);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
closeError = error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (closeError) {
|
|
305
|
+
throw closeError;
|
|
306
|
+
}
|
|
165
307
|
}
|
|
308
|
+
}
|
|
166
309
|
|
|
167
|
-
|
|
310
|
+
function writeFileAtomically(configPath, content, errorMessage, validateBeforeWrite) {
|
|
311
|
+
validateBeforeWrite();
|
|
312
|
+
|
|
313
|
+
const tempPath = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
314
|
+
let fd = openSync(tempPath, getSafeTempOpenFlags(), 0o600);
|
|
315
|
+
let renamed = false;
|
|
316
|
+
let closeError = null;
|
|
168
317
|
try {
|
|
169
|
-
|
|
170
|
-
validateOpenConfigFile(fd, configPath, label);
|
|
318
|
+
validateOpenFile(fd, tempPath, errorMessage);
|
|
171
319
|
fchmodSync(fd, 0o600);
|
|
172
|
-
ftruncateSync(fd, 0);
|
|
173
320
|
writeFileSync(fd, content, 'utf8');
|
|
321
|
+
const writeFd = fd;
|
|
322
|
+
fd = null;
|
|
323
|
+
closeSync(writeFd);
|
|
324
|
+
validateBeforeWrite();
|
|
325
|
+
const readFd = openSync(tempPath, getSafeReadOpenFlags());
|
|
326
|
+
try {
|
|
327
|
+
validateOpenFile(readFd, tempPath, errorMessage);
|
|
328
|
+
} finally {
|
|
329
|
+
closeSync(readFd);
|
|
330
|
+
}
|
|
331
|
+
renameSync(tempPath, configPath);
|
|
332
|
+
renamed = true;
|
|
174
333
|
} finally {
|
|
175
|
-
|
|
334
|
+
if (fd !== null) {
|
|
335
|
+
try {
|
|
336
|
+
closeSync(fd);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
closeError = error;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
if (!renamed && existsSync(tempPath)) {
|
|
343
|
+
unlinkSync(tempPath);
|
|
344
|
+
}
|
|
345
|
+
} finally {
|
|
346
|
+
if (closeError) {
|
|
347
|
+
throw closeError;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
176
350
|
}
|
|
177
351
|
}
|
|
178
352
|
|
|
179
353
|
function writeProjectConfig(projectRoot, peaksRoot, configPath, content) {
|
|
180
|
-
|
|
354
|
+
writeFileAtomically(configPath, content, 'Project config path changed during write', () => validateProjectConfigPaths(projectRoot, peaksRoot, configPath));
|
|
181
355
|
}
|
|
182
356
|
|
|
183
357
|
function writeUserConfig(userRoot, peaksRoot, configPath, content) {
|
|
184
|
-
|
|
358
|
+
writeFileAtomically(configPath, content, 'User config path changed during write', () => validateUserConfigPaths(userRoot, peaksRoot, configPath));
|
|
185
359
|
}
|
|
186
360
|
|
|
187
361
|
function resolveProjectRoot(options) {
|
|
@@ -259,6 +433,7 @@ export function installBundledSkills(options = {}) {
|
|
|
259
433
|
const installed = [];
|
|
260
434
|
const skipped = [];
|
|
261
435
|
mkdirSync(targetRoot, { recursive: true });
|
|
436
|
+
const validateSkillsRoot = createInstallRootValidator(targetRoot, 'Peaks skills');
|
|
262
437
|
|
|
263
438
|
for (const skillName of readdirSync(skillsRoot)) {
|
|
264
439
|
const sourcePath = join(skillsRoot, skillName);
|
|
@@ -272,12 +447,15 @@ export function installBundledSkills(options = {}) {
|
|
|
272
447
|
const current = getPathStats(targetPath);
|
|
273
448
|
if (current) {
|
|
274
449
|
const managedTarget = getManagedTarget(targetPath);
|
|
275
|
-
|
|
450
|
+
const linkTarget = current.isSymbolicLink() ? readlinkSync(targetPath) : null;
|
|
451
|
+
if (linkTarget === sourcePath) {
|
|
276
452
|
installed.push(skillName);
|
|
277
453
|
continue;
|
|
278
454
|
}
|
|
279
|
-
if (isBrokenSymlink(current, targetPath) && managedTarget ===
|
|
455
|
+
if ((current.isSymbolicLink() || isBrokenSymlink(current, targetPath)) && managedTarget === linkTarget) {
|
|
456
|
+
validateSkillsRoot();
|
|
280
457
|
unlinkSync(targetPath);
|
|
458
|
+
validateSkillsRoot();
|
|
281
459
|
unlinkSync(`${targetPath}.peaks-managed`);
|
|
282
460
|
} else {
|
|
283
461
|
skipped.push(skillName);
|
|
@@ -285,8 +463,19 @@ export function installBundledSkills(options = {}) {
|
|
|
285
463
|
}
|
|
286
464
|
}
|
|
287
465
|
|
|
466
|
+
validateSkillsRoot();
|
|
288
467
|
symlinkSync(sourcePath, targetPath, process.platform === 'win32' ? 'junction' : 'dir');
|
|
289
|
-
|
|
468
|
+
try {
|
|
469
|
+
validateSkillsRoot();
|
|
470
|
+
markManagedPeaksLink(targetPath, sourcePath);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
validateSkillsRoot();
|
|
473
|
+
const created = getPathStats(targetPath);
|
|
474
|
+
if (created?.isSymbolicLink() && readlinkSync(targetPath) === sourcePath) {
|
|
475
|
+
unlinkSync(targetPath);
|
|
476
|
+
}
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
290
479
|
installed.push(skillName);
|
|
291
480
|
}
|
|
292
481
|
|
|
@@ -305,6 +494,7 @@ export function installBundledOutputStyles(options = {}) {
|
|
|
305
494
|
const installed = [];
|
|
306
495
|
const skipped = [];
|
|
307
496
|
mkdirSync(targetRoot, { recursive: true });
|
|
497
|
+
const validateOutputStylesRoot = createInstallRootValidator(targetRoot, 'Peaks output styles');
|
|
308
498
|
|
|
309
499
|
for (const outputStyleName of readdirSync(outputStylesRoot)) {
|
|
310
500
|
const sourcePath = join(outputStylesRoot, outputStyleName);
|
|
@@ -317,8 +507,14 @@ export function installBundledOutputStyles(options = {}) {
|
|
|
317
507
|
const current = getPathStats(targetPath);
|
|
318
508
|
if (current) {
|
|
319
509
|
const managedTarget = getManagedTarget(targetPath);
|
|
320
|
-
|
|
510
|
+
const managedTargetIdentity = getManagedPeaksOutputStyleIdentity(managedTarget, targetPath, sourcePath, outputStyleName);
|
|
511
|
+
if (isSameFileIdentity(targetPath, managedTargetIdentity)) {
|
|
512
|
+
validateOutputStylesRoot();
|
|
513
|
+
if (!isSameFileIdentity(targetPath, managedTargetIdentity)) {
|
|
514
|
+
throw new Error('Peaks output style path changed during unlink');
|
|
515
|
+
}
|
|
321
516
|
unlinkSync(targetPath);
|
|
517
|
+
validateOutputStylesRoot();
|
|
322
518
|
unlinkSync(`${targetPath}.peaks-managed`);
|
|
323
519
|
} else {
|
|
324
520
|
skipped.push(outputStyleName);
|
|
@@ -326,8 +522,25 @@ export function installBundledOutputStyles(options = {}) {
|
|
|
326
522
|
}
|
|
327
523
|
}
|
|
328
524
|
|
|
329
|
-
|
|
330
|
-
|
|
525
|
+
const markerPath = `${targetPath}.peaks-managed`;
|
|
526
|
+
validateManagedMarkerPath(markerPath);
|
|
527
|
+
if (!current && existsSync(markerPath)) {
|
|
528
|
+
validateOutputStylesRoot();
|
|
529
|
+
unlinkSync(markerPath);
|
|
530
|
+
}
|
|
531
|
+
const createdTargetIdentity = writeFileExclusively(targetPath, readPackageSourceFile(sourcePath), 'Peaks output style path changed during write', validateOutputStylesRoot);
|
|
532
|
+
try {
|
|
533
|
+
writeFileExclusively(markerPath, createManagedOutputStyleMarker(sourcePath, outputStyleName), 'Peaks managed marker path changed during write', () => {
|
|
534
|
+
validateOutputStylesRoot();
|
|
535
|
+
validateManagedMarkerPath(markerPath);
|
|
536
|
+
});
|
|
537
|
+
} catch (error) {
|
|
538
|
+
validateOutputStylesRoot();
|
|
539
|
+
if (isSameFileIdentity(targetPath, createdTargetIdentity)) {
|
|
540
|
+
unlinkSync(targetPath);
|
|
541
|
+
}
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
331
544
|
installed.push(outputStyleName);
|
|
332
545
|
}
|
|
333
546
|
|