claude-dev-env 7.6.0 → 7.7.0
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/.agents/skills/session-continuity/README.md +19 -9
- package/AGENTS.md +48 -71
- package/_shared/advisor/advisor-protocol.md +2 -2
- package/_shared/advisor/reference/advisor-block.md +1 -1
- package/_shared/advisor/reference/astra-rung.md +2 -2
- package/_shared/advisor/scripts/codex_astra_advisor.py +122 -23
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +76 -9
- package/_shared/advisor/scripts/tests/test_advisor_route_constants.py +51 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +52 -2
- package/bin/install-constants.mjs +3 -0
- package/bin/install-session-continuity.mjs +73 -30
- package/bin/install-session-continuity.test.mjs +52 -1
- package/bin/install-transaction.mjs +41 -44
- package/bin/install.cursor-rules.test.mjs +311 -3
- package/bin/install.mjs +226 -95
- package/bin/install.test.mjs +57 -0
- package/bin/install.transaction.test.mjs +61 -1
- package/bin/pstack-model-preferences.codex.json +1 -0
- package/bin/pstack.mjs +28 -6
- package/bin/pstack.test.mjs +48 -0
- package/bin/select-pstack-models.test.mjs +375 -26
- package/bin/subagent-model-policy.test.mjs +399 -0
- package/docs/codex-compatibility.md +19 -5
- package/docs/subagent-model-routing.md +50 -0
- package/hooks/hooks.json +10 -0
- package/hooks/routing/subagent_model_routing.mjs +81 -0
- package/hooks/routing/subagent_model_routing.test.mjs +218 -0
- package/package.json +1 -1
- package/rules/pstack-models.md +22 -0
- package/rules/subagent-model-policy.json +85 -0
- package/scripts/resolve_advisor_model_route.mjs +52 -0
- package/scripts/select_pstack_models.mjs +203 -38
- package/scripts/subagent_model_policy.mjs +620 -0
- package/scripts/tests/resolve_advisor_model_route.test.mjs +51 -0
- package/scripts/tests/test_codex_compat_materializer.py +15 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""Behavioral tests for the executable Codex Astra advisor path."""
|
|
2
|
-
import io
|
|
3
2
|
import importlib.util
|
|
3
|
+
import io
|
|
4
4
|
import json
|
|
5
5
|
import subprocess
|
|
6
6
|
import sys
|
|
7
7
|
from collections.abc import Callable
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from types import ModuleType
|
|
10
|
+
|
|
10
11
|
import pytest
|
|
11
12
|
|
|
12
13
|
_ProcessRunner = Callable[..., subprocess.CompletedProcess[str]]
|
|
@@ -28,6 +29,7 @@ def _load_astra_module() -> ModuleType:
|
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
astra_advisor = _load_astra_module()
|
|
32
|
+
|
|
31
33
|
SCRIPTS_ROOT = Path(__file__).parent.parent
|
|
32
34
|
USAGE_PROBE_PATH = (
|
|
33
35
|
SCRIPTS_ROOT.parents[1] / "pr-loop" / "scripts" / "codex_usage_probe.py"
|
|
@@ -87,7 +89,37 @@ def test_astra_flag_rejects_legacy_sol_setting() -> None:
|
|
|
87
89
|
|
|
88
90
|
def test_resolve_advisor_effort_uses_shared_setting_and_default() -> None:
|
|
89
91
|
assert astra_advisor.resolve_advisor_effort({"ADVISOR_EFFORT": "HIGH"}) == "high"
|
|
90
|
-
|
|
92
|
+
with pytest.raises(RuntimeError, match="effort is unknown"):
|
|
93
|
+
astra_advisor.resolve_advisor_effort({"ADVISOR_EFFORT": "unknown"})
|
|
94
|
+
assert astra_advisor.resolve_advisor_effort({"ADVISOR_EFFORT": "Light"}) == "low"
|
|
95
|
+
assert astra_advisor.resolve_advisor_effort({"ADVISOR_EFFORT": "XHIGH"}) == "medium"
|
|
96
|
+
assert astra_advisor.resolve_advisor_effort({"ADVISOR_EFFORT": "MAX"}) == "medium"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_advisor_route_uses_edited_policy_model_and_effort(tmp_path: Path) -> None:
|
|
100
|
+
policy = json.loads(
|
|
101
|
+
(SCRIPTS_ROOT.parents[2] / "rules" / "subagent-model-policy.json").read_text(
|
|
102
|
+
encoding="utf-8"
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
for each_entry in policy["roleReplacements"]:
|
|
106
|
+
if (
|
|
107
|
+
each_entry["role"] == "advisor"
|
|
108
|
+
and each_entry["requested"] == {"model": "astra", "effort": "xhigh"}
|
|
109
|
+
):
|
|
110
|
+
each_entry["selected"] = {"model": "luna", "effort": "high"}
|
|
111
|
+
policy_path = tmp_path / "subagent-model-policy.json"
|
|
112
|
+
policy_path.write_text(json.dumps(policy), encoding="utf-8")
|
|
113
|
+
assert astra_advisor.resolve_advisor_pair(
|
|
114
|
+
{"ADVISOR_EFFORT": "xhigh"}, policy_path
|
|
115
|
+
) == ("gpt-5.6-luna", "high")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_advisor_route_blocks_malformed_policy(tmp_path: Path) -> None:
|
|
119
|
+
policy_path = tmp_path / "subagent-model-policy.json"
|
|
120
|
+
policy_path.write_text('{"schemaVersion": 1}', encoding="utf-8")
|
|
121
|
+
with pytest.raises(RuntimeError, match="models must be an object"):
|
|
122
|
+
astra_advisor.resolve_advisor_pair({}, policy_path)
|
|
91
123
|
|
|
92
124
|
|
|
93
125
|
def test_resolve_codex_executable_prefers_override_and_falls_back_to_path(
|
|
@@ -118,6 +150,7 @@ def test_argument_parser_accepts_astra_and_rejects_sol() -> None:
|
|
|
118
150
|
parser = astra_advisor.build_argument_parser()
|
|
119
151
|
parsed = parser.parse_args(["--bind", "--cwd", ".", "--enable-astra"])
|
|
120
152
|
assert parsed.is_astra_requested
|
|
153
|
+
assert parser.parse_args(["--bind", "--cwd", ".", "--effort", "light"]).astra_effort == "light"
|
|
121
154
|
with pytest.raises(SystemExit):
|
|
122
155
|
parser.parse_args(["--bind", "--cwd", ".", "--enable-sol"])
|
|
123
156
|
|
|
@@ -190,6 +223,23 @@ def test_bind_runs_probe_then_codex() -> None:
|
|
|
190
223
|
assert calls[1] == astra_advisor.build_codex_arguments("codex")
|
|
191
224
|
|
|
192
225
|
|
|
226
|
+
def test_bind_routes_astra_xhigh_to_the_policy_effort() -> None:
|
|
227
|
+
calls: list[list[str]] = []
|
|
228
|
+
settings = {**ENABLED_SETTINGS, astra_advisor.ADVISOR_EFFORT_ENV_VAR: "xhigh"}
|
|
229
|
+
reply = astra_advisor.run_codex_astra_advisor(
|
|
230
|
+
"consult",
|
|
231
|
+
Path("."),
|
|
232
|
+
None,
|
|
233
|
+
USAGE_PROBE_PATH,
|
|
234
|
+
settings,
|
|
235
|
+
None,
|
|
236
|
+
_two_step_runner(calls),
|
|
237
|
+
)
|
|
238
|
+
assert reply.successful
|
|
239
|
+
config_index = calls[1].index("--config")
|
|
240
|
+
assert calls[1][config_index + 1] == 'model_reasoning_effort="medium"'
|
|
241
|
+
|
|
242
|
+
|
|
193
243
|
def test_disabled_flag_returns_declined_fallback() -> None:
|
|
194
244
|
reply = astra_advisor.run_codex_astra_advisor(
|
|
195
245
|
"consult",
|
|
@@ -56,6 +56,7 @@ export const RUN_BACKUP_DIRECTORY_NAME_PATTERN =
|
|
|
56
56
|
export const PACKAGE_AGENTS_HOME_DIRECTORY_NAME = '.agents';
|
|
57
57
|
|
|
58
58
|
export const PSTACK_MODEL_RULE_FILE_NAME = 'pstack-models.mdc';
|
|
59
|
+
export const SUBAGENT_MODEL_POLICY_FILE_NAME = 'subagent-model-policy.json';
|
|
59
60
|
export const PSTACK_SEEDED_MODEL_PREFERENCES_FILE_NAMES = Object.freeze([
|
|
60
61
|
'pstack-model-preferences.claude.json',
|
|
61
62
|
'pstack-model-preferences.codex.json',
|
|
@@ -181,3 +182,5 @@ export const WINDOWS_PYTHON_LAUNCHER_COMMAND = 'py -3';
|
|
|
181
182
|
* Maximum time each Python interpreter probe may run during detection.
|
|
182
183
|
*/
|
|
183
184
|
export const PYTHON_PROBE_TIMEOUT_MILLISECONDS = 10_000;
|
|
185
|
+
export const ADDITIONAL_SETTINGS_BLOB_PREFIX = 'settings-';
|
|
186
|
+
export const CODEX_NATIVE_ROUTING_MATCHER = 'multi_agent_v1__spawn_agent';
|
|
@@ -44,41 +44,84 @@ export function mergeContinuityHooks(existing, host, script) {
|
|
|
44
44
|
return mergedConfiguration;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
export const CONTINUITY_SCRIPT_MARKER = '/session-continuity/continuity.mjs';
|
|
48
|
+
|
|
49
|
+
export function isContinuityHookCommand(command) {
|
|
50
|
+
return typeof command === 'string' && command.includes(CONTINUITY_SCRIPT_MARKER);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function continuityScriptPath(roots) {
|
|
54
|
+
return join(roots.skillsInstallDirectory, 'session-continuity', 'continuity.mjs');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function continuityHostConfigurationPaths(roots) {
|
|
58
|
+
return {
|
|
59
|
+
claude: join(roots.managedRoot, 'settings.json'),
|
|
60
|
+
codex: join(dirname(roots.codexRulesInstallDirectory), 'hooks.json'),
|
|
61
|
+
cursor: join(roots.cursorInstallDirectory, 'hooks.json'),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function removeContinuityHooks(configuration) {
|
|
66
|
+
let removedCount = 0;
|
|
67
|
+
for (const [event, entries] of Object.entries(configuration.hooks || {})) {
|
|
68
|
+
const kept = entries.flatMap(entry => {
|
|
69
|
+
if (!entry.hooks) {
|
|
70
|
+
if (!isContinuityHookCommand(entry.command)) return [entry];
|
|
71
|
+
removedCount += 1;
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const hooks = entry.hooks.filter(hook => !isContinuityHookCommand(hook.command));
|
|
75
|
+
removedCount += entry.hooks.length - hooks.length;
|
|
76
|
+
return hooks.length ? [{ ...entry, hooks }] : [];
|
|
77
|
+
});
|
|
78
|
+
if (kept.length) configuration.hooks[event] = kept;
|
|
79
|
+
else delete configuration.hooks[event];
|
|
80
|
+
}
|
|
81
|
+
return removedCount;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function continuityConfigurationPlan(host, path, script) {
|
|
85
|
+
if (!existsSync(dirname(path))) throw new Error(`Host config directory is absent: ${dirname(path)}`);
|
|
86
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
87
|
+
const merged = mergeContinuityHooks(existing ? JSON.parse(existing) : {}, host, script);
|
|
88
|
+
return { host, path, existing, content: JSON.stringify(merged, null, 2) + '\n' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function applyContinuityConfigurationPlan(plan) {
|
|
92
|
+
if (plan.existing === plan.content) return { host: plan.host, path: plan.path, changed: false };
|
|
93
|
+
if (plan.existing !== null) {
|
|
94
|
+
const backup = `${plan.path}.before-session-continuity`;
|
|
95
|
+
if (!existsSync(backup)) writeFileSync(backup, plan.existing, { flag: 'wx', mode: 0o600 });
|
|
96
|
+
}
|
|
97
|
+
const current = existsSync(plan.path) ? readFileSync(plan.path, 'utf8') : null;
|
|
98
|
+
if (current !== plan.existing) throw new Error(`Config changed during setup: ${plan.path}. Rerun setup.`);
|
|
99
|
+
const temporary = `${plan.path}.continuity-${process.pid}.tmp`;
|
|
100
|
+
writeFileSync(temporary, plan.content, { flag: 'wx', mode: 0o600 });
|
|
101
|
+
renameSync(temporary, plan.path);
|
|
102
|
+
if (readFileSync(plan.path, 'utf8') !== plan.content) throw new Error(`Config read-back mismatch: ${plan.path}`);
|
|
103
|
+
return { host: plan.host, path: plan.path, changed: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function configureContinuityHosts(roots, selected) {
|
|
50
107
|
const unsupported = selected.find(host => !supportedHosts.includes(host));
|
|
51
108
|
if (unsupported) throw new Error(`Unsupported host ${unsupported}`);
|
|
52
|
-
const
|
|
53
|
-
const roots = resolveInstallRoot();
|
|
54
|
-
const script = join(roots.skillsInstallDirectory, 'session-continuity', 'continuity.mjs');
|
|
109
|
+
const script = continuityScriptPath(roots);
|
|
55
110
|
if (!existsSync(script) || !existsSync(join(dirname(script), 'SKILL.md'))) {
|
|
56
111
|
throw new Error('Run the full claude-dev-env installer from this checkout first so the companion is in the canonical agents home.');
|
|
57
112
|
}
|
|
58
|
-
const paths =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (plan.existing === plan.content) { console.log(`Already configured: ${plan.path}`); continue; }
|
|
71
|
-
if (plan.existing !== null) {
|
|
72
|
-
const backup = `${plan.path}.before-session-continuity`;
|
|
73
|
-
if (!existsSync(backup)) writeFileSync(backup, plan.existing, { flag: 'wx', mode: 0o600 });
|
|
74
|
-
}
|
|
75
|
-
const current = existsSync(plan.path) ? readFileSync(plan.path, 'utf8') : null;
|
|
76
|
-
if (current !== plan.existing) throw new Error(`Config changed during setup: ${plan.path}. Rerun setup.`);
|
|
77
|
-
const temporary = `${plan.path}.continuity-${process.pid}.tmp`;
|
|
78
|
-
writeFileSync(temporary, plan.content, { flag: 'wx', mode: 0o600 });
|
|
79
|
-
renameSync(temporary, plan.path);
|
|
80
|
-
if (readFileSync(plan.path, 'utf8') !== plan.content) throw new Error(`Config read-back mismatch: ${plan.path}`);
|
|
81
|
-
console.log(`Configured and read back: ${plan.path}`);
|
|
113
|
+
const paths = continuityHostConfigurationPaths(roots);
|
|
114
|
+
return selected
|
|
115
|
+
.map(host => continuityConfigurationPlan(host, paths[host], script))
|
|
116
|
+
.map(applyContinuityConfigurationPlan);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function main() {
|
|
120
|
+
const requested = process.argv.slice(2);
|
|
121
|
+
const selected = requested.length ? requested : supportedHosts;
|
|
122
|
+
const { resolveInstallRoot } = await import('./resolve-install-root.mjs');
|
|
123
|
+
for (const result of configureContinuityHosts(resolveInstallRoot(), selected)) {
|
|
124
|
+
console.log(`${result.changed ? 'Configured and read back' : 'Already configured'}: ${result.path}`);
|
|
82
125
|
}
|
|
83
126
|
console.log('Review and trust the new hooks in each host.');
|
|
84
127
|
}
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
|
-
import {
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
configureContinuityHosts,
|
|
8
|
+
continuityHookConfiguration,
|
|
9
|
+
mergeContinuityHooks,
|
|
10
|
+
removeContinuityHooks,
|
|
11
|
+
} from './install-session-continuity.mjs';
|
|
12
|
+
|
|
13
|
+
function installRootFixture() {
|
|
14
|
+
const root = mkdtempSync(join(tmpdir(), 'continuity-roots-'));
|
|
15
|
+
const skillsInstallDirectory = join(root, 'agents', 'skills');
|
|
16
|
+
const companionDirectory = join(skillsInstallDirectory, 'session-continuity');
|
|
17
|
+
mkdirSync(companionDirectory, { recursive: true });
|
|
18
|
+
writeFileSync(join(companionDirectory, 'continuity.mjs'), 'export default null;\n');
|
|
19
|
+
writeFileSync(join(companionDirectory, 'SKILL.md'), '# companion\n');
|
|
20
|
+
const managedRoot = join(root, 'claude');
|
|
21
|
+
const codexRulesInstallDirectory = join(root, 'codex', 'rules');
|
|
22
|
+
const cursorInstallDirectory = join(root, 'cursor');
|
|
23
|
+
for (const directory of [managedRoot, codexRulesInstallDirectory, cursorInstallDirectory]) {
|
|
24
|
+
mkdirSync(directory, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
return { managedRoot, skillsInstallDirectory, codexRulesInstallDirectory, cursorInstallDirectory };
|
|
27
|
+
}
|
|
4
28
|
|
|
5
29
|
test('setup preserves other hooks and settings, is idempotent, and rejects competing profiles', () => {
|
|
6
30
|
const existing = { permissions: { deny: ['secret'] }, hooks: { SessionStart: [{ hooks: [{ type: 'command', command: 'existing-hook' }] }] } };
|
|
@@ -53,3 +77,30 @@ for (const host of ['claude', 'codex']) {
|
|
|
53
77
|
}
|
|
54
78
|
});
|
|
55
79
|
}
|
|
80
|
+
|
|
81
|
+
test('configureContinuityHosts writes each host config, reads it back, and repeats without a change', () => {
|
|
82
|
+
const roots = installRootFixture();
|
|
83
|
+
const settingsPath = join(roots.managedRoot, 'settings.json');
|
|
84
|
+
writeFileSync(settingsPath, JSON.stringify({ permissions: { deny: ['secret'] } }, null, 2) + '\n');
|
|
85
|
+
const first = configureContinuityHosts(roots, ['claude', 'codex', 'cursor']);
|
|
86
|
+
assert.deepEqual(first.map(result => result.changed), [true, true, true]);
|
|
87
|
+
const written = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
88
|
+
assert.deepEqual(written.permissions.deny, ['secret']);
|
|
89
|
+
const command = written.hooks.SessionStart[0].hooks[0].command;
|
|
90
|
+
assert.equal(command.endsWith('session-continuity/continuity.mjs" hook claude'), true);
|
|
91
|
+
const second = configureContinuityHosts(roots, ['claude', 'codex', 'cursor']);
|
|
92
|
+
assert.deepEqual(second.map(result => result.changed), [false, false, false]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('removeContinuityHooks takes out only the companion registrations', () => {
|
|
96
|
+
const roots = installRootFixture();
|
|
97
|
+
configureContinuityHosts(roots, ['claude']);
|
|
98
|
+
const settingsPath = join(roots.managedRoot, 'settings.json');
|
|
99
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
100
|
+
settings.hooks.SessionStart.unshift({ hooks: [{ type: 'command', command: 'python other_hook.py' }] });
|
|
101
|
+
const removedCount = removeContinuityHooks(settings);
|
|
102
|
+
assert.equal(removedCount, 3);
|
|
103
|
+
assert.deepEqual(Object.keys(settings.hooks), ['SessionStart']);
|
|
104
|
+
assert.equal(settings.hooks.SessionStart.length, 1);
|
|
105
|
+
assert.equal(settings.hooks.SessionStart[0].hooks[0].command, 'python other_hook.py');
|
|
106
|
+
});
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from 'node:fs';
|
|
31
31
|
import { dirname, join } from 'node:path';
|
|
32
32
|
import { execFileSync } from 'node:child_process';
|
|
33
|
+
import { ADDITIONAL_SETTINGS_BLOB_PREFIX } from './install-constants.mjs';
|
|
33
34
|
|
|
34
35
|
/** Env var that forces a throw after a named mutation phase (tests only). */
|
|
35
36
|
export const INSTALL_FAULT_ENV = 'CLAUDE_DEV_ENV_INSTALL_FAULT';
|
|
@@ -180,19 +181,6 @@ function classifyPath(absolutePath, io) {
|
|
|
180
181
|
return { kind: ENTRY_KIND_MISSING };
|
|
181
182
|
}
|
|
182
183
|
|
|
183
|
-
/**
|
|
184
|
-
* Capture prior settings, manifest, hooksPath, and package-owned files.
|
|
185
|
-
*
|
|
186
|
-
* @param {{
|
|
187
|
-
* managedRoot: string,
|
|
188
|
-
* manifestFilePath: string,
|
|
189
|
-
* settingsPath: string,
|
|
190
|
-
* priorManifestFiles?: string[]|null,
|
|
191
|
-
* journalParentDirectory?: string,
|
|
192
|
-
* io?: object,
|
|
193
|
-
* }} input
|
|
194
|
-
* @returns {object}
|
|
195
|
-
*/
|
|
196
184
|
export function capturePriorInstallSnapshot(input) {
|
|
197
185
|
const io = input.io || {};
|
|
198
186
|
const exists = io.existsSync || existsSync;
|
|
@@ -208,10 +196,23 @@ export function capturePriorInstallSnapshot(input) {
|
|
|
208
196
|
const filesDirectory = join(journalRoot, FILES_DIRECTORY_NAME);
|
|
209
197
|
mkdir(filesDirectory, { recursive: true });
|
|
210
198
|
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
199
|
+
const allSettingsPaths = [
|
|
200
|
+
input.settingsPath,
|
|
201
|
+
...(Array.isArray(input.additionalSettingsPaths) ? input.additionalSettingsPaths : []),
|
|
202
|
+
].filter((eachPath, index, allPaths) => (
|
|
203
|
+
typeof eachPath === 'string'
|
|
204
|
+
&& eachPath !== ''
|
|
205
|
+
&& allPaths.indexOf(eachPath) === index
|
|
206
|
+
));
|
|
207
|
+
const settingsSnapshots = allSettingsPaths.map((settingsPath, index) => {
|
|
208
|
+
const settingsExisted = exists(settingsPath);
|
|
209
|
+
const blobName = index === 0
|
|
210
|
+
? SETTINGS_BLOB_NAME
|
|
211
|
+
: `${ADDITIONAL_SETTINGS_BLOB_PREFIX}${index}.json`;
|
|
212
|
+
if (settingsExisted) copyFile(settingsPath, join(journalRoot, blobName));
|
|
213
|
+
return { path: settingsPath, existed: settingsExisted, blobName };
|
|
214
|
+
});
|
|
215
|
+
const primarySettingsSnapshot = settingsSnapshots[0];
|
|
215
216
|
|
|
216
217
|
const manifestExisted = exists(input.manifestFilePath);
|
|
217
218
|
if (manifestExisted) {
|
|
@@ -255,7 +256,8 @@ export function capturePriorInstallSnapshot(input) {
|
|
|
255
256
|
managedRoot: input.managedRoot,
|
|
256
257
|
settingsPath: input.settingsPath,
|
|
257
258
|
manifestFilePath: input.manifestFilePath,
|
|
258
|
-
|
|
259
|
+
settingsSnapshots,
|
|
260
|
+
settingsExisted: primarySettingsSnapshot?.existed ?? false,
|
|
259
261
|
manifestExisted,
|
|
260
262
|
priorHooksPath,
|
|
261
263
|
allFileEntries,
|
|
@@ -267,23 +269,14 @@ export function capturePriorInstallSnapshot(input) {
|
|
|
267
269
|
managedRoot: input.managedRoot,
|
|
268
270
|
settingsPath: input.settingsPath,
|
|
269
271
|
manifestFilePath: input.manifestFilePath,
|
|
270
|
-
|
|
272
|
+
settingsSnapshots: Object.freeze(settingsSnapshots.map(eachSnapshot => Object.freeze(eachSnapshot))),
|
|
273
|
+
settingsExisted: primarySettingsSnapshot?.existed ?? false,
|
|
271
274
|
manifestExisted,
|
|
272
275
|
priorHooksPath,
|
|
273
276
|
allFileEntries: Object.freeze(allFileEntries.map((eachEntry) => Object.freeze({ ...eachEntry }))),
|
|
274
277
|
});
|
|
275
278
|
}
|
|
276
279
|
|
|
277
|
-
/**
|
|
278
|
-
* Restore the captured prior installation and drop extra write-set paths.
|
|
279
|
-
*
|
|
280
|
-
* @param {ReturnType<typeof capturePriorInstallSnapshot>} snapshot
|
|
281
|
-
* @param {{
|
|
282
|
-
* allWrittenPaths?: string[],
|
|
283
|
-
* io?: object,
|
|
284
|
-
* }} [options]
|
|
285
|
-
* @returns {void}
|
|
286
|
-
*/
|
|
287
280
|
export function restorePriorInstallSnapshot(snapshot, options = {}) {
|
|
288
281
|
const io = options.io || {};
|
|
289
282
|
const exists = io.existsSync || existsSync;
|
|
@@ -294,6 +287,13 @@ export function restorePriorInstallSnapshot(snapshot, options = {}) {
|
|
|
294
287
|
const lstat = io.lstatSync || lstatSync;
|
|
295
288
|
|
|
296
289
|
const filesDirectory = join(snapshot.journalRoot, FILES_DIRECTORY_NAME);
|
|
290
|
+
const settingsSnapshots = snapshot.settingsSnapshots?.length > 0
|
|
291
|
+
? snapshot.settingsSnapshots
|
|
292
|
+
: [{
|
|
293
|
+
path: snapshot.settingsPath,
|
|
294
|
+
existed: snapshot.settingsExisted,
|
|
295
|
+
blobName: SETTINGS_BLOB_NAME,
|
|
296
|
+
}];
|
|
297
297
|
for (const eachEntry of snapshot.allFileEntries) {
|
|
298
298
|
try {
|
|
299
299
|
if (exists(eachEntry.absolutePath)) {
|
|
@@ -302,9 +302,7 @@ export function restorePriorInstallSnapshot(snapshot, options = {}) {
|
|
|
302
302
|
unlink(eachEntry.absolutePath);
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
|
-
} catch {
|
|
306
|
-
// keep restoring remaining entries
|
|
307
|
-
}
|
|
305
|
+
} catch {}
|
|
308
306
|
if (eachEntry.kind === ENTRY_KIND_FILE && eachEntry.blobName) {
|
|
309
307
|
mkdir(dirname(eachEntry.absolutePath), { recursive: true });
|
|
310
308
|
copyFile(join(filesDirectory, eachEntry.blobName), eachEntry.absolutePath);
|
|
@@ -324,7 +322,8 @@ export function restorePriorInstallSnapshot(snapshot, options = {}) {
|
|
|
324
322
|
if (priorPathSet.has(eachWrittenPath)) {
|
|
325
323
|
continue;
|
|
326
324
|
}
|
|
327
|
-
if (
|
|
325
|
+
if (settingsSnapshots.some(eachSnapshot => eachSnapshot.path === eachWrittenPath)
|
|
326
|
+
|| eachWrittenPath === snapshot.manifestFilePath) {
|
|
328
327
|
continue;
|
|
329
328
|
}
|
|
330
329
|
try {
|
|
@@ -339,14 +338,14 @@ export function restorePriorInstallSnapshot(snapshot, options = {}) {
|
|
|
339
338
|
}
|
|
340
339
|
}
|
|
341
340
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
341
|
+
for (const eachSnapshot of settingsSnapshots) {
|
|
342
|
+
if (eachSnapshot.existed) {
|
|
343
|
+
mkdir(dirname(eachSnapshot.path), { recursive: true });
|
|
344
|
+
copyFile(join(snapshot.journalRoot, eachSnapshot.blobName), eachSnapshot.path);
|
|
345
|
+
} else if (exists(eachSnapshot.path)) {
|
|
346
|
+
try {
|
|
347
|
+
unlink(eachSnapshot.path);
|
|
348
|
+
} catch {}
|
|
350
349
|
}
|
|
351
350
|
}
|
|
352
351
|
|
|
@@ -356,9 +355,7 @@ export function restorePriorInstallSnapshot(snapshot, options = {}) {
|
|
|
356
355
|
} else if (exists(snapshot.manifestFilePath)) {
|
|
357
356
|
try {
|
|
358
357
|
unlink(snapshot.manifestFilePath);
|
|
359
|
-
} catch {
|
|
360
|
-
// leave in place when unlink fails
|
|
361
|
-
}
|
|
358
|
+
} catch {}
|
|
362
359
|
}
|
|
363
360
|
|
|
364
361
|
writeGlobalCoreHooksPath(snapshot.priorHooksPath, io);
|