@vzn/vx 0.0.636 → 0.0.638

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 (151) hide show
  1. package/README.md +39 -11
  2. package/docs/caching.md +246 -213
  3. package/docs/cli.md +791 -705
  4. package/docs/execution.md +68 -76
  5. package/docs/features.md +47 -42
  6. package/docs/patterns.md +26 -25
  7. package/docs/schema.md +392 -205
  8. package/docs/security.md +76 -39
  9. package/launcher.sh +43 -0
  10. package/package.json +5 -5
  11. package/schemas/cache.json +41 -19
  12. package/schemas/info.json +23 -11
  13. package/schemas/last.json +48 -2
  14. package/schemas/lock.json +1 -1
  15. package/schemas/plan.json +4 -4
  16. package/schemas/show.json +5 -3
  17. package/schemas/why.json +15 -1
  18. package/skills/vx/SKILL.md +28 -13
  19. package/src/bin.ts +30 -9
  20. package/src/cache/archive.ts +11 -0
  21. package/src/cache/cache.ts +124 -53
  22. package/src/cache/chained-cache.ts +4 -0
  23. package/src/cache/git-inputs.ts +277 -63
  24. package/src/cache/index.ts +15 -1
  25. package/src/cache/inputs.ts +102 -36
  26. package/src/cache/key-fold.ts +8 -7
  27. package/src/cache/layer.ts +15 -18
  28. package/src/cache/layered-cache.ts +20 -102
  29. package/src/cache/plugin-cache.ts +248 -0
  30. package/src/cache/policy.ts +32 -27
  31. package/src/cache/run-history.ts +104 -14
  32. package/src/cache/run-store.ts +77 -0
  33. package/src/cache/schema.ts +58 -31
  34. package/src/cache/scoped-cache.ts +237 -0
  35. package/src/cli/cache.ts +173 -34
  36. package/src/cli/completions.ts +11 -7
  37. package/src/cli/core-alias.ts +1 -1
  38. package/src/cli/foreign-flags.ts +13 -12
  39. package/src/cli/help.ts +110 -70
  40. package/src/cli/index.ts +67 -24
  41. package/src/cli/info.ts +43 -27
  42. package/src/cli/init.ts +115 -407
  43. package/src/cli/last.ts +157 -9
  44. package/src/cli/lock.ts +356 -122
  45. package/src/cli/plan-format.ts +7 -5
  46. package/src/cli/plugin-commands.ts +81 -14
  47. package/src/cli/plugin-templates.ts +11 -11
  48. package/src/cli/run.ts +116 -160
  49. package/src/cli/select.ts +50 -47
  50. package/src/cli/show.ts +83 -59
  51. package/src/cli/task-verb.ts +6 -13
  52. package/src/cli/upgrade.ts +415 -149
  53. package/src/cli/watch-cycle.ts +9 -2
  54. package/src/cli/watch-filter.ts +39 -61
  55. package/src/cli/watch-fs.ts +29 -16
  56. package/src/cli/watch-judge.ts +18 -4
  57. package/src/cli/watch-set.ts +16 -42
  58. package/src/cli/watch.ts +227 -230
  59. package/src/cli/why.ts +38 -18
  60. package/src/cli/workspace-config.ts +33 -70
  61. package/src/config.ts +169 -70
  62. package/src/exec/env.ts +1 -31
  63. package/src/exec/executor.ts +1 -5
  64. package/src/exec/kill-tree.ts +2 -1
  65. package/src/exec/sandbox-runtime.ts +12 -1
  66. package/src/exec/sandbox-violations.ts +9 -1
  67. package/src/graph/dependency-spec.ts +1 -1
  68. package/src/graph/index.ts +1 -0
  69. package/src/graph/priorities.ts +1 -1
  70. package/src/graph/scheduler.ts +27 -7
  71. package/src/graph/task-graph.ts +76 -24
  72. package/src/handoff.ts +169 -0
  73. package/src/index.ts +35 -58
  74. package/src/orchestrator/affected-tasks.ts +15 -8
  75. package/src/orchestrator/command-windows.ts +44 -0
  76. package/src/orchestrator/deferred-outputs.ts +12 -10
  77. package/src/orchestrator/doctor.ts +98 -110
  78. package/src/orchestrator/download-policy.ts +3 -3
  79. package/src/orchestrator/events.ts +19 -3
  80. package/src/orchestrator/excluded-keys.ts +0 -2
  81. package/src/orchestrator/execute-task.ts +147 -69
  82. package/src/orchestrator/failure-mode.ts +19 -10
  83. package/src/orchestrator/fingerprint-watch.ts +18 -15
  84. package/src/orchestrator/forecast.ts +10 -2
  85. package/src/orchestrator/framed-output.ts +9 -5
  86. package/src/orchestrator/graph-stage.ts +5 -5
  87. package/src/orchestrator/history.ts +126 -30
  88. package/src/orchestrator/hit-restore.ts +15 -3
  89. package/src/orchestrator/index.ts +20 -5
  90. package/src/orchestrator/local-shortcircuit.ts +2 -1
  91. package/src/orchestrator/lockfile-claim.ts +79 -20
  92. package/src/orchestrator/logger.ts +80 -41
  93. package/src/orchestrator/metrics.ts +78 -9
  94. package/src/orchestrator/miss-save.ts +3 -0
  95. package/src/orchestrator/options.ts +42 -29
  96. package/src/orchestrator/output-host.ts +72 -0
  97. package/src/orchestrator/plan.ts +6 -8
  98. package/src/orchestrator/plugin-host.ts +100 -66
  99. package/src/orchestrator/plugin.ts +173 -67
  100. package/src/orchestrator/prepare.ts +163 -78
  101. package/src/orchestrator/projects.ts +105 -99
  102. package/src/orchestrator/remote-prefetch.ts +11 -6
  103. package/src/orchestrator/run-context.ts +25 -6
  104. package/src/orchestrator/run-failures.ts +17 -8
  105. package/src/orchestrator/run-lock.ts +1 -1
  106. package/src/orchestrator/run-records.ts +1 -1
  107. package/src/orchestrator/run-report.ts +9 -9
  108. package/src/orchestrator/run-settings.ts +48 -0
  109. package/src/orchestrator/run.ts +244 -119
  110. package/src/orchestrator/sandbox-fix.ts +39 -20
  111. package/src/orchestrator/sandbox-request.ts +84 -13
  112. package/src/orchestrator/signals.ts +1 -1
  113. package/src/orchestrator/stable-keys.ts +13 -11
  114. package/src/orchestrator/status-line.ts +49 -7
  115. package/src/orchestrator/summary.ts +115 -106
  116. package/src/orchestrator/task-hash.ts +12 -83
  117. package/src/orchestrator/telemetry.ts +4 -4
  118. package/src/settings.ts +428 -0
  119. package/src/{orchestrator → util}/colors.ts +5 -0
  120. package/src/util/config-frame.ts +31 -2
  121. package/src/util/errors.ts +33 -1
  122. package/src/util/essential-env.ts +31 -0
  123. package/src/util/hide-env.ts +107 -0
  124. package/src/util/index.ts +16 -3
  125. package/src/util/paths.ts +10 -0
  126. package/src/util/refusal-notice.ts +111 -0
  127. package/src/util/report.ts +31 -0
  128. package/src/util/settle.ts +49 -30
  129. package/src/util/size.ts +23 -4
  130. package/src/util/timing.ts +3 -2
  131. package/src/util/ui.ts +186 -0
  132. package/src/util/ulid.ts +1 -1
  133. package/src/util/verbs.ts +0 -15
  134. package/src/workspace/affected.ts +84 -232
  135. package/src/workspace/config-eval.ts +15 -20
  136. package/src/workspace/config-imports.ts +78 -3
  137. package/src/workspace/config-schema.ts +209 -268
  138. package/src/workspace/dependency-rules.ts +65 -0
  139. package/src/workspace/filter.ts +2 -2
  140. package/src/workspace/fingerprint.ts +11 -121
  141. package/src/workspace/index.ts +20 -27
  142. package/src/workspace/load-reads.ts +4 -4
  143. package/src/workspace/lockfile.ts +96 -140
  144. package/src/workspace/migration.ts +307 -215
  145. package/src/workspace/package-graph.ts +30 -216
  146. package/src/workspace/project-loader.ts +80 -285
  147. package/src/workspace/repo-id.ts +2 -1
  148. package/src/workspace/workspace.ts +363 -831
  149. package/src/cache/config-evals.ts +0 -130
  150. package/src/workspace/config-cache.ts +0 -661
  151. package/src/workspace/migrate-scripts.ts +0 -1121
