primitive-admin 1.2.0-alpha.6 → 1.2.0-alpha.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.
Files changed (110) hide show
  1. package/README.md +9 -3
  2. package/assets/skill/skills/primitive-platform/SKILL.md +12 -6
  3. package/dist/src/commands/apps-children.d.ts +1 -1
  4. package/dist/src/commands/apps-children.js +172 -28
  5. package/dist/src/commands/apps-children.js.map +1 -1
  6. package/dist/src/commands/auth.js +147 -38
  7. package/dist/src/commands/auth.js.map +1 -1
  8. package/dist/src/commands/config.js +43 -4
  9. package/dist/src/commands/config.js.map +1 -1
  10. package/dist/src/commands/database-types.js +313 -13
  11. package/dist/src/commands/database-types.js.map +1 -1
  12. package/dist/src/commands/databases.js +143 -54
  13. package/dist/src/commands/databases.js.map +1 -1
  14. package/dist/src/commands/documents.js +70 -11
  15. package/dist/src/commands/documents.js.map +1 -1
  16. package/dist/src/commands/init.d.ts +7 -0
  17. package/dist/src/commands/init.js +9 -1
  18. package/dist/src/commands/init.js.map +1 -1
  19. package/dist/src/commands/llm.js +6 -9
  20. package/dist/src/commands/llm.js.map +1 -1
  21. package/dist/src/commands/settings.d.ts +9 -12
  22. package/dist/src/commands/settings.js +79 -31
  23. package/dist/src/commands/settings.js.map +1 -1
  24. package/dist/src/commands/sync-app-settings.d.ts +12 -2
  25. package/dist/src/commands/sync-app-settings.js +35 -10
  26. package/dist/src/commands/sync-app-settings.js.map +1 -1
  27. package/dist/src/commands/sync.d.ts +118 -6
  28. package/dist/src/commands/sync.js +638 -65
  29. package/dist/src/commands/sync.js.map +1 -1
  30. package/dist/src/lib/api-client.d.ts +25 -6
  31. package/dist/src/lib/api-client.js +106 -72
  32. package/dist/src/lib/api-client.js.map +1 -1
  33. package/dist/src/lib/app-settings-descriptor.d.ts +38 -0
  34. package/dist/src/lib/app-settings-descriptor.js +96 -4
  35. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  36. package/dist/src/lib/child-apps-local.d.ts +45 -21
  37. package/dist/src/lib/child-apps-local.js +68 -30
  38. package/dist/src/lib/child-apps-local.js.map +1 -1
  39. package/dist/src/lib/credentials-home.d.ts +51 -0
  40. package/dist/src/lib/credentials-home.js +169 -0
  41. package/dist/src/lib/credentials-home.js.map +1 -0
  42. package/dist/src/lib/credentials-lock.d.ts +80 -0
  43. package/dist/src/lib/credentials-lock.js +222 -0
  44. package/dist/src/lib/credentials-lock.js.map +1 -0
  45. package/dist/src/lib/credentials-store.d.ts +92 -5
  46. package/dist/src/lib/credentials-store.js +377 -53
  47. package/dist/src/lib/credentials-store.js.map +1 -1
  48. package/dist/src/lib/env-resolver-core.d.ts +1 -1
  49. package/dist/src/lib/env-resolver-core.js.map +1 -1
  50. package/dist/src/lib/function-bundle.d.ts +38 -18
  51. package/dist/src/lib/function-bundle.js +60 -35
  52. package/dist/src/lib/function-bundle.js.map +1 -1
  53. package/dist/src/lib/function-collect.d.ts +1 -1
  54. package/dist/src/lib/function-collect.js +4 -0
  55. package/dist/src/lib/function-collect.js.map +1 -1
  56. package/dist/src/lib/function-document-types.d.ts +5 -3
  57. package/dist/src/lib/function-document-types.js +10 -14
  58. package/dist/src/lib/function-document-types.js.map +1 -1
  59. package/dist/src/lib/function-source-roots.d.ts +67 -0
  60. package/dist/src/lib/function-source-roots.js +148 -0
  61. package/dist/src/lib/function-source-roots.js.map +1 -0
  62. package/dist/src/lib/function-sync.d.ts +27 -0
  63. package/dist/src/lib/function-sync.js +16 -3
  64. package/dist/src/lib/function-sync.js.map +1 -1
  65. package/dist/src/lib/function-triggers.d.ts +3 -15
  66. package/dist/src/lib/function-triggers.js +10 -72
  67. package/dist/src/lib/function-triggers.js.map +1 -1
  68. package/dist/src/lib/function-typecheck.d.ts +37 -2
  69. package/dist/src/lib/function-typecheck.js +210 -28
  70. package/dist/src/lib/function-typecheck.js.map +1 -1
  71. package/dist/src/lib/generated-config-surfaces.d.ts +148 -0
  72. package/dist/src/lib/generated-config-surfaces.js +229 -35
  73. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  74. package/dist/src/lib/generated-sdk-types.d.ts +1 -1
  75. package/dist/src/lib/generated-sdk-types.js +1 -1
  76. package/dist/src/lib/generated-sdk-types.js.map +1 -1
  77. package/dist/src/lib/local-state.d.ts +21 -6
  78. package/dist/src/lib/local-state.js +78 -34
  79. package/dist/src/lib/local-state.js.map +1 -1
  80. package/dist/src/lib/login-scope-lines.d.ts +1 -1
  81. package/dist/src/lib/login-scope-lines.js +1 -1
  82. package/dist/src/lib/login-scope-lines.js.map +1 -1
  83. package/dist/src/lib/project-config.d.ts +7 -0
  84. package/dist/src/lib/project-config.js +22 -1
  85. package/dist/src/lib/project-config.js.map +1 -1
  86. package/dist/src/lib/push-failure-report.d.ts +45 -0
  87. package/dist/src/lib/push-failure-report.js +57 -0
  88. package/dist/src/lib/push-failure-report.js.map +1 -0
  89. package/dist/src/lib/root-import-decision.d.ts +9 -4
  90. package/dist/src/lib/root-import-decision.js +24 -3
  91. package/dist/src/lib/root-import-decision.js.map +1 -1
  92. package/dist/src/lib/scope-request.d.ts +8 -5
  93. package/dist/src/lib/scope-request.js +17 -8
  94. package/dist/src/lib/scope-request.js.map +1 -1
  95. package/dist/src/lib/sync-selectors.d.ts +5 -1
  96. package/dist/src/lib/sync-selectors.js +23 -13
  97. package/dist/src/lib/sync-selectors.js.map +1 -1
  98. package/dist/src/lib/transient-retry.d.ts +9 -0
  99. package/dist/src/lib/transient-retry.js +37 -0
  100. package/dist/src/lib/transient-retry.js.map +1 -1
  101. package/dist/src/lib/vars-override.d.ts +105 -0
  102. package/dist/src/lib/vars-override.js +143 -0
  103. package/dist/src/lib/vars-override.js.map +1 -0
  104. package/package.json +2 -2
  105. package/dist/src/lib/generated-workflow-model-fields.d.ts +0 -20
  106. package/dist/src/lib/generated-workflow-model-fields.js +0 -53
  107. package/dist/src/lib/generated-workflow-model-fields.js.map +0 -1
  108. package/dist/src/lib/workflow-field-descriptor.d.ts +0 -91
  109. package/dist/src/lib/workflow-field-descriptor.js +0 -153
  110. package/dist/src/lib/workflow-field-descriptor.js.map +0 -1
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Where a project's login lives (#4492).
3
+ *
4
+ * A project's credentials file is `<projectRoot>/.primitive/credentials.json`,
5
+ * except in a linked git worktree: there it is the main worktree's file for
6
+ * the same project, so a new worktree of a logged-in project is logged in.
7
+ * Every worktree reads and writes that one file; everything else under
8
+ * `.primitive/` (the selection, machine-local environments, sync state,
9
+ * backups) stays per worktree.
10
+ *
11
+ * The answer is the project root itself whenever there is no main checkout to
12
+ * share with: outside a git repository, without a git that knows
13
+ * `--path-format` (2.31), in the main worktree, in a bare repository's
14
+ * worktrees, and in a linked worktree whose main checkout does not hold the
15
+ * project. Nothing here throws.
16
+ */
17
+ import { execFileSync } from "child_process";
18
+ import { existsSync, realpathSync, statSync } from "fs";
19
+ import { basename, dirname, isAbsolute, join, relative } from "path";
20
+ import { PROJECT_CONFIG_FILENAME, PROJECT_LOCAL_STATE_DIR, PROJECT_TREE_DIR, } from "./env-resolver-core.js";
21
+ const resolutions = new Map();
22
+ function git(cwd, args) {
23
+ return execFileSync("git", args, {
24
+ cwd,
25
+ encoding: "utf-8",
26
+ stdio: ["ignore", "pipe", "ignore"],
27
+ }).trim();
28
+ }
29
+ /** The project root's path relative to its worktree's top level, or null. */
30
+ function relativeToTopLevel(topLevel, projectRoot) {
31
+ const rel = relative(realpathSync(topLevel), realpathSync(projectRoot));
32
+ return rel.startsWith("..") || isAbsolute(rel) ? null : rel;
33
+ }
34
+ function holdsProject(projectRoot) {
35
+ return existsSync(join(projectRoot, PROJECT_TREE_DIR, PROJECT_CONFIG_FILENAME));
36
+ }
37
+ function resolve(projectRoot) {
38
+ const own = { root: projectRoot, gitFailed: false };
39
+ let gitDir;
40
+ let commonDir;
41
+ let topLevel;
42
+ try {
43
+ [gitDir, commonDir, topLevel] = git(projectRoot, [
44
+ "rev-parse",
45
+ "--path-format=absolute",
46
+ "--git-dir",
47
+ "--git-common-dir",
48
+ "--show-toplevel",
49
+ ]).split("\n");
50
+ }
51
+ catch {
52
+ return { root: projectRoot, gitFailed: true };
53
+ }
54
+ if (!gitDir || !commonDir || !topLevel)
55
+ return own;
56
+ if (gitDir === commonDir)
57
+ return own; // the main worktree
58
+ if (basename(commonDir) !== ".git")
59
+ return own; // a bare repository's worktree
60
+ try {
61
+ const rel = relativeToTopLevel(topLevel, projectRoot);
62
+ if (rel === null)
63
+ return own;
64
+ const candidate = join(dirname(commonDir), rel);
65
+ return holdsProject(candidate) ? { root: candidate, gitFailed: false } : own;
66
+ }
67
+ catch {
68
+ return own;
69
+ }
70
+ }
71
+ function resolutionFor(projectRoot) {
72
+ let resolution = resolutions.get(projectRoot);
73
+ if (!resolution) {
74
+ resolution = resolve(projectRoot);
75
+ resolutions.set(projectRoot, resolution);
76
+ }
77
+ return resolution;
78
+ }
79
+ /**
80
+ * The project root whose `.primitive/credentials.json` holds `projectRoot`'s
81
+ * login: the same project in the main worktree when `projectRoot` is in a
82
+ * linked git worktree and the main worktree holds the project, otherwise
83
+ * `projectRoot` itself. Cached per process per project root.
84
+ */
85
+ export function credentialsRootFor(projectRoot) {
86
+ return resolutionFor(projectRoot).root;
87
+ }
88
+ /**
89
+ * Forget every cached {@link credentialsRootFor} answer. One invocation
90
+ * resolves one project, so nothing in the CLI calls this; it exists for tests.
91
+ */
92
+ export function resetCredentialsHomeCache() {
93
+ resolutions.clear();
94
+ }
95
+ /**
96
+ * Whether the nearest `.git` above `projectRoot` is a file, which is how a
97
+ * linked worktree is marked. Read without running git.
98
+ */
99
+ export function linkedWorktreeMarker(projectRoot) {
100
+ for (let dir = projectRoot;; dir = dirname(dir)) {
101
+ const marker = join(dir, ".git");
102
+ try {
103
+ if (existsSync(marker))
104
+ return statSync(marker).isFile();
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ if (dirname(dir) === dir)
110
+ return false;
111
+ }
112
+ }
113
+ /**
114
+ * Why a linked worktree is not using the main checkout's login, when that is
115
+ * git's doing: git could not answer and the project sits in a linked
116
+ * worktree. Null otherwise.
117
+ */
118
+ export function oldGitHint(projectRoot) {
119
+ return resolutionFor(projectRoot).gitFailed && linkedWorktreeMarker(projectRoot)
120
+ ? "This looks like a linked git worktree; sharing the main checkout's login needs git 2.31 or newer."
121
+ : null;
122
+ }
123
+ /**
124
+ * The linked worktree's own `.primitive/credentials.json` when it exists but
125
+ * the main worktree's is the one used, or null. It is never read or changed.
126
+ */
127
+ export function ignoredLocalCredentialsFile(projectRoot) {
128
+ if (credentialsRootFor(projectRoot) === projectRoot)
129
+ return null;
130
+ const own = join(projectRoot, PROJECT_LOCAL_STATE_DIR, "credentials.json");
131
+ return existsSync(own) ? own : null;
132
+ }
133
+ function samePath(a, b) {
134
+ try {
135
+ return realpathSync(a) === realpathSync(b);
136
+ }
137
+ catch {
138
+ return a === b;
139
+ }
140
+ }
141
+ /**
142
+ * How many worktrees of `projectRoot`'s repository hold the project and share
143
+ * its login: the worktrees `git worktree list` names, except a bare entry,
144
+ * whose copy of the project resolves to the same credentials home. At least
145
+ * 1; 1 on any git failure.
146
+ */
147
+ export function sharedWorktreeCount(projectRoot) {
148
+ try {
149
+ const rel = relativeToTopLevel(git(projectRoot, ["rev-parse", "--show-toplevel"]), projectRoot);
150
+ if (rel === null)
151
+ return 1;
152
+ const home = credentialsRootFor(projectRoot);
153
+ let count = 0;
154
+ for (const entry of git(projectRoot, ["worktree", "list", "--porcelain"]).split(/\n\s*\n/)) {
155
+ const lines = entry.split("\n");
156
+ const worktree = lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
157
+ if (!worktree || lines.includes("bare"))
158
+ continue;
159
+ const candidate = join(worktree, rel);
160
+ if (holdsProject(candidate) && samePath(credentialsRootFor(candidate), home))
161
+ count++;
162
+ }
163
+ return Math.max(count, 1);
164
+ }
165
+ catch {
166
+ return 1;
167
+ }
168
+ }
169
+ //# sourceMappingURL=credentials-home.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials-home.js","sourceRoot":"","sources":["../../../src/lib/credentials-home.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The cross-process lock and atomic replace every project-mode credentials
3
+ * write goes through (#4492).
4
+ *
5
+ * Several CLI processes write one credentials file — one agent per git
6
+ * worktree, all sharing the main worktree's login — so a write is a locked
7
+ * read-modify-write, and the file is replaced by a rename, never rewritten in
8
+ * place: a reader never sees a partial file.
9
+ *
10
+ * The lock is `credentials.json.lock`, created exclusively and holding its
11
+ * owner's token (`<pid>:<random>`). A lock older than {@link LOCK_STALE_MS}
12
+ * was left by a crashed process; a contender reclaims it by renaming it to a
13
+ * name of its own, which exactly one contender can do, and removes it only if
14
+ * it still holds the token it read — otherwise a live lock was moved, and it
15
+ * is put back unless another writer took the lock meanwhile. A holder proves,
16
+ * immediately before it commits, that it still owns the lock and that the file
17
+ * is still the version it read — a holder whose lock was moved away and put
18
+ * back may have missed another writer's commit in between — and releases only
19
+ * its own lock.
20
+ *
21
+ * Node has no advisory file lock, so one window remains: between a holder's
22
+ * checks and its rename, a contender that just misreclaimed that lock could
23
+ * let a third writer in. Every other interleaving ends in a failed write,
24
+ * never a lost update.
25
+ */
26
+ /** A lock untouched for this long was left by a crashed process. */
27
+ export declare const LOCK_STALE_MS = 10000;
28
+ /** How long a writer waits for a held lock before it fails. */
29
+ export declare const LOCK_WAIT_MS = 5000;
30
+ /** The lock on a credentials directory, held by this process. */
31
+ export interface CredentialsLock {
32
+ /** The lock file. */
33
+ readonly path: string;
34
+ /** This holder's token, the lock file's content while it holds it. */
35
+ readonly token: string;
36
+ /** Whether the lock file still holds this holder's token. */
37
+ ownsLock(): boolean;
38
+ /** Remove the lock if, and only if, this holder still owns it. Never throws. */
39
+ release(): void;
40
+ }
41
+ /** A write that could not take, or lost, the lock on a credentials file. */
42
+ export declare class CredentialsLockedError extends Error {
43
+ constructor(lockPath: string);
44
+ }
45
+ /**
46
+ * Reclaim the stale lock at `lockPath`, whose content was `seen` when it was
47
+ * judged stale: rename it to a name of `token`'s own, then remove it if it
48
+ * still holds `seen`, or put it back if it does not (a live writer took the
49
+ * lock in between). Gone already is not an error: another contender won.
50
+ */
51
+ export declare function reclaimStaleLock(lockPath: string, token: string, seen: string): void;
52
+ /**
53
+ * Put a live lock that a reclaimer moved to `moved` back at `lockPath` — but
54
+ * only if no other writer has taken the lock since, which a rename would
55
+ * silently replace. When one has, the displaced holder has lost the lock and
56
+ * fails its ownership check. The caller removes `moved` either way.
57
+ */
58
+ export declare function restoreDisplacedLock(moved: string, lockPath: string): void;
59
+ /**
60
+ * The version of the file at `path` as a writer read it: what an atomic
61
+ * replace changes (the inode) and what an in-place write changes (size and
62
+ * times). `"absent"` when there is no file.
63
+ */
64
+ export declare function fileVersion(path: string): string;
65
+ /**
66
+ * Take the lock on the credentials file in `dir`, waiting up to
67
+ * {@link LOCK_WAIT_MS} for another writer and reclaiming a stale lock. Throws
68
+ * {@link CredentialsLockedError} naming the lock when it stays held.
69
+ */
70
+ export declare function acquireCredentialsLock(dir: string): CredentialsLock;
71
+ /**
72
+ * Replace `path` with `content` while `lock` is held: write a private
73
+ * temporary file beside it, check the lock is still this holder's and, when
74
+ * `readVersion` is given, that `path` is still the {@link fileVersion} the
75
+ * holder read under the lock, then rename the temporary file over `path`. A
76
+ * holder that lost the lock, or whose read another writer's commit has
77
+ * overtaken, removes its temporary file and throws
78
+ * {@link CredentialsLockedError}, leaving `path` as it was.
79
+ */
80
+ export declare function replaceFileUnderLock(lock: CredentialsLock, path: string, content: string, readVersion?: string): void;
@@ -0,0 +1,222 @@
1
+ /**
2
+ * The cross-process lock and atomic replace every project-mode credentials
3
+ * write goes through (#4492).
4
+ *
5
+ * Several CLI processes write one credentials file — one agent per git
6
+ * worktree, all sharing the main worktree's login — so a write is a locked
7
+ * read-modify-write, and the file is replaced by a rename, never rewritten in
8
+ * place: a reader never sees a partial file.
9
+ *
10
+ * The lock is `credentials.json.lock`, created exclusively and holding its
11
+ * owner's token (`<pid>:<random>`). A lock older than {@link LOCK_STALE_MS}
12
+ * was left by a crashed process; a contender reclaims it by renaming it to a
13
+ * name of its own, which exactly one contender can do, and removes it only if
14
+ * it still holds the token it read — otherwise a live lock was moved, and it
15
+ * is put back unless another writer took the lock meanwhile. A holder proves,
16
+ * immediately before it commits, that it still owns the lock and that the file
17
+ * is still the version it read — a holder whose lock was moved away and put
18
+ * back may have missed another writer's commit in between — and releases only
19
+ * its own lock.
20
+ *
21
+ * Node has no advisory file lock, so one window remains: between a holder's
22
+ * checks and its rename, a contender that just misreclaimed that lock could
23
+ * let a third writer in. Every other interleaving ends in a failed write,
24
+ * never a lost update.
25
+ */
26
+ import { randomBytes } from "crypto";
27
+ import { chmodSync, linkSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "fs";
28
+ import { basename, dirname, join } from "path";
29
+ /** A lock untouched for this long was left by a crashed process. */
30
+ export const LOCK_STALE_MS = 10_000;
31
+ /** How long a writer waits for a held lock before it fails. */
32
+ export const LOCK_WAIT_MS = 5_000;
33
+ const RETRY_MS = 25;
34
+ const LOCKED_FILE = "credentials.json";
35
+ /** A write that could not take, or lost, the lock on a credentials file. */
36
+ export class CredentialsLockedError extends Error {
37
+ constructor(lockPath) {
38
+ super(`credentials file is locked: ${lockPath}`);
39
+ this.name = "CredentialsLockedError";
40
+ }
41
+ }
42
+ function sleep(ms) {
43
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
44
+ }
45
+ const isMissing = (err) => err?.code === "ENOENT";
46
+ /**
47
+ * Reclaim the stale lock at `lockPath`, whose content was `seen` when it was
48
+ * judged stale: rename it to a name of `token`'s own, then remove it if it
49
+ * still holds `seen`, or put it back if it does not (a live writer took the
50
+ * lock in between). Gone already is not an error: another contender won.
51
+ */
52
+ export function reclaimStaleLock(lockPath, token, seen) {
53
+ const moved = `${lockPath}.stale.${token}`;
54
+ try {
55
+ renameSync(lockPath, moved);
56
+ }
57
+ catch (err) {
58
+ if (isMissing(err))
59
+ return;
60
+ throw err;
61
+ }
62
+ let content;
63
+ try {
64
+ content = readFileSync(moved, "utf-8");
65
+ }
66
+ catch {
67
+ return;
68
+ }
69
+ if (content !== seen)
70
+ restoreDisplacedLock(moved, lockPath);
71
+ try {
72
+ unlinkSync(moved);
73
+ }
74
+ catch {
75
+ // Swept by the next release.
76
+ }
77
+ }
78
+ /**
79
+ * Put a live lock that a reclaimer moved to `moved` back at `lockPath` — but
80
+ * only if no other writer has taken the lock since, which a rename would
81
+ * silently replace. When one has, the displaced holder has lost the lock and
82
+ * fails its ownership check. The caller removes `moved` either way.
83
+ */
84
+ export function restoreDisplacedLock(moved, lockPath) {
85
+ try {
86
+ linkSync(moved, lockPath);
87
+ }
88
+ catch {
89
+ // Taken by another writer, or no hard links here: the displaced holder
90
+ // has lost the lock.
91
+ }
92
+ }
93
+ /**
94
+ * The version of the file at `path` as a writer read it: what an atomic
95
+ * replace changes (the inode) and what an in-place write changes (size and
96
+ * times). `"absent"` when there is no file.
97
+ */
98
+ export function fileVersion(path) {
99
+ try {
100
+ const s = statSync(path);
101
+ return `${s.dev}:${s.ino}:${s.size}:${s.mtimeMs}:${s.ctimeMs}`;
102
+ }
103
+ catch (err) {
104
+ if (isMissing(err))
105
+ return "absent";
106
+ throw err;
107
+ }
108
+ }
109
+ /** Remove what crashed writers left in `dir`: old reclaimed locks and temp files. */
110
+ function sweep(dir) {
111
+ try {
112
+ for (const name of readdirSync(dir)) {
113
+ if (!name.startsWith(`${LOCKED_FILE}.lock.stale.`) && !(name.startsWith(`${LOCKED_FILE}.`) && name.endsWith(".tmp"))) {
114
+ continue;
115
+ }
116
+ const path = join(dir, name);
117
+ if (Date.now() - statSync(path).mtimeMs > LOCK_STALE_MS)
118
+ unlinkSync(path);
119
+ }
120
+ }
121
+ catch {
122
+ // Best effort.
123
+ }
124
+ }
125
+ /**
126
+ * Take the lock on the credentials file in `dir`, waiting up to
127
+ * {@link LOCK_WAIT_MS} for another writer and reclaiming a stale lock. Throws
128
+ * {@link CredentialsLockedError} naming the lock when it stays held.
129
+ */
130
+ export function acquireCredentialsLock(dir) {
131
+ const path = join(dir, `${LOCKED_FILE}.lock`);
132
+ const token = `${process.pid}:${randomBytes(8).toString("hex")}`;
133
+ const deadline = Date.now() + LOCK_WAIT_MS;
134
+ for (;;) {
135
+ try {
136
+ writeFileSync(path, token, { flag: "wx", mode: 0o600 });
137
+ break;
138
+ }
139
+ catch (err) {
140
+ if (err?.code !== "EEXIST")
141
+ throw err;
142
+ }
143
+ let held;
144
+ let seen;
145
+ try {
146
+ held = statSync(path);
147
+ seen = readFileSync(path, "utf-8");
148
+ }
149
+ catch (err) {
150
+ if (isMissing(err))
151
+ continue;
152
+ throw err;
153
+ }
154
+ if (Date.now() - held.mtimeMs > LOCK_STALE_MS) {
155
+ reclaimStaleLock(path, token, seen);
156
+ continue;
157
+ }
158
+ if (Date.now() >= deadline)
159
+ throw new CredentialsLockedError(path);
160
+ sleep(RETRY_MS);
161
+ }
162
+ const ownsLock = () => {
163
+ try {
164
+ return readFileSync(path, "utf-8") === token;
165
+ }
166
+ catch {
167
+ return false;
168
+ }
169
+ };
170
+ return {
171
+ path,
172
+ token,
173
+ ownsLock,
174
+ release() {
175
+ try {
176
+ if (ownsLock())
177
+ unlinkSync(path);
178
+ }
179
+ catch {
180
+ // Already gone.
181
+ }
182
+ sweep(dir);
183
+ },
184
+ };
185
+ }
186
+ /**
187
+ * Replace `path` with `content` while `lock` is held: write a private
188
+ * temporary file beside it, check the lock is still this holder's and, when
189
+ * `readVersion` is given, that `path` is still the {@link fileVersion} the
190
+ * holder read under the lock, then rename the temporary file over `path`. A
191
+ * holder that lost the lock, or whose read another writer's commit has
192
+ * overtaken, removes its temporary file and throws
193
+ * {@link CredentialsLockedError}, leaving `path` as it was.
194
+ */
195
+ export function replaceFileUnderLock(lock, path, content, readVersion) {
196
+ const tmp = join(dirname(path), `${basename(path)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
197
+ writeFileSync(tmp, content, { mode: 0o600 });
198
+ try {
199
+ if (!lock.ownsLock())
200
+ throw new CredentialsLockedError(lock.path);
201
+ if (readVersion !== undefined && fileVersion(path) !== readVersion) {
202
+ throw new CredentialsLockedError(lock.path);
203
+ }
204
+ renameSync(tmp, path);
205
+ }
206
+ catch (err) {
207
+ try {
208
+ unlinkSync(tmp);
209
+ }
210
+ catch {
211
+ // Never written, or already renamed.
212
+ }
213
+ throw err;
214
+ }
215
+ try {
216
+ chmodSync(path, 0o600);
217
+ }
218
+ catch {
219
+ // Best-effort on Windows / unusual filesystems.
220
+ }
221
+ }
222
+ //# sourceMappingURL=credentials-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials-lock.js","sourceRoot":"","sources":["../../../src/lib/credentials-lock.ts"],"names":[],"mappings":"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"}
@@ -37,6 +37,26 @@
37
37
  * `primitive login` reach the same line, so there is no flag shape for a
38
38
  * pre-parse to miss. And it only PERMITS the global file — inside a project
39
39
  * all four still use the per-environment slots.
40
+ *
41
+ * A child environment (#4511) — a machine-local environment on the server of
42
+ * the committed environment whose tree it shares — has no slot of its own: it
43
+ * reads and writes its parent's, so one login and one logout cover the parent
44
+ * and all of its children.
45
+ *
46
+ * A slot records the server that issued it (#4492). A slot is keyed by
47
+ * environment name while the environment's `apiUrl` is committed, so a branch
48
+ * switch can point `dev` at another server than the one its stored tokens
49
+ * came from. A slot recording another server than the invoking environment's
50
+ * is never used (its tokens are not sent), and credentials from another server
51
+ * than the slot's environment's are never stored. A slot written before this
52
+ * records no server; it is read against the environment's `apiUrl`, and its
53
+ * next write records one.
54
+ *
55
+ * In a linked git worktree the project's file is the main worktree's
56
+ * (#4492, `credentials-home.ts`), so one login serves every worktree of the
57
+ * repository that holds the project. Every project-mode write is a locked
58
+ * read-modify-write with an atomic replace (`credentials-lock.ts`), because
59
+ * several processes — one per worktree — write that one file.
40
60
  */
41
61
  import type { Credentials } from "../types/index.js";
42
62
  export declare const PROJECT_LOCAL_DIR = ".primitive";
@@ -48,6 +68,12 @@ export declare function isProjectMode(): boolean;
48
68
  * whether we're in project mode or legacy mode.
49
69
  */
50
70
  export declare function getCredentialsFilePath(): string;
71
+ /**
72
+ * The credentials file of the project at `projectRoot`: its own
73
+ * `.primitive/credentials.json`, or in a linked git worktree the main
74
+ * worktree's for the same project (#4492).
75
+ */
76
+ export declare function credentialsFileForProject(projectRoot: string): string;
51
77
  /**
52
78
  * Declare that THIS command may fall back to `~/.primitive/credentials.json`
53
79
  * when no project is in scope. Called at the top of `login`, `logout`, `init`
@@ -60,6 +86,12 @@ export declare function allowGlobalCredentials(): void;
60
86
  * boundary in one process, the way `resetEnvResolver()` does for the resolver.
61
87
  */
62
88
  export declare function resetGlobalCredentialsAllowance(): void;
89
+ /**
90
+ * Whether two server URLs name the same server: scheme and host compare
91
+ * case-insensitively and the path with trailing slashes removed. A URL that
92
+ * does not parse compares as its trailing-slash-trimmed string.
93
+ */
94
+ export declare function sameServer(a: string, b: string): boolean;
63
95
  /** The variable a CI job puts its admin token in (#3890). */
64
96
  export declare const PRIMITIVE_TOKEN_VAR = "PRIMITIVE_TOKEN";
65
97
  /** The token in `PRIMITIVE_TOKEN`, or null when it is unset or blank. */
@@ -71,26 +103,79 @@ export declare function environmentToken(): string | null;
71
103
  *
72
104
  * In project mode, we blend the per-env stored creds with the serverUrl
73
105
  * and appId from primitive/config.json to produce the Credentials the rest of
74
- * the CLI expects.
106
+ * the CLI expects. A child environment reads its parent's slot (#4511), and
107
+ * any child slot found to be a copy of that login is removed.
75
108
  *
76
109
  * `PRIMITIVE_TOKEN` (#3890), when set, takes precedence over any stored
77
110
  * login and no file is read. It does not loosen the outside-a-project
78
111
  * refusal: the environment is resolved first, exactly as for a stored login.
79
112
  */
80
113
  export declare function loadCredentialsStore(): Credentials | null;
114
+ /**
115
+ * The current environment's stored login when its slot records another server
116
+ * than the environment's, blended with the server it RECORDS; null otherwise.
117
+ * For `logout` alone, which revokes such a login at the server that issued it
118
+ * and so never sends its tokens anywhere else.
119
+ */
120
+ export declare function mismatchedLogin(): Credentials | null;
121
+ /**
122
+ * The environments that share the current environment's login (#4511): the
123
+ * one whose slot holds it, and its child environments on this machine. Null
124
+ * outside a project, where there is one global login.
125
+ */
126
+ export declare function loginEnvironments(): {
127
+ parent: string;
128
+ children: string[];
129
+ } | null;
130
+ /**
131
+ * What a login, logout or not-logged-in refusal adds about a linked git
132
+ * worktree (#4492). Each is null when it has nothing to say: outside a
133
+ * project, and where the project's login is its own.
134
+ */
135
+ export declare const worktreeLoginNotes: {
136
+ /** After a login stored in another worktree's file: where it went. */
137
+ storedElsewhere(): string | null;
138
+ /** After a logout of `slot`'s login, when more than one worktree uses it. */
139
+ sharedLogout(slot: string): string | null;
140
+ /** A linked worktree's own credentials file, which is never read. */
141
+ ignoredFile(): string | null;
142
+ /** After "not logged in", when git is why a linked worktree has no login. */
143
+ notLoggedIn(): string | null;
144
+ };
145
+ /**
146
+ * The logins still held in the slots of the current login's child
147
+ * environments: sessions signed in to a child before children used their
148
+ * parent's login, and copies of the parent's that kept a step-up or a refresh
149
+ * token of their own. No command reads them; `logout` revokes them before
150
+ * {@link clearLeftoverChildLogins} removes them. Empty outside a project.
151
+ */
152
+ export declare function leftoverChildLogins(): Credentials[];
81
153
  /**
82
154
  * Saves credentials. In project mode, writes to the per-env slot in the
83
155
  * project credentials file. In legacy mode, writes the global file.
84
156
  *
85
- * The serverUrl on the incoming Credentials is used as-is for legacy
86
- * mode, and ignored in project mode (the env's apiUrl is canonical).
157
+ * In project mode the slot records the incoming `serverUrl`, which must be
158
+ * the same server as the slot's environment's `apiUrl`: credentials from
159
+ * another server are refused with `server-mismatch` and nothing is written
160
+ * (#4492). In a child environment the parent's slot is written (#4511).
87
161
  */
88
162
  export declare function saveCredentialsStore(credentials: Credentials): void;
89
163
  /**
90
164
  * Clears credentials for the current environment (project mode) or
91
- * deletes the legacy file (legacy mode).
165
+ * deletes the legacy file (legacy mode). In project mode that is the slot
166
+ * holding the environment's login, whichever environment of the tree is
167
+ * current, and the child slots holding only that login's session (#4511),
168
+ * which its caller has just revoked. A child slot holding another session or
169
+ * a step-up of its own stays for `logout` to revoke
170
+ * ({@link clearLeftoverChildLogins}).
92
171
  */
93
172
  export declare function clearCredentialsStore(): void;
173
+ /**
174
+ * Removes every slot the current login's child environments kept, once
175
+ * `logout` has revoked the logins {@link leftoverChildLogins} returned. No-op
176
+ * outside a project.
177
+ */
178
+ export declare function clearLeftoverChildLogins(): void;
94
179
  /**
95
180
  * Removes the credentials slot for the named environment in a specific
96
181
  * project's credentials file. Used by `env remove` so stale tokens from a
@@ -109,7 +194,9 @@ export declare function removeCredentialsSlotInProject(projectRoot: string, envN
109
194
  * Writes the credentials slot for the named environment in a specific
110
195
  * project's credentials file. Used by `primitive init` to carry the session
111
196
  * it just authenticated into the freshly scaffolded project, so the first
112
- * in-project CLI command works without a second `primitive login`.
197
+ * in-project CLI command works without a second `primitive login`. The slot
198
+ * records the credentials' server, which must be the named environment's
199
+ * `apiUrl` in the project's config (#4492).
113
200
  */
114
201
  export declare function seedCredentialsForEnvInProject(projectRoot: string, envName: string, credentials: Credentials): void;
115
202
  /**