@gajae-code/coding-agent 0.11.7 → 0.11.8
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/CHANGELOG.md +55 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/config/keybindings.d.ts +33 -3
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/settings-schema.d.ts +15 -2
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
- package/dist/types/main.d.ts +8 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +3 -2
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +5 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +25 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +11 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +17 -2
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-storage.d.ts +18 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/image-gen.d.ts +47 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli.ts +18 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/keybindings.ts +157 -24
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +17 -2
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +75 -3
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +5 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/internal-urls/local-protocol.ts +33 -16
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +25 -10
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/components/model-selector.ts +42 -8
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +20 -4
- package/src/modes/components/welcome.ts +22 -18
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +7 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +46 -54
- package/src/modes/interactive-mode.ts +41 -27
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/transports/http.ts +13 -6
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +18 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +240 -65
- package/src/sdk/bus/lifecycle-commands.ts +47 -3
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +27 -2
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
- package/src/sdk/bus/telegram-daemon-control.ts +145 -33
- package/src/sdk/bus/telegram-daemon.ts +835 -110
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +47 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +2 -0
- package/src/sdk/startup-capability.ts +9 -1
- package/src/session/agent-session.ts +52 -5
- package/src/session/blob-store.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +854 -84
- package/src/session/internal/managed-session-storage.ts +251 -199
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +310 -327
- package/src/session/session-storage.ts +128 -11
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +198 -88
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/image-gen.ts +178 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/url-guard.ts +3 -1
- package/src/web/scrapers/docs-rs.ts +30 -5
|
@@ -18,6 +18,12 @@ import {
|
|
|
18
18
|
verifyOwnerOnlyPathSecurity,
|
|
19
19
|
verifyOwnerOnlyPathSecurityExpected,
|
|
20
20
|
} from "@gajae-code/natives";
|
|
21
|
+
import {
|
|
22
|
+
classifyNativePublishOutcome,
|
|
23
|
+
formatNativePublishDiagnostic,
|
|
24
|
+
mayCleanCurrentStaging,
|
|
25
|
+
type NativePublishOutcome,
|
|
26
|
+
} from "./native-publish-outcome";
|
|
21
27
|
|
|
22
28
|
export const MANAGED_ARTIFACT_MAX_DEPTH = 32;
|
|
23
29
|
export const MANAGED_ARTIFACT_MAX_FILES = 10_000;
|
|
@@ -28,6 +34,82 @@ const LOCK_HEARTBEAT_MS = 10_000;
|
|
|
28
34
|
const LOCK_WAIT_MS = 5_000;
|
|
29
35
|
|
|
30
36
|
const LOCK_STALE_RECHECK_MS = 100;
|
|
37
|
+
export class ManagedPublishError extends Error {
|
|
38
|
+
readonly classification:
|
|
39
|
+
| "destination_conflict"
|
|
40
|
+
| "atomic_unavailable"
|
|
41
|
+
| "invalid_request"
|
|
42
|
+
| "durability_not_provable"
|
|
43
|
+
| "identity_mismatch"
|
|
44
|
+
| "io_error"
|
|
45
|
+
| "durability_failed";
|
|
46
|
+
readonly diagnostic: string;
|
|
47
|
+
|
|
48
|
+
constructor(classification: ManagedPublishError["classification"], outcome: NativePublishOutcome) {
|
|
49
|
+
super(classification);
|
|
50
|
+
this.name = "ManagedPublishError";
|
|
51
|
+
this.classification = classification;
|
|
52
|
+
this.diagnostic = formatNativePublishDiagnostic(outcome);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function publishFailure(outcome: NativePublishOutcome): ManagedPublishError {
|
|
57
|
+
const classification =
|
|
58
|
+
outcome.reason === "destination_exists"
|
|
59
|
+
? "destination_conflict"
|
|
60
|
+
: outcome.reason === "atomic_unavailable"
|
|
61
|
+
? "atomic_unavailable"
|
|
62
|
+
: outcome.reason === "invalid_request"
|
|
63
|
+
? "invalid_request"
|
|
64
|
+
: outcome.reason === "durability_not_provable"
|
|
65
|
+
? "durability_not_provable"
|
|
66
|
+
: outcome.reason === "identity_violation"
|
|
67
|
+
? "identity_mismatch"
|
|
68
|
+
: outcome.reason === "io_failure"
|
|
69
|
+
? "io_error"
|
|
70
|
+
: "durability_failed";
|
|
71
|
+
return new ManagedPublishError(classification, outcome);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A same-filesystem rename updates the moved root's ctime but no other tree identity. */
|
|
75
|
+
function sameDirectoryTreeSnapshotAfterRename(
|
|
76
|
+
left: NativeDirectoryTreeSnapshot,
|
|
77
|
+
right: NativeDirectoryTreeSnapshot,
|
|
78
|
+
): boolean {
|
|
79
|
+
return (
|
|
80
|
+
left.rootDev === right.rootDev &&
|
|
81
|
+
left.rootIno === right.rootIno &&
|
|
82
|
+
left.entries.length === right.entries.length &&
|
|
83
|
+
left.entries.every((entry, index) => {
|
|
84
|
+
const other = right.entries[index];
|
|
85
|
+
return (
|
|
86
|
+
other !== undefined &&
|
|
87
|
+
entry.relativePath === other.relativePath &&
|
|
88
|
+
entry.kind === other.kind &&
|
|
89
|
+
entry.dev === other.dev &&
|
|
90
|
+
entry.ino === other.ino &&
|
|
91
|
+
entry.size === other.size &&
|
|
92
|
+
entry.mtimeNs === other.mtimeNs &&
|
|
93
|
+
(entry.relativePath === "" || entry.ctimeNs === other.ctimeNs) &&
|
|
94
|
+
entry.sha256 === other.sha256
|
|
95
|
+
);
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class ManagedTreeMoveOutcomeError extends Error {
|
|
101
|
+
constructor(
|
|
102
|
+
message: string,
|
|
103
|
+
readonly stagingCleanupSafe: boolean,
|
|
104
|
+
) {
|
|
105
|
+
super(message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Cleanup is safe only when the native move reports that it did not commit. */
|
|
110
|
+
export function mayCleanManagedTreeStaging(error: unknown): boolean {
|
|
111
|
+
return !(error instanceof ManagedTreeMoveOutcomeError) || error.stagingCleanupSafe;
|
|
112
|
+
}
|
|
31
113
|
|
|
32
114
|
export type ManagedSessionSecurityPolicy = "default" | "windows-existing-verify-first";
|
|
33
115
|
|
|
@@ -39,6 +121,8 @@ export type ManagedStorageFailure =
|
|
|
39
121
|
| "source_changed"
|
|
40
122
|
| "unsafe_artifacts"
|
|
41
123
|
| "durability_failed"
|
|
124
|
+
| "atomic_unavailable"
|
|
125
|
+
| "durability_not_provable"
|
|
42
126
|
| "migration_retired"
|
|
43
127
|
| "managed_storage_unsupported";
|
|
44
128
|
|
|
@@ -97,6 +181,9 @@ export function validateNativeSecurityResult(
|
|
|
97
181
|
) {
|
|
98
182
|
throw new Error("Malformed ACL security failure evidence");
|
|
99
183
|
}
|
|
184
|
+
if (operation === "verify" && result.operation !== "query") {
|
|
185
|
+
throw new Error("Verify failure unexpectedly reports ACL mutation");
|
|
186
|
+
}
|
|
100
187
|
} else if (result.operation !== undefined || result.attribute !== undefined) {
|
|
101
188
|
throw new Error("Unexpected ACL fields on owner-only security failure");
|
|
102
189
|
}
|
|
@@ -183,6 +270,40 @@ export interface ManagedDirectoryRoot {
|
|
|
183
270
|
readonly ino: bigint;
|
|
184
271
|
}
|
|
185
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Establish the first managed directory beneath a potentially shared ancestor.
|
|
275
|
+
* The ancestor is used only to create the managed chain; every component from
|
|
276
|
+
* `configuredRoot` downward is independently type, symlink, owner, mode, and
|
|
277
|
+
* ACL checked. This deliberately does not impose owner-only policy on `/tmp`.
|
|
278
|
+
*/
|
|
279
|
+
export function prepareManagedDirectoryRoot(
|
|
280
|
+
configuredRoot: string,
|
|
281
|
+
policy: ManagedSessionSecurityPolicy = "default",
|
|
282
|
+
): ManagedDirectoryRoot {
|
|
283
|
+
const target = path.resolve(configuredRoot);
|
|
284
|
+
const missing: string[] = [];
|
|
285
|
+
let existing = target;
|
|
286
|
+
for (;;) {
|
|
287
|
+
try {
|
|
288
|
+
assertSafeDirectory(existing);
|
|
289
|
+
break;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
292
|
+
const parent = path.dirname(existing);
|
|
293
|
+
if (parent === existing) throw new Error(`Managed root is unavailable: ${configuredRoot}`);
|
|
294
|
+
missing.unshift(path.basename(existing));
|
|
295
|
+
existing = parent;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
let current = fs.realpathSync.native(existing);
|
|
299
|
+
for (const component of missing) {
|
|
300
|
+
current = path.join(current, component);
|
|
301
|
+
const created = ensureDirectoryComponent(current);
|
|
302
|
+
secureManagedDirectory(current, created, policy);
|
|
303
|
+
}
|
|
304
|
+
if (missing.length === 0) secureManagedDirectory(current, false, policy);
|
|
305
|
+
return managedDirectoryRoot(current);
|
|
306
|
+
}
|
|
186
307
|
export function managedDirectoryRoot(configuredRoot: string): ManagedDirectoryRoot {
|
|
187
308
|
const canonicalPath = fs.realpathSync.native(configuredRoot);
|
|
188
309
|
const stat = fs.lstatSync(canonicalPath, { bigint: true });
|
|
@@ -339,8 +460,8 @@ export class ManagedSessionDescendantStore {
|
|
|
339
460
|
retained?: { authority: RecoveryFsRoot; authorityBaseDir: string },
|
|
340
461
|
policy?: ManagedSessionSecurityPolicy,
|
|
341
462
|
) {
|
|
342
|
-
assertManagedDirectoryRoot(root);
|
|
343
463
|
managedRelativePath(root, baseDir);
|
|
464
|
+
|
|
344
465
|
this.#root = root;
|
|
345
466
|
this.#baseDir = path.resolve(baseDir);
|
|
346
467
|
this.#policy = policy ?? "default";
|
|
@@ -350,20 +471,17 @@ export class ManagedSessionDescendantStore {
|
|
|
350
471
|
const captured = retained.authority.snapshotManagedTree(relative);
|
|
351
472
|
if (!captured.ok || !captured.snapshot)
|
|
352
473
|
throw new Error(captured.code ?? "Managed subtree identity unavailable");
|
|
353
|
-
const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
354
|
-
if (
|
|
355
|
-
captured.snapshot.rootDev !== subtreeStat.dev.toString() ||
|
|
356
|
-
captured.snapshot.rootIno !== subtreeStat.ino.toString()
|
|
357
|
-
)
|
|
358
|
-
throw new Error("Managed descendant root identity changed");
|
|
359
474
|
this.#subtreeRoot = Object.freeze({
|
|
360
475
|
canonicalPath: this.#baseDir,
|
|
361
|
-
dev:
|
|
362
|
-
ino:
|
|
476
|
+
dev: BigInt(captured.snapshot.rootDev),
|
|
477
|
+
ino: BigInt(captured.snapshot.rootIno),
|
|
363
478
|
});
|
|
364
479
|
this.#authority = retained.authority;
|
|
480
|
+
this.#assertBound();
|
|
481
|
+
|
|
365
482
|
return;
|
|
366
483
|
}
|
|
484
|
+
assertManagedDirectoryRoot(root);
|
|
367
485
|
ensureManagedDirectory(this.#baseDir, root, this.#policy);
|
|
368
486
|
const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
369
487
|
this.#subtreeRoot = Object.freeze({ canonicalPath: this.#baseDir, dev: subtreeStat.dev, ino: subtreeStat.ino });
|
|
@@ -557,7 +675,7 @@ export class ManagedSessionDescendantStore {
|
|
|
557
675
|
appendSync(relativePath: string, bytes: Uint8Array): void {
|
|
558
676
|
this.#assertBound();
|
|
559
677
|
const resolved = this.#resolve(relativePath);
|
|
560
|
-
|
|
678
|
+
let existing = this.readExpected(relativePath);
|
|
561
679
|
if (!existing) throw new Error("managed_append_missing");
|
|
562
680
|
if (this.#authority) {
|
|
563
681
|
const appended = this.#authority.appendManaged(
|
|
@@ -574,20 +692,49 @@ export class ManagedSessionDescendantStore {
|
|
|
574
692
|
this.#assertBound();
|
|
575
693
|
return;
|
|
576
694
|
}
|
|
695
|
+
// On Darwin, the first write-append open can change only ctime (e.g. com.apple.provenance)
|
|
696
|
+
// while leaving dev/ino/size/mtime/content intact. Allow one bounded refresh+retry for that
|
|
697
|
+
// transition only; any other identity change or a second ctime transition stays fail-closed.
|
|
698
|
+
// See https://github.com/Yeachan-Heo/gajae-code/issues/2944
|
|
577
699
|
let fd: number | undefined;
|
|
700
|
+
let darwinCtimeRefreshConsumed = false;
|
|
578
701
|
try {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
702
|
+
for (;;) {
|
|
703
|
+
fd = fs.openSync(resolved, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_NOFOLLOW);
|
|
704
|
+
const before = identity(fs.fstatSync(fd, { bigint: true }));
|
|
705
|
+
if (!sameIdentity(before, existing.identity)) {
|
|
706
|
+
const allowDarwinCtimeRefresh =
|
|
707
|
+
process.platform === "darwin" &&
|
|
708
|
+
!darwinCtimeRefreshConsumed &&
|
|
709
|
+
isCtimeOnlyIdentityMismatch(before, existing.identity);
|
|
710
|
+
if (!allowDarwinCtimeRefresh) throw new Error("identity_mismatch");
|
|
711
|
+
fs.closeSync(fd);
|
|
712
|
+
fd = undefined;
|
|
713
|
+
darwinCtimeRefreshConsumed = true;
|
|
714
|
+
const original = existing;
|
|
715
|
+
const refreshed = this.readExpected(relativePath);
|
|
716
|
+
if (
|
|
717
|
+
!refreshed ||
|
|
718
|
+
!sameStableIdentityIgnoringCtime(refreshed.identity, original.identity) ||
|
|
719
|
+
refreshed.identity.sha256 !== original.identity.sha256 ||
|
|
720
|
+
!refreshed.bytes.equals(original.bytes)
|
|
721
|
+
) {
|
|
722
|
+
throw new Error("identity_mismatch");
|
|
723
|
+
}
|
|
724
|
+
existing = refreshed;
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
secureFileDescriptor(resolved, fd, "verify");
|
|
728
|
+
let offset = 0;
|
|
729
|
+
while (offset < bytes.byteLength) offset += fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
730
|
+
fs.fsyncSync(fd);
|
|
731
|
+
secureFileDescriptor(resolved, fd, "verify");
|
|
732
|
+
const after = identity(fs.fstatSync(fd, { bigint: true }));
|
|
733
|
+
const named = identity(fs.lstatSync(resolved, { bigint: true }));
|
|
734
|
+
if (!sameIdentity(after, named) || after.dev !== before.dev || after.ino !== before.ino)
|
|
735
|
+
throw new Error("identity_mismatch");
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
591
738
|
} finally {
|
|
592
739
|
if (fd !== undefined) fs.closeSync(fd);
|
|
593
740
|
}
|
|
@@ -608,6 +755,7 @@ export class ManagedSessionDescendantStore {
|
|
|
608
755
|
if (!created.ok) throw new Error(created.code ?? "managed_publish_failed");
|
|
609
756
|
if (!created.identity) throw new Error("managed_publish_identity_unavailable");
|
|
610
757
|
let captured: ReturnType<RecoveryFsRoot["readManaged"]> | undefined;
|
|
758
|
+
let published: unknown;
|
|
611
759
|
try {
|
|
612
760
|
captured = this.#authority.readManaged(temporary);
|
|
613
761
|
if (!captured.ok || !captured.identity || !captured.data)
|
|
@@ -633,7 +781,7 @@ export class ManagedSessionDescendantStore {
|
|
|
633
781
|
digest,
|
|
634
782
|
);
|
|
635
783
|
if (!synced.ok) throw new Error(synced.code ?? "managed_publish_fsync_failed");
|
|
636
|
-
|
|
784
|
+
published = this.#authority.renameManagedFileNoReplace(
|
|
637
785
|
temporary,
|
|
638
786
|
relative,
|
|
639
787
|
captured.identity.dev,
|
|
@@ -643,35 +791,14 @@ export class ManagedSessionDescendantStore {
|
|
|
643
791
|
captured.identity.ctimeNs,
|
|
644
792
|
digest,
|
|
645
793
|
);
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
published.code === "already_exists"
|
|
649
|
-
? "destination_conflict"
|
|
650
|
-
: (published.code ?? "managed_publish_failed"),
|
|
651
|
-
);
|
|
794
|
+
const outcome = classifyNativePublishOutcome(published, "retained_file");
|
|
795
|
+
if (!outcome.ok) throw publishFailure(outcome);
|
|
652
796
|
} catch (error) {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
installed.identity &&
|
|
659
|
-
installed.data &&
|
|
660
|
-
installed.identity.dev === captured.identity.dev &&
|
|
661
|
-
installed.identity.ino === captured.identity.ino &&
|
|
662
|
-
(installed.identity.sha256 ?? createHash("sha256").update(installed.data).digest("hex")) === digest
|
|
663
|
-
) {
|
|
664
|
-
const removed = this.#authority.removeManaged(
|
|
665
|
-
relative,
|
|
666
|
-
installed.identity.dev,
|
|
667
|
-
installed.identity.ino,
|
|
668
|
-
installed.identity.size,
|
|
669
|
-
installed.identity.mtimeNs,
|
|
670
|
-
installed.identity.ctimeNs,
|
|
671
|
-
digest,
|
|
672
|
-
);
|
|
673
|
-
if (!removed.ok) throw new Error(removed.code ?? "managed_publish_reconcile_failed");
|
|
674
|
-
}
|
|
797
|
+
// A committed or unknown native outcome is evidence, not authorization to
|
|
798
|
+
// probe or remove the destination. Only a validated pre-mutation result
|
|
799
|
+
// may clean this operation's still-named staging object.
|
|
800
|
+
const outcome = captured ? classifyNativePublishOutcome(published, "retained_file") : undefined;
|
|
801
|
+
if (captured?.ok && captured.identity && captured.data && outcome && mayCleanCurrentStaging(outcome)) {
|
|
675
802
|
const staged = this.#authority.readManaged(temporary);
|
|
676
803
|
if (
|
|
677
804
|
staged.ok &&
|
|
@@ -689,7 +816,8 @@ export class ManagedSessionDescendantStore {
|
|
|
689
816
|
staged.identity.ctimeNs,
|
|
690
817
|
staged.identity.sha256 ?? createHash("sha256").update(staged.data).digest("hex"),
|
|
691
818
|
);
|
|
692
|
-
if (!removed.ok
|
|
819
|
+
if (!removed.ok && removed.code !== "cleanup_pending")
|
|
820
|
+
throw new Error(removed.code ?? "managed_publish_reconcile_failed");
|
|
693
821
|
}
|
|
694
822
|
}
|
|
695
823
|
throw error;
|
|
@@ -762,7 +890,17 @@ export class ManagedSessionDescendantStore {
|
|
|
762
890
|
sha256: expected.identity.sha256,
|
|
763
891
|
quarantineName: `.gjc-remove-${process.pid}-${randomUUID()}`,
|
|
764
892
|
});
|
|
765
|
-
if (
|
|
893
|
+
if (
|
|
894
|
+
!removed.ok &&
|
|
895
|
+
!(
|
|
896
|
+
removed.code === "cleanup_pending" &&
|
|
897
|
+
(removed.detachedPath ??
|
|
898
|
+
removed.retainedSuccessorPath ??
|
|
899
|
+
removed.retainedPlaceholderPath ??
|
|
900
|
+
removed.retainedUnknownPath) !== undefined
|
|
901
|
+
)
|
|
902
|
+
)
|
|
903
|
+
throw new Error(removed.code ?? "managed_remove_failed");
|
|
766
904
|
this.#assertBound();
|
|
767
905
|
return;
|
|
768
906
|
}
|
|
@@ -775,7 +913,8 @@ export class ManagedSessionDescendantStore {
|
|
|
775
913
|
expected.identity.ctimeNs.toString(),
|
|
776
914
|
expected.identity.sha256,
|
|
777
915
|
);
|
|
778
|
-
if (!removed.ok
|
|
916
|
+
if (!removed.ok && !(removed.code === "cleanup_pending" && removed.recoveryPath !== undefined))
|
|
917
|
+
throw new Error(removed.code ?? "managed_remove_failed");
|
|
779
918
|
this.#assertBound();
|
|
780
919
|
}
|
|
781
920
|
/** Read and remove one managed descendant through retained authority. */
|
|
@@ -799,7 +938,8 @@ export class ManagedSessionDescendantStore {
|
|
|
799
938
|
existing.identity.ctimeNs,
|
|
800
939
|
existing.identity.sha256 ?? createHash("sha256").update(existing.data).digest("hex"),
|
|
801
940
|
);
|
|
802
|
-
if (!removed.ok
|
|
941
|
+
if (!removed.ok && !(removed.code === "cleanup_pending" && removed.recoveryPath !== undefined))
|
|
942
|
+
throw new Error(removed.code ?? "managed_consume_failed");
|
|
803
943
|
this.#assertBound();
|
|
804
944
|
return existing.data;
|
|
805
945
|
}
|
|
@@ -885,92 +1025,31 @@ export class ManagedSessionDescendantStore {
|
|
|
885
1025
|
sourceRelativePath: string,
|
|
886
1026
|
destinationRelativePath: string,
|
|
887
1027
|
expected: NativeDirectoryTreeSnapshot,
|
|
888
|
-
):
|
|
1028
|
+
): NativeDirectoryTreeSnapshot {
|
|
889
1029
|
this.#assertBound();
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
try {
|
|
896
|
-
this.ensureDirectory(destinationRelativePath);
|
|
897
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
898
|
-
for (const entry of expected.entries) {
|
|
899
|
-
if (entry.relativePath === "") continue;
|
|
900
|
-
const target = path.posix.join(destinationRelativePath.replaceAll(path.sep, "/"), entry.relativePath);
|
|
901
|
-
if (entry.kind === "directory") {
|
|
902
|
-
this.ensureDirectory(target);
|
|
903
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
const sourceEntry = this.readExpected(
|
|
907
|
-
path.posix.join(sourceRelativePath.replaceAll(path.sep, "/"), entry.relativePath),
|
|
908
|
-
);
|
|
909
|
-
if (
|
|
910
|
-
!sourceEntry ||
|
|
911
|
-
sourceEntry.identity.dev.toString() !== entry.dev ||
|
|
912
|
-
sourceEntry.identity.ino.toString() !== entry.ino ||
|
|
913
|
-
sourceEntry.identity.size.toString() !== entry.size ||
|
|
914
|
-
sourceEntry.identity.mtimeNs.toString() !== entry.mtimeNs ||
|
|
915
|
-
sourceEntry.identity.sha256 !== entry.sha256
|
|
916
|
-
)
|
|
917
|
-
throw new Error("artifact_source_changed");
|
|
918
|
-
this.publishNoReplaceSync(target, sourceEntry.bytes);
|
|
919
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
920
|
-
}
|
|
921
|
-
destinationSnapshot = this.captureTree(destinationRelativePath);
|
|
922
|
-
const comparable = (tree: NativeDirectoryTreeSnapshot) =>
|
|
923
|
-
tree.entries.map(entry => ({
|
|
924
|
-
relativePath: entry.relativePath,
|
|
925
|
-
kind: entry.kind,
|
|
926
|
-
size: entry.size,
|
|
927
|
-
sha256: entry.sha256,
|
|
928
|
-
}));
|
|
929
|
-
if (JSON.stringify(comparable(destinationSnapshot)) !== JSON.stringify(comparable(expected)))
|
|
930
|
-
throw new Error("artifact_destination_mismatch");
|
|
931
|
-
const terminalSource = snapshotDirectoryTree(source);
|
|
932
|
-
if (
|
|
933
|
-
!terminalSource.ok ||
|
|
934
|
-
!terminalSource.snapshot ||
|
|
935
|
-
JSON.stringify(terminalSource.snapshot) !== JSON.stringify(expected)
|
|
1030
|
+
const moved = this.#authority
|
|
1031
|
+
? this.#authority.renameManagedTreeNoReplace(
|
|
1032
|
+
this.#relative(this.#resolve(sourceRelativePath)),
|
|
1033
|
+
this.#relative(this.#resolve(destinationRelativePath)),
|
|
1034
|
+
expected,
|
|
936
1035
|
)
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
this.#relative(this.#resolve(destinationRelativePath)),
|
|
953
|
-
expected,
|
|
954
|
-
);
|
|
955
|
-
if (!moved.ok) {
|
|
956
|
-
try {
|
|
957
|
-
const installed = this.captureTree(destinationRelativePath);
|
|
958
|
-
const comparable = (tree: NativeDirectoryTreeSnapshot) =>
|
|
959
|
-
tree.entries.map(entry => ({
|
|
960
|
-
relativePath: entry.relativePath,
|
|
961
|
-
kind: entry.kind,
|
|
962
|
-
size: entry.size,
|
|
963
|
-
sha256: entry.sha256,
|
|
964
|
-
}));
|
|
965
|
-
if (JSON.stringify(comparable(installed)) !== JSON.stringify(comparable(expected)))
|
|
966
|
-
throw new Error("managed_publish_reconcile_failed");
|
|
967
|
-
this.removeTreeExpected(destinationRelativePath, installed);
|
|
968
|
-
} catch (error) {
|
|
969
|
-
if (!(error instanceof Error) || error.message !== "not_found") throw error;
|
|
970
|
-
}
|
|
971
|
-
throw new Error(moved.code ?? "managed_move_failed");
|
|
972
|
-
}
|
|
1036
|
+
: renameNoReplacePath(this.#resolve(sourceRelativePath), this.#resolve(destinationRelativePath));
|
|
1037
|
+
const outcome = classifyNativePublishOutcome(moved, this.#authority ? "retained_tree" : "direct_rename");
|
|
1038
|
+
if (!outcome.ok)
|
|
1039
|
+
throw new ManagedTreeMoveOutcomeError(publishFailure(outcome).message, mayCleanCurrentStaging(outcome));
|
|
1040
|
+
const movedSnapshot = this.#authority
|
|
1041
|
+
? this.#authority.snapshotManagedTree(this.#relative(this.#resolve(destinationRelativePath)))
|
|
1042
|
+
: snapshotDirectoryTree(this.#resolve(destinationRelativePath));
|
|
1043
|
+
if (
|
|
1044
|
+
!movedSnapshot.ok ||
|
|
1045
|
+
!movedSnapshot.snapshot ||
|
|
1046
|
+
!sameDirectoryTreeSnapshotAfterRename(movedSnapshot.snapshot, expected)
|
|
1047
|
+
)
|
|
1048
|
+
throw new ManagedTreeMoveOutcomeError("artifact_destination_mismatch", false);
|
|
1049
|
+
if (!this.#authority) fsyncDirectory(this.#baseDir);
|
|
1050
|
+
|
|
973
1051
|
this.#assertBound();
|
|
1052
|
+
return movedSnapshot.snapshot;
|
|
974
1053
|
}
|
|
975
1054
|
|
|
976
1055
|
removeTreeExpected(relativePath: string, expected: NativeDirectoryTreeSnapshot): void {
|
|
@@ -1098,6 +1177,24 @@ function sameIdentity(left: ManagedFileSnapshot["identity"], right: ManagedFileS
|
|
|
1098
1177
|
);
|
|
1099
1178
|
}
|
|
1100
1179
|
|
|
1180
|
+
/** Content/path identity that remains meaningful across a Darwin write-open ctime-only transition. */
|
|
1181
|
+
function sameStableIdentityIgnoringCtime(
|
|
1182
|
+
left: ManagedFileSnapshot["identity"],
|
|
1183
|
+
right: ManagedFileSnapshot["identity"],
|
|
1184
|
+
): boolean {
|
|
1185
|
+
return (
|
|
1186
|
+
left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/** True when observed identity matches expected on every field except ctimeNs. */
|
|
1191
|
+
function isCtimeOnlyIdentityMismatch(
|
|
1192
|
+
observed: ManagedFileSnapshot["identity"],
|
|
1193
|
+
expected: ManagedFileSnapshot["identity"],
|
|
1194
|
+
): boolean {
|
|
1195
|
+
return sameStableIdentityIgnoringCtime(observed, expected) && observed.ctimeNs !== expected.ctimeNs;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1101
1198
|
function parseLock(pathname: string): LockRecord | undefined {
|
|
1102
1199
|
try {
|
|
1103
1200
|
const stat = fs.lstatSync(pathname);
|
|
@@ -1228,9 +1325,10 @@ export async function publishManagedFileNoReplace(
|
|
|
1228
1325
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
|
|
1229
1326
|
let fd: number | undefined;
|
|
1230
1327
|
let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1231
|
-
let stagingSnapshot: ManagedFileSnapshot | undefined;
|
|
1232
|
-
let committed = false;
|
|
1233
1328
|
let failure: unknown;
|
|
1329
|
+
let outcome: NativePublishOutcome | undefined;
|
|
1330
|
+
let renameAttempted = false;
|
|
1331
|
+
|
|
1234
1332
|
try {
|
|
1235
1333
|
assertOwned?.();
|
|
1236
1334
|
fd = fs.openSync(
|
|
@@ -1245,17 +1343,13 @@ export async function publishManagedFileNoReplace(
|
|
|
1245
1343
|
secureFileDescriptor(staging, fd, "verify");
|
|
1246
1344
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1247
1345
|
stagingIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1248
|
-
stagingSnapshot = {
|
|
1249
|
-
bytes: Buffer.from(bytes),
|
|
1250
|
-
identity: identity(staged, createHash("sha256").update(bytes).digest("hex")),
|
|
1251
|
-
};
|
|
1252
1346
|
assertOwned?.();
|
|
1253
1347
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1348
|
+
renameAttempted = true;
|
|
1349
|
+
outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
|
|
1350
|
+
|
|
1351
|
+
if (!outcome.ok) throw publishFailure(outcome);
|
|
1352
|
+
|
|
1259
1353
|
const named = fs.lstatSync(destination, { bigint: true });
|
|
1260
1354
|
if (
|
|
1261
1355
|
!named.isFile() ||
|
|
@@ -1270,25 +1364,12 @@ export async function publishManagedFileNoReplace(
|
|
|
1270
1364
|
fd = undefined;
|
|
1271
1365
|
|
|
1272
1366
|
fsyncDirectory(parent);
|
|
1273
|
-
committed = true;
|
|
1274
1367
|
} catch (error) {
|
|
1275
1368
|
failure = error;
|
|
1276
1369
|
} finally {
|
|
1277
1370
|
if (fd !== undefined) fs.closeSync(fd);
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
const installed = captureManagedFileNoFollow(destination);
|
|
1281
|
-
if (
|
|
1282
|
-
installed.identity.dev === stagingSnapshot.identity.dev &&
|
|
1283
|
-
installed.identity.ino === stagingSnapshot.identity.ino &&
|
|
1284
|
-
installed.identity.sha256 === stagingSnapshot.identity.sha256
|
|
1285
|
-
)
|
|
1286
|
-
await unlinkManagedFileVerified(destination, installed.identity);
|
|
1287
|
-
} catch (error) {
|
|
1288
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") failure ??= error;
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
if (stagingIdentity) {
|
|
1371
|
+
|
|
1372
|
+
if (stagingIdentity && (!renameAttempted || (outcome && mayCleanCurrentStaging(outcome)))) {
|
|
1292
1373
|
await fsp
|
|
1293
1374
|
.lstat(staging, { bigint: true })
|
|
1294
1375
|
.then(stat => {
|
|
@@ -1314,9 +1395,9 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1314
1395
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
|
|
1315
1396
|
let fd: number | undefined;
|
|
1316
1397
|
let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1317
|
-
let stagingSnapshot: ManagedFileSnapshot | undefined;
|
|
1318
|
-
let committed = false;
|
|
1319
1398
|
let failure: unknown;
|
|
1399
|
+
let outcome: NativePublishOutcome | undefined;
|
|
1400
|
+
|
|
1320
1401
|
try {
|
|
1321
1402
|
fd = fs.openSync(
|
|
1322
1403
|
staging,
|
|
@@ -1330,16 +1411,10 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1330
1411
|
secureFileDescriptor(staging, fd, "verify");
|
|
1331
1412
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1332
1413
|
stagingIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1333
|
-
stagingSnapshot = {
|
|
1334
|
-
bytes: Buffer.from(bytes),
|
|
1335
|
-
identity: identity(staged, createHash("sha256").update(bytes).digest("hex")),
|
|
1336
|
-
};
|
|
1337
1414
|
|
|
1338
|
-
|
|
1339
|
-
if (!
|
|
1340
|
-
|
|
1341
|
-
throw new Error("durability_failed");
|
|
1342
|
-
}
|
|
1415
|
+
outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
|
|
1416
|
+
if (!outcome.ok) throw publishFailure(outcome);
|
|
1417
|
+
|
|
1343
1418
|
const named = fs.lstatSync(destination, { bigint: true });
|
|
1344
1419
|
if (!named.isFile() || named.isSymbolicLink() || named.dev !== staged.dev || named.ino !== staged.ino) {
|
|
1345
1420
|
throw new Error("destination_identity_changed");
|
|
@@ -1348,35 +1423,12 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1348
1423
|
fs.closeSync(fd);
|
|
1349
1424
|
fd = undefined;
|
|
1350
1425
|
fsyncDirectory(parent);
|
|
1351
|
-
committed = true;
|
|
1352
1426
|
} catch (error) {
|
|
1353
1427
|
failure = error;
|
|
1354
1428
|
} finally {
|
|
1355
1429
|
if (fd !== undefined) fs.closeSync(fd);
|
|
1356
1430
|
}
|
|
1357
|
-
if (stagingIdentity) {
|
|
1358
|
-
if (!committed && stagingSnapshot) {
|
|
1359
|
-
try {
|
|
1360
|
-
const installed = captureManagedFileNoFollow(destination);
|
|
1361
|
-
if (
|
|
1362
|
-
installed.identity.dev === stagingSnapshot.identity.dev &&
|
|
1363
|
-
installed.identity.ino === stagingSnapshot.identity.ino &&
|
|
1364
|
-
installed.identity.sha256 === stagingSnapshot.identity.sha256
|
|
1365
|
-
) {
|
|
1366
|
-
const removed = exactUnlink(destination, {
|
|
1367
|
-
dev: installed.identity.dev,
|
|
1368
|
-
ino: installed.identity.ino,
|
|
1369
|
-
size: BigInt(installed.identity.size),
|
|
1370
|
-
mtimeNs: installed.identity.mtimeNs,
|
|
1371
|
-
sha256: installed.identity.sha256,
|
|
1372
|
-
quarantineName: `.gjc-remove-${process.pid}-${randomUUID()}`,
|
|
1373
|
-
});
|
|
1374
|
-
if (!removed.ok) failure ??= new Error(removed.code ?? "managed_publish_reconcile_failed");
|
|
1375
|
-
}
|
|
1376
|
-
} catch (error) {
|
|
1377
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") failure ??= error;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1431
|
+
if (stagingIdentity && outcome && mayCleanCurrentStaging(outcome)) {
|
|
1380
1432
|
try {
|
|
1381
1433
|
const named = fs.lstatSync(staging, { bigint: true });
|
|
1382
1434
|
if (named.dev !== stagingIdentity.dev || named.ino !== stagingIdentity.ino) {
|