@@ -1,41 +1,43 @@
1
- // vx-lock.json — frozen resolved-config lockfile.
1
+ // .vx/projects.lock.json — frozen resolved-config lockfile.
2
2
  //
3
- // `vx lock` evaluates every project's vx.config.* in the CURRENT
4
- // environment and freezes the resolved objects (plus a content hash of
5
- // each config file) into `vx-lock.json` at the workspace root. A run
6
- // under `--frozen` loads configs from it instead of evaluating —
7
- // frozen-env semantics; a plain run always evaluates live. See
8
- // docs/design/config-lock-2026-06.md.
3
+ // `vx lock` finds every project's vx.config.*, evaluates each in the
4
+ // CURRENT environment and freezes the resolved objects, by directory, into
5
+ // `.vx/projects.lock.json` at the workspace root. Every run loads configs from
6
+ // it and never evaluates one (owner, 2026-10-14); only `vx lock` writes
7
+ // it. See docs/design/config-lock-2026-06.md.
9
8
  //
10
9
  // Verification asymmetry (deliberate):
11
- // - `--frozen` runs TRUST the lock: no staleness check, eval-free.
12
- // - `vx lock --check` AUDITS it: the file hashes, then full
13
- // re-evaluation + deep equality, which catches eval-time env drift
14
- // that file hashes cannot see.
10
+ // - runs TRUST the lock: no staleness check, no schema validation,
11
+ // eval-free.
12
+ // - `vx lock --check` AUDITS it: full re-evaluation + deep equality.
15
13
 
