@primitive.ai/prim 0.1.0-alpha.41 → 0.1.0-alpha.43
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 +64 -6
- package/dist/{chunk-UWAJCF7P.js → chunk-BSZGI5RL.js} +1 -1
- package/dist/{chunk-S2O4P4A3.js → chunk-F5QCFBJ6.js} +14 -3
- package/dist/{chunk-HSZPTVKU.js → chunk-F7O7V6ZM.js} +88 -12
- package/dist/chunk-IMAIBPUC.js +210 -0
- package/dist/{chunk-H4OR42TJ.js → chunk-OKUXEBPT.js} +149 -36
- package/dist/{chunk-AAGJFO7C.js → chunk-QY75BQMF.js} +25 -1
- package/dist/chunk-S5NUHFAD.js +55 -0
- package/dist/chunk-UISU3A7W.js +202 -0
- package/dist/{chunk-26VA3ADF.js → chunk-W6PAKEDO.js} +32 -6
- package/dist/daemon/server.js +1150 -77
- package/dist/hooks/post-commit.js +5 -5
- package/dist/hooks/post-tool-use.js +31 -19
- package/dist/hooks/pre-commit.js +2 -2
- package/dist/hooks/pre-tool-use.js +3 -5
- package/dist/hooks/prim-hook.js +77 -20
- package/dist/hooks/session-start.js +79 -24
- package/dist/index.js +1609 -320
- package/dist/statusline-main.js +280 -0
- package/package.json +6 -5
- package/dist/chunk-LUPD2JSH.js +0 -78
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ One command does the whole install — auth, session hooks, daemon, git hooks,
|
|
|
34
34
|
skill, and the welcome:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
prim setup # add --agent codex or --agent hermes
|
|
37
|
+
prim setup # add --agent codex or --agent hermes; --no-daemon explicitly opts out
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
Or run the steps individually:
|
|
@@ -81,10 +81,10 @@ and the manual fallback — live in [`setup.md`](./setup.md).
|
|
|
81
81
|
prim setup # Run the whole install in one shot
|
|
82
82
|
prim setup --agent codex # Same, for OpenAI Codex
|
|
83
83
|
prim setup --agent hermes # Same, for Hermes Agent (global-only config)
|
|
84
|
-
prim setup --no-daemon #
|
|
84
|
+
prim setup --no-daemon # Stop it and persistently opt out of supervised delivery
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
Orchestrates auth → session hooks → daemon → git hooks → skill → welcome,
|
|
87
|
+
Orchestrates auth → session hooks → supervised daemon → capture-health gate → git hooks → skill → welcome,
|
|
88
88
|
re-running each underlying command so every step behaves exactly as if run by
|
|
89
89
|
hand (including the browser login). Idempotent — safe to re-run.
|
|
90
90
|
|
|
@@ -118,11 +118,64 @@ prim codex install # Install OpenAI Codex hooks (project scope)
|
|
|
118
118
|
prim hermes install # Install Hermes Agent hooks (global ~/.hermes/config.yaml)
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
#### Claude decision feedback
|
|
122
|
+
|
|
123
|
+
Claude Code receives eventual, human-visible feedback when automatic capture
|
|
124
|
+
creates an accepted `change` Decision. A later Claude `Stop` or fresh
|
|
125
|
+
`SessionStart` in the same Git worktree can display:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
[prim] response → created Decision (dec_a1b2c3d4): Use the stable API (https://app.getprimitive.ai/decisions/r571n1dqjdrtyxxpf0fnzee4gn8aed6q)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This is a hook `systemMessage` for the person using Claude Code; it is not
|
|
132
|
+
injected into the model's context. Delivery is at-least-once at the stdout
|
|
133
|
+
handoff boundary: prim acknowledges only after writing the hook response, so a
|
|
134
|
+
failed acknowledgment can show the same notification again. Notifications are
|
|
135
|
+
eligible for 24 hours. The originating session gets limited preference, but any
|
|
136
|
+
concurrent Claude session in the same worktree may consume the backlog. A hook
|
|
137
|
+
claims at most 40 notifications and renders at most 8,000 Unicode code points;
|
|
138
|
+
`hasMore` work is left for a later Stop or SessionStart rather than extending
|
|
139
|
+
the current hook.
|
|
140
|
+
|
|
141
|
+
Worktree scope comes from an opaque UUID stored under the worktree's Git
|
|
142
|
+
metadata (equivalent to `git rev-parse --git-path prim/workspace-id`), never an
|
|
143
|
+
absolute repository path. Linked worktrees receive distinct IDs, moving a
|
|
144
|
+
worktree preserves its ID, and a clone creates a new one. A corrupt or
|
|
145
|
+
unwritable identity is never silently replaced: capture falls back to the
|
|
146
|
+
legacy envelope and `prim doctor` reports the limitation. Disable/uninstall
|
|
147
|
+
does not delete the identity.
|
|
148
|
+
|
|
149
|
+
Feedback uses the invoking CLI's credentials for direct HTTPS calls rather
|
|
150
|
+
than the daemon, avoiding cross-organization token ambiguity. One absolute
|
|
151
|
+
three-second in-process budget covers token refresh, lease, parsing, rendering,
|
|
152
|
+
and acknowledgment. This is not a hard three-second wall clock: it cannot
|
|
153
|
+
preempt shell/PATH or `npx` resolution, Node startup, Claude's own hook
|
|
154
|
+
scheduling, or synchronous filesystem work. The Git fallback has a separate
|
|
155
|
+
short timeout.
|
|
156
|
+
|
|
157
|
+
Alternatives considered:
|
|
158
|
+
|
|
159
|
+
| Option | Benefit | Limitation |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| Existing hooks + direct HTTPS (current) | No new install surface; uses the invoking credentials | In-process budget is not a host-enforced wall clock |
|
|
162
|
+
| Claude's native hook timeout | Host-enforced termination | Settings migration; may kill a cold `npx` startup |
|
|
163
|
+
| Daemon routing | Lower steady-state latency | Daemon token/org may not match a concurrent session |
|
|
164
|
+
| Dedicated feedback binary | Lower startup overhead | New distribution and install migration |
|
|
165
|
+
| Synchronous pre-Stop classification | Stronger same-turn immediacy | Adds model latency and cost to the hook path |
|
|
166
|
+
| Detached delivery | Does not block Stop | Cannot return the current hook's `systemMessage` |
|
|
167
|
+
|
|
168
|
+
Run `prim claude status` to verify both existing feedback handlers are installed
|
|
169
|
+
and `prim doctor` to inspect the worktree identity and server capability. No new
|
|
170
|
+
hook registration or binary is required when upgrading an existing correct
|
|
171
|
+
installation.
|
|
172
|
+
|
|
121
173
|
### Daemon
|
|
122
174
|
|
|
123
|
-
A long-lived companion process that
|
|
124
|
-
and powers the "team: N online"
|
|
125
|
-
|
|
175
|
+
A supervised long-lived companion process that continuously drains captured
|
|
176
|
+
Moves, accelerates in-session decision checks, and powers the "team: N online"
|
|
177
|
+
presence count. `prim setup` requires it to become healthy unless the explicit
|
|
178
|
+
`--no-daemon` opt-out is supplied; hooks still fail soft if it later degrades.
|
|
126
179
|
|
|
127
180
|
```bash
|
|
128
181
|
prim daemon start # start (stop / restart / status)
|
|
@@ -178,6 +231,11 @@ and offers to install into `.husky/`.
|
|
|
178
231
|
prim statusline # Render the team-presence statusline (reads the daemon)
|
|
179
232
|
```
|
|
180
233
|
|
|
234
|
+
Claude Code has one custom status-line slot. Installation uses a staged,
|
|
235
|
+
lightweight Primitive renderer when that slot is empty or already Primitive's;
|
|
236
|
+
an existing custom status line is preserved and reported explicitly. Use
|
|
237
|
+
`prim daemon status` or `prim doctor` for the same health signal in that case.
|
|
238
|
+
|
|
181
239
|
### Welcome
|
|
182
240
|
|
|
183
241
|
```bash
|
|
@@ -3,11 +3,21 @@ import { randomUUID } from "crypto";
|
|
|
3
3
|
import { platform } from "os";
|
|
4
4
|
|
|
5
5
|
// src/protocol/move.ts
|
|
6
|
-
var
|
|
6
|
+
var LEGACY_ENVELOPE_VERSION = 1;
|
|
7
|
+
var AGENT_ENVELOPE_VERSION = 2;
|
|
8
|
+
var ENVELOPE_VERSION = LEGACY_ENVELOPE_VERSION;
|
|
9
|
+
function withAgentProvenance(move, producer, workspaceId) {
|
|
10
|
+
return {
|
|
11
|
+
...move,
|
|
12
|
+
env: { ...move.env, workspaceId },
|
|
13
|
+
envelopeVersion: AGENT_ENVELOPE_VERSION,
|
|
14
|
+
producer
|
|
15
|
+
};
|
|
16
|
+
}
|
|
7
17
|
|
|
8
18
|
// src/hooks/prim-hook-core.ts
|
|
9
|
-
function toMove(parsed, cliVersion, agent = "claude_code") {
|
|
10
|
-
|
|
19
|
+
function toMove(parsed, cliVersion, agent = "claude_code", workspaceId) {
|
|
20
|
+
const move = {
|
|
11
21
|
moveId: randomUUID(),
|
|
12
22
|
capturedAt: Date.now(),
|
|
13
23
|
sessionId: parsed.session_id ?? "",
|
|
@@ -24,6 +34,7 @@ function toMove(parsed, cliVersion, agent = "claude_code") {
|
|
|
24
34
|
// keeping the Claude wire shape byte-identical.
|
|
25
35
|
...agent === "claude_code" ? {} : { producer: agent }
|
|
26
36
|
};
|
|
37
|
+
return workspaceId ? withAgentProvenance(move, agent, workspaceId) : move;
|
|
27
38
|
}
|
|
28
39
|
function toCommitMove(commit, cliVersion, cwd) {
|
|
29
40
|
return {
|
|
@@ -1,11 +1,83 @@
|
|
|
1
|
+
// src/lib/activation.ts
|
|
2
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
3
|
+
import { existsSync, readFileSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
|
|
7
|
+
// src/lib/git.ts
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
|
+
function gitToplevel(cwd) {
|
|
10
|
+
try {
|
|
11
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
12
|
+
cwd,
|
|
13
|
+
encoding: "utf-8",
|
|
14
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
15
|
+
}).trim();
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/lib/activation.ts
|
|
22
|
+
var PRIM_ACTIVE_KEY = "prim.active";
|
|
23
|
+
var PROJECT_INSTALL_FILES = [".claude/settings.json", ".codex/hooks.json"];
|
|
24
|
+
var PRIM_HOOK_BINS = ["prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
|
|
25
|
+
function repoActiveFlag(cwd) {
|
|
26
|
+
try {
|
|
27
|
+
const value = execFileSync2("git", ["config", "--get", PRIM_ACTIVE_KEY], {
|
|
28
|
+
cwd,
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
31
|
+
}).trim();
|
|
32
|
+
if (value === "true") return "true";
|
|
33
|
+
if (value === "false") return "false";
|
|
34
|
+
return void 0;
|
|
35
|
+
} catch {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function setRepoActive(cwd, active) {
|
|
40
|
+
execFileSync2("git", ["config", "--local", PRIM_ACTIVE_KEY, active ? "true" : "false"], {
|
|
41
|
+
cwd,
|
|
42
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function activateRepoBestEffort(cwd) {
|
|
46
|
+
try {
|
|
47
|
+
setRepoActive(cwd, true);
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function hasProjectPrimInstall(cwd) {
|
|
52
|
+
const root = gitToplevel(cwd);
|
|
53
|
+
if (root === null || root === homedir()) return false;
|
|
54
|
+
for (const rel of PROJECT_INSTALL_FILES) {
|
|
55
|
+
const path = join(root, rel);
|
|
56
|
+
if (existsSync(path)) {
|
|
57
|
+
try {
|
|
58
|
+
const content = readFileSync(path, "utf-8");
|
|
59
|
+
if (PRIM_HOOK_BINS.some((bin) => content.includes(bin))) return true;
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
function isRepoActiveForCapture(cwd) {
|
|
67
|
+
const flag = repoActiveFlag(cwd);
|
|
68
|
+
if (flag === "true") return true;
|
|
69
|
+
if (flag === "false") return false;
|
|
70
|
+
return hasProjectPrimInstall(cwd);
|
|
71
|
+
}
|
|
72
|
+
|
|
1
73
|
// src/lib/bin-cache.ts
|
|
2
74
|
import { chmodSync, mkdirSync, renameSync, writeFileSync } from "fs";
|
|
3
|
-
import { homedir } from "os";
|
|
4
|
-
import { join as
|
|
75
|
+
import { homedir as homedir2 } from "os";
|
|
76
|
+
import { join as join3 } from "path";
|
|
5
77
|
|
|
6
78
|
// src/lib/bin-path.ts
|
|
7
|
-
import { existsSync, readFileSync } from "fs";
|
|
8
|
-
import { dirname, isAbsolute, join } from "path";
|
|
79
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
80
|
+
import { dirname, isAbsolute, join as join2 } from "path";
|
|
9
81
|
import { fileURLToPath } from "url";
|
|
10
82
|
var PKG_NAME = "@primitive.ai/prim";
|
|
11
83
|
var ROOT_WALK_LIMIT = 6;
|
|
@@ -19,10 +91,10 @@ function locateRoot() {
|
|
|
19
91
|
}
|
|
20
92
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
21
93
|
for (let depth = 0; depth < ROOT_WALK_LIMIT; depth++) {
|
|
22
|
-
const manifestPath =
|
|
23
|
-
if (
|
|
94
|
+
const manifestPath = join2(dir, "package.json");
|
|
95
|
+
if (existsSync2(manifestPath)) {
|
|
24
96
|
try {
|
|
25
|
-
const manifest = JSON.parse(
|
|
97
|
+
const manifest = JSON.parse(readFileSync2(manifestPath, "utf-8"));
|
|
26
98
|
if (manifest.name === PKG_NAME && manifest.bin) {
|
|
27
99
|
resolvedRoot = { dir, bin: manifest.bin };
|
|
28
100
|
return resolvedRoot;
|
|
@@ -45,7 +117,7 @@ function binFile(bin) {
|
|
|
45
117
|
if (!root || !rel) {
|
|
46
118
|
return null;
|
|
47
119
|
}
|
|
48
|
-
return isAbsolute(rel) ? rel :
|
|
120
|
+
return isAbsolute(rel) ? rel : join2(root.dir, rel);
|
|
49
121
|
}
|
|
50
122
|
function hookShimCommand(bin, args = "", opts = {}) {
|
|
51
123
|
const invoke = (cmd) => args ? `${cmd} ${args}` : cmd;
|
|
@@ -74,8 +146,8 @@ function commandMatchesBin(command, bin) {
|
|
|
74
146
|
// src/lib/bin-cache.ts
|
|
75
147
|
var CACHED_BINS = ["prim", "prim-hook", "prim-pre-tool-use", "prim-post-tool-use"];
|
|
76
148
|
function binCacheDir() {
|
|
77
|
-
const base = process.env.XDG_CACHE_HOME ||
|
|
78
|
-
return
|
|
149
|
+
const base = process.env.XDG_CACHE_HOME || join3(homedir2(), ".cache");
|
|
150
|
+
return join3(base, "prim", "bin");
|
|
79
151
|
}
|
|
80
152
|
function writeAtomic(path, content) {
|
|
81
153
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
@@ -93,11 +165,11 @@ function warmBinCache() {
|
|
|
93
165
|
const dir = binCacheDir();
|
|
94
166
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
95
167
|
chmodSync(dir, 448);
|
|
96
|
-
writeAtomic(
|
|
168
|
+
writeAtomic(join3(dir, "node"), process.execPath);
|
|
97
169
|
for (const bin of CACHED_BINS) {
|
|
98
170
|
const file = binFile(bin);
|
|
99
171
|
if (file) {
|
|
100
|
-
writeAtomic(
|
|
172
|
+
writeAtomic(join3(dir, bin), file);
|
|
101
173
|
}
|
|
102
174
|
}
|
|
103
175
|
} catch {
|
|
@@ -105,6 +177,10 @@ function warmBinCache() {
|
|
|
105
177
|
}
|
|
106
178
|
|
|
107
179
|
export {
|
|
180
|
+
gitToplevel,
|
|
181
|
+
setRepoActive,
|
|
182
|
+
activateRepoBestEffort,
|
|
183
|
+
isRepoActiveForCapture,
|
|
108
184
|
binFile,
|
|
109
185
|
hookShimCommand,
|
|
110
186
|
detachedHookShimCommand,
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// src/lib/workspace-id.ts
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import {
|
|
5
|
+
constants,
|
|
6
|
+
chmodSync,
|
|
7
|
+
closeSync,
|
|
8
|
+
fchmodSync,
|
|
9
|
+
fsyncSync,
|
|
10
|
+
linkSync,
|
|
11
|
+
lstatSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
openSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
statSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
writeFileSync
|
|
18
|
+
} from "fs";
|
|
19
|
+
import { dirname, isAbsolute, join, resolve } from "path";
|
|
20
|
+
var WORKSPACE_ID_RELATIVE_PATH = join("prim", "workspace-id");
|
|
21
|
+
var MAX_ANCESTORS = 64;
|
|
22
|
+
var MAX_GIT_FILE_BYTES = 4096;
|
|
23
|
+
var MAX_ID_FILE_BYTES = 128;
|
|
24
|
+
var GIT_FALLBACK_TIMEOUT_MS = 250;
|
|
25
|
+
var RACE_READ_ATTEMPTS = 5;
|
|
26
|
+
var RACE_READ_DELAY_MS = 5;
|
|
27
|
+
var CANONICAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
28
|
+
var raceDelay = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
29
|
+
function isCanonicalWorkspaceId(value) {
|
|
30
|
+
return CANONICAL_UUID.test(value);
|
|
31
|
+
}
|
|
32
|
+
function inspectWorkspaceId(cwd = process.cwd()) {
|
|
33
|
+
const resolved = resolveWorkspaceIdPath(cwd);
|
|
34
|
+
if (resolved.status !== "resolved") return resolved;
|
|
35
|
+
return inspectPath(resolved.path);
|
|
36
|
+
}
|
|
37
|
+
function getOrCreateWorkspaceId(cwd = process.cwd()) {
|
|
38
|
+
const inspection = inspectWorkspaceId(cwd);
|
|
39
|
+
if (inspection.status !== "missing") return inspection;
|
|
40
|
+
return createWorkspaceId(inspection.path);
|
|
41
|
+
}
|
|
42
|
+
function resolveWorkspaceIdPath(cwd) {
|
|
43
|
+
if (!process.env.GIT_DIR && !process.env.GIT_WORK_TREE) {
|
|
44
|
+
const fromFilesystem = resolveFromFilesystem(cwd);
|
|
45
|
+
if (fromFilesystem) return { status: "resolved", path: fromFilesystem };
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const output = execFileSync(
|
|
49
|
+
"git",
|
|
50
|
+
["rev-parse", "--is-inside-work-tree", "--git-path", WORKSPACE_ID_RELATIVE_PATH],
|
|
51
|
+
{
|
|
52
|
+
cwd,
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
maxBuffer: 4096,
|
|
55
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
56
|
+
timeout: GIT_FALLBACK_TIMEOUT_MS,
|
|
57
|
+
windowsHide: true
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
const [insideWorktree, gitPath, ...extra] = output.trim().split(/\r?\n/);
|
|
61
|
+
if (insideWorktree !== "true" || !gitPath || extra.length > 0 || gitPath.includes("\0")) {
|
|
62
|
+
return { status: "not_git" };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
status: "resolved",
|
|
66
|
+
path: isAbsolute(gitPath) ? resolve(gitPath) : resolve(cwd, gitPath)
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const code = errorCode(error);
|
|
70
|
+
if (code === "ETIMEDOUT" || code === "EACCES" || code === "EPERM") {
|
|
71
|
+
return { status: "unavailable", operation: "resolve", errorCode: code };
|
|
72
|
+
}
|
|
73
|
+
return { status: "not_git" };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function resolveFromFilesystem(cwd) {
|
|
77
|
+
let current = resolve(cwd);
|
|
78
|
+
for (let depth = 0; depth < MAX_ANCESTORS; depth += 1) {
|
|
79
|
+
const dotGit = join(current, ".git");
|
|
80
|
+
try {
|
|
81
|
+
const stat = lstatSync(dotGit);
|
|
82
|
+
if (stat.isDirectory() && isGitDirectory(dotGit)) {
|
|
83
|
+
return join(dotGit, WORKSPACE_ID_RELATIVE_PATH);
|
|
84
|
+
}
|
|
85
|
+
if (stat.isFile() && stat.size <= MAX_GIT_FILE_BYTES) {
|
|
86
|
+
const contents = readFileSync(dotGit, "utf8");
|
|
87
|
+
const match = /^gitdir: (.+?)(?:\r?\n)?$/.exec(contents);
|
|
88
|
+
if (match?.[1]) {
|
|
89
|
+
const gitDirectory = isAbsolute(match[1]) ? resolve(match[1]) : resolve(current, match[1]);
|
|
90
|
+
if (isGitDirectory(gitDirectory)) {
|
|
91
|
+
return join(gitDirectory, WORKSPACE_ID_RELATIVE_PATH);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const code = errorCode(error);
|
|
98
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const parent = dirname(current);
|
|
103
|
+
if (parent === current) break;
|
|
104
|
+
current = parent;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
function isGitDirectory(path) {
|
|
109
|
+
try {
|
|
110
|
+
return statSync(join(path, "HEAD")).isFile();
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function inspectPath(path) {
|
|
116
|
+
let stat;
|
|
117
|
+
try {
|
|
118
|
+
stat = lstatSync(path);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const code = errorCode(error);
|
|
121
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { status: "missing", path };
|
|
122
|
+
return { status: "unavailable", path, operation: "read", errorCode: code };
|
|
123
|
+
}
|
|
124
|
+
if (!stat.isFile()) return { status: "corrupt", path, reason: "not_file" };
|
|
125
|
+
if (stat.size > MAX_ID_FILE_BYTES) {
|
|
126
|
+
return { status: "corrupt", path, reason: "invalid_content" };
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const contents = readFileSync(path, "utf8");
|
|
130
|
+
const workspaceId = contents.endsWith("\n") ? contents.slice(0, -1) : contents;
|
|
131
|
+
if ((contents === workspaceId || contents === `${workspaceId}
|
|
132
|
+
`) && isCanonicalWorkspaceId(workspaceId)) {
|
|
133
|
+
return { status: "ready", workspaceId, path, created: false };
|
|
134
|
+
}
|
|
135
|
+
return { status: "corrupt", path, reason: "invalid_content" };
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return {
|
|
138
|
+
status: "unavailable",
|
|
139
|
+
path,
|
|
140
|
+
operation: "read",
|
|
141
|
+
errorCode: errorCode(error)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function createWorkspaceId(path) {
|
|
146
|
+
const parent = dirname(path);
|
|
147
|
+
const workspaceId = randomUUID();
|
|
148
|
+
const temporaryPath = join(parent, `.workspace-id-${process.pid}-${randomUUID()}.tmp`);
|
|
149
|
+
let temporaryCreated = false;
|
|
150
|
+
let publishing = false;
|
|
151
|
+
try {
|
|
152
|
+
mkdirSync(parent, { recursive: true, mode: 448 });
|
|
153
|
+
chmodSync(parent, 448);
|
|
154
|
+
const fd = openSync(
|
|
155
|
+
temporaryPath,
|
|
156
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY,
|
|
157
|
+
384
|
|
158
|
+
);
|
|
159
|
+
temporaryCreated = true;
|
|
160
|
+
try {
|
|
161
|
+
writeFileSync(fd, `${workspaceId}
|
|
162
|
+
`, "utf8");
|
|
163
|
+
fchmodSync(fd, 384);
|
|
164
|
+
fsyncSync(fd);
|
|
165
|
+
} finally {
|
|
166
|
+
closeSync(fd);
|
|
167
|
+
}
|
|
168
|
+
publishing = true;
|
|
169
|
+
linkSync(temporaryPath, path);
|
|
170
|
+
return { status: "ready", workspaceId, path, created: true };
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (publishing && errorCode(error) === "EEXIST") return inspectAfterRace(path);
|
|
173
|
+
return {
|
|
174
|
+
status: "unavailable",
|
|
175
|
+
path,
|
|
176
|
+
operation: "create",
|
|
177
|
+
errorCode: errorCode(error)
|
|
178
|
+
};
|
|
179
|
+
} finally {
|
|
180
|
+
if (temporaryCreated) {
|
|
181
|
+
try {
|
|
182
|
+
unlinkSync(temporaryPath);
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function inspectAfterRace(path) {
|
|
189
|
+
let result = { status: "missing", path };
|
|
190
|
+
for (let attempt = 0; attempt < RACE_READ_ATTEMPTS; attempt += 1) {
|
|
191
|
+
result = inspectPath(path);
|
|
192
|
+
if (result.status === "ready") return result;
|
|
193
|
+
if (attempt + 1 < RACE_READ_ATTEMPTS) {
|
|
194
|
+
Atomics.wait(raceDelay, 0, 0, RACE_READ_DELAY_MS);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (result.status === "missing") {
|
|
198
|
+
return { status: "unavailable", path, operation: "create", errorCode: "EEXIST" };
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
function errorCode(error) {
|
|
203
|
+
if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
|
|
204
|
+
return typeof error.code === "string" ? error.code : void 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export {
|
|
208
|
+
inspectWorkspaceId,
|
|
209
|
+
getOrCreateWorkspaceId
|
|
210
|
+
};
|