@giovannijecha/jecode 0.8.4 → 0.8.5
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/README.md +8 -6
- package/dist/accounts.js +47 -10
- package/dist/atomic.js +24 -14
- package/dist/batch.js +37 -4
- package/dist/bounded-file.js +212 -0
- package/dist/commands.js +2 -0
- package/dist/config.js +2 -1
- package/dist/context/automatic.js +35 -0
- package/dist/context/compactor.js +32 -2
- package/dist/context/manual.js +20 -3
- package/dist/context/request-projection.js +130 -0
- package/dist/controller-request.js +33 -11
- package/dist/credential-commands.js +22 -6
- package/dist/credentials.js +56 -18
- package/dist/directory-anchor.js +91 -0
- package/dist/file-identity.js +12 -0
- package/dist/model-command.js +5 -4
- package/dist/openai-account-command.js +13 -2
- package/dist/process-lease.js +329 -0
- package/dist/provider-commands.js +11 -5
- package/dist/provider-errors.js +37 -4
- package/dist/provider-label.js +13 -0
- package/dist/providers/anthropic-stream.js +4 -1
- package/dist/providers/anthropic-wire.js +8 -3
- package/dist/providers/anthropic.js +39 -19
- package/dist/providers/catalog.js +4 -4
- package/dist/providers/failure.js +181 -0
- package/dist/providers/http.js +82 -23
- package/dist/providers/ollama-stream.js +5 -1
- package/dist/providers/ollama.js +31 -19
- package/dist/providers/openai-codex.js +67 -41
- package/dist/providers/openai-stream.js +26 -2
- package/dist/providers/openai.js +51 -24
- package/dist/providers/sse.js +52 -8
- package/dist/request-identity.js +32 -0
- package/dist/sessions/lease.js +132 -49
- package/dist/sessions/runtime.js +15 -8
- package/dist/sessions/store.js +366 -142
- package/dist/settings.js +62 -10
- package/dist/stable-directory.js +148 -0
- package/dist/store-lock.js +68 -84
- package/dist/tools/args.js +2 -2
- package/dist/tools/fs.js +124 -102
- package/dist/tools/search.js +65 -100
- package/dist/tools/text-boundary.js +7 -33
- package/dist/tui/app-workflows.js +32 -4
- package/dist/tui/components/footer.js +1 -1
- package/dist/tui/feedback.js +4 -0
- package/dist/tui/session-view.js +7 -2
- package/dist/tui/workspace.js +21 -7
- package/dist/user-store.js +23 -31
- package/package.json +3 -4
- package/dist/tools/ripgrep.js +0 -230
package/dist/settings.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Persistent, non-secret defaults for interactive and batch sessions.
|
|
2
|
-
import { chmod, mkdir } from "node:fs/promises";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import { atomicWrite } from "./atomic.js";
|
|
4
|
+
import { assertDirectoryAnchor, captureDirectDirectorySync, preparePrivateDirectory, } from "./directory-anchor.js";
|
|
5
5
|
import { MAX_COMPACTION_PERCENT, MIN_COMPACTION_PERCENT } from "./context/policy.js";
|
|
6
6
|
import { EFFORTS } from "./effort.js";
|
|
7
7
|
import { providerNames } from "./providers/index.js";
|
|
8
8
|
import { parseOllamaEndpoint } from "./providers/ollama-endpoint.js";
|
|
9
9
|
import { withStoreLock } from "./store-lock.js";
|
|
10
10
|
import { userDataLabel, userDataPath } from "./user-data.js";
|
|
11
|
-
import { assertStoreText, readBoundedJsonSync, USER_STORE_LIMITS } from "./user-store.js";
|
|
11
|
+
import { assertStoreText, readBoundedJsonForMutationSync, readBoundedJsonSync, USER_STORE_LIMITS, } from "./user-store.js";
|
|
12
12
|
export { EFFORTS } from "./effort.js";
|
|
13
13
|
let saved;
|
|
14
14
|
export function readSettings() {
|
|
@@ -19,17 +19,19 @@ export function readSettings() {
|
|
|
19
19
|
export async function updateSettings(patch) {
|
|
20
20
|
const file = settingsPath();
|
|
21
21
|
const directory = path.dirname(file);
|
|
22
|
-
await
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const next = normalize({ ...readStore(file), ...patch });
|
|
22
|
+
const anchor = await preparePrivateDirectory(directory, "settings store directory");
|
|
23
|
+
const anchoredFile = path.join(anchor.path, path.basename(file));
|
|
24
|
+
return withStoreLock(anchoredFile, async () => {
|
|
25
|
+
const next = normalize({ ...readStoreForMutation(anchoredFile, anchor), ...patch });
|
|
27
26
|
const text = `${JSON.stringify(next, null, 2)}\n`;
|
|
28
27
|
assertStoreText(text, USER_STORE_LIMITS.settingsBytes);
|
|
29
|
-
await atomicWrite(
|
|
28
|
+
await atomicWrite(anchoredFile, text, {
|
|
29
|
+
mode: 0o600,
|
|
30
|
+
validate: async () => assertDirectoryAnchor(anchor),
|
|
31
|
+
});
|
|
30
32
|
saved = next;
|
|
31
33
|
return file;
|
|
32
|
-
});
|
|
34
|
+
}, undefined, async () => assertDirectoryAnchor(anchor));
|
|
33
35
|
}
|
|
34
36
|
export function settingsPath() {
|
|
35
37
|
return userDataPath("settings.json");
|
|
@@ -43,7 +45,9 @@ export function reloadSettings() {
|
|
|
43
45
|
}
|
|
44
46
|
function readStore(file = settingsPath()) {
|
|
45
47
|
try {
|
|
46
|
-
|
|
48
|
+
const directory = captureDirectDirectorySync(path.dirname(file), "settings store directory");
|
|
49
|
+
const anchoredFile = path.join(directory.path, path.basename(file));
|
|
50
|
+
return normalize(readBoundedJsonSync(anchoredFile, USER_STORE_LIMITS.settingsBytes, directory));
|
|
47
51
|
}
|
|
48
52
|
catch {
|
|
49
53
|
// Missing, unreadable, and malformed stores all fall back safely. A bad
|
|
@@ -51,6 +55,54 @@ function readStore(file = settingsPath()) {
|
|
|
51
55
|
return {};
|
|
52
56
|
}
|
|
53
57
|
}
|
|
58
|
+
function readStoreForMutation(file, directory) {
|
|
59
|
+
const value = readBoundedJsonForMutationSync(file, USER_STORE_LIMITS.settingsBytes, "settings store", directory);
|
|
60
|
+
if (value === undefined)
|
|
61
|
+
return {};
|
|
62
|
+
if (!record(value))
|
|
63
|
+
throw new Error("settings store has an unsupported structure");
|
|
64
|
+
assertMutableSettings(value);
|
|
65
|
+
return normalize(value);
|
|
66
|
+
}
|
|
67
|
+
function assertMutableSettings(value) {
|
|
68
|
+
const current = new Set([
|
|
69
|
+
"provider",
|
|
70
|
+
"models",
|
|
71
|
+
"ollamaHost",
|
|
72
|
+
"effort",
|
|
73
|
+
"reducedMotion",
|
|
74
|
+
"maxTokens",
|
|
75
|
+
"compactionPercent",
|
|
76
|
+
]);
|
|
77
|
+
const retired = new Set(["theme", "palette", "maxSteps"]);
|
|
78
|
+
if (Object.keys(value).some((key) => !current.has(key) && !retired.has(key))) {
|
|
79
|
+
throw new Error("settings store has an unsupported structure");
|
|
80
|
+
}
|
|
81
|
+
const providers = providerNames();
|
|
82
|
+
if ("provider" in value && member(value["provider"], providers) === undefined) {
|
|
83
|
+
throw new Error("settings store has an invalid provider");
|
|
84
|
+
}
|
|
85
|
+
if ("models" in value) {
|
|
86
|
+
const models = value["models"];
|
|
87
|
+
if (!record(models) || Object.keys(models).some((provider) => !providers.includes(provider)) ||
|
|
88
|
+
Object.values(models).some((model) => !boundedNonempty(model, USER_STORE_LIMITS.model)))
|
|
89
|
+
throw new Error("settings store has invalid models");
|
|
90
|
+
}
|
|
91
|
+
if ("ollamaHost" in value && endpoint(value["ollamaHost"]) === undefined) {
|
|
92
|
+
throw new Error("settings store has an invalid Ollama endpoint");
|
|
93
|
+
}
|
|
94
|
+
if ("effort" in value && member(value["effort"], EFFORTS) === undefined) {
|
|
95
|
+
throw new Error("settings store has an invalid reasoning effort");
|
|
96
|
+
}
|
|
97
|
+
if ("reducedMotion" in value && typeof value["reducedMotion"] !== "boolean") {
|
|
98
|
+
throw new Error("settings store has an invalid reduced-motion preference");
|
|
99
|
+
}
|
|
100
|
+
if ("maxTokens" in value && positiveInteger(value["maxTokens"]) === undefined) {
|
|
101
|
+
throw new Error("settings store has an invalid token limit");
|
|
102
|
+
}
|
|
103
|
+
if ("compactionPercent" in value && percentage(value["compactionPercent"]) === undefined)
|
|
104
|
+
throw new Error("settings store has an invalid compaction threshold");
|
|
105
|
+
}
|
|
54
106
|
function normalize(value) {
|
|
55
107
|
if (!record(value))
|
|
56
108
|
return {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Bounded directory enumeration with post-open identity and root checks.
|
|
2
|
+
import { lstat, opendir, realpath } from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { stableFileExpectation } from "./bounded-file.js";
|
|
5
|
+
import { fileIdentity, sameFileIdentity } from "./file-identity.js";
|
|
6
|
+
export class StableDirectoryError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "StableDirectoryError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function readStableDirectory(root, directory, options) {
|
|
13
|
+
if (!Number.isSafeInteger(options.maxEntries) || options.maxEntries < 0) {
|
|
14
|
+
throw new RangeError("directory entry limit is invalid");
|
|
15
|
+
}
|
|
16
|
+
throwIfAborted(options.signal);
|
|
17
|
+
const canonicalRoot = await realpath(root);
|
|
18
|
+
const canonicalDirectory = await realpath(directory);
|
|
19
|
+
if (!inside(canonicalRoot, canonicalDirectory))
|
|
20
|
+
throw escaped();
|
|
21
|
+
const before = await lstat(canonicalDirectory, { bigint: true });
|
|
22
|
+
requireDirectory(before);
|
|
23
|
+
await options.beforeOpen?.();
|
|
24
|
+
throwIfAborted(options.signal);
|
|
25
|
+
const names = [];
|
|
26
|
+
let capped = false;
|
|
27
|
+
try {
|
|
28
|
+
const handle = await opendir(canonicalDirectory);
|
|
29
|
+
for await (const entry of handle) {
|
|
30
|
+
throwIfAborted(options.signal);
|
|
31
|
+
if (names.length >= options.maxEntries) {
|
|
32
|
+
capped = true;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
if (directName(entry.name))
|
|
36
|
+
names.push(entry.name);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (changedPath(error))
|
|
41
|
+
throw changed();
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
await assertDirectory(canonicalRoot, canonicalDirectory, before);
|
|
45
|
+
const entries = [];
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
throwIfAborted(options.signal);
|
|
48
|
+
const entry = await inspectEntry(canonicalRoot, canonicalDirectory, name);
|
|
49
|
+
if (entry !== undefined)
|
|
50
|
+
entries.push(entry);
|
|
51
|
+
}
|
|
52
|
+
await assertDirectory(canonicalRoot, canonicalDirectory, before);
|
|
53
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
54
|
+
return { entries, capped };
|
|
55
|
+
}
|
|
56
|
+
async function inspectEntry(root, directory, name) {
|
|
57
|
+
const target = path.join(directory, name);
|
|
58
|
+
let before;
|
|
59
|
+
try {
|
|
60
|
+
before = await lstat(target, { bigint: true });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (changedPath(error))
|
|
64
|
+
return undefined;
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
if (before.isSymbolicLink())
|
|
68
|
+
return undefined;
|
|
69
|
+
const kind = before.isFile() ? "file" : before.isDirectory() ? "directory" : undefined;
|
|
70
|
+
if (kind === undefined)
|
|
71
|
+
return undefined;
|
|
72
|
+
let canonical;
|
|
73
|
+
try {
|
|
74
|
+
canonical = await realpath(target);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (changedPath(error))
|
|
78
|
+
return undefined;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
if (!inside(root, canonical) || !samePath(target, canonical))
|
|
82
|
+
return undefined;
|
|
83
|
+
let after;
|
|
84
|
+
try {
|
|
85
|
+
after = await lstat(target, { bigint: true });
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (changedPath(error))
|
|
89
|
+
return undefined;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
if (after.isSymbolicLink() ||
|
|
93
|
+
!sameFileIdentity(fileIdentity(before), fileIdentity(after)) ||
|
|
94
|
+
(kind === "file" ? !after.isFile() : !after.isDirectory()))
|
|
95
|
+
return undefined;
|
|
96
|
+
return kind === "file"
|
|
97
|
+
? { name, kind, expected: stableFileExpectation(after) }
|
|
98
|
+
: { name, kind };
|
|
99
|
+
}
|
|
100
|
+
async function assertDirectory(root, directory, expected) {
|
|
101
|
+
let current;
|
|
102
|
+
let canonical;
|
|
103
|
+
try {
|
|
104
|
+
[current, canonical] = await Promise.all([
|
|
105
|
+
lstat(directory, { bigint: true }),
|
|
106
|
+
realpath(directory),
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (changedPath(error))
|
|
111
|
+
throw changed();
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
requireDirectory(current);
|
|
115
|
+
if (!inside(root, canonical) || !samePath(directory, canonical) ||
|
|
116
|
+
!sameFileIdentity(fileIdentity(expected), fileIdentity(current)))
|
|
117
|
+
throw changed();
|
|
118
|
+
}
|
|
119
|
+
function requireDirectory(details) {
|
|
120
|
+
if (details.isSymbolicLink() || !details.isDirectory())
|
|
121
|
+
throw changed();
|
|
122
|
+
}
|
|
123
|
+
function directName(name) {
|
|
124
|
+
return name !== "." && name !== ".." && path.basename(name) === name;
|
|
125
|
+
}
|
|
126
|
+
function inside(root, target) {
|
|
127
|
+
const relative = path.relative(root, target);
|
|
128
|
+
return relative === "" ||
|
|
129
|
+
(!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`));
|
|
130
|
+
}
|
|
131
|
+
function samePath(left, right) {
|
|
132
|
+
return path.relative(left, right) === "";
|
|
133
|
+
}
|
|
134
|
+
function changedPath(error) {
|
|
135
|
+
const code = error.code;
|
|
136
|
+
return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
|
|
137
|
+
}
|
|
138
|
+
function changed() {
|
|
139
|
+
return new StableDirectoryError("directory changed while it was being listed");
|
|
140
|
+
}
|
|
141
|
+
function escaped() {
|
|
142
|
+
return new StableDirectoryError("directory escapes the workspace root");
|
|
143
|
+
}
|
|
144
|
+
function throwIfAborted(signal) {
|
|
145
|
+
if (signal?.aborted !== true)
|
|
146
|
+
return;
|
|
147
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("aborted");
|
|
148
|
+
}
|
package/dist/store-lock.js
CHANGED
|
@@ -3,121 +3,105 @@
|
|
|
3
3
|
// Every writer rereads inside the lock. That prevents two Jecode processes
|
|
4
4
|
// from replacing unrelated settings, API keys, or rotated OAuth credentials
|
|
5
5
|
// with snapshots they cached before the other process wrote.
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { mkdir, open, readFile, rmdir, stat, unlink } from "node:fs/promises";
|
|
8
6
|
import * as path from "node:path";
|
|
7
|
+
import { lstat } from "node:fs/promises";
|
|
8
|
+
import { BoundedFileError, readBoundedText } from "./bounded-file.js";
|
|
9
|
+
import { pidIsAlive, ProcessLeaseError, processLeaseToken, tryAcquireProcessLease, } from "./process-lease.js";
|
|
9
10
|
const WAIT_MS = 50;
|
|
10
11
|
const WAIT_LIMIT_MS = 20_000;
|
|
11
12
|
const STALE_MS = 60_000;
|
|
12
|
-
|
|
13
|
+
const LEGACY_OWNER_BYTES = 256;
|
|
14
|
+
export async function withStoreLock(file, body, signal, validate) {
|
|
13
15
|
throwIfAborted(signal);
|
|
16
|
+
await validate?.();
|
|
14
17
|
const directory = `${file}.lock`;
|
|
15
|
-
const token =
|
|
18
|
+
const token = processLeaseToken();
|
|
16
19
|
const started = Date.now();
|
|
17
|
-
|
|
20
|
+
let lease;
|
|
21
|
+
while (lease === undefined) {
|
|
22
|
+
await validate?.();
|
|
23
|
+
try {
|
|
24
|
+
lease = await tryAcquireProcessLease(directory, token, {
|
|
25
|
+
staleMs: STALE_MS,
|
|
26
|
+
setupGraceMs: 1_000,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (!(error instanceof ProcessLeaseError))
|
|
31
|
+
throw error;
|
|
32
|
+
throw await incompatibleLock(directory, file);
|
|
33
|
+
}
|
|
34
|
+
if (lease !== undefined)
|
|
35
|
+
break;
|
|
18
36
|
if (signal?.aborted === true)
|
|
19
37
|
throw abortReason(signal);
|
|
20
38
|
if (Date.now() - started >= WAIT_LIMIT_MS) {
|
|
21
39
|
throw new Error(`timed out waiting for ${path.basename(file)}`);
|
|
22
40
|
}
|
|
23
|
-
await recoverStale(directory);
|
|
24
41
|
await wait(WAIT_MS, signal);
|
|
25
42
|
}
|
|
43
|
+
let primaryFailure;
|
|
26
44
|
try {
|
|
27
45
|
throwIfAborted(signal);
|
|
46
|
+
await lease.assertOwned();
|
|
47
|
+
await validate?.();
|
|
28
48
|
return await body();
|
|
29
49
|
}
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
catch (error) {
|
|
51
|
+
primaryFailure = error;
|
|
52
|
+
throw error;
|
|
32
53
|
}
|
|
33
|
-
|
|
34
|
-
async function acquire(directory, token) {
|
|
35
|
-
let created = false;
|
|
36
|
-
try {
|
|
37
|
-
await mkdir(directory, { mode: 0o700 });
|
|
38
|
-
created = true;
|
|
39
|
-
const owner = await open(path.join(directory, "owner"), "wx", 0o600);
|
|
54
|
+
finally {
|
|
40
55
|
try {
|
|
41
|
-
await
|
|
42
|
-
|
|
56
|
+
if (!(await lease.release()) && primaryFailure === undefined) {
|
|
57
|
+
throw new Error("store lock ownership was lost");
|
|
58
|
+
}
|
|
43
59
|
}
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (primaryFailure === undefined)
|
|
62
|
+
throw error;
|
|
46
63
|
}
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
if (!created && error.code === "EEXIST")
|
|
51
|
-
return false;
|
|
52
|
-
if (created)
|
|
53
|
-
await removeLock(directory);
|
|
54
|
-
throw error;
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
|
-
async function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// whereas renaming the shared path can steal that new lock in an ABA race.
|
|
67
|
-
await removeLock(directory);
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
const code = error.code;
|
|
71
|
-
if (code !== "ENOENT" && code !== "EACCES" && code !== "EPERM")
|
|
72
|
-
throw error;
|
|
66
|
+
async function incompatibleLock(directory, file) {
|
|
67
|
+
const legacy = await legacyOwner(directory);
|
|
68
|
+
if (legacy !== undefined) {
|
|
69
|
+
if (legacy.pid !== undefined && pidIsAlive(legacy.pid)) {
|
|
70
|
+
return new Error(`${path.basename(file)} is locked by an older Jecode process ` +
|
|
71
|
+
`(PID ${legacy.pid}) — close it and retry`);
|
|
72
|
+
}
|
|
73
|
+
return new Error(`${path.basename(file)} has an incompatible legacy lock — close older Jecode ` +
|
|
74
|
+
`processes, then remove this exact lock directory and retry: ${directory}`);
|
|
73
75
|
}
|
|
76
|
+
return new Error(`${path.basename(file)} has an unsafe lock — inspect this exact directory before retrying: ` +
|
|
77
|
+
directory);
|
|
74
78
|
}
|
|
75
|
-
async function
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
token = await readFile(path.join(directory, "owner"), "utf8");
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
const code = error.code;
|
|
82
|
-
if (code === "ENOENT")
|
|
83
|
-
return false;
|
|
84
|
-
if (code === "EACCES" || code === "EPERM")
|
|
85
|
-
return true;
|
|
86
|
-
throw error;
|
|
87
|
-
}
|
|
88
|
-
const match = /^([1-9]\d*):/.exec(token.trim());
|
|
89
|
-
if (match === null)
|
|
90
|
-
return false;
|
|
91
|
-
const pid = Number(match[1]);
|
|
92
|
-
if (!Number.isSafeInteger(pid) || pid > 0x7fff_ffff)
|
|
93
|
-
return false;
|
|
79
|
+
async function legacyOwner(directory) {
|
|
80
|
+
const owner = path.join(directory, "owner");
|
|
94
81
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
82
|
+
const details = await lstat(owner, { bigint: true });
|
|
83
|
+
if (details.isSymbolicLink() || !details.isFile() ||
|
|
84
|
+
details.size > BigInt(LEGACY_OWNER_BYTES))
|
|
85
|
+
return { pid: undefined };
|
|
86
|
+
const token = (await readBoundedText(owner, LEGACY_OWNER_BYTES, {
|
|
87
|
+
label: "legacy store lock",
|
|
88
|
+
})).trim();
|
|
89
|
+
const match = /^([1-9]\d*):/.exec(token);
|
|
90
|
+
if (match === null)
|
|
91
|
+
return { pid: undefined };
|
|
92
|
+
const pid = Number(match[1]);
|
|
93
|
+
return Number.isSafeInteger(pid) && pid <= 0x7fff_ffff
|
|
94
|
+
? { pid }
|
|
95
|
+
: { pid: undefined };
|
|
97
96
|
}
|
|
98
97
|
catch (error) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
if (error.code === "ENOENT")
|
|
99
|
+
return undefined;
|
|
100
|
+
if (error instanceof BoundedFileError)
|
|
101
|
+
return { pid: undefined };
|
|
102
|
+
return undefined;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
-
async function release(directory, token) {
|
|
106
|
-
try {
|
|
107
|
-
const owner = path.join(directory, "owner");
|
|
108
|
-
if ((await readFile(owner, "utf8")) !== token)
|
|
109
|
-
return;
|
|
110
|
-
await unlink(owner);
|
|
111
|
-
await rmdir(directory);
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// A recovered stale lock or an already-removed directory is no longer ours.
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
async function removeLock(directory) {
|
|
118
|
-
await unlink(path.join(directory, "owner")).catch(() => undefined);
|
|
119
|
-
await rmdir(directory).catch(() => undefined);
|
|
120
|
-
}
|
|
121
105
|
function wait(ms, signal) {
|
|
122
106
|
return new Promise((resolve, reject) => {
|
|
123
107
|
if (signal?.aborted === true) {
|
package/dist/tools/args.js
CHANGED
|
@@ -24,8 +24,8 @@ export function optionalInt(args, name) {
|
|
|
24
24
|
if (value === undefined || value === null)
|
|
25
25
|
return undefined;
|
|
26
26
|
const n = typeof value === "string" ? Number(value) : value;
|
|
27
|
-
if (typeof n !== "number" || !Number.
|
|
28
|
-
throw new Error(`"${name}" must be
|
|
27
|
+
if (typeof n !== "number" || !Number.isSafeInteger(n)) {
|
|
28
|
+
throw new Error(`"${name}" must be a safe integer`);
|
|
29
29
|
}
|
|
30
30
|
return n;
|
|
31
31
|
}
|