14
+ import { readFileSync } from 'node:fs'
16
15
  import { rename, rm } from 'node:fs/promises'
17
16
  import path from 'node:path'
18
17
  import type { ProjectConfig } from '../config.js'
19
18
  import { relPosix, UserError, xxh3 } from '../util/index.js'
20
19
  import { VERSION } from '../version.js'
21
- import { CONFIG_EVAL_VERSION, type ConfigEvalStore } from './config-cache.js'
22
20
  import { validateProjectConfig } from './config-schema.js'
21
+ import { PROJECT_CONFIG_FILENAMES } from './workspace.js'
23
22
 
24
- export const LOCKFILE_NAME = 'vx-lock.json'
25
- export const LOCKFILE_VERSION = 1
23
+ export const LOCKFILE_NAME = '.vx/projects.lock.json'
24
+ export const LOCKFILE_VERSION = 3
26
25
 
27
- export interface LockfileEntry {
28
- /** Workspace-root-relative POSIX path to the project's config file. */
29
- configPath: string
30
- /** xxh3 hex of the config file bytes at lock time. */
31
- configHash: string
32
- /** The resolved (post-evaluation) config object, JSON-normalized. */
26
+ /** One project as the lock holds it. */
27
+ export interface LockedProject {
28
+ /** The config file's name in the directory (`vx.config.ts`). */
29
+ file: string
30
+ /** Its resolved (post-evaluation) config, JSON-normalized. */
33
31
  config: ProjectConfig
34
32
  }
35
33
 
36
34
  export interface Lockfile {
37
35
  version: number
38
- projects: Record<string, LockfileEntry>
36
+ /**
37
+ * Root-relative POSIX directory (`.` for the root) → its project. Keyed
38
+ * by directory, so a name a plugin gives never invalidates the lock.
39
+ */
40
+ projects: Record<string, LockedProject>
39
41
  /** Written by `writeLockfile` from the bytes; never read off the object. */
40
42
  validated?: string
41
43
  }
@@ -44,12 +46,38 @@ export function lockfilePath(root: string): string {
44
46
  return path.join(root, LOCKFILE_NAME)
45
47
  }
46
48
 
47
- /** What a `--frozen` verb says when there is no lock to read. */
48
- export const FROZEN_WITHOUT_LOCK =
49
- "--frozen requires vx-lock.json at the workspace root — run 'vx lock' and commit it"
49
+ /** What a run says when a crash came from a hand-broken lock entry (M, 2026-10-14). */
50
+ export const LOCK_INVALID = `${LOCKFILE_NAME} is invalid, run \`vx lock\``
50
51
 
51
52
  /**
52
- * Read and shape-validate `vx-lock.json`. Returns `null` when no lock
53
+ * Whether the lock holds an entry `vx lock` could not have written. Runs
54
+ * validate no entry, so a hand-broken one crashed deep in a run with a
55
+ * raw TypeError; asked only on that crash's path, so a run pays nothing.
56
+ */
57
+ export function lockEntryBroken(root: string): boolean {
58
+ let projects: unknown
59
+ try {
60
+ projects = (JSON.parse(readFileSync(lockfilePath(root), 'utf8')) as { projects?: unknown })
61
+ .projects
62
+ } catch {
63
+ return false
64
+ }
65
+ if (typeof projects !== 'object' || projects === null) return false
66
+ for (const [dir, entry] of Object.entries(projects)) {
67
+ try {
68
+ validateProjectConfig((entry as LockedProject).config, `${LOCKFILE_NAME}: "${dir}"`, null)
69
+ } catch {
70
+ return true
71
+ }
72
+ }
73
+ return false
74
+ }
75
+
76
+ /** What a run says when there is no lock to read. */
77
+ export const NO_LOCK = 'no lock: run `vx lock`'
78
+
79
+ /**
80
+ * Read and shape-validate `.vx/projects.lock.json`. Returns `null` when no lock
53
81
  * exists (the common case — the feature is opt-in). The lock is a
54
82
  * user-editable file, so this is a system boundary: malformed content
55
83
  * throws a `UserError` instead of crashing deeper in the run.
@@ -84,68 +112,57 @@ export async function readLockfile(root: string): Promise<Lockfile | null> {
84
112
  }
85
113
  // Same trap, and this one is worse than a bad message: an ARRAY of entries is
86
114
  // ACCEPTED, because `Object.entries` happily walks it — yielding a project
87
- // literally named "0". `--frozen` then looks up real project names against a
88
- // lock that declares none of them.
115
+ // at directory "0".
89
116
  if (typeof lock.projects !== 'object' || lock.projects === null || Array.isArray(lock.projects)) {
90
117
  throw new UserError(`${LOCKFILE_NAME}: \`projects\` must be an object — re-run \`vx lock\``)
91
118
  }
92
- for (const [name, entry] of Object.entries(lock.projects)) {
93
- const e = entry as Partial<LockfileEntry> | null
119
+ for (const [dir, entry] of Object.entries(lock.projects)) {
120
+ const { file, config } = (entry ?? {}) as { file?: unknown; config?: unknown }
94
121
  if (
95
- !e ||
96
- typeof e.configPath !== 'string' ||
97
- typeof e.configHash !== 'string' ||
98
- typeof e.config !== 'object' ||
99
- e.config === null
122
+ typeof entry !== 'object' ||
123
+ entry === null ||
124
+ Array.isArray(entry) ||
125
+ typeof file !== 'string' ||
126
+ !PROJECT_CONFIG_FILENAMES.includes(file) ||
127
+ typeof config !== 'object' ||
128
+ config === null ||
129
+ Array.isArray(config)
100
130
  ) {
101
- throw new UserError(`${LOCKFILE_NAME}: entry for "${name}" is malformed — re-run \`vx lock\``)
131
+ throw new UserError(`${LOCKFILE_NAME}: entry for "${dir}" is malformed — re-run \`vx lock\``)
102
132
  }
103
133
  }
104
- lockDigests.set(parsed, xxh3(text).toString(16))
105
- const stamped = VALIDATED_LINE.exec(text)
106
- if (stamped !== null && stamped[1] === validatedDigest(text.slice(0, stamped.index) + '\n}')) {
107
- selfValidated.add(parsed)
108
- }
109
134
  return parsed as Lockfile
110
135
  }
111
136
 
112
- /** The digest of the bytes each lock `readLockfile` returned was parsed from. */
113
- const lockDigests = new WeakMap<object, string>()
114
-
115
137
  /**
116
- * The lock's own record that this vx validated its entries: `vx lock`
117
- * evaluates and validates every config, then writes a digest of the bytes
118
- * it wrote, under vx's version (validation's rules are vx's alone, so Bun's
119
- * is left out and does not churn a committed lock), as the last field. A lock
120
- * whose digest matches is validated already, and a cold `--frozen` run
121
- * skips the ~30 ms of validating 1,090 entries before its first task. An
122
- * edit, a merge, or another vx version leaves it unmatched, and the
123
- * entries are validated as before.
138
+ * `vx lock` evaluates and validates every config, then writes a digest of
139
+ * the bytes it wrote, under vx's version, as the last field: the lock's
140
+ * record of which vx validated it. No reader consults it.
124
141
  */
125
- const VALIDATED_LINE = /,\n {2}"validated": "([0-9a-f]+)"\n\}\n?$/
126
- const selfValidated = new WeakSet<object>()
127
-
128
142
  function validatedDigest(body: string): string {
129
- return xxh3(`vx-lock-valid-v${CONFIG_EVAL_VERSION}\0${VERSION}\0${body}`).toString(16)
143
+ return xxh3(`vx-lock-valid-v3\0${VERSION}\0${body}`).toString(16)
130
144
  }
