chati-dev 4.5.27 → 4.5.29
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/chati.js +151 -123
- package/framework/agents/build/dev.md +4 -4
- package/framework/agents/deploy/devops.md +3 -3
- package/framework/agents/discover/brief.md +18 -6
- package/framework/agents/discover/brownfield-wu.md +3 -3
- package/framework/agents/discover/greenfield-wu.md +4 -4
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +4 -4
- package/framework/agents/plan/phases.md +3 -3
- package/framework/agents/plan/tasks.md +47 -19
- package/framework/agents/plan/ux-brand-architect.md +1 -1
- package/framework/agents/plan/ux.md +26 -2
- package/framework/agents/quality/qa-implementation.md +8 -8
- package/framework/agents/quality/qa-planning.md +58 -4
- package/framework/agents/quality/qa-visual.md +4 -4
- package/framework/config.yaml +2 -2
- package/framework/constitution.md +12 -1
- package/framework/context/governance.md +2 -1
- package/framework/context/root.md +3 -1
- package/framework/domains/constitution.yaml +1 -1
- package/framework/domains/global.yaml +1 -1
- package/framework/domains/workflows/standard-flow.yaml +7 -8
- package/framework/hooks/advance-trigger.js +3 -2
- package/framework/hooks/brief-validator.js +1 -1
- package/framework/hooks/post-dev.js +1 -1
- package/framework/hooks/team-quality-gate.js +9 -2
- package/framework/i18n/en.yaml +1 -0
- package/framework/i18n/es.yaml +1 -0
- package/framework/i18n/fr.yaml +1 -0
- package/framework/i18n/pt.yaml +1 -0
- package/framework/manifest.json +83 -83
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +14 -6
- package/framework/orchestrator/chati.md +61 -23
- package/framework/rules/human-writing-style.md +7 -0
- package/framework/schemas/session.schema.json +25 -2
- package/framework/schemas/task.schema.json +26 -0
- package/framework/tasks/orchestrator-handoff.md +10 -10
- package/framework/tasks/orchestrator-resume.md +3 -3
- package/framework/workflows/brownfield-fullstack.yaml +6 -3
- package/framework/workflows/brownfield-service.yaml +5 -3
- package/framework/workflows/brownfield-ui.yaml +5 -3
- package/framework/workflows/greenfield-fullstack.yaml +6 -3
- package/framework/workflows/standard-flow.yaml +8 -3
- package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
- package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
- package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
- package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
- package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
- package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
- package/node_modules/@chati/core/native/execution-admission.h +59 -0
- package/node_modules/@chati/core/native/execution-gate.c +41 -0
- package/node_modules/@chati/core/native/linux-process.c +41 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
- package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
- package/node_modules/@chati/core/package.json +2 -1
- package/node_modules/@chati/core/scripts/build-native.js +36 -0
- package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
- package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
- package/node_modules/@chati/core/src/darwin-process.js +79 -0
- package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
- package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
- package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
- package/node_modules/@chati/core/src/execution-gate.js +8 -0
- package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
- package/node_modules/@chati/core/src/index.js +18 -0
- package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
- package/node_modules/@chati/core/src/linux-process.js +133 -0
- package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
- package/node_modules/@chati/planning/src/index.js +33 -0
- package/node_modules/@chati/planning/src/planning-material.js +64 -0
- package/node_modules/@chati/planning/src/verification-plan.js +57 -0
- package/node_modules/@chati/rail/package.json +1 -0
- package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
- package/node_modules/@chati/rail/src/index.js +812 -167
- package/node_modules/@chati/rail/src/provider-processing.js +118 -0
- package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
- package/node_modules/@chati/release-lane/src/index.js +40 -43
- package/node_modules/@chati/review-council/src/index.js +223 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
- package/package-artifact-manifest.json +1 -1
- package/package-artifact-manifest.sig +1 -1
- package/package.json +3 -2
- package/scripts/verify-real-harness-e2e.js +20 -8
- package/src/installer/core.js +147 -107
- package/src/installer/package-artifact.js +7 -0
- package/src/installer/preflight.js +13 -0
- package/src/installer/provider-overlay.js +1 -2
- package/src/installer/templates.js +58 -0
- package/src/installer-v2/catalog-client.js +31 -1
- package/src/installer-v2/clickup-connection-storage.js +92 -0
- package/src/installer-v2/clickup-mcp-client.js +155 -0
- package/src/installer-v2/clickup-oauth-loopback.js +96 -0
- package/src/installer-v2/clickup-oauth.js +233 -0
- package/src/installer-v2/index.js +141 -15
- package/src/installer-v2/installation-authority.js +98 -4
- package/src/installer-v2/provider-executable.js +137 -20
- package/src/installer-v2/wizard-installation.js +11 -0
- package/src/orchestrator/agent-selector.js +39 -1
- package/src/orchestrator/browser-artifact-store.js +131 -0
- package/src/orchestrator/cli.js +762 -205
- package/src/orchestrator/clickup-mcp-observer.js +226 -0
- package/src/orchestrator/clickup-runtime.js +45 -24
- package/src/orchestrator/council-gate-runtime.js +903 -0
- package/src/orchestrator/evidence-authority-enrollment.js +75 -0
- package/src/orchestrator/evidence-authority-request.js +46 -0
- package/src/orchestrator/handoff-engine.js +4 -11
- package/src/orchestrator/pipeline-manager.js +42 -4
- package/src/orchestrator/planning-contract-revision.js +711 -0
- package/src/orchestrator/planning-reconciliation.js +302 -0
- package/src/orchestrator/planning-runtime.js +476 -29
- package/src/orchestrator/rail-adjudication-evidence.js +15 -24
- package/src/orchestrator/rail-human-effort-authority.js +46 -0
- package/src/orchestrator/rail-human-effort.js +149 -0
- package/src/orchestrator/rail-runtime.js +291 -47
- package/src/orchestrator/rail-verification.js +278 -0
- package/src/orchestrator/review-runtime.js +37 -2
- package/src/orchestrator/runtime-installation-v2.js +143 -24
- package/src/orchestrator/session-manager.js +1457 -29
- package/src/tasks/handoff.js +57 -33
- package/src/terminal/adapters/claude-adapter.js +3 -0
- package/src/terminal/adapters/codex-adapter.js +13 -0
- package/src/terminal/adapters/grok-adapter.js +1 -0
- package/src/terminal/agent-attempt-budget.js +35 -0
- package/src/terminal/browser-endpoint-proxy.js +174 -0
- package/src/terminal/browser-observer-driver.js +94 -0
- package/src/terminal/browser-observer-supervisor.js +73 -0
- package/src/terminal/browser-observer.js +95 -0
- package/src/terminal/browser-process-facts.js +75 -0
- package/src/terminal/browser-runtime.js +131 -0
- package/src/terminal/darwin-authority-policy.js +213 -0
- package/src/terminal/darwin-browser-application.js +116 -0
- package/src/terminal/darwin-browser-observer.js +57 -0
- package/src/terminal/darwin-execution.js +185 -0
- package/src/terminal/executable-snapshot.js +202 -0
- package/src/terminal/handoff-parser.js +48 -2
- package/src/terminal/isolation.js +14 -13
- package/src/terminal/prompt-builder.js +176 -15
- package/src/terminal/provider-preflight.js +47 -27
- package/src/terminal/rail-execution-worktree.js +22 -6
- package/src/terminal/rail-process-observer.js +10 -0
- package/src/terminal/rail-prompts.js +21 -3
- package/src/terminal/rail-readonly-workspace.js +54 -5
- package/src/terminal/rail-worktree-checkpoint.js +315 -0
- package/src/terminal/rail-worktree-restoration.js +485 -0
- package/src/terminal/run-agent.js +164 -42
- package/src/terminal/run-council-gate.js +338 -0
- package/src/terminal/run-planning-reconciliation.js +49 -0
- package/src/terminal/run-rail-adjudication.js +29 -7
- package/src/terminal/run-rail-recovery.js +97 -0
- package/src/terminal/run-rail-review.js +36 -15
- package/src/terminal/run-rail-rework.js +26 -15
- package/src/terminal/run-rail-task.js +22 -9
- package/src/terminal/runtime-native-helper.js +85 -0
- package/src/terminal/spawner.js +899 -142
- package/src/terminal/verification-process-facts.js +49 -0
- package/src/upgrade/entrypoints.js +68 -0
- package/src/upgrade/generation.js +194 -0
- package/src/upgrade/maintenance.js +79 -0
- package/src/upgrade/paths.js +81 -0
- package/src/upgrade/upgrade.js +267 -0
- package/src/utils/execution-role.js +15 -0
- package/src/utils/handoff-paths.js +59 -0
- package/src/wizard/i18n.js +1 -0
- package/src/wizard/index.js +17 -5
- package/src/wizard/questions.js +24 -12
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Typed process facts, not authentication. Only the private native transport
|
|
2
|
+
* may produce Darwin observations for the trusted canonical verifier. */
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
5
|
+
import { canonicalize, darwinSupervisorNonce, parseDarwinReadyMessage, parseDarwinCompletionMessage, sha256 } from '@chati/core';
|
|
6
|
+
|
|
7
|
+
const keys = ['schema_version', 'invocation_context', 'helper_sha256', 'policy_digest', 'working_dir',
|
|
8
|
+
'arguments_digest', 'stdin_sha256', 'executable_binding_digest', 'ready', 'completion'];
|
|
9
|
+
const digest = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
10
|
+
const bytesHash = value => createHash('sha256').update(value).digest('hex');
|
|
11
|
+
function invalid() { throw Object.assign(new Error('verification requires a complete native observation of the exact frozen check'), { code: 'VERIFICATION_COMPLETION_INVALID' }); }
|
|
12
|
+
|
|
13
|
+
// Derive facts from the validated/pinned binding, without adding normalizer
|
|
14
|
+
// defaults that were absent from the frozen command's hashed representation.
|
|
15
|
+
export function projectVerificationExecutableBinding(validated, declared) {
|
|
16
|
+
return Object.freeze({ executable_path: validated.executable_path, executable_sha256: validated.executable_sha256,
|
|
17
|
+
...(Object.hasOwn(declared, 'supporting_executables')
|
|
18
|
+
? { supporting_executables: structuredClone(validated.supporting_executables) } : {}) });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createDarwinVerificationBinding({ context, helperSha256, policyDigest, ready, completion, workingDir, executableBinding, args, stdin }) {
|
|
22
|
+
return Object.freeze(canonicalize({ schema_version: 1, invocation_context: context,
|
|
23
|
+
helper_sha256: helperSha256, policy_digest: policyDigest, working_dir: workingDir,
|
|
24
|
+
executable_binding_digest: sha256(executableBinding), arguments_digest: sha256(args), stdin_sha256: bytesHash(stdin), ready, completion }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function verificationProcessCompleted(facts, expected) {
|
|
28
|
+
if (facts?.containment_kind !== 'darwin_scoped_process_group_v1') {
|
|
29
|
+
return facts?.containment_kind === 'bubblewrap_pid_namespace_v1' && facts.process_tree_stopped === true
|
|
30
|
+
&& !Object.hasOwn(facts, 'darwin_verification_binding');
|
|
31
|
+
}
|
|
32
|
+
const binding = facts.darwin_verification_binding;
|
|
33
|
+
if (!binding || Array.isArray(binding) || Object.keys(binding).length !== keys.length || keys.some(key => !Object.hasOwn(binding, key))
|
|
34
|
+
|| binding.schema_version !== 1 || facts.process_tree_stopped !== false || facts.process_group_stopped !== true
|
|
35
|
+
|| !digest(binding.helper_sha256) || !digest(binding.policy_digest) || !digest(facts.containment_digest)
|
|
36
|
+
|| !expected || !isAbsolute(expected.workingDir) || resolve(expected.workingDir) !== binding.working_dir
|
|
37
|
+
|| binding.invocation_context?.authority_kind !== 'verification_check'
|
|
38
|
+
|| binding.invocation_context.executable_binding_digest !== binding.executable_binding_digest
|
|
39
|
+
|| binding.executable_binding_digest !== sha256(expected.executableBinding)
|
|
40
|
+
|| sha256(facts.executable_binding) !== binding.executable_binding_digest
|
|
41
|
+
|| binding.arguments_digest !== sha256(expected.args) || binding.stdin_sha256 !== bytesHash(expected.stdin)
|
|
42
|
+
|| facts.stdin_sha256 !== binding.stdin_sha256) invalid();
|
|
43
|
+
try {
|
|
44
|
+
const ready = parseDarwinReadyMessage(`${JSON.stringify(binding.ready)}\n`, { ...binding.ready,
|
|
45
|
+
nonce: darwinSupervisorNonce(binding.invocation_context) });
|
|
46
|
+
parseDarwinCompletionMessage(`${JSON.stringify(binding.completion)}\n`, ready, facts.exit_code);
|
|
47
|
+
} catch { invalid(); }
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { closeSync, constants, fstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
|
+
import { lstatAtSync, openAtSync } from '@chati/core';
|
|
5
|
+
import { generateClaudeRouter, generateCodexSkill, generateGeminiRouter, generateGrokRouter, generateGrokGovernanceRules } from '../installer/templates.js';
|
|
6
|
+
import { openUpgradeDirectory } from './paths.js';
|
|
7
|
+
|
|
8
|
+
const definitions = Object.freeze([
|
|
9
|
+
['claude', '.claude/commands/chati.md', generateClaudeRouter],
|
|
10
|
+
['codex', '.agents/skills/chati/SKILL.md', generateCodexSkill],
|
|
11
|
+
['grok', '.grok/commands/chati.md', generateGrokRouter],
|
|
12
|
+
['grok', '.grok/rules/chati-governance.md', generateGrokGovernanceRules],
|
|
13
|
+
['gemini', '.gemini/commands/chati.toml', generateGeminiRouter],
|
|
14
|
+
]);
|
|
15
|
+
export const MANAGED_ENTRYPOINT_PATHS = Object.freeze(definitions.map(([, path]) => path));
|
|
16
|
+
|
|
17
|
+
// Exact outputs of production-verified candidate116, both neutral and adapted
|
|
18
|
+
// paths. Signed inventory 65d4466261894dbe167773ee2d80d7be1af55339151864a7dfaace4460f19fd2;
|
|
19
|
+
// templates e8741cbff4b58c391703c6acd83112328055682dbcdaef5cbed3a212abdf32db;
|
|
20
|
+
// core ef9b65f6592f5de4d2367ff54d7a2267c3ad6c182bb70310581a6385016a5825.
|
|
21
|
+
// Never derive legacy ownership from the mutable project's current manifest.
|
|
22
|
+
const legacyHashes = Object.freeze({
|
|
23
|
+
'.claude/commands/chati.md': ['4fddf1519bad9cbd73d1abea2d4090e03faf1aac6330253b2dc0ea0c5c58fc8c', '5894f7307afbc1033fc74eda2ec22020d075db92f3050da126813316785d5d99'],
|
|
24
|
+
'.agents/skills/chati/SKILL.md': ['910c94e5ab8b08a8e357dcce81224a7cb8c898a098105bbcfd7883846a84c076', '43a975bd31e3be126a2ae563071efcb6463a4652ea3e0ab6591f49aab8ed59dd'],
|
|
25
|
+
'.grok/commands/chati.md': ['864ecd0964d28ea4b28a55e4250c3a23035ccee0f9641bc71afe7dadd459928d', '9d69a817220aaaa6f62ad2e33b0d184903dd9d0e83d0066017ebdebeb03be219'],
|
|
26
|
+
'.grok/rules/chati-governance.md': ['dde0f1787ba4ddebdd7471b4f9333435894e99275f7d47df710d1259ac7ee80e'],
|
|
27
|
+
'.gemini/commands/chati.toml': ['440e9625122c58710fc01effaf18e0dc6c32e522bd041b44f3057f71c1940d11', '164bd298566e92021152b9318efcc8ba90bd279f7b7234d675fa72ad791b3535'],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export function managedEntrypoints(providers) {
|
|
31
|
+
return definitions.filter(([provider]) => providers.includes(provider)).map(([provider, path, render]) => ({
|
|
32
|
+
path, content: render({ orchestratorPath: `.chati.dev/.adapted/${provider}/orchestrator/chati.md` }),
|
|
33
|
+
neutral: render(),
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readEntrypoint(root, path) {
|
|
38
|
+
const parent = openUpgradeDirectory(root, dirname(path));
|
|
39
|
+
if (parent === undefined) return null;
|
|
40
|
+
let file;
|
|
41
|
+
try {
|
|
42
|
+
try { file = openAtSync(parent, basename(path), constants.O_RDONLY | constants.O_NONBLOCK); }
|
|
43
|
+
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
44
|
+
const stat = fstatSync(file);
|
|
45
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 1024 * 1024) throw Object.assign(new Error(`Unsafe entrypoint: ${path}`), { code: 'UNSAFE_UPGRADE_PATH' });
|
|
46
|
+
const bytes = readFileSync(file);
|
|
47
|
+
const after = lstatAtSync(parent, basename(path));
|
|
48
|
+
if (after.dev !== stat.dev || after.ino !== stat.ino || bytes.length > 1024 * 1024) throw Object.assign(new Error(`Entrypoint changed: ${path}`), { code: 'UPGRADE_STORAGE_CHANGED' });
|
|
49
|
+
return bytes;
|
|
50
|
+
} finally { if (file !== undefined) closeSync(file); closeSync(parent); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Recognition precedes all staging; user customizations are never merged. */
|
|
54
|
+
export function stageManagedEntrypoints(projectDir, stagingDir, providers) {
|
|
55
|
+
const entries = managedEntrypoints(providers);
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const bytes = readEntrypoint(projectDir, entry.path);
|
|
58
|
+
if (bytes === null || bytes.equals(Buffer.from(entry.content)) || bytes.equals(Buffer.from(entry.neutral))) continue;
|
|
59
|
+
if (!legacyHashes[entry.path]?.includes(createHash('sha256').update(bytes).digest('hex'))) {
|
|
60
|
+
throw Object.assign(new Error(`Managed entrypoint was edited or is an unrecognized version: ${entry.path}. No active files were replaced. Preserve the customization and resolve this conflict before upgrading.`), { code: 'UPGRADE_ENTRYPOINT_CONFLICT' });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const { path, content } of entries) {
|
|
64
|
+
mkdirSync(dirname(join(stagingDir, path)), { recursive: true });
|
|
65
|
+
writeFileSync(join(stagingDir, path), content, { flag: 'wx' });
|
|
66
|
+
}
|
|
67
|
+
return entries.map(entry => entry.path);
|
|
68
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { closeSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import * as yaml from 'js-yaml';
|
|
7
|
+
import semver from 'semver';
|
|
8
|
+
import { copyInstalledRuntime, writeClaudeSettingsWithMerge } from '../installer/core.js';
|
|
9
|
+
import { generateManifest, saveManifest, verifyFrameworkSource } from '../installer/manifest.js';
|
|
10
|
+
import { verifyCurrentPackageArtifact } from '../installer/package-artifact.js';
|
|
11
|
+
import { validateInstallation } from '../installer/validator.js';
|
|
12
|
+
import { loadRuntimeInstallationV2 } from '../orchestrator/runtime-installation-v2.js';
|
|
13
|
+
import { openPinnedProviderExecutable, openPinnedProviderSupportingExecutables } from '../installer-v2/provider-executable.js';
|
|
14
|
+
import { findMigrations } from './migrator.js';
|
|
15
|
+
import { requireLicenseEntitlement } from '../license/client.js';
|
|
16
|
+
import { MANAGED_ENTRYPOINT_PATHS, stageManagedEntrypoints } from './entrypoints.js';
|
|
17
|
+
|
|
18
|
+
export const UPGRADE_PACKAGE_ROOT = fileURLToPath(new URL('../../', import.meta.url));
|
|
19
|
+
export const UPGRADE_TARGETS = Object.freeze(['.chati.dev', '.claude/settings.json', ...MANAGED_ENTRYPOINT_PATHS, '.chati/manifest.json']);
|
|
20
|
+
export const upgradeFailure = (code, message) => { throw Object.assign(new Error(message), { code }); };
|
|
21
|
+
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
|
22
|
+
export function upgradeTestHook(phase, detail = {}) {
|
|
23
|
+
globalThis[Symbol.for('chati.upgrade.test-hooks')]?.onPhase?.(phase, detail);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Bounded to an explicitly selected installation subtree, not the project.
|
|
27
|
+
* Links/special files are refused before staging or promotion can follow them. */
|
|
28
|
+
export function upgradeSnapshot(path) {
|
|
29
|
+
let stat;
|
|
30
|
+
try { stat = lstatSync(path); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
31
|
+
const entries = [];
|
|
32
|
+
const walk = (current, relative = '') => {
|
|
33
|
+
const item = lstatSync(current);
|
|
34
|
+
if (item.isDirectory()) {
|
|
35
|
+
entries.push([relative, 'directory', item.mode & 0o777]);
|
|
36
|
+
for (const name of readdirSync(current).sort()) walk(join(current, name), relative ? `${relative}/${name}` : name);
|
|
37
|
+
} else if (item.isFile() && item.nlink === 1) {
|
|
38
|
+
entries.push([relative, 'file', item.mode & 0o777, digest(readFileSync(current))]);
|
|
39
|
+
} else upgradeFailure('UNSAFE_UPGRADE_PATH', `Upgrade target contains a link or special file: ${current}`);
|
|
40
|
+
};
|
|
41
|
+
walk(path);
|
|
42
|
+
return { identity: `${stat.dev}:${stat.ino}`, digest: digest(JSON.stringify(entries)), type: stat.isDirectory() ? 'directory' : 'file' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function copyUpgradeTree(source, destination, { exclude = [] } = {}) {
|
|
46
|
+
const stat = lstatSync(source);
|
|
47
|
+
if (stat.isDirectory()) {
|
|
48
|
+
mkdirSync(destination, { recursive: true, mode: stat.mode & 0o777 });
|
|
49
|
+
for (const name of readdirSync(source)) {
|
|
50
|
+
// Prior managed backups stay in the retired generation, never recursively
|
|
51
|
+
// duplicate every historical generation into the new runtime.
|
|
52
|
+
if (/^\.backup-v/.test(name) || exclude.includes(name)) continue;
|
|
53
|
+
copyUpgradeTree(join(source, name), join(destination, name));
|
|
54
|
+
}
|
|
55
|
+
} else if (stat.isFile() && stat.nlink === 1) {
|
|
56
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
57
|
+
copyFileSync(source, destination);
|
|
58
|
+
} else upgradeFailure('UNSAFE_UPGRADE_PATH', `Cannot stage linked or special source: ${source}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function verifyUpgradePackage(targetVersion) {
|
|
62
|
+
const version = JSON.parse(readFileSync(join(UPGRADE_PACKAGE_ROOT, 'package.json'), 'utf8')).version;
|
|
63
|
+
if (targetVersion !== undefined && targetVersion !== version) upgradeFailure('UPGRADE_PACKAGE_VERSION_MISMATCH', `This command contains v${version}, not ${targetVersion}`);
|
|
64
|
+
const integrity = verifyCurrentPackageArtifact(UPGRADE_PACKAGE_ROOT);
|
|
65
|
+
if (!integrity.valid) upgradeFailure('UPGRADE_PACKAGE_UNTRUSTED', `Package verification failed: ${integrity.reason}`);
|
|
66
|
+
const framework = verifyFrameworkSource(join(UPGRADE_PACKAGE_ROOT, 'framework'), { expectedVersion: version });
|
|
67
|
+
if (!framework.valid) upgradeFailure('UPGRADE_PACKAGE_UNTRUSTED', `Framework verification failed: ${framework.reason}`);
|
|
68
|
+
return { version, package_sha256: digest(readFileSync(join(UPGRADE_PACKAGE_ROOT, 'package-artifact-manifest.json'))) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function assertUpgradeBindings(projectDir) {
|
|
72
|
+
const artifact = loadRuntimeInstallationV2(projectDir);
|
|
73
|
+
if (!artifact) return null;
|
|
74
|
+
try {
|
|
75
|
+
for (const binding of artifact.installation.enabled_providers) {
|
|
76
|
+
const descriptors = [];
|
|
77
|
+
try {
|
|
78
|
+
descriptors.push(openPinnedProviderExecutable(binding, { harnessId: binding.harness_id, projectDir }).descriptor);
|
|
79
|
+
for (const pinned of openPinnedProviderSupportingExecutables(binding, { harnessId: binding.harness_id, projectDir })) descriptors.push(pinned.descriptor);
|
|
80
|
+
} finally { for (const fd of descriptors) closeSync(fd); }
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
upgradeFailure('UPGRADE_BINDING_RECONFIGURATION_REQUIRED', `Existing executable binding is no longer valid (${error.code}). Explicitly reconfigure the installation before upgrading; no binding was changed.`);
|
|
84
|
+
}
|
|
85
|
+
return artifact;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function authorizeUpgradeProfile(artifact) {
|
|
89
|
+
if (artifact?.installation?.profile === 'authorized-internal') await requireLicenseEntitlement('internal');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function readUpgradeInstallation(projectDir, { version, offline = false }) {
|
|
93
|
+
const framework = join(projectDir, '.chati.dev');
|
|
94
|
+
if (!existsSync(framework) || existsSync(join(projectDir, 'chati.dev'))) {
|
|
95
|
+
upgradeFailure('UPGRADE_LAYOUT_REQUIRES_MIGRATION', 'Upgrade requires one canonical .chati.dev installation. Legacy or ambiguous layouts need explicit migration.');
|
|
96
|
+
}
|
|
97
|
+
const config = yaml.load(readFileSync(join(framework, 'config.yaml'), 'utf8'));
|
|
98
|
+
if (!semver.valid(config?.version) || semver.lt(config.version, '4.3.0') || semver.gt(config.version, version)) {
|
|
99
|
+
upgradeFailure('UPGRADE_VERSION_REQUIRES_MIGRATION', 'Automatic upgrade supports versions from 4.3.0 through the current package; older layouts or downgrades require an explicit migration.');
|
|
100
|
+
}
|
|
101
|
+
if (findMigrations(projectDir, config.version, version).length) upgradeFailure('UPGRADE_MIGRATION_REQUIRES_REVIEW', 'This upgrade requires a migration outside the runtime-only write set. No project files were changed.');
|
|
102
|
+
const cli = join(framework, '_cli');
|
|
103
|
+
if (existsSync(cli) && !lstatSync(cli).isDirectory()) upgradeFailure('UNSAFE_UPGRADE_PATH', 'Existing _cli must be a real directory');
|
|
104
|
+
const session = yaml.load(readFileSync(join(projectDir, '.chati/session.yaml'), 'utf8'));
|
|
105
|
+
if (!session || typeof session !== 'object' || Array.isArray(session)) upgradeFailure('UPGRADE_SESSION_INVALID', 'A valid session is required before upgrade');
|
|
106
|
+
if ((session.current_agent && session.session_status !== 'paused')
|
|
107
|
+
|| Object.values(session.agents || {}).some(agent => ['running', 'reserved'].includes(agent.execution_claim_state))) {
|
|
108
|
+
upgradeFailure('UPGRADE_RUNTIME_ACTIVE', 'Pause/exit active sessions and resolve execution claims before upgrading. Session state was preserved.');
|
|
109
|
+
}
|
|
110
|
+
let protocol;
|
|
111
|
+
try { protocol = JSON.parse(readFileSync(join(cli, 'version.json'), 'utf8')).maintenance_protocol; }
|
|
112
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
113
|
+
if (protocol !== 1 && !offline) upgradeFailure('UPGRADE_OFFLINE_CONFIRMATION_REQUIRED', 'This installed version cannot exclude legacy runtime processes. Close all Chati sessions, then repeat upgrade --offline. Online upgrade is not supported for this version.');
|
|
114
|
+
const artifact = assertUpgradeBindings(projectDir);
|
|
115
|
+
await authorizeUpgradeProfile(artifact);
|
|
116
|
+
const providers = artifact ? artifact.installation.enabled_providers.map(binding => binding.harness_id)
|
|
117
|
+
: Object.entries(config.providers || {}).filter(([, value]) => value?.enabled === true).map(([name]) => name);
|
|
118
|
+
if (providers.some(name => !['claude', 'codex', 'grok', 'gemini'].includes(name))) upgradeFailure('UPGRADE_PROVIDER_UNSUPPORTED', 'Installed provider configuration requires explicit reconfiguration');
|
|
119
|
+
const protectedPaths = ['.chati/session.yaml', '.chati/v2/installation.json', 'CLAUDE.md'];
|
|
120
|
+
const protectedState = Object.fromEntries(protectedPaths.map(path => [path, upgradeSnapshot(join(projectDir, path))]));
|
|
121
|
+
return { config, artifact, providers: [...new Set(providers)], protectedState };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function assertUpgradeStatePreserved(projectDir, protectedState) {
|
|
125
|
+
for (const [path, before] of Object.entries(protectedState)) {
|
|
126
|
+
if (JSON.stringify(upgradeSnapshot(join(projectDir, path))) !== JSON.stringify(before)) upgradeFailure('UPGRADE_STATE_CHANGED', `Protected project state changed: ${path}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function prepareUpgradeGeneration(projectDir, stagingDir, installation, packageIdentity) {
|
|
131
|
+
const { config, artifact, providers } = installation;
|
|
132
|
+
const entrypointTargets = stageManagedEntrypoints(projectDir, stagingDir, providers);
|
|
133
|
+
const frameworkDir = join(stagingDir, '.chati.dev');
|
|
134
|
+
// These are fully managed namespaces. Overlaying the old CLI could retain
|
|
135
|
+
// removed code or nested dependencies that shadow the new package's graph.
|
|
136
|
+
// The original tree remains untouched and is retired intact at promotion.
|
|
137
|
+
copyUpgradeTree(join(projectDir, '.chati.dev'), frameworkDir, { exclude: ['_cli', 'framework', '.adapted'] });
|
|
138
|
+
upgradeTestHook('before-runtime-copy', { stagingDir });
|
|
139
|
+
copyInstalledRuntime({ frameworkDir, providers, runtimeVersion: artifact ? 2 : undefined, version: packageIdentity.version, strict: true });
|
|
140
|
+
// The router compares installer_version with the bundled CLI stamp: an
|
|
141
|
+
// upgraded generation must advance both, or every later run warns that
|
|
142
|
+
// an upgrade is still required.
|
|
143
|
+
writeFileSync(join(frameworkDir, 'config.yaml'), yaml.dump({ ...config, version: packageIdentity.version, installer_version: packageIdentity.version }, { lineWidth: -1 }));
|
|
144
|
+
copyUpgradeTree(join(projectDir, '.chati/session.yaml'), join(stagingDir, '.chati/session.yaml'));
|
|
145
|
+
if (artifact) copyUpgradeTree(join(projectDir, '.chati/v2/installation.json'), join(stagingDir, '.chati/v2/installation.json'));
|
|
146
|
+
const memories = join(projectDir, '.chati/memories/shared/durable');
|
|
147
|
+
if (existsSync(memories) && lstatSync(memories).isDirectory()) mkdirSync(join(stagingDir, '.chati/memories/shared/durable'), { recursive: true });
|
|
148
|
+
const targets = ['.chati.dev'];
|
|
149
|
+
const claude = join(projectDir, '.claude');
|
|
150
|
+
if (existsSync(claude)) {
|
|
151
|
+
if (!lstatSync(claude).isDirectory() || lstatSync(claude).isSymbolicLink()) upgradeFailure('UNSAFE_UPGRADE_PATH', '.claude must be a real directory');
|
|
152
|
+
const settings = join(claude, 'settings.json');
|
|
153
|
+
if (existsSync(settings)) {
|
|
154
|
+
const parsed = JSON.parse(readFileSync(settings, 'utf8'));
|
|
155
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) upgradeFailure('UPGRADE_SETTINGS_INVALID', 'Claude settings must be a JSON object');
|
|
156
|
+
copyUpgradeTree(settings, join(stagingDir, '.claude/settings.json'));
|
|
157
|
+
}
|
|
158
|
+
writeClaudeSettingsWithMerge(join(stagingDir, '.claude/settings.json'), { runtimeVersion: artifact ? 2 : undefined });
|
|
159
|
+
targets.push('.claude/settings.json');
|
|
160
|
+
}
|
|
161
|
+
targets.push(...entrypointTargets);
|
|
162
|
+
// A former manifest covering user instructions or other state cannot be
|
|
163
|
+
// silently narrowed to the runtime write set by this transaction.
|
|
164
|
+
const formerManifest = join(projectDir, '.chati/manifest.json');
|
|
165
|
+
if (existsSync(formerManifest)) {
|
|
166
|
+
const manifest = JSON.parse(readFileSync(formerManifest, 'utf8'));
|
|
167
|
+
if (!manifest.files || Object.keys(manifest.files).some(path => !path.startsWith('.chati.dev/') && path !== '.claude/settings.json' && !MANAGED_ENTRYPOINT_PATHS.includes(path))) {
|
|
168
|
+
upgradeFailure('UPGRADE_MANIFEST_REQUIRES_MIGRATION', 'Existing manifest covers paths outside this upgrade write set');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const paths = [];
|
|
172
|
+
const collect = (path, relative) => {
|
|
173
|
+
const stat = lstatSync(path);
|
|
174
|
+
if (stat.isDirectory()) for (const name of readdirSync(path)) collect(join(path, name), `${relative}/${name}`);
|
|
175
|
+
else if (stat.isFile() && stat.nlink === 1) paths.push(relative);
|
|
176
|
+
else upgradeFailure('UNSAFE_UPGRADE_PATH', `Invalid staged path: ${relative}`);
|
|
177
|
+
};
|
|
178
|
+
for (const path of targets) collect(join(stagingDir, path), path);
|
|
179
|
+
saveManifest(stagingDir, generateManifest(stagingDir, paths.sort(), packageIdentity.version));
|
|
180
|
+
targets.push('.chati/manifest.json');
|
|
181
|
+
const expected = Object.fromEntries(targets.map(path => [path, upgradeSnapshot(join(stagingDir, path))]));
|
|
182
|
+
upgradeTestHook('before-validation', { stagingDir });
|
|
183
|
+
const validation = await validateInstallation(stagingDir);
|
|
184
|
+
if (validation.passed !== validation.total) upgradeFailure('UPGRADE_VALIDATION_FAILED', `Staged installation passed ${validation.passed}/${validation.total} checks`);
|
|
185
|
+
const modules = ['orchestrator/cli.js', 'terminal/run-agent.js', 'terminal/run-rail-task.js', 'terminal/run-rail-recovery.js'];
|
|
186
|
+
const urls = modules.map(path => pathToFileURL(join(frameworkDir, '_cli', path)).href);
|
|
187
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !['NODE_OPTIONS', 'NODE_PATH'].includes(key)));
|
|
188
|
+
execFileSync(process.execPath, ['--input-type=module', '-e', `for (const url of ${JSON.stringify(urls)}) await import(url);`], { cwd: stagingDir, env, stdio: 'pipe', timeout: 20000 });
|
|
189
|
+
for (const path of targets) {
|
|
190
|
+
if (JSON.stringify(upgradeSnapshot(join(stagingDir, path))) !== JSON.stringify(expected[path])) upgradeFailure('UPGRADE_STAGING_CHANGED', `Staged inventory changed during validation: ${path}`);
|
|
191
|
+
}
|
|
192
|
+
assertUpgradeStatePreserved(projectDir, installation.protectedState);
|
|
193
|
+
return { targets, validation: { passed: validation.passed, total: validation.total } };
|
|
194
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, fsyncSync, lstatSync, openSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { lstatAtSync, mkdirAtSync, openAtSync, tryExclusiveFileLockSync, trySharedFileLockSync } from '@chati/core';
|
|
4
|
+
|
|
5
|
+
export const UPGRADE_PENDING_FILE = 'pending.json';
|
|
6
|
+
const LOCK_FILE = 'maintenance.lock';
|
|
7
|
+
const identity = (stat) => `${stat.dev}:${stat.ino}`;
|
|
8
|
+
const fail = (code, message) => { throw Object.assign(new Error(message), { code }); };
|
|
9
|
+
|
|
10
|
+
/** Persistent descriptor lock, never unlinked or stolen by age. Readers may
|
|
11
|
+
* coexist; the upgrade owns an exclusive descriptor until its promise settles. */
|
|
12
|
+
export async function withUpgradeMaintenance(projectDir, action, { exclusive = false, createState = false } = {}) {
|
|
13
|
+
const unsafeCode = exclusive ? 'UNSAFE_UPGRADE_PATH' : 'UNSAFE_RUNTIME_PATH';
|
|
14
|
+
const changedCode = exclusive ? 'UPGRADE_STORAGE_CHANGED' : 'UNSAFE_RUNTIME_PATH';
|
|
15
|
+
if (!projectDir && !exclusive) return action();
|
|
16
|
+
const root = realpathSync(projectDir);
|
|
17
|
+
const statePath = join(root, '.chati');
|
|
18
|
+
let stateStat;
|
|
19
|
+
try { stateStat = lstatSync(statePath); }
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code !== 'ENOENT') throw error;
|
|
22
|
+
if (!createState) { if (!exclusive) return action(); throw error; }
|
|
23
|
+
// Only an already authorized installer may bootstrap the shared lock.
|
|
24
|
+
const rootFd = openSync(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
25
|
+
try {
|
|
26
|
+
try { mkdirAtSync(rootFd, '.chati'); fsyncSync(rootFd); }
|
|
27
|
+
catch (mkdirError) { if (mkdirError.code !== 'EEXIST') throw mkdirError; }
|
|
28
|
+
} finally { closeSync(rootFd); }
|
|
29
|
+
stateStat = lstatSync(statePath);
|
|
30
|
+
}
|
|
31
|
+
if (!stateStat.isDirectory() || stateStat.isSymbolicLink()) fail(unsafeCode, '.chati must be a real directory');
|
|
32
|
+
const rootStat = lstatSync(root);
|
|
33
|
+
let stateFd; let lockFd;
|
|
34
|
+
const directories = [];
|
|
35
|
+
try {
|
|
36
|
+
stateFd = openSync(statePath, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
37
|
+
directories.push({ fd: stateFd, path: statePath, identity: identity(stateStat) });
|
|
38
|
+
if (identity(fstatSync(stateFd)) !== identity(stateStat)) fail(unsafeCode, '.chati changed while acquiring maintenance');
|
|
39
|
+
let storageDir = statePath;
|
|
40
|
+
for (const name of ['runtime', 'upgrade']) {
|
|
41
|
+
try { mkdirAtSync(stateFd, name); fsyncSync(stateFd); } catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
42
|
+
const stat = lstatAtSync(stateFd, name);
|
|
43
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) fail(unsafeCode, 'upgrade storage must contain only real directories');
|
|
44
|
+
stateFd = openAtSync(stateFd, name, constants.O_RDONLY | constants.O_DIRECTORY);
|
|
45
|
+
storageDir = join(storageDir, name);
|
|
46
|
+
directories.push({ fd: stateFd, path: storageDir, identity: identity(stat) });
|
|
47
|
+
if (identity(fstatSync(stateFd)) !== identity(stat)) fail(unsafeCode, 'upgrade storage changed while opening');
|
|
48
|
+
}
|
|
49
|
+
lockFd = openAtSync(stateFd, LOCK_FILE, constants.O_RDWR | constants.O_CREAT | constants.O_NONBLOCK, 0o600);
|
|
50
|
+
const lockStat = fstatSync(lockFd);
|
|
51
|
+
if (!lockStat.isFile() || lockStat.nlink !== 1) fail(unsafeCode, 'maintenance lock must be a single-link regular file');
|
|
52
|
+
const assertCurrent = () => {
|
|
53
|
+
if (identity(lstatSync(root)) !== identity(rootStat)
|
|
54
|
+
|| directories.some(directory => identity(lstatSync(directory.path)) !== directory.identity)
|
|
55
|
+
|| identity(lstatAtSync(stateFd, LOCK_FILE)) !== identity(lockStat)) {
|
|
56
|
+
fail(changedCode, 'maintenance storage identity changed');
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
assertCurrent();
|
|
60
|
+
const acquired = exclusive ? tryExclusiveFileLockSync(lockFd) : trySharedFileLockSync(lockFd);
|
|
61
|
+
if (!acquired) fail('UPGRADE_IN_PROGRESS', exclusive
|
|
62
|
+
? 'A Chati runtime or another upgrade is active. Close active sessions before upgrading.'
|
|
63
|
+
: 'An upgrade is in progress. Resume this command after the upgrade finishes.');
|
|
64
|
+
assertCurrent();
|
|
65
|
+
if (!exclusive) {
|
|
66
|
+
try {
|
|
67
|
+
lstatAtSync(stateFd, UPGRADE_PENDING_FILE);
|
|
68
|
+
fail('UPGRADE_RECOVERY_REQUIRED', 'An interrupted upgrade must be recovered with chati-dev upgrade before starting runtime commands.');
|
|
69
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
70
|
+
}
|
|
71
|
+
globalThis[Symbol.for('chati.upgrade.test-hooks')]?.onMaintenance?.({ exclusive, root });
|
|
72
|
+
const result = await action({ root, stateFd, storageDir, assertCurrent });
|
|
73
|
+
assertCurrent();
|
|
74
|
+
return result;
|
|
75
|
+
} finally {
|
|
76
|
+
if (lockFd !== undefined) closeSync(lockFd);
|
|
77
|
+
for (const directory of directories.reverse()) closeSync(directory.fd);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, fsyncSync, openSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, sep } from 'node:path';
|
|
3
|
+
import { lstatAtSync, mkdirAtSync, openAtSync } from '@chati/core';
|
|
4
|
+
|
|
5
|
+
const identity = stat => `${stat.dev}:${stat.ino}`;
|
|
6
|
+
const fail = (code, message) => { throw Object.assign(new Error(message), { code }); };
|
|
7
|
+
|
|
8
|
+
/** Walk from a pinned root, never following even an intermediate symlink. */
|
|
9
|
+
export function openUpgradeDirectory(root, relativeDirectory, { create = false, expected } = {}) {
|
|
10
|
+
const parts = relativeDirectory === '.' || relativeDirectory === '' ? [] : relativeDirectory.split(sep);
|
|
11
|
+
if (isAbsolute(relativeDirectory) || parts.some(part => !part || part === '.' || part === '..')) {
|
|
12
|
+
fail('UNSAFE_UPGRADE_PATH', 'Upgrade directory must be project-relative');
|
|
13
|
+
}
|
|
14
|
+
let fd = openSync(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
15
|
+
const check = (path, stat) => {
|
|
16
|
+
if (!stat.isDirectory()) fail('UNSAFE_UPGRADE_PATH', 'Upgrade ancestor must be a real directory');
|
|
17
|
+
if (expected && Object.hasOwn(expected, path) && expected[path] !== identity(stat)) {
|
|
18
|
+
fail('UPGRADE_STORAGE_CHANGED', `Upgrade parent changed: ${path || '.'}`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
check('', fstatSync(fd));
|
|
23
|
+
let relative = '';
|
|
24
|
+
for (const part of parts) {
|
|
25
|
+
relative = relative ? `${relative}${sep}${part}` : part;
|
|
26
|
+
let stat;
|
|
27
|
+
try { stat = lstatAtSync(fd, part); }
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code !== 'ENOENT') throw error;
|
|
30
|
+
if (expected?.[relative]) fail('UPGRADE_STORAGE_CHANGED', `Upgrade parent disappeared: ${relative}`);
|
|
31
|
+
if (!create) { closeSync(fd); return undefined; }
|
|
32
|
+
mkdirAtSync(fd, part, 0o700); fsyncSync(fd);
|
|
33
|
+
stat = lstatAtSync(fd, part);
|
|
34
|
+
if (expected) expected[relative] = identity(stat);
|
|
35
|
+
}
|
|
36
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) fail('UNSAFE_UPGRADE_PATH', `Upgrade ancestor is linked or not a directory: ${relative}`);
|
|
37
|
+
check(relative, stat);
|
|
38
|
+
const next = openAtSync(fd, part, constants.O_RDONLY | constants.O_DIRECTORY);
|
|
39
|
+
if (identity(fstatSync(next)) !== identity(stat)) {
|
|
40
|
+
closeSync(next); fail('UPGRADE_STORAGE_CHANGED', `Upgrade parent changed while opening: ${relative}`);
|
|
41
|
+
}
|
|
42
|
+
closeSync(fd); fd = next;
|
|
43
|
+
}
|
|
44
|
+
return fd;
|
|
45
|
+
} catch (error) { closeSync(fd); throw error; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function snapshotUpgradeParents(root, targets) {
|
|
49
|
+
const paths = new Set(['']);
|
|
50
|
+
for (const target of targets) {
|
|
51
|
+
let path = dirname(target);
|
|
52
|
+
while (path !== '.') { paths.add(path); path = dirname(path); }
|
|
53
|
+
}
|
|
54
|
+
return Object.fromEntries([...paths].sort().map(path => {
|
|
55
|
+
const fd = openUpgradeDirectory(root, path);
|
|
56
|
+
if (fd === undefined) return [path, null];
|
|
57
|
+
try { return [path, identity(fstatSync(fd))]; } finally { closeSync(fd); }
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function assertUpgradeParentsUnchanged(root, expected) {
|
|
62
|
+
for (const [path, before] of Object.entries(expected)) {
|
|
63
|
+
const fd = openUpgradeDirectory(root, path, { expected });
|
|
64
|
+
try {
|
|
65
|
+
if ((fd === undefined ? null : identity(fstatSync(fd))) !== before) {
|
|
66
|
+
fail('UPGRADE_STORAGE_CHANGED', `Upgrade parent changed: ${path || '.'}`);
|
|
67
|
+
}
|
|
68
|
+
} finally { if (fd !== undefined) closeSync(fd); }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createUpgradeParents(root, targets, expected) {
|
|
73
|
+
assertUpgradeParentsUnchanged(root, expected);
|
|
74
|
+
const current = { ...expected };
|
|
75
|
+
for (const target of targets) {
|
|
76
|
+
const fd = openUpgradeDirectory(root, dirname(target), { create: true, expected: current });
|
|
77
|
+
closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
assertUpgradeParentsUnchanged(root, current);
|
|
80
|
+
return current;
|
|
81
|
+
}
|