@gajae-code/coding-agent 0.15.4 → 0.15.6
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 +46 -2
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/update-cli.d.ts +27 -5
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +99 -0
- package/dist/types/config/file-lock.d.ts +49 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -4
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/cli/session-cli.d.ts +42 -1
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +19 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -5
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -3
- package/src/async/job-manager.ts +671 -39
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +142 -6
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +33 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2799 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1028 -123
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +14 -1
- package/src/gjc-runtime/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
- package/src/sdk/bus/prompt-reconciliation.ts +33 -6
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +161 -5
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +1240 -107
- package/src/sdk/host/session-runtime.ts +1029 -115
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +297 -111
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +887 -95
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +29 -10
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +142 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +786 -67
- package/src/tools/index.ts +7 -5
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -1720,6 +1720,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
1720
1720
|
},
|
|
1721
1721
|
},
|
|
1722
1722
|
|
|
1723
|
+
"telemetry.enabled": {
|
|
1724
|
+
type: "boolean",
|
|
1725
|
+
default: false,
|
|
1726
|
+
ui: {
|
|
1727
|
+
tab: "interaction",
|
|
1728
|
+
label: "Anonymous Usage Telemetry",
|
|
1729
|
+
description:
|
|
1730
|
+
"Send minimal allowlisted update and adoption events. Disabled by default; no prompts, paths, environment, identity, or arbitrary errors are collected.",
|
|
1731
|
+
},
|
|
1732
|
+
},
|
|
1733
|
+
|
|
1723
1734
|
"starReminder.enabled": {
|
|
1724
1735
|
type: "boolean",
|
|
1725
1736
|
default: true,
|
package/src/config/settings.ts
CHANGED
|
@@ -329,7 +329,12 @@ function getByPath(obj: RawSettings, segments: string[]): unknown {
|
|
|
329
329
|
|
|
330
330
|
const PATH_SCOPED_ARRAY_SETTINGS = new Set<SettingPath>(["enabledModels", "disabledProviders"]);
|
|
331
331
|
/** Operator-owned settings which must never be workspace-controlled or runtime-overridden. */
|
|
332
|
-
const GLOBAL_ONLY_SETTINGS = new Set<SettingPath>([
|
|
332
|
+
const GLOBAL_ONLY_SETTINGS = new Set<SettingPath>([
|
|
333
|
+
"crashReport.upstream",
|
|
334
|
+
"crashReport.upstreamDsn",
|
|
335
|
+
"ui.language",
|
|
336
|
+
"telemetry.enabled",
|
|
337
|
+
]);
|
|
333
338
|
const LEGACY_THEME_NAME_REPLACEMENTS = {
|
|
334
339
|
dark: "red-claw",
|
|
335
340
|
light: "blue-crab",
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { getAgentDir } from "@gajae-code/utils";
|
|
3
|
-
import {
|
|
3
|
+
import { ConfigFile } from "../config/config-file";
|
|
4
|
+
import {
|
|
5
|
+
type AcceptedModelPresetRegistry,
|
|
6
|
+
loadAcceptedModelPresetRegistryAsync,
|
|
7
|
+
} from "../config/model-preset-registry";
|
|
4
8
|
import { UnknownModelProfileError, validateModelProfileName } from "../config/model-profile-contract";
|
|
5
9
|
import { mergeModelProfiles } from "../config/model-profiles";
|
|
6
|
-
import { ModelsConfigSchema } from "../config/models-config-schema";
|
|
10
|
+
import { type ModelsConfig, ModelsConfigSchema } from "../config/models-config-schema";
|
|
7
11
|
|
|
8
12
|
export interface CoordinatorModelProfile {
|
|
9
13
|
name: string;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
|
-
* Loads the merged
|
|
17
|
+
* Loads the merged embedded + accepted registry + custom model-profile names
|
|
14
18
|
* without loading the session-host model registry. The child still owns
|
|
15
19
|
* profile activation; this validates the selection before session creation.
|
|
16
20
|
*/
|
|
@@ -20,10 +24,11 @@ export type CoordinatorModelProfileLoader = () =>
|
|
|
20
24
|
|
|
21
25
|
const MAX_ECHOED_MPRESET_LENGTH = 128;
|
|
22
26
|
/**
|
|
23
|
-
* Thrown by the default loader when `models.yml`
|
|
24
|
-
* unreadable. This lets the resolver fail closed with a
|
|
25
|
-
* instead of silently collapsing a broken registry to
|
|
26
|
-
* (which would misreport a caller's valid custom
|
|
27
|
+
* Thrown by the default loader when `models.yml` or the accepted preset
|
|
28
|
+
* registry is invalid or unreadable. This lets the resolver fail closed with a
|
|
29
|
+
* distinct, stable reason instead of silently collapsing a broken registry to
|
|
30
|
+
* the built-ins-only set (which would misreport a caller's valid custom
|
|
31
|
+
* profile as unknown).
|
|
27
32
|
*/
|
|
28
33
|
export class CoordinatorModelProfileRegistryError extends Error {
|
|
29
34
|
constructor(cause?: unknown) {
|
|
@@ -34,23 +39,33 @@ export class CoordinatorModelProfileRegistryError extends Error {
|
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
function coordinatorModelProfiles(
|
|
37
|
-
profiles?:
|
|
42
|
+
profiles?: ModelsConfig["profiles"],
|
|
43
|
+
registryProfiles?: AcceptedModelPresetRegistry["profiles"],
|
|
38
44
|
): Map<string, CoordinatorModelProfile> {
|
|
39
|
-
return new Map([...mergeModelProfiles(profiles).keys()].map(name => [name, { name }]));
|
|
45
|
+
return new Map([...mergeModelProfiles(profiles, registryProfiles).keys()].map(name => [name, { name }]));
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
export
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
export function createCoordinatorModelProfileLoader(agentDir: string): CoordinatorModelProfileLoader {
|
|
49
|
+
return async () => loadCoordinatorModelProfilesFromAgentDir(agentDir);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function loadCoordinatorModelProfilesFromAgentDir(
|
|
53
|
+
agentDir: string,
|
|
54
|
+
): Promise<Map<string, CoordinatorModelProfile>> {
|
|
55
|
+
const accepted = await loadAcceptedModelPresetRegistryAsync(agentDir);
|
|
56
|
+
if (accepted.error) throw new CoordinatorModelProfileRegistryError(accepted.error);
|
|
45
57
|
try {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
return coordinatorModelProfiles(
|
|
58
|
+
const modelsConfig = new ConfigFile("models", ModelsConfigSchema, path.join(agentDir, "models.yml"));
|
|
59
|
+
const loaded = modelsConfig.tryLoad();
|
|
60
|
+
if (loaded.status === "error") throw loaded.error;
|
|
61
|
+
return coordinatorModelProfiles(loaded.value?.profiles, accepted.profiles);
|
|
50
62
|
} catch (error) {
|
|
51
63
|
throw new CoordinatorModelProfileRegistryError(error);
|
|
52
64
|
}
|
|
53
|
-
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const loadCoordinatorModelProfiles: CoordinatorModelProfileLoader = async () =>
|
|
68
|
+
loadCoordinatorModelProfilesFromAgentDir(getAgentDir());
|
|
54
69
|
|
|
55
70
|
export type CoordinatorMpresetResolution =
|
|
56
71
|
| { ok: true; mpreset: string | null }
|
|
@@ -112,7 +112,7 @@ function parseRootList(value: string | undefined): string[] {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
function resolveManagedWorktreeRoot(root: string, configured: string | undefined): string {
|
|
115
|
-
const template = (configured?.trim() || "{repo}
|
|
115
|
+
const template = (configured?.trim() || "{repo}/.worktrees").replace(/^~(?=\/|$)/, os.homedir());
|
|
116
116
|
const resolved = template.replaceAll("{repo}", path.basename(root));
|
|
117
117
|
return path.resolve(path.isAbsolute(resolved) ? resolved : path.join(path.dirname(root), resolved));
|
|
118
118
|
}
|
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordinator directory scans that must not treat native exact-unlink debris
|
|
3
|
+
* as live JSON, and must not make start/status unreadable on a large dirent pile.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "node:fs/promises";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
|
|
8
|
+
class ProjectionScanRaceError extends Error {
|
|
9
|
+
readonly code = "PROJECTION_SCAN_RACED";
|
|
10
|
+
|
|
11
|
+
constructor(message: string) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "ProjectionScanRaceError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PROJECTION_SCAN_UNSUPPORTED_CODE = "coordinator_projection_safe_read_unsupported";
|
|
18
|
+
|
|
19
|
+
class ProjectionScanUnsupportedError extends Error {
|
|
20
|
+
readonly code = PROJECTION_SCAN_UNSUPPORTED_CODE;
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
super(PROJECTION_SCAN_UNSUPPORTED_CODE);
|
|
24
|
+
this.name = "ProjectionScanUnsupportedError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Test-only platform seam; production always follows the host platform. */
|
|
29
|
+
export const ProjectionScanTestHooks: {
|
|
30
|
+
platform?: NodeJS.Platform;
|
|
31
|
+
} = {};
|
|
32
|
+
|
|
33
|
+
function projectionPlatform(): NodeJS.Platform {
|
|
34
|
+
return ProjectionScanTestHooks.platform ?? process.platform;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Post-filter parse-candidate cap. Exhaustion returns an explicit incomplete result. */
|
|
38
|
+
export const COORDINATOR_JSON_SCAN_CAP = 10_000;
|
|
39
|
+
|
|
40
|
+
export interface ProjectionScanStat {
|
|
41
|
+
size: number | bigint;
|
|
42
|
+
dev?: number | bigint;
|
|
43
|
+
ino?: number | bigint;
|
|
44
|
+
nlink?: number | bigint;
|
|
45
|
+
mtimeNs?: number | bigint;
|
|
46
|
+
ctimeNs?: number | bigint;
|
|
47
|
+
isDirectory?(): boolean;
|
|
48
|
+
isFile(): boolean;
|
|
49
|
+
isSymbolicLink(): boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ProjectionScanFs {
|
|
53
|
+
readdir(dir: string): Promise<string[]>;
|
|
54
|
+
lstat(file: string): Promise<ProjectionScanStat>;
|
|
55
|
+
readFile(file: string, encoding: "utf8", expected?: ProjectionScanStat): Promise<string>;
|
|
56
|
+
/** Optional descriptor-bound reader used by the production filesystem. */
|
|
57
|
+
readFileSafe?: (file: string, encoding: "utf8", expected?: ProjectionScanStat) => Promise<string>;
|
|
58
|
+
/** Optional pinned directory authority used for enumeration and every child operation. */
|
|
59
|
+
openDirectory?: (dir: string) => Promise<ProjectionScanDirectory>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ProjectionScanDirectory {
|
|
63
|
+
readonly stat: ProjectionScanStat;
|
|
64
|
+
readdir(): Promise<string[]>;
|
|
65
|
+
lstat(entry: string): Promise<ProjectionScanStat>;
|
|
66
|
+
readFile(entry: string, encoding: "utf8", expected?: ProjectionScanStat): Promise<string>;
|
|
67
|
+
close(): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ProjectionScanResult {
|
|
71
|
+
values: unknown[];
|
|
72
|
+
parsed: number;
|
|
73
|
+
capped: boolean;
|
|
74
|
+
skippedDebris: number;
|
|
75
|
+
skippedEmpty: number;
|
|
76
|
+
/** Candidates that changed or disappeared after enumeration. */
|
|
77
|
+
raced: number;
|
|
78
|
+
/** True when the scan cannot be authoritative for its caller. */
|
|
79
|
+
incomplete: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read one discovered candidate without ever following a final-component symlink or
|
|
84
|
+
* blocking on a FIFO. POSIX opens are descriptor-bound by O_NOFOLLOW/O_NONBLOCK. Windows
|
|
85
|
+
* has neither flag, so the path is bracketed by lstat/fstat/lstat identity checks and the
|
|
86
|
+
* bytes still come from the opened handle rather than the mutable pathname.
|
|
87
|
+
*/
|
|
88
|
+
function sameProjectionFile(left: ProjectionScanStat, right: ProjectionScanStat): boolean {
|
|
89
|
+
if (!left.isFile() || !right.isFile()) return false;
|
|
90
|
+
if (
|
|
91
|
+
left.dev === undefined ||
|
|
92
|
+
right.dev === undefined ||
|
|
93
|
+
left.ino === undefined ||
|
|
94
|
+
right.ino === undefined ||
|
|
95
|
+
String(left.dev) !== String(right.dev) ||
|
|
96
|
+
String(left.ino) !== String(right.ino) ||
|
|
97
|
+
String(left.size) !== String(right.size)
|
|
98
|
+
)
|
|
99
|
+
return false;
|
|
100
|
+
if (left.nlink !== undefined && right.nlink !== undefined && String(left.nlink) !== String(right.nlink))
|
|
101
|
+
return false;
|
|
102
|
+
if (left.mtimeNs !== undefined && right.mtimeNs !== undefined && String(left.mtimeNs) !== String(right.mtimeNs))
|
|
103
|
+
return false;
|
|
104
|
+
if (left.ctimeNs !== undefined && right.ctimeNs !== undefined && String(left.ctimeNs) !== String(right.ctimeNs))
|
|
105
|
+
return false;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readProjectionFileSafe(file: string, encoding: "utf8", expected?: ProjectionScanStat): Promise<string> {
|
|
110
|
+
if (encoding !== "utf8") throw new TypeError("Coordinator projection reads require utf8.");
|
|
111
|
+
const platform = projectionPlatform();
|
|
112
|
+
const noFollow = fs.constants.O_NOFOLLOW;
|
|
113
|
+
const nonBlock = fs.constants.O_NONBLOCK;
|
|
114
|
+
if (platform !== "win32" && (typeof noFollow !== "number" || typeof nonBlock !== "number"))
|
|
115
|
+
throw new ProjectionScanUnsupportedError();
|
|
116
|
+
const flags = fs.constants.O_RDONLY | (platform === "win32" ? 0 : (nonBlock as number) | (noFollow as number));
|
|
117
|
+
let before: import("node:fs").BigIntStats | undefined;
|
|
118
|
+
if (platform === "win32") {
|
|
119
|
+
before = await fs.lstat(file, { bigint: true });
|
|
120
|
+
if (before.isSymbolicLink() || !before.isFile())
|
|
121
|
+
throw new ProjectionScanRaceError("candidate is not a regular file");
|
|
122
|
+
}
|
|
123
|
+
let handle: fs.FileHandle | undefined;
|
|
124
|
+
try {
|
|
125
|
+
handle = await fs.open(file, flags);
|
|
126
|
+
const opened = await handle.stat({ bigint: true });
|
|
127
|
+
if (!opened.isFile()) throw new ProjectionScanRaceError("candidate changed to a non-regular file");
|
|
128
|
+
if (expected && !sameProjectionFile(expected, opened))
|
|
129
|
+
throw new ProjectionScanRaceError("candidate changed while opening");
|
|
130
|
+
if (before) {
|
|
131
|
+
if (
|
|
132
|
+
before.dev !== opened.dev ||
|
|
133
|
+
before.ino !== opened.ino ||
|
|
134
|
+
before.nlink !== opened.nlink ||
|
|
135
|
+
before.size !== opened.size ||
|
|
136
|
+
before.mtimeNs !== opened.mtimeNs ||
|
|
137
|
+
before.ctimeNs !== opened.ctimeNs
|
|
138
|
+
)
|
|
139
|
+
throw new ProjectionScanRaceError("candidate changed while opening");
|
|
140
|
+
const relinked = await fs.lstat(file, { bigint: true });
|
|
141
|
+
if (
|
|
142
|
+
relinked.isSymbolicLink() ||
|
|
143
|
+
!relinked.isFile() ||
|
|
144
|
+
relinked.dev !== opened.dev ||
|
|
145
|
+
relinked.ino !== opened.ino ||
|
|
146
|
+
relinked.nlink !== opened.nlink ||
|
|
147
|
+
relinked.size !== opened.size ||
|
|
148
|
+
relinked.mtimeNs !== opened.mtimeNs ||
|
|
149
|
+
relinked.ctimeNs !== opened.ctimeNs
|
|
150
|
+
)
|
|
151
|
+
throw new ProjectionScanRaceError("candidate changed before read");
|
|
152
|
+
}
|
|
153
|
+
return await handle.readFile({ encoding: "utf8" });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if ((error as NodeJS.ErrnoException).code === "ELOOP")
|
|
156
|
+
throw new ProjectionScanRaceError("candidate became a symlink");
|
|
157
|
+
throw error;
|
|
158
|
+
} finally {
|
|
159
|
+
await handle?.close().catch(() => undefined);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function sameProjectionRoot(left: ProjectionScanStat, right: ProjectionScanStat): boolean {
|
|
164
|
+
const leftDirectory = left.isDirectory?.();
|
|
165
|
+
const rightDirectory = right.isDirectory?.();
|
|
166
|
+
return (
|
|
167
|
+
leftDirectory !== false &&
|
|
168
|
+
rightDirectory !== false &&
|
|
169
|
+
left.dev !== undefined &&
|
|
170
|
+
right.dev !== undefined &&
|
|
171
|
+
left.ino !== undefined &&
|
|
172
|
+
right.ino !== undefined &&
|
|
173
|
+
String(left.dev) === String(right.dev) &&
|
|
174
|
+
String(left.ino) === String(right.ino)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function hasProjectionRootIdentity(stat: ProjectionScanStat): boolean {
|
|
179
|
+
return stat.dev !== undefined && stat.ino !== undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function trimProjectionRootPath(dir: string): string {
|
|
183
|
+
const parsedRoot = path.parse(dir).root;
|
|
184
|
+
let trimmed = dir;
|
|
185
|
+
while (trimmed.length > parsedRoot.length && (trimmed.endsWith("/") || trimmed.endsWith("\\")))
|
|
186
|
+
trimmed = trimmed.slice(0, -1);
|
|
187
|
+
while (trimmed.length > parsedRoot.length && (trimmed.endsWith("/.") || trimmed.endsWith("\\."))) {
|
|
188
|
+
trimmed = trimmed.slice(0, -2);
|
|
189
|
+
while (trimmed.length > parsedRoot.length && (trimmed.endsWith("/") || trimmed.endsWith("\\")))
|
|
190
|
+
trimmed = trimmed.slice(0, -1);
|
|
191
|
+
}
|
|
192
|
+
return trimmed || parsedRoot;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Windows has no descriptor-relative `openat` binding in Node. Keep the root pathname
|
|
197
|
+
* authoritative by checking its identity before and after every mutable-path operation.
|
|
198
|
+
*/
|
|
199
|
+
async function openProjectionDirectoryWindowsSafe(dir: string): Promise<ProjectionScanDirectory> {
|
|
200
|
+
const rootPath = trimProjectionRootPath(dir);
|
|
201
|
+
let opened: import("node:fs").BigIntStats;
|
|
202
|
+
try {
|
|
203
|
+
opened = await fs.lstat(rootPath, { bigint: true });
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if ((error as NodeJS.ErrnoException).code === "ELOOP")
|
|
206
|
+
throw new ProjectionScanRaceError("scan root became a reparse point");
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
if (opened.isSymbolicLink() || !opened.isDirectory())
|
|
210
|
+
throw new ProjectionScanRaceError("scan root is not a regular directory");
|
|
211
|
+
|
|
212
|
+
const assertRoot = async (): Promise<void> => {
|
|
213
|
+
let settled: import("node:fs").BigIntStats;
|
|
214
|
+
try {
|
|
215
|
+
settled = await fs.lstat(rootPath, { bigint: true });
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
218
|
+
if (code === "ENOENT" || code === "ELOOP")
|
|
219
|
+
throw new ProjectionScanRaceError("scan root changed during enumeration");
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
if (settled.isSymbolicLink() || !settled.isDirectory() || !sameProjectionRoot(opened, settled))
|
|
223
|
+
throw new ProjectionScanRaceError("scan root changed during enumeration");
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const entryPath = (entry: string): string => {
|
|
227
|
+
// Directory entries come from readdir and are therefore single components on
|
|
228
|
+
// Windows. Keep the invariant explicit so an injected or corrupted entry can
|
|
229
|
+
// never turn a child operation into mutable pathname traversal.
|
|
230
|
+
if (
|
|
231
|
+
entry.length === 0 ||
|
|
232
|
+
entry === "." ||
|
|
233
|
+
entry === ".." ||
|
|
234
|
+
entry.includes("/") ||
|
|
235
|
+
entry.includes("\\") ||
|
|
236
|
+
entry.includes(":")
|
|
237
|
+
)
|
|
238
|
+
throw new ProjectionScanRaceError("scan entry is not a single directory component");
|
|
239
|
+
return path.join(rootPath, entry);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const bracketRoot = async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
243
|
+
let value!: T;
|
|
244
|
+
let operationError: unknown;
|
|
245
|
+
let started = false;
|
|
246
|
+
try {
|
|
247
|
+
await assertRoot();
|
|
248
|
+
started = true;
|
|
249
|
+
value = await operation();
|
|
250
|
+
} catch (error) {
|
|
251
|
+
operationError = error;
|
|
252
|
+
}
|
|
253
|
+
if (started) {
|
|
254
|
+
try {
|
|
255
|
+
await assertRoot();
|
|
256
|
+
} catch (error) {
|
|
257
|
+
operationError = error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (operationError !== undefined) {
|
|
261
|
+
const code = (operationError as NodeJS.ErrnoException).code;
|
|
262
|
+
if (code === "ENOENT" || code === "ELOOP")
|
|
263
|
+
throw new ProjectionScanRaceError("scan operation raced with a replacement or reparse point");
|
|
264
|
+
throw operationError;
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
stat: opened,
|
|
271
|
+
readdir: () => bracketRoot(() => fs.readdir(rootPath)),
|
|
272
|
+
lstat: entry =>
|
|
273
|
+
bracketRoot(async () => {
|
|
274
|
+
const stat = await fs.lstat(entryPath(entry), { bigint: true });
|
|
275
|
+
if (stat.isSymbolicLink()) throw new ProjectionScanRaceError("candidate became a reparse point");
|
|
276
|
+
return stat;
|
|
277
|
+
}),
|
|
278
|
+
readFile: (entry, encoding, expected) =>
|
|
279
|
+
bracketRoot(() => readProjectionFileSafe(entryPath(entry), encoding, expected)),
|
|
280
|
+
close: async () => {},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Open a no-follow directory authority and keep it alive through enumeration, child
|
|
286
|
+
* lstat, and child reads. Node has no `openat` binding, so POSIX uses the proc fd path
|
|
287
|
+
* as the descriptor-relative namespace; a replacement root or parent cannot redirect it.
|
|
288
|
+
*/
|
|
289
|
+
async function openProjectionDirectorySafe(dir: string): Promise<ProjectionScanDirectory> {
|
|
290
|
+
const platform = projectionPlatform();
|
|
291
|
+
if (platform === "win32") return openProjectionDirectoryWindowsSafe(dir);
|
|
292
|
+
if (platform !== "linux") throw new ProjectionScanUnsupportedError();
|
|
293
|
+
const noFollow = fs.constants.O_NOFOLLOW;
|
|
294
|
+
const nonBlock = fs.constants.O_NONBLOCK;
|
|
295
|
+
if (typeof noFollow !== "number" || typeof nonBlock !== "number") throw new ProjectionScanUnsupportedError();
|
|
296
|
+
const directoryFlag = typeof fs.constants.O_DIRECTORY === "number" ? fs.constants.O_DIRECTORY : 0;
|
|
297
|
+
const flags = fs.constants.O_RDONLY | (nonBlock as number) | (noFollow as number) | directoryFlag;
|
|
298
|
+
let handle: fs.FileHandle | undefined;
|
|
299
|
+
try {
|
|
300
|
+
handle = await fs.open(dir, flags);
|
|
301
|
+
const opened = await handle.stat({ bigint: true });
|
|
302
|
+
if (!opened.isDirectory()) throw new ProjectionScanRaceError("scan root is not a directory");
|
|
303
|
+
const pinnedPath = `/proc/self/fd/${handle.fd}`;
|
|
304
|
+
const ownedHandle = handle;
|
|
305
|
+
handle = undefined;
|
|
306
|
+
const assertRoot = async (): Promise<void> => {
|
|
307
|
+
const settled = await ownedHandle.stat({ bigint: true });
|
|
308
|
+
if (!sameProjectionRoot(opened, settled)) throw new ProjectionScanRaceError("scan root descriptor changed");
|
|
309
|
+
};
|
|
310
|
+
return {
|
|
311
|
+
stat: opened,
|
|
312
|
+
readdir: async () => {
|
|
313
|
+
const entries = await fs.readdir(pinnedPath);
|
|
314
|
+
await assertRoot();
|
|
315
|
+
return entries;
|
|
316
|
+
},
|
|
317
|
+
lstat: async entry => {
|
|
318
|
+
const stat = await fs.lstat(path.join(pinnedPath, entry), { bigint: true });
|
|
319
|
+
await assertRoot();
|
|
320
|
+
return stat;
|
|
321
|
+
},
|
|
322
|
+
readFile: async (entry, encoding, expected) => {
|
|
323
|
+
const source = await readProjectionFileSafe(path.join(pinnedPath, entry), encoding, expected);
|
|
324
|
+
await assertRoot();
|
|
325
|
+
return source;
|
|
326
|
+
},
|
|
327
|
+
close: async () => {
|
|
328
|
+
await ownedHandle.close().catch(() => undefined);
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if ((error as NodeJS.ErrnoException).code === "ELOOP")
|
|
333
|
+
throw new ProjectionScanRaceError("scan root became a symlink");
|
|
334
|
+
throw error;
|
|
335
|
+
} finally {
|
|
336
|
+
await handle?.close().catch(() => undefined);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const failUnpinnedProjectionOperation = async (): Promise<never> => {
|
|
341
|
+
throw new ProjectionScanUnsupportedError();
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const defaultFs: ProjectionScanFs = {
|
|
345
|
+
// The default implementation never permits an accidental mutable-path fallback;
|
|
346
|
+
// all production operations must go through openProjectionDirectorySafe above.
|
|
347
|
+
readdir: failUnpinnedProjectionOperation,
|
|
348
|
+
lstat: failUnpinnedProjectionOperation,
|
|
349
|
+
readFile: failUnpinnedProjectionOperation,
|
|
350
|
+
readFileSafe: failUnpinnedProjectionOperation,
|
|
351
|
+
openDirectory: openProjectionDirectorySafe,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export function isCoordinatorScanDebrisName(name: string): boolean {
|
|
355
|
+
return name.startsWith(".");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function scanCoordinatorJsonFiles(
|
|
359
|
+
dir: string,
|
|
360
|
+
io: ProjectionScanFs = defaultFs,
|
|
361
|
+
cap: number = COORDINATOR_JSON_SCAN_CAP,
|
|
362
|
+
authority?: ProjectionScanDirectory,
|
|
363
|
+
): Promise<ProjectionScanResult> {
|
|
364
|
+
let rootStat: ProjectionScanStat;
|
|
365
|
+
try {
|
|
366
|
+
rootStat = authority?.stat ?? (await io.lstat(dir));
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
369
|
+
return {
|
|
370
|
+
values: [],
|
|
371
|
+
parsed: 0,
|
|
372
|
+
capped: false,
|
|
373
|
+
skippedDebris: 0,
|
|
374
|
+
skippedEmpty: 0,
|
|
375
|
+
raced: 0,
|
|
376
|
+
incomplete: false,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
if (rootStat.isSymbolicLink() || rootStat.isDirectory?.() === false) {
|
|
382
|
+
return {
|
|
383
|
+
values: [],
|
|
384
|
+
parsed: 0,
|
|
385
|
+
capped: true,
|
|
386
|
+
skippedDebris: 0,
|
|
387
|
+
skippedEmpty: 0,
|
|
388
|
+
raced: 1,
|
|
389
|
+
incomplete: true,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
let entries: string[];
|
|
393
|
+
try {
|
|
394
|
+
entries = authority ? await authority.readdir() : await io.readdir(dir);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
397
|
+
return {
|
|
398
|
+
values: [],
|
|
399
|
+
parsed: 0,
|
|
400
|
+
capped: true,
|
|
401
|
+
skippedDebris: 0,
|
|
402
|
+
skippedEmpty: 0,
|
|
403
|
+
raced: 1,
|
|
404
|
+
incomplete: true,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (error instanceof ProjectionScanRaceError) {
|
|
408
|
+
return {
|
|
409
|
+
values: [],
|
|
410
|
+
parsed: 0,
|
|
411
|
+
capped: true,
|
|
412
|
+
skippedDebris: 0,
|
|
413
|
+
skippedEmpty: 0,
|
|
414
|
+
raced: 1,
|
|
415
|
+
incomplete: true,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let skippedDebris = 0;
|
|
422
|
+
let skippedEmpty = 0;
|
|
423
|
+
let raced = 0;
|
|
424
|
+
const parseCandidates: Array<{ entry: string; stat: ProjectionScanStat }> = [];
|
|
425
|
+
for (const entry of entries) {
|
|
426
|
+
if (!entry.endsWith(".json") || isCoordinatorScanDebrisName(entry)) {
|
|
427
|
+
if (entry.endsWith(".json") && isCoordinatorScanDebrisName(entry)) skippedDebris += 1;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const file = path.join(dir, entry);
|
|
431
|
+
let stat: ProjectionScanStat;
|
|
432
|
+
try {
|
|
433
|
+
stat = authority ? await authority.lstat(entry) : await io.lstat(file);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (
|
|
436
|
+
(error as NodeJS.ErrnoException).code === "ENOENT" ||
|
|
437
|
+
(error as NodeJS.ErrnoException).code === "ELOOP" ||
|
|
438
|
+
error instanceof ProjectionScanRaceError
|
|
439
|
+
) {
|
|
440
|
+
raced += 1;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
446
|
+
// The candidate was a parseable-looking directory entry when enumeration
|
|
447
|
+
// completed, but its type changed before inspection. Treat that as a
|
|
448
|
+
// raced candidate rather than ordinary empty/non-regular debris: callers
|
|
449
|
+
// must not consume a partial projection set.
|
|
450
|
+
raced += 1;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
parseCandidates.push({ entry, stat });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const capped = parseCandidates.length > cap;
|
|
457
|
+
const toParse = capped ? parseCandidates.slice(0, cap) : parseCandidates;
|
|
458
|
+
const values: unknown[] = [];
|
|
459
|
+
for (const candidate of toParse) {
|
|
460
|
+
const { entry, stat } = candidate;
|
|
461
|
+
const file = path.join(dir, entry);
|
|
462
|
+
let source: string;
|
|
463
|
+
try {
|
|
464
|
+
source = authority
|
|
465
|
+
? await authority.readFile(entry, "utf8", stat)
|
|
466
|
+
: await (io.readFileSafe ?? io.readFile)(file, "utf8", stat);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (
|
|
469
|
+
(error as NodeJS.ErrnoException).code === "ENOENT" ||
|
|
470
|
+
(error as NodeJS.ErrnoException).code === "ELOOP" ||
|
|
471
|
+
error instanceof ProjectionScanRaceError
|
|
472
|
+
) {
|
|
473
|
+
raced += 1;
|
|
474
|
+
skippedEmpty += 1;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
values.push(JSON.parse(source));
|
|
481
|
+
} catch (error) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`invalid coordinator projection ${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
let finalRoot: ProjectionScanStat;
|
|
488
|
+
try {
|
|
489
|
+
finalRoot = authority?.stat ?? (await io.lstat(dir));
|
|
490
|
+
} catch (error) {
|
|
491
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
492
|
+
return {
|
|
493
|
+
values: [],
|
|
494
|
+
parsed: 0,
|
|
495
|
+
capped: true,
|
|
496
|
+
skippedDebris,
|
|
497
|
+
skippedEmpty,
|
|
498
|
+
raced: raced + 1,
|
|
499
|
+
incomplete: true,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (hasProjectionRootIdentity(rootStat) && !sameProjectionRoot(rootStat, finalRoot)) {
|
|
503
|
+
return {
|
|
504
|
+
values: [],
|
|
505
|
+
parsed: 0,
|
|
506
|
+
capped: true,
|
|
507
|
+
skippedDebris,
|
|
508
|
+
skippedEmpty,
|
|
509
|
+
raced: raced + 1,
|
|
510
|
+
incomplete: true,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
values: values.filter(value => value !== null),
|
|
515
|
+
parsed: values.length,
|
|
516
|
+
// Existing authoritative callers already refuse a capped scan. Treat a raced
|
|
517
|
+
// candidate as the same incomplete boundary so they cannot consume a partial
|
|
518
|
+
// projection set while the result still reports the precise race count below.
|
|
519
|
+
capped: capped || raced > 0,
|
|
520
|
+
skippedDebris,
|
|
521
|
+
skippedEmpty,
|
|
522
|
+
raced,
|
|
523
|
+
incomplete: capped || raced > 0,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export async function listCoordinatorJsonFiles(
|
|
528
|
+
dir: string,
|
|
529
|
+
io: ProjectionScanFs = defaultFs,
|
|
530
|
+
cap: number = COORDINATOR_JSON_SCAN_CAP,
|
|
531
|
+
): Promise<ProjectionScanResult> {
|
|
532
|
+
let authority: ProjectionScanDirectory | undefined;
|
|
533
|
+
try {
|
|
534
|
+
authority = io.openDirectory ? await io.openDirectory(dir) : undefined;
|
|
535
|
+
return await scanCoordinatorJsonFiles(dir, io, cap, authority);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (
|
|
538
|
+
error instanceof ProjectionScanRaceError ||
|
|
539
|
+
error instanceof ProjectionScanUnsupportedError ||
|
|
540
|
+
(error as NodeJS.ErrnoException).code === PROJECTION_SCAN_UNSUPPORTED_CODE
|
|
541
|
+
) {
|
|
542
|
+
return {
|
|
543
|
+
values: [],
|
|
544
|
+
parsed: 0,
|
|
545
|
+
capped: true,
|
|
546
|
+
skippedDebris: 0,
|
|
547
|
+
skippedEmpty: 0,
|
|
548
|
+
raced: 1,
|
|
549
|
+
incomplete: true,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
553
|
+
// No authority was returned: this is the lazy first-use shape for an
|
|
554
|
+
// otherwise valid projection namespace, not a partial scan. Once an
|
|
555
|
+
// authority exists, ENOENT is handled by scanCoordinatorJsonFiles as a
|
|
556
|
+
// raced/incomplete result and cannot reach this branch.
|
|
557
|
+
return {
|
|
558
|
+
values: [],
|
|
559
|
+
parsed: 0,
|
|
560
|
+
capped: false,
|
|
561
|
+
skippedDebris: 0,
|
|
562
|
+
skippedEmpty: 0,
|
|
563
|
+
raced: 0,
|
|
564
|
+
incomplete: false,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
throw error;
|
|
568
|
+
} finally {
|
|
569
|
+
await authority?.close().catch(() => undefined);
|
|
570
|
+
}
|
|
571
|
+
}
|