131
145
 
132
146
  export async function writeLockfile(root: string, lock: Lockfile): Promise<void> {
133
147
  // Written beside its name and renamed over it: `Bun.write` truncates in
134
- // place, so a `--frozen` run, or `vx watch --frozen` woken by the write
148
+ // place, so a run, or `vx watch` woken by the write
135
149
  // itself (item 971), could read an empty or half-written lock and fail
136
150
  // on "not valid JSON" (item 972).
137
151
  const file = lockfilePath(root)
138
152
  const tmp = `${file}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`
139
153
  try {
140
- const body = JSON.stringify({ version: lock.version, projects: lock.projects }, null, 2)
141
- await Bun.write(tmp, `${body.slice(0, -2)},\n "validated": "${validatedDigest(body)}"\n}\n`)
154
+ // Minified: the file only vx reads, and it loads faster than pretty
155
+ // JSON (measured, 10k projects: 15.6 ms against 22.5 ms). `vx.lock.json`
156
+ // beside each config is the copy for people.
157
+ const body = JSON.stringify({ version: lock.version, projects: lock.projects })
158
+ await Bun.write(tmp, `${body.slice(0, -1)},"validated":"${validatedDigest(body)}"}\n`)
142
159
  await rename(tmp, file)
143
160
  } catch (err) {
144
161
  // Best effort: where the write was refused the temp cannot be named
145
162
  // either (ENOTDIR), and that must not replace the refusal itself.
146
163
  await rm(tmp, { force: true }).catch(() => {})
147
164
  // The refusal names the file the user knows: a read-only checkout read
148
- // `open '…/vx-lock.json.tmp-…'` where it had read the lock's own name.
165
+ // `open '…/.vx/projects.lock.json.tmp-…'` where it had read the lock's own name.
149
166
  const e = err as NodeJS.ErrnoException
150
167
  if (typeof e.message === 'string') e.message = e.message.split(tmp).join(file)
151
168
  if (e.path === tmp) e.path = file
@@ -155,93 +172,32 @@ export async function writeLockfile(root: string, lock: Lockfile): Promise<void>
155
172
 
156
173
  /**
157
174
  * Run-time config load from the lock: return the FROZEN resolved config
158
- * for each project, in order. No evaluation happens — this is the fast,
159
- * eval-free trust path — and no staleness check either (see below): the
160
- * lock's contract is "what runs is what was locked", so a config file
161
- * edited since `vx lock` still runs as locked until `vx lock --check`
162
- * says otherwise. An unlocked project is a hard error; silently falling
163
- * back to evaluation would break frozen-env semantics.
164
- *
165
- * `store` remembers which entries of which lock bytes this validator
166
- * accepted, beside the config evaluations and keyed like them (vx's and
167
- * Bun's versions): validating 1,000 entries was ~30 ms of every warm
168
- * `--frozen` run, for a verdict that cannot change until the bytes or vx
169
- * do.
175
+ * for each project, in order. No evaluation happens, and no staleness
176
+ * check or schema validation either: the lock's contract is "what runs is
177
+ * what was locked", `vx lock` validated every entry it wrote, and
178
+ * `vx lock --check` re-evaluates them all. An unlocked project is a hard
179
+ * error; a run never evaluates.
170
180
  */
171
- export async function frozenProjectConfigs(
181
+ export function frozenProjectConfigs(
172
182
  lock: Lockfile,
173
- metas: ReadonlyArray<{ name: string; configPath: string }>,
183
+ metas: ReadonlyArray<{ name: string; dir: string; configPath: string }>,
174
184
  root: string,
175
- store?: ConfigEvalStore,
176
- ): Promise<ProjectConfig[]> {
177
- const entries = metas.map((meta) => {
178
- const rel = relPosix(root, meta.configPath)
185
+ ): ProjectConfig[] {
186
+ return metas.map((meta) => {
187
+ const dir = lockKey(root, meta.dir)
179
188
  // Own entries only: `constructor` read Object's, "locked" at undefined (D-152).
180
- const entry = Object.hasOwn(lock.projects, meta.name) ? lock.projects[meta.name] : undefined
181
- if (entry !== undefined && entry.configPath !== rel) {
182
- // A renamed or moved config: "has no entry for a" named an entry the
183
- // lock holds, under its old path (D-88).
184
- throw new UserError(
185
- `${LOCKFILE_NAME} locks "${meta.name}" at ${entry.configPath}, but its config is ${rel} now — ` +
186
- `run \`vx lock\` to refresh, or delete ${LOCKFILE_NAME}`,
187
- )
188
- }
189
- if (!entry) {
189
+ const entry = Object.hasOwn(lock.projects, dir) ? lock.projects[dir] : undefined
190
+ if (entry === undefined) {
190
191
  throw new UserError(
191
- `${LOCKFILE_NAME} has no entry for "${meta.name}" (${rel}) — ` +
192
- `run \`vx lock\` to refresh, or delete ${LOCKFILE_NAME}`,
192
+ `${LOCKFILE_NAME} has no entry for "${meta.name}" (${relPosix(root, meta.configPath)}) — ` +
193
+ 'run `vx lock`',
193
194
  )
194
195
  }
195
- return entry
196
+ return entry.config
196
197
  })
197
- // No staleness checks here, deliberately: --frozen runs after
198
- // `vx lock --check` in any sane pipeline, and that audit re-
199
- // evaluates everything — a per-file byte-hash re-check would be
200
- // redundant work and a weaker guarantee pretending to add safety
201
- // (it can't see import closures or env anyway). configHash stays
202
- // in the file solely for --check's fast file-changed reporting.
203
- // The lock is hand-editable; the stored config crosses the same
204
- // boundary a freshly evaluated one does.
205
- const digest = store === undefined ? undefined : lockDigests.get(lock)
206
- const prefix =
207
- digest === undefined
208
- ? undefined
209
- : `vx-lock-valid-v${CONFIG_EVAL_VERSION}\0${VERSION}\0${Bun.version}\0${digest}`
210
- // One row for a lock whose every entry was accepted: a warm `--all` run
211
- // asked for 1,090 per-project rows in one `IN` query, ~10 ms of its
212
- // config load, where the eval-cache path it replaces is cheaper (X-181).
213
- // A scoped run still validates, and remembers, only what it loads.
214
- if (
215
- selfValidated.has(lock) ||
216
- (prefix !== undefined && store !== undefined && store.getConfigEval(prefix) !== null)
217
- ) {
218
- return entries.map((e) => e.config)
219
- }
220
- const keys = prefix === undefined ? undefined : metas.map((m) => `${prefix}\0${m.name}`)
221
- const known =
222
- keys === undefined || store === undefined
223
- ? new Map<string, string>()
224
- : store.getConfigEvals !== undefined
225
- ? store.getConfigEvals(keys)
226
- : new Map(keys.flatMap((k) => (store.getConfigEval(k) === null ? [] : [[k, '1'] as const])))
227
- const accepted: Array<readonly [string, string]> = []
228
- for (let i = 0; i < entries.length; i++) {
229
- const key = keys?.[i]
230
- if (key !== undefined && known.has(key)) continue
231
- validateProjectConfig(entries[i]!.config, `${LOCKFILE_NAME} (${metas[i]!.name})`)
232
- if (key !== undefined) accepted.push([key, '1'])
233
- }
234
- // Every meta found its entry above, so as many distinct names as the
235
- // lock holds is every entry of it.
236
- if (
237
- prefix !== undefined &&
238
- new Set(metas.map((m) => m.name)).size === Object.keys(lock.projects).length
239
- ) {
240
- accepted.push([prefix, '1'])
241
- }
242
- if (store !== undefined && accepted.length > 0) {
243
- if (store.putConfigEvals !== undefined) store.putConfigEvals(accepted)
244
- else for (const [k, v] of accepted) store.putConfigEval(k, v)
245
- }
246
- return entries.map((e) => e.config)
198
+ }
199
+
200
+ /** A project directory as the lock keys it: root-relative POSIX, `.` for the root. */
201
+ export function lockKey(root: string, dir: string): string {
202
+ return relPosix(root, dir) || '.'
247
203
  }