@cjhyy/code-shell-core 0.8.9 → 0.8.11
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/dist/automation/scheduler.js +49 -0
- package/dist/automation/store.d.ts +1 -1
- package/dist/automation/store.js +184 -10
- package/dist/cli/agent-server-stdio.js +7 -0
- package/dist/credentials/store.d.ts +14 -0
- package/dist/credentials/store.js +245 -42
- package/dist/engine/engine.js +45 -6
- package/dist/engine/file-history-hook.js +24 -5
- package/dist/engine/run-types.d.ts +9 -0
- package/dist/engine/turn-loop.js +9 -8
- package/dist/goal/lifecycle.d.ts +2 -0
- package/dist/goal/lifecycle.js +56 -33
- package/dist/index.d.ts +2 -3
- package/dist/index.internal.d.ts +1 -0
- package/dist/index.internal.js +1 -0
- package/dist/index.js +2 -2
- package/dist/links/cli.d.ts +2 -0
- package/dist/links/cli.js +11 -4
- package/dist/model-catalog/index.js +19 -4
- package/dist/model-catalog/save-entry.js +122 -61
- package/dist/model-catalog/types.js +27 -23
- package/dist/panel-apps/installer.js +27 -14
- package/dist/panel-apps/registry.js +60 -12
- package/dist/plugins/installedPlugins.d.ts +4 -0
- package/dist/plugins/installedPlugins.js +70 -30
- package/dist/plugins/installer/types.d.ts +12 -12
- package/dist/plugins/installer/update.js +37 -38
- package/dist/plugins/knownMarketplaces.d.ts +7 -3
- package/dist/plugins/knownMarketplaces.js +127 -23
- package/dist/plugins/pluginCatalog.js +18 -4
- package/dist/plugins/pluginHookApproval.js +56 -60
- package/dist/plugins/pluginMcpApproval.js +50 -52
- package/dist/profile/catalog-store.js +39 -4
- package/dist/profile/catalog.js +55 -15
- package/dist/profile/store.js +51 -21
- package/dist/protocol/chat-session-manager.d.ts +9 -0
- package/dist/protocol/chat-session-manager.js +13 -0
- package/dist/protocol/chat-session.d.ts +5 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/server.d.ts +2 -0
- package/dist/protocol/server.js +75 -29
- package/dist/protocol/types.d.ts +8 -0
- package/dist/run/FileRunStore.d.ts +2 -0
- package/dist/run/FileRunStore.js +153 -18
- package/dist/run/Heartbeat.js +63 -4
- package/dist/services/auto-dream.js +39 -17
- package/dist/services/session-memory.js +107 -8
- package/dist/session/file-history.d.ts +63 -2
- package/dist/session/file-history.js +593 -86
- package/dist/session/session-manager.d.ts +1 -0
- package/dist/session/session-manager.js +52 -21
- package/dist/session/transcript.js +33 -3
- package/dist/session/undo-target.d.ts +15 -6
- package/dist/session/undo-target.js +26 -9
- package/dist/settings/manager.d.ts +22 -3
- package/dist/settings/manager.js +185 -50
- package/dist/settings/schema.d.ts +3 -3
- package/dist/sources/adapters/local-files.js +49 -4
- package/dist/sources/catalog.js +64 -18
- package/dist/sources/types.d.ts +3 -3
- package/dist/sources/types.js +7 -4
- package/dist/themes/installer.js +192 -28
- package/dist/tool-system/builtin/add-marketplace.js +21 -1
- package/dist/tool-system/builtin/cron.d.ts +2 -1
- package/dist/tool-system/builtin/cron.js +20 -6
- package/dist/tool-system/builtin/index.js +44 -0
- package/dist/tool-system/builtin/install-capability.d.ts +52 -0
- package/dist/tool-system/builtin/install-capability.js +1057 -0
- package/dist/tool-system/builtin/skill.js +3 -1
- package/dist/tool-system/executor.js +1 -0
- package/dist/tool-system/registry.js +5 -0
- package/dist/tool-system/sandbox/index.d.ts +1 -0
- package/dist/tool-system/sandbox/index.js +4 -1
- package/dist/utils/file-mutex.d.ts +2 -0
- package/dist/utils/file-mutex.js +29 -4
- package/package.json +2 -1
|
@@ -290,6 +290,7 @@ export declare class SessionManager {
|
|
|
290
290
|
*/
|
|
291
291
|
saveStateOrUpdateFields(state: SessionState, partial: SessionStateFieldPatch): boolean;
|
|
292
292
|
private saveStateAttempt;
|
|
293
|
+
private writeStateAtomically;
|
|
293
294
|
private acquireStateLock;
|
|
294
295
|
/** Recover only the old implementation's regular-file orphan lock. */
|
|
295
296
|
private prepareLegacyStateLock;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session lifecycle manager.
|
|
3
3
|
*/
|
|
4
|
-
import { closeSync, existsSync, mkdirSync, lstatSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { closeSync, chmodSync, existsSync, mkdirSync, lstatSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
@@ -304,7 +304,9 @@ export class SessionManager {
|
|
|
304
304
|
resolveCapabilities()
|
|
305
305
|
.map((capability) => capability.sessionWorkspace)
|
|
306
306
|
.find((candidate) => candidate !== undefined);
|
|
307
|
-
mkdirSync(this.sessionsDir, { recursive: true });
|
|
307
|
+
mkdirSync(this.sessionsDir, { recursive: true, mode: 0o700 });
|
|
308
|
+
if (process.platform !== "win32")
|
|
309
|
+
chmodSync(this.sessionsDir, 0o700);
|
|
308
310
|
this.cleanupStaleForkStaging();
|
|
309
311
|
}
|
|
310
312
|
processLocalKey(sessionId) {
|
|
@@ -418,13 +420,16 @@ export class SessionManager {
|
|
|
418
420
|
startedAt: state.startedAt,
|
|
419
421
|
kind,
|
|
420
422
|
});
|
|
423
|
+
if (transcript.flushFailed()) {
|
|
424
|
+
throw new SessionError(`Failed to persist initial transcript for ${sessionId}`);
|
|
425
|
+
}
|
|
421
426
|
const bundle = { state, transcript };
|
|
422
427
|
this.storeProcessLocalBundle(bundle);
|
|
423
428
|
return bundle;
|
|
424
429
|
}
|
|
425
430
|
const sessionDir = join(this.sessionsDir, sessionId);
|
|
426
431
|
try {
|
|
427
|
-
mkdirSync(sessionDir);
|
|
432
|
+
mkdirSync(sessionDir, { mode: 0o700 });
|
|
428
433
|
}
|
|
429
434
|
catch (err) {
|
|
430
435
|
if (err.code === "EEXIST") {
|
|
@@ -434,20 +439,26 @@ export class SessionManager {
|
|
|
434
439
|
}
|
|
435
440
|
// Atomic write (tmp+rename) like saveState, so a crash during this one-time
|
|
436
441
|
// create can't leave a torn state.json that resume() then fails to parse.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
442
|
+
try {
|
|
443
|
+
const stateTarget = join(sessionDir, "state.json");
|
|
444
|
+
this.writeStateAtomically(stateTarget, state, "create");
|
|
445
|
+
const transcript = new Transcript(join(sessionDir, "transcript.jsonl"));
|
|
446
|
+
transcript.append("session_meta", {
|
|
447
|
+
sessionId,
|
|
448
|
+
cwd,
|
|
449
|
+
model,
|
|
450
|
+
provider,
|
|
451
|
+
startedAt: state.startedAt,
|
|
452
|
+
kind,
|
|
453
|
+
});
|
|
454
|
+
return { state, transcript };
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
// A failed first materialization must not reserve the session id forever
|
|
458
|
+
// with an empty/partial directory.
|
|
459
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
451
462
|
}
|
|
452
463
|
/** Whether a persisted or process-local session exists. */
|
|
453
464
|
exists(sessionId) {
|
|
@@ -1025,6 +1036,10 @@ export class SessionManager {
|
|
|
1025
1036
|
throw new SessionError(`Session state is corrupt for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1026
1037
|
}
|
|
1027
1038
|
const transcriptFile = join(sessionDir, "transcript.jsonl");
|
|
1039
|
+
if (process.platform !== "win32") {
|
|
1040
|
+
chmodSync(sessionDir, 0o700);
|
|
1041
|
+
chmodSync(stateFile, 0o600);
|
|
1042
|
+
}
|
|
1028
1043
|
const transcript = Transcript.loadFromFile(transcriptFile);
|
|
1029
1044
|
state.kind = normalizedSessionKind(state.kind);
|
|
1030
1045
|
state.status = "active";
|
|
@@ -1369,9 +1384,7 @@ export class SessionManager {
|
|
|
1369
1384
|
state.title = persisted.title;
|
|
1370
1385
|
}
|
|
1371
1386
|
state.stateRevision = (persistedRevision ?? incomingRevision ?? 0) + 1;
|
|
1372
|
-
|
|
1373
|
-
writeFileSync(tmp, JSON.stringify(stateForPersistence(state), null, 2), "utf-8");
|
|
1374
|
-
renameSync(tmp, target);
|
|
1387
|
+
this.writeStateAtomically(target, stateForPersistence(state));
|
|
1375
1388
|
return { ok: true };
|
|
1376
1389
|
}
|
|
1377
1390
|
finally {
|
|
@@ -1383,6 +1396,24 @@ export class SessionManager {
|
|
|
1383
1396
|
}
|
|
1384
1397
|
}
|
|
1385
1398
|
}
|
|
1399
|
+
writeStateAtomically(target, state, label = "state") {
|
|
1400
|
+
const tmp = `${target}.${process.pid}.${nanoid(8)}.${label}.tmp`;
|
|
1401
|
+
try {
|
|
1402
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2), {
|
|
1403
|
+
encoding: "utf-8",
|
|
1404
|
+
mode: 0o600,
|
|
1405
|
+
});
|
|
1406
|
+
renameSync(tmp, target);
|
|
1407
|
+
}
|
|
1408
|
+
finally {
|
|
1409
|
+
try {
|
|
1410
|
+
rmSync(tmp, { force: true });
|
|
1411
|
+
}
|
|
1412
|
+
catch {
|
|
1413
|
+
// Preserve the original write/rename error; cleanup is best effort.
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1386
1417
|
acquireStateLock(target) {
|
|
1387
1418
|
const lockPath = `${target}.lock`;
|
|
1388
1419
|
for (let attempt = 0; attempt <= SESSION_STATE_LOCK_RETRY_DELAYS_MS.length; attempt++) {
|
|
@@ -1644,7 +1675,7 @@ export class SessionManager {
|
|
|
1644
1675
|
const stagingDir = join(this.sessionsDir, `.pending-fork-${targetSessionId}-${nanoid(8)}`);
|
|
1645
1676
|
let published = false;
|
|
1646
1677
|
try {
|
|
1647
|
-
mkdirSync(stagingDir);
|
|
1678
|
+
mkdirSync(stagingDir, { mode: 0o700 });
|
|
1648
1679
|
writeFileSync(join(stagingDir, "state.json"), JSON.stringify(state, null, 2), {
|
|
1649
1680
|
encoding: "utf-8",
|
|
1650
1681
|
mode: 0o600,
|
|
@@ -2,10 +2,36 @@
|
|
|
2
2
|
* Transcript — JSONL event log (NOT chat history).
|
|
3
3
|
* toMessages() derives Message[] from events for sending to LLM.
|
|
4
4
|
*/
|
|
5
|
-
import { appendFileSync,
|
|
5
|
+
import { appendFileSync, chmodSync, closeSync, existsSync, fchmodSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, writeFileSync, } from "node:fs";
|
|
6
6
|
import { dirname } from "node:path";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
import { logger } from "../logging/logger.js";
|
|
9
|
+
function appendTranscriptLine(filePath, data) {
|
|
10
|
+
// Use one append-mode descriptor so concurrent OS writers cannot overwrite
|
|
11
|
+
// one another. Also repair the record boundary after a crash-torn final line;
|
|
12
|
+
// otherwise the next valid event is concatenated to the fragment and both
|
|
13
|
+
// are discarded on replay.
|
|
14
|
+
const fd = openSync(filePath, "a+", 0o600);
|
|
15
|
+
try {
|
|
16
|
+
if (process.platform !== "win32")
|
|
17
|
+
fchmodSync(fd, 0o600);
|
|
18
|
+
const size = fstatSync(fd).size;
|
|
19
|
+
let prefix = "";
|
|
20
|
+
if (size > 0) {
|
|
21
|
+
const lastByte = Buffer.allocUnsafe(1);
|
|
22
|
+
readSync(fd, lastByte, 0, 1, size - 1);
|
|
23
|
+
if (lastByte[0] !== 0x0a)
|
|
24
|
+
prefix = "\n";
|
|
25
|
+
}
|
|
26
|
+
appendFileSync(fd, prefix + data, "utf-8");
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
closeSync(fd);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const defaultTranscriptWriter = (filePath, data) => {
|
|
33
|
+
appendTranscriptLine(filePath, data);
|
|
34
|
+
};
|
|
9
35
|
const CONTEXT_EVENT_TYPES = new Set([
|
|
10
36
|
"message",
|
|
11
37
|
"tool_use",
|
|
@@ -68,7 +94,7 @@ export class Transcript {
|
|
|
68
94
|
getFilePath() {
|
|
69
95
|
return this.filePath;
|
|
70
96
|
}
|
|
71
|
-
constructor(filePath, writer =
|
|
97
|
+
constructor(filePath, writer = defaultTranscriptWriter, options = {}) {
|
|
72
98
|
this.filePath = filePath;
|
|
73
99
|
this.writer = writer;
|
|
74
100
|
this.persistent = options.persistent !== false;
|
|
@@ -76,7 +102,11 @@ export class Transcript {
|
|
|
76
102
|
return;
|
|
77
103
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
78
104
|
if (!existsSync(filePath)) {
|
|
79
|
-
writeFileSync(filePath, "", "utf-8");
|
|
105
|
+
writeFileSync(filePath, "", { encoding: "utf-8", mode: 0o600 });
|
|
106
|
+
}
|
|
107
|
+
else if (process.platform !== "win32") {
|
|
108
|
+
// Tighten transcripts created by older releases on first use.
|
|
109
|
+
chmodSync(filePath, 0o600);
|
|
80
110
|
}
|
|
81
111
|
}
|
|
82
112
|
/** A process-local transcript that never creates or appends a file. */
|
|
@@ -55,12 +55,21 @@ export declare function latestTurnUndoTargets(snapshots: FileSnapshot[]): FileSn
|
|
|
55
55
|
* `redoLatestTurn` should re-apply — but ONLY when that turn is still the latest
|
|
56
56
|
* undone state (i.e. nothing newer happened since the undo). Otherwise [].
|
|
57
57
|
*
|
|
58
|
-
* Rule: let R = the
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
58
|
+
* Rule: let R = the LOWEST turnSeq present in `redoRecords` (a RedoRecord exists
|
|
59
|
+
* only for a turn that was undone and not yet redone). Undo walks BACKWARD down
|
|
60
|
+
* the turn stack, so after `undo(2); undo(1)` both records are pending and the
|
|
61
|
+
* one to re-apply first is turn 1 — the most recently undone, i.e. the lowest.
|
|
62
|
+
* Selecting the greatest instead replayed turns out of order: `redo` jumped
|
|
63
|
+
* straight to turn 2, skipping turn 1's state entirely and stranding turn 1's
|
|
64
|
+
* record forever (its snapshots stay `undone`, so it never becomes selectable).
|
|
65
|
+
* With two files that also produced a state that never existed — turn 1 still
|
|
66
|
+
* reverted while turn 2 was applied.
|
|
67
|
+
*
|
|
68
|
+
* R is redoable iff NO snapshot belongs to a strictly newer LIVE (non-undone)
|
|
69
|
+
* turn — a fresh edit after the undo supersedes the redo and invalidates it
|
|
70
|
+
* (spec: "新轮使 redo 失效"). Created-only turns have no pre-turn snapshot, so
|
|
71
|
+
* the RedoRecord itself is the sole evidence and is honoured as long as nothing
|
|
72
|
+
* newer superseded it.
|
|
64
73
|
*
|
|
65
74
|
* Empty `redoRecords` → []. fs-free so /redo's target decision is unit-testable.
|
|
66
75
|
*/
|
|
@@ -114,25 +114,42 @@ export function latestTurnUndoTargets(snapshots) {
|
|
|
114
114
|
* `redoLatestTurn` should re-apply — but ONLY when that turn is still the latest
|
|
115
115
|
* undone state (i.e. nothing newer happened since the undo). Otherwise [].
|
|
116
116
|
*
|
|
117
|
-
* Rule: let R = the
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
117
|
+
* Rule: let R = the LOWEST turnSeq present in `redoRecords` (a RedoRecord exists
|
|
118
|
+
* only for a turn that was undone and not yet redone). Undo walks BACKWARD down
|
|
119
|
+
* the turn stack, so after `undo(2); undo(1)` both records are pending and the
|
|
120
|
+
* one to re-apply first is turn 1 — the most recently undone, i.e. the lowest.
|
|
121
|
+
* Selecting the greatest instead replayed turns out of order: `redo` jumped
|
|
122
|
+
* straight to turn 2, skipping turn 1's state entirely and stranding turn 1's
|
|
123
|
+
* record forever (its snapshots stay `undone`, so it never becomes selectable).
|
|
124
|
+
* With two files that also produced a state that never existed — turn 1 still
|
|
125
|
+
* reverted while turn 2 was applied.
|
|
126
|
+
*
|
|
127
|
+
* R is redoable iff NO snapshot belongs to a strictly newer LIVE (non-undone)
|
|
128
|
+
* turn — a fresh edit after the undo supersedes the redo and invalidates it
|
|
129
|
+
* (spec: "新轮使 redo 失效"). Created-only turns have no pre-turn snapshot, so
|
|
130
|
+
* the RedoRecord itself is the sole evidence and is honoured as long as nothing
|
|
131
|
+
* newer superseded it.
|
|
123
132
|
*
|
|
124
133
|
* Empty `redoRecords` → []. fs-free so /redo's target decision is unit-testable.
|
|
125
134
|
*/
|
|
126
135
|
export function latestRedoTargets(redoRecords, snapshots) {
|
|
127
136
|
if (redoRecords.length === 0)
|
|
128
137
|
return [];
|
|
138
|
+
// The most recently undone turn is the LOWEST pending one (undo pops the
|
|
139
|
+
// stack from the top down), and it is the next one to re-apply.
|
|
140
|
+
let nextRedoTurn = Infinity;
|
|
141
|
+
for (const r of redoRecords) {
|
|
142
|
+
if (r.turnSeq < nextRedoTurn)
|
|
143
|
+
nextRedoTurn = r.turnSeq;
|
|
144
|
+
}
|
|
145
|
+
// Supersession is still judged against the NEWEST pending redo turn: a live
|
|
146
|
+
// snapshot newer than that is a fresh edit made after the undo, which
|
|
147
|
+
// invalidates the whole pending redo chain rather than just one turn.
|
|
129
148
|
let maxRedoTurn = -Infinity;
|
|
130
149
|
for (const r of redoRecords) {
|
|
131
150
|
if (r.turnSeq > maxRedoTurn)
|
|
132
151
|
maxRedoTurn = r.turnSeq;
|
|
133
152
|
}
|
|
134
|
-
// If any LIVE snapshot belongs to a strictly newer turn, the redo turn is no
|
|
135
|
-
// longer the latest undone state → not redoable.
|
|
136
153
|
for (const s of snapshots) {
|
|
137
154
|
if (s.undone)
|
|
138
155
|
continue;
|
|
@@ -140,5 +157,5 @@ export function latestRedoTargets(redoRecords, snapshots) {
|
|
|
140
157
|
if (k > maxRedoTurn)
|
|
141
158
|
return [];
|
|
142
159
|
}
|
|
143
|
-
return redoRecords.filter((r) => r.turnSeq ===
|
|
160
|
+
return redoRecords.filter((r) => r.turnSeq === nextRedoTurn);
|
|
144
161
|
}
|
|
@@ -145,24 +145,43 @@ export declare class SettingsManager {
|
|
|
145
145
|
* cache invalidation mirror saveUserSetting.
|
|
146
146
|
*/
|
|
147
147
|
saveProjectSetting(key: string, value: unknown, cwd: string): void;
|
|
148
|
+
/**
|
|
149
|
+
* Persist a machine-private setting for one project. The local layer has
|
|
150
|
+
* higher precedence than the shared project layer and lives at
|
|
151
|
+
* `${cwd}/.code-shell/settings.local.json`, matching the file already read
|
|
152
|
+
* by {@link load}. It is useful for MCP endpoints or policy that should not
|
|
153
|
+
* be shared with collaborators.
|
|
154
|
+
*/
|
|
155
|
+
saveLocalSetting(key: string, value: unknown, cwd: string): void;
|
|
148
156
|
/**
|
|
149
157
|
* Delete a single dotted key from the PROJECT-level config file. Used to
|
|
150
158
|
* express "inherit" — we don't persist the literal "inherit"; we remove the
|
|
151
159
|
* override key. No-ops if the file or any intermediate segment is absent.
|
|
152
160
|
*/
|
|
153
161
|
deleteProjectSetting(key: string, cwd: string): void;
|
|
162
|
+
/** Delete one dotted key from the machine-private project settings layer. */
|
|
163
|
+
deleteLocalSetting(key: string, cwd: string): void;
|
|
164
|
+
/** Delete one dotted key from the user settings layer. */
|
|
165
|
+
deleteUserSetting(key: string): void;
|
|
166
|
+
private deleteSettingFromFile;
|
|
154
167
|
/**
|
|
155
168
|
* Read ONE scope's raw settings file, validated but UNMERGED. Capability
|
|
156
169
|
* overlay math needs the project overlay and the user/global baseline
|
|
157
170
|
* separately — the merged get() collapses provenance and can't express
|
|
158
171
|
* tri-state inheritance. user → ~/.code-shell/settings.json, project →
|
|
159
|
-
* ${cwd}/.code-shell/settings.json
|
|
160
|
-
*
|
|
172
|
+
* ${cwd}/.code-shell/settings.json, local →
|
|
173
|
+
* ${cwd}/.code-shell/settings.local.json. Only keys actually present in the
|
|
174
|
+
* file are returned (defaults are not synthesized), so an absent file → {}.
|
|
161
175
|
*/
|
|
162
|
-
getForScope(scope: "user" | "project", cwd?: string): Partial<ValidatedSettings>;
|
|
176
|
+
getForScope(scope: "user" | "project" | "local", cwd?: string): Partial<ValidatedSettings>;
|
|
163
177
|
private projectSettingsPath;
|
|
178
|
+
private localSettingsPath;
|
|
179
|
+
private validateProjectCwd;
|
|
180
|
+
/** Resolve the project root once and refuse a linked/non-directory state root. */
|
|
181
|
+
private tryProjectSettingsPath;
|
|
164
182
|
private readJsonObject;
|
|
165
183
|
private atomicWriteJson;
|
|
184
|
+
private writeBackup;
|
|
166
185
|
/**
|
|
167
186
|
* Read-modify-write a settings file under a cross-process lock.
|
|
168
187
|
*
|