heyspex 0.2.0 → 0.2.1

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 (145) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/bin/hs.mjs +163 -11
  3. package/dist/cli/hs-check.mjs +88 -46
  4. package/dist/cli/hs-claim.mjs +84 -39
  5. package/dist/cli/hs-connect.mjs +583 -129
  6. package/dist/cli/hs-defer.mjs +251 -30
  7. package/dist/cli/hs-dev.mjs +20 -8
  8. package/dist/cli/hs-env.mjs +62 -34
  9. package/dist/cli/hs-index-rebuild.mjs +288 -83
  10. package/dist/cli/hs-index-reconcile.mjs +397 -111
  11. package/dist/cli/hs-init.mjs +2143 -91
  12. package/dist/cli/hs-install.mjs +628 -106
  13. package/dist/cli/hs-join.mjs +686 -236
  14. package/dist/cli/hs-vault-pull.mjs +3189 -179
  15. package/dist/cli/hs-vault-push.mjs +6433 -0
  16. package/dist/cli/hs-vault.mjs +396 -178
  17. package/dist/cli/hs-workspace.mjs +289 -151
  18. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
  19. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
  20. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
  21. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -28
  22. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
  23. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
  24. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
  25. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
  26. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
  27. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
  28. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -26
  29. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
  30. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
  31. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
  32. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
  33. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
  34. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -22
  35. package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
  36. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
  37. package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
  38. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
  39. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
  40. package/dist/cli-templates/harness-commands/cursor/hs-review.md +17 -17
  41. package/dist/cli-templates/harness-commands/cursor/hs-view.md +24 -22
  42. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
  43. package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
  44. package/docs/copilot-plugin-install.md +162 -0
  45. package/docs/driving-heyspex-from-copilot.md +165 -0
  46. package/package.json +20 -2
  47. package/plugins/heyspex/.claude-plugin/plugin.json +12 -12
  48. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -15
  49. package/plugins/heyspex/README.md +309 -0
  50. package/plugins/heyspex/bin/run-hook.mjs +20 -2
  51. package/plugins/heyspex/hooks/cursor-hooks.json +81 -0
  52. package/plugins/heyspex/hooks/hooks.json +94 -37
  53. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -410
  54. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +5 -0
  55. package/scripts/claude-hooks/_run.mjs +316 -23
  56. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  57. package/scripts/claude-hooks/claim-liveness-shared.mjs +2 -0
  58. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  59. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  60. package/scripts/claude-hooks/inject-communication-style.mjs +20 -0
  61. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -247
  62. package/scripts/claude-hooks/session-end.mjs +2 -0
  63. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  64. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  65. package/scripts/claude-hooks/wip-checkpoint.mjs +5 -1
  66. package/scripts/cursor-hooks/manifest.mjs +46 -0
  67. package/scripts/heyspex-mode.mjs +20 -0
  68. package/scripts/hs-mode.mjs +1 -1
  69. package/scripts/hs-view.mjs +180 -17
  70. package/scripts/install-claude-code-hooks.mjs +3 -1
  71. package/scripts/lib/console-url-resolver.mjs +3 -2
  72. package/scripts/lib/customer-kit.mjs +327 -10
  73. package/scripts/lib/dev-mode.mjs +7 -6
  74. package/scripts/lib/directive-obligations.mjs +349 -0
  75. package/scripts/lib/hook-dispositions.mjs +210 -0
  76. package/scripts/lib/hook-vault-header.mjs +30 -0
  77. package/scripts/lib/hs-source-branch.mjs +196 -0
  78. package/scripts/lib/hs-source-log.mjs +2 -1
  79. package/scripts/lib/hs-source-preflight.mjs +18 -4
  80. package/scripts/lib/hs-source-push.mjs +386 -356
  81. package/scripts/lib/window-placement.mjs +109 -0
  82. package/spexharness/src/adapters/claude-code.mjs +11 -3
  83. package/spexharness/src/adapters/copilot.mjs +236 -0
  84. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  85. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  86. package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
  87. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  88. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  89. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  90. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  91. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  92. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  93. package/spexharness/src/intent-classifier.mjs +4 -2
  94. package/spexharness/src/intent-directive.mjs +626 -541
  95. package/spexharness/src/rule-table.mjs +31 -1
  96. package/vendor/heyspex-mcp/account-switch.mjs +6 -5
  97. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
  98. package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
  99. package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
  100. package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
  101. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  102. package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
  103. package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
  104. package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
  105. package/vendor/heyspex-mcp/cli.mjs +7 -1
  106. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  107. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
  108. package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
  109. package/vendor/heyspex-mcp/console-transport.mjs +7 -1
  110. package/vendor/heyspex-mcp/credentials.mjs +9 -8
  111. package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
  112. package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
  113. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
  114. package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
  115. package/vendor/heyspex-mcp/http-transport.mjs +2 -0
  116. package/vendor/heyspex-mcp/list-tools.mjs +1 -1
  117. package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
  118. package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
  119. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  120. package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
  121. package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
  122. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
  123. package/vendor/heyspex-mcp/plan-push.mjs +3 -2
  124. package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
  125. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
  126. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
  127. package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
  128. package/vendor/heyspex-mcp/promote-research.mjs +6 -0
  129. package/vendor/heyspex-mcp/read-path.mjs +162 -6
  130. package/vendor/heyspex-mcp/request-context.mjs +25 -1
  131. package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
  132. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  133. package/vendor/heyspex-mcp/session-owner.mjs +3 -2
  134. package/vendor/heyspex-mcp/session-tools.mjs +10 -0
  135. package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
  136. package/vendor/heyspex-mcp/split-research.mjs +3 -0
  137. package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
  138. package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
  139. package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
  140. package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
  141. package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
  142. package/vendor/heyspex-mcp/vault-io.mjs +720 -724
  143. package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
  144. package/vendor/heyspex-mcp/whoami.mjs +2 -1
  145. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -18,6 +18,10 @@ var __esm = (fn, res) => function __init() {
18
18
  var __commonJS = (cb, mod) => function __require2() {
19
19
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
20
20
  };
21
+ var __export = (target, all) => {
22
+ for (var name in all)
23
+ __defProp(target, name, { get: all[name], enumerable: true });
24
+ };
21
25
  var __copyProps = (to, from, except, desc2) => {
22
26
  if (from && typeof from === "object" || typeof from === "function") {
23
27
  for (let key of __getOwnPropNames(from))
@@ -59,11 +63,26 @@ var init_hs_command = __esm({
59
63
  }
60
64
  });
61
65
 
66
+ // scripts/lib/heyspex-home.mjs
67
+ import { homedir } from "node:os";
68
+ function heyspexHome(env = process.env) {
69
+ const override = env?.HEYSPEX_HOME;
70
+ if (typeof override === "string" && override.trim().length > 0) {
71
+ return override.trim();
72
+ }
73
+ return homedir();
74
+ }
75
+ var init_heyspex_home = __esm({
76
+ "scripts/lib/heyspex-home.mjs"() {
77
+ "use strict";
78
+ }
79
+ });
80
+
62
81
  // vendor/heyspex-mcp/credentials.mjs
63
82
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
64
- import os, { homedir } from "node:os";
83
+ import os, { homedir as homedir2 } from "node:os";
65
84
  import { dirname as dirname2, join as join2, resolve } from "node:path";
66
- function credentialsFilePath(homeDir = homedir()) {
85
+ function credentialsFilePath(homeDir = heyspexHome()) {
67
86
  return join2(homeDir, ".heyspex", "credentials.yaml");
68
87
  }
69
88
  function parseMcpToken(raw) {
@@ -159,7 +178,7 @@ function readLegacyConfigAccountId(read, homeDir) {
159
178
  const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
160
179
  return value || null;
161
180
  }
162
- function readCurrentAccountId({ homeDir = homedir(), readFile } = {}) {
181
+ function readCurrentAccountId({ homeDir = heyspexHome(), readFile } = {}) {
163
182
  const read = readFile ?? ((p) => readFileSync(p, "utf8"));
164
183
  const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
165
184
  if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
@@ -172,6 +191,7 @@ var MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT_ID, ACCOUNT_FI
172
191
  var init_credentials = __esm({
173
192
  "vendor/heyspex-mcp/credentials.mjs"() {
174
193
  "use strict";
194
+ init_heyspex_home();
175
195
  MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
176
196
  CREDENTIALS_SCHEMA_VERSION = 2;
177
197
  LEGACY_ACCOUNT_ID = "legacy";
@@ -183,9 +203,9 @@ var init_credentials = __esm({
183
203
 
184
204
  // vendor/heyspex-mcp/session-owner.mjs
185
205
  import { readFileSync as readFileSync2 } from "node:fs";
186
- import { homedir as homedir2 } from "node:os";
206
+ import { homedir as homedir3 } from "node:os";
187
207
  import { join as join3 } from "node:path";
188
- function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir2(), readFile } = {}) {
208
+ function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile } = {}) {
189
209
  const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
190
210
  if (fromEnv) return fromEnv;
191
211
  return readCurrentAccountId({ homeDir, readFile });
@@ -193,6 +213,7 @@ function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir2(),
193
213
  var init_session_owner = __esm({
194
214
  "vendor/heyspex-mcp/session-owner.mjs"() {
195
215
  "use strict";
216
+ init_heyspex_home();
196
217
  init_credentials();
197
218
  }
198
219
  });
@@ -227,20 +248,47 @@ var init_vault_less_endpoints = __esm({
227
248
  }
228
249
  });
229
250
 
251
+ // scripts/lib/shared-path-helpers.mjs
252
+ import { isAbsolute, resolve as resolve2, sep } from "node:path";
253
+ var init_shared_path_helpers = __esm({
254
+ "scripts/lib/shared-path-helpers.mjs"() {
255
+ "use strict";
256
+ }
257
+ });
258
+
259
+ // vendor/heyspex-mcp/write-outbox.mjs
260
+ import {
261
+ readdirSync,
262
+ readFileSync as readFileSync3,
263
+ writeFileSync as writeFileSync2,
264
+ mkdirSync as mkdirSync2,
265
+ existsSync as existsSync2,
266
+ renameSync as renameSync2,
267
+ rmSync
268
+ } from "node:fs";
269
+ import { basename, join as join4, isAbsolute as isAbsolute2 } from "node:path";
270
+ var init_write_outbox = __esm({
271
+ "vendor/heyspex-mcp/write-outbox.mjs"() {
272
+ "use strict";
273
+ init_shared_path_helpers();
274
+ }
275
+ });
276
+
230
277
  // vendor/heyspex-mcp/vault-resolve.mjs
231
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
232
- import { homedir as homedir3, tmpdir } from "node:os";
233
- import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
278
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
279
+ import { homedir as homedir4, tmpdir } from "node:os";
280
+ import { dirname as dirname3, join as join5, resolve as resolve3 } from "node:path";
234
281
  var init_vault_resolve = __esm({
235
282
  "vendor/heyspex-mcp/vault-resolve.mjs"() {
236
283
  "use strict";
284
+ init_heyspex_home();
237
285
  }
238
286
  });
239
287
 
240
288
  // vendor/heyspex-mcp/console-fetch.mjs
241
- import { readFileSync as readFileSync4 } from "node:fs";
242
- import { homedir as homedir4 } from "node:os";
243
- import { join as join5 } from "node:path";
289
+ import { readFileSync as readFileSync5 } from "node:fs";
290
+ import { homedir as homedir5 } from "node:os";
291
+ import { join as join6 } from "node:path";
244
292
  function isCloudVaultId(value) {
245
293
  return typeof value === "string" && CLOUD_VAULT_ID_RE.test(value.trim());
246
294
  }
@@ -248,16 +296,33 @@ var CLOUD_VAULT_ID_RE, SCOPE_REFUSAL_CODES;
248
296
  var init_console_fetch = __esm({
249
297
  "vendor/heyspex-mcp/console-fetch.mjs"() {
250
298
  "use strict";
299
+ init_heyspex_home();
251
300
  init_credentials();
252
301
  init_session_owner();
253
302
  init_vault_less_endpoints();
254
303
  init_hs_command();
304
+ init_write_outbox();
255
305
  init_vault_resolve();
256
306
  CLOUD_VAULT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
257
307
  SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
258
308
  }
259
309
  });
260
310
 
311
+ // spex-console/src/lib/heyspex-home.ts
312
+ import { homedir as homedir6 } from "node:os";
313
+ function heyspexHome2(env = process.env) {
314
+ const override = env?.HEYSPEX_HOME;
315
+ if (typeof override === "string" && override.trim().length > 0) {
316
+ return override.trim();
317
+ }
318
+ return homedir6();
319
+ }
320
+ var init_heyspex_home2 = __esm({
321
+ "spex-console/src/lib/heyspex-home.ts"() {
322
+ "use strict";
323
+ }
324
+ });
325
+
261
326
  // spex-console/src/lib/vault-registry/types.ts
262
327
  function emptyVaultRegistry() {
263
328
  return {
@@ -278,14 +343,13 @@ var init_types = __esm({
278
343
  });
279
344
 
280
345
  // spex-console/src/lib/vault-registry/registry-file.ts
281
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
282
- import { homedir as homedir5 } from "node:os";
283
- import { dirname as dirname4, join as join6, resolve as resolve3 } from "node:path";
346
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
347
+ import { dirname as dirname4, join as join7, resolve as resolve4 } from "node:path";
284
348
  function vaultRegistryDir(opts = {}) {
285
- return opts.registryDir ?? join6(homedir5(), ".heyspex");
349
+ return opts.registryDir ?? join7(heyspexHome2(), ".heyspex");
286
350
  }
287
351
  function vaultRegistryFilePath(opts = {}) {
288
- return join6(vaultRegistryDir(opts), "registry.json");
352
+ return join7(vaultRegistryDir(opts), "registry.json");
289
353
  }
290
354
  function isNonEmptyString(v) {
291
355
  return typeof v === "string" && v.trim().length > 0;
@@ -386,9 +450,9 @@ function parseVaultRegistryFile(json) {
386
450
  }
387
451
  function readRegistryJson(opts) {
388
452
  const filePath = vaultRegistryFilePath(opts);
389
- if (!existsSync3(filePath)) return { kind: "missing" };
453
+ if (!existsSync4(filePath)) return { kind: "missing" };
390
454
  try {
391
- const raw = readFileSync5(filePath, "utf8");
455
+ const raw = readFileSync6(filePath, "utf8");
392
456
  const json = JSON.parse(raw);
393
457
  if (registryFileSchemaOf(json) === null) return { kind: "unreadable" };
394
458
  return { kind: "ok", raw, json };
@@ -417,29 +481,32 @@ function loadVaultRegistry(opts = {}) {
417
481
  var init_registry_file = __esm({
418
482
  "spex-console/src/lib/vault-registry/registry-file.ts"() {
419
483
  "use strict";
484
+ init_heyspex_home2();
420
485
  init_types();
421
486
  init_types();
422
487
  }
423
488
  });
424
489
 
425
490
  // scripts/lib/dev-mode.mjs
426
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
427
- import { homedir as homedir6 } from "node:os";
428
- import { join as join7 } from "node:path";
491
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
492
+ import { homedir as homedir7 } from "node:os";
493
+ import { join as join8 } from "node:path";
429
494
  var init_dev_mode = __esm({
430
495
  "scripts/lib/dev-mode.mjs"() {
431
496
  "use strict";
497
+ init_heyspex_home();
432
498
  init_credentials();
433
499
  }
434
500
  });
435
501
 
436
502
  // scripts/lib/console-url-resolver.mjs
437
- import { readFileSync as readFileSync7 } from "node:fs";
438
- import { homedir as homedir7 } from "node:os";
439
- import { join as join8 } from "node:path";
503
+ import { readFileSync as readFileSync8 } from "node:fs";
504
+ import { homedir as homedir8 } from "node:os";
505
+ import { join as join9 } from "node:path";
440
506
  var init_console_url_resolver = __esm({
441
507
  "scripts/lib/console-url-resolver.mjs"() {
442
508
  "use strict";
509
+ init_heyspex_home();
443
510
  init_dev_mode();
444
511
  }
445
512
  });
@@ -482,13 +549,12 @@ var init_default_workspace = __esm({
482
549
  });
483
550
 
484
551
  // spex-console/src/lib/cli/vault-workspace-shared.ts
485
- import { readFileSync as readFileSync8 } from "node:fs";
486
- import os2 from "node:os";
552
+ import { readFileSync as readFileSync9 } from "node:fs";
487
553
  function resolveConfiguredAccountId(opts = {}) {
488
554
  return resolveSessionOwnerUncached({
489
555
  env: opts.env ?? process.env,
490
- homeDir: opts.homeDir ?? os2.homedir(),
491
- readFile: opts.readFile ?? ((p) => readFileSync8(p, "utf-8"))
556
+ homeDir: opts.homeDir ?? heyspexHome2(),
557
+ readFile: opts.readFile ?? ((p) => readFileSync9(p, "utf-8"))
492
558
  });
493
559
  }
494
560
  function resolveCloudAccountIdOrRefuse(opts = {}) {
@@ -521,6 +587,7 @@ var CLOUD_ACCOUNT_ID_REQUIRED_MESSAGE;
521
587
  var init_vault_workspace_shared = __esm({
522
588
  "spex-console/src/lib/cli/vault-workspace-shared.ts"() {
523
589
  "use strict";
590
+ init_heyspex_home2();
524
591
  init_session_owner();
525
592
  init_default_workspace();
526
593
  CLOUD_ACCOUNT_ID_REQUIRED_MESSAGE = "refusing -- no signed-in account (no HS_ACCOUNT_ID env var, and no account_id: in ~/.heyspex/config.yaml). Run `hs login` first; nothing was written.";
@@ -974,12 +1041,12 @@ var require_stringify = __commonJS({
974
1041
  throw new TypeError('expected "' + language + '.stringify" to be a function');
975
1042
  }
976
1043
  data = Object.assign({}, file.data, data);
977
- const open = opts.delimiters[0];
1044
+ const open2 = opts.delimiters[0];
978
1045
  const close = opts.delimiters[1];
979
1046
  const matter4 = engine.stringify(data, options2).trim();
980
1047
  let buf = "";
981
1048
  if (matter4 !== "{}") {
982
- buf = newline(open) + newline(matter4) + newline(close);
1049
+ buf = newline(open2) + newline(matter4) + newline(close);
983
1050
  }
984
1051
  if (typeof file.excerpt === "string" && file.excerpt !== "") {
985
1052
  if (str2.indexOf(file.excerpt.trim()) === -1) {
@@ -1007,11 +1074,11 @@ var require_excerpt = __commonJS({
1007
1074
  if (typeof opts.excerpt === "function") {
1008
1075
  return opts.excerpt(file, opts);
1009
1076
  }
1010
- const sep = file.data.excerpt_separator || opts.excerpt_separator;
1011
- if (sep == null && (opts.excerpt === false || opts.excerpt == null)) {
1077
+ const sep2 = file.data.excerpt_separator || opts.excerpt_separator;
1078
+ if (sep2 == null && (opts.excerpt === false || opts.excerpt == null)) {
1012
1079
  return file;
1013
1080
  }
1014
- const delimiter2 = typeof opts.excerpt === "string" ? opts.excerpt : sep || opts.delimiters[0];
1081
+ const delimiter2 = typeof opts.excerpt === "string" ? opts.excerpt : sep2 || opts.delimiters[0];
1015
1082
  const idx = file.content.indexOf(delimiter2);
1016
1083
  if (idx !== -1) {
1017
1084
  file.excerpt = file.content.slice(0, idx);
@@ -1076,7 +1143,7 @@ var require_parse = __commonJS({
1076
1143
  var require_gray_matter = __commonJS({
1077
1144
  "node_modules/gray-matter/index.js"(exports2, module2) {
1078
1145
  "use strict";
1079
- var fs9 = __require("fs");
1146
+ var fs12 = __require("fs");
1080
1147
  var sections = require_section_matter();
1081
1148
  var defaults = require_defaults();
1082
1149
  var stringify = require_stringify();
@@ -1103,18 +1170,18 @@ var require_gray_matter = __commonJS({
1103
1170
  }
1104
1171
  function parseMatter(file, options2) {
1105
1172
  const opts = defaults(options2);
1106
- const open = opts.delimiters[0];
1173
+ const open2 = opts.delimiters[0];
1107
1174
  const close = "\n" + opts.delimiters[1];
1108
1175
  let str2 = file.content;
1109
1176
  if (opts.language) {
1110
1177
  file.language = opts.language;
1111
1178
  }
1112
- const openLen = open.length;
1113
- if (!utils.startsWith(str2, open, openLen)) {
1179
+ const openLen = open2.length;
1180
+ if (!utils.startsWith(str2, open2, openLen)) {
1114
1181
  excerpt(file, opts);
1115
1182
  return file;
1116
1183
  }
1117
- if (str2.charAt(openLen) === open.slice(-1)) {
1184
+ if (str2.charAt(openLen) === open2.slice(-1)) {
1118
1185
  return file;
1119
1186
  }
1120
1187
  str2 = str2.slice(openLen);
@@ -1160,7 +1227,7 @@ var require_gray_matter = __commonJS({
1160
1227
  return stringify(file, data, options2);
1161
1228
  };
1162
1229
  matter4.read = function(filepath, options2) {
1163
- const str2 = fs9.readFileSync(filepath, "utf8");
1230
+ const str2 = fs12.readFileSync(filepath, "utf8");
1164
1231
  const file = matter4(str2, options2);
1165
1232
  file.path = filepath;
1166
1233
  return file;
@@ -1170,9 +1237,9 @@ var require_gray_matter = __commonJS({
1170
1237
  };
1171
1238
  matter4.language = function(str2, options2) {
1172
1239
  const opts = defaults(options2);
1173
- const open = opts.delimiters[0];
1240
+ const open2 = opts.delimiters[0];
1174
1241
  if (matter4.test(str2)) {
1175
- str2 = str2.slice(open.length);
1242
+ str2 = str2.slice(open2.length);
1176
1243
  }
1177
1244
  const language = str2.slice(0, str2.search(/\r?\n/));
1178
1245
  return {
@@ -1214,7 +1281,7 @@ var init_diagnose_skeleton = __esm({
1214
1281
  });
1215
1282
 
1216
1283
  // dist/cli/._sql-orm-inert.cli-shim.mjs
1217
- var inert, customType, sql, and, eq, isNull, inArray, bigint, bigserial, boolean, foreignKey, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex;
1284
+ var inert, customType, sql, and, eq, isNull, desc, gt, inArray, bigint, bigserial, boolean, foreignKey, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex;
1218
1285
  var init_sql_orm_inert_cli_shim = __esm({
1219
1286
  "dist/cli/._sql-orm-inert.cli-shim.mjs"() {
1220
1287
  "use strict";
@@ -1237,6 +1304,8 @@ var init_sql_orm_inert_cli_shim = __esm({
1237
1304
  and = inert;
1238
1305
  eq = inert;
1239
1306
  isNull = inert;
1307
+ desc = inert;
1308
+ gt = inert;
1240
1309
  inArray = inert;
1241
1310
  bigint = inert;
1242
1311
  bigserial = inert;
@@ -1265,7 +1334,7 @@ var init_interfaces = __esm({
1265
1334
  });
1266
1335
 
1267
1336
  // spex-console/src/lib/db/schema/postgres.ts
1268
- var edgeTypeEnum, validationSeverityEnum, outboxOpEnum, tsvector, specs, tags, edges, wikilinks, specLinks, aliases, validationIssues, indexMeta, frontmatterFields, eventOutbox, captureSessions, handoffContext, claimEventKindEnum, activeClaimStatusEnum, pendingReasonEnum, focusEventKindEnum, specEventKindEnum, focusPin, captureFocusPin, captureFocusSelection, focusSelection, focusSkips, activeClaims, claimWaiters, fileClaims, claimEvents, focusEvents, focusAreas, focusAreasEvents, specEvents, rateLimitBuckets, userVaults, milestoneSnapshots, modelRegistryEntries, launchIntentStatusEnum, launchIntents, companionRelayPresence, companionSettingsStore, consoleSettingsStore, wipLedger, wipLedgerArchive, gatherResults, agentSessions, usageEvents, streamEvents, jobStreams, resolutionStatusEnum, resolutionFocusPin, resolutionLedger, workspaces, vaults, accountSettings, vaultGrants, pendingVaultGrants, githubInstallations, fleetMachines, fleetTasks, fleetResults, userApiKeys, apiKeys, mcpSessions, pairingCodes, companionSessions, inviteRequests, notifications, webhookIngestions, specSettlements;
1337
+ var edgeTypeEnum, validationSeverityEnum, outboxOpEnum, tsvector, specs, tags, edges, wikilinks, specLinks, aliases, validationIssues, indexMeta, frontmatterFields, eventOutbox, captureSessions, handoffContext, claimEventKindEnum, activeClaimStatusEnum, pendingReasonEnum, focusEventKindEnum, specEventKindEnum, focusPin, captureFocusPin, captureFocusSelection, focusSelection, focusSkips, activeClaims, claimWaiters, fileClaims, claimEvents, focusEvents, focusAreas, focusAreasEvents, specEvents, rateLimitBuckets, userVaults, milestoneSnapshots, modelRegistryEntries, launchIntentStatusEnum, launchIntents, companionRelayPresence, companionSettingsStore, consoleSettingsStore, wipLedger, wipLedgerArchive, gatherResults, agentSessions, usageEvents, streamEvents, jobStreams, resolutionStatusEnum, resolutionFocusPin, resolutionLedger, workspaces, vaults, accountSettings, vaultGrants, pendingVaultGrants, githubInstallations, fleetMachines, fleetTasks, fleetResults, userApiKeys, apiKeys, mcpSessions, pairingCodes, companionSessions, inviteRequests, notifications, webhookIngestions, specSettlements, sessionBranchDeclarations;
1269
1338
  var init_postgres = __esm({
1270
1339
  "spex-console/src/lib/db/schema/postgres.ts"() {
1271
1340
  "use strict";
@@ -1707,7 +1776,13 @@ var init_postgres = __esm({
1707
1776
  "priority_change",
1708
1777
  "parent_change",
1709
1778
  "claim_acquired",
1710
- "claim_released"
1779
+ "claim_released",
1780
+ // Collaborator write audit (HS-FWB6, migration 0077).
1781
+ "spec_created",
1782
+ "spec_edited",
1783
+ "spec_deleted",
1784
+ "capture_written",
1785
+ "wip_recorded"
1711
1786
  ]);
1712
1787
  focusPin = pgTable(
1713
1788
  "focus_pin",
@@ -2900,18 +2975,50 @@ var init_postgres = __esm({
2900
2975
  // `owner/name`
2901
2976
  branch: text("branch").notNull(),
2902
2977
  commitSha: text("commit_sha").notNull(),
2903
- settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`)
2978
+ settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`),
2979
+ // The pull request that carries this settlement
2980
+ // ([[HS-9D25-CWPX-EPIC-branch-lifecycle-open-a-pull-request]]). NULL until
2981
+ // the branch has gone quiet and a PR was opened or found; a write made
2982
+ // after its PR merged stays NULL, which is what triggers the next PR.
2983
+ pullRequestNumber: integer("pull_request_number"),
2984
+ pullRequestUrl: text("pull_request_url")
2904
2985
  },
2905
2986
  (table) => [
2906
2987
  // "latest settlement for this spec" — the read path's only query.
2907
- index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc())
2988
+ index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc()),
2989
+ // The PR sweep's only query: branches with settlements not yet in a PR.
2990
+ index("spec_settlements_unproposed_idx").on(table.vaultId, table.repo, table.branch).where(sql`${table.pullRequestNumber} IS NULL`)
2991
+ ]
2992
+ );
2993
+ sessionBranchDeclarations = pgTable(
2994
+ "session_branch_declarations",
2995
+ {
2996
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
2997
+ vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
2998
+ actorKey: text("actor_key").notNull(),
2999
+ sessionId: text("session_id").notNull(),
3000
+ branch: text("branch").notNull(),
3001
+ declaredAt: timestamp("declared_at", { withTimezone: true }).notNull().default(sql`now()`),
3002
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull()
3003
+ },
3004
+ (table) => [
3005
+ uniqueIndex("session_branch_declarations_uq").on(
3006
+ table.vaultId,
3007
+ table.actorKey,
3008
+ table.sessionId
3009
+ ),
3010
+ index("session_branch_declarations_lookup_idx").on(
3011
+ table.vaultId,
3012
+ table.actorKey,
3013
+ table.declaredAt.desc()
3014
+ )
2908
3015
  ]
2909
3016
  );
2910
3017
  }
2911
3018
  });
2912
3019
 
2913
3020
  // spex-console/src/lib/api-key-service.ts
2914
- import { createHash, randomBytes, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
3021
+ import { createHash as createHash2, randomBytes, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
2915
3022
  var DEFAULT_ROTATION_GRACE_WINDOW_MS;
2916
3023
  var init_api_key_service = __esm({
2917
3024
  "spex-console/src/lib/api-key-service.ts"() {
@@ -3070,7 +3177,7 @@ var init_satellite = __esm({
3070
3177
  });
3071
3178
 
3072
3179
  // spex-console/src/lib/vault-resolve/index.ts
3073
- import { existsSync as existsSync4, readFileSync as readFileSync10 } from "node:fs";
3180
+ import { existsSync as existsSync5, readFileSync as readFileSync11 } from "node:fs";
3074
3181
  import { tmpdir as tmpdir2 } from "node:os";
3075
3182
  import path4 from "node:path";
3076
3183
  var init_vault_resolve2 = __esm({
@@ -3109,7 +3216,7 @@ var init_resolve_status = __esm({
3109
3216
  });
3110
3217
 
3111
3218
  // spex-console/src/lib/vault-resolve/active.ts
3112
- import { existsSync as existsSync5 } from "node:fs";
3219
+ import { existsSync as existsSync6 } from "node:fs";
3113
3220
  import path5 from "node:path";
3114
3221
  function fallbackSpecsDir() {
3115
3222
  const override = process.env.HEYSPEX_VAULT_DIR;
@@ -3142,7 +3249,7 @@ var init_active = __esm({
3142
3249
  });
3143
3250
 
3144
3251
  // spex-console/src/lib/control-plane/statusline-focus-cache.ts
3145
- import fs2 from "node:fs";
3252
+ import fs4 from "node:fs";
3146
3253
  import path6 from "node:path";
3147
3254
  var init_statusline_focus_cache = __esm({
3148
3255
  "spex-console/src/lib/control-plane/statusline-focus-cache.ts"() {
@@ -3462,8 +3569,8 @@ function singleFlightDebounce(fn, delayMs) {
3462
3569
  })();
3463
3570
  }
3464
3571
  return function debounced() {
3465
- return new Promise((resolve4, reject) => {
3466
- const resolver = { resolve: resolve4, reject };
3572
+ return new Promise((resolve7, reject) => {
3573
+ const resolver = { resolve: resolve7, reject };
3467
3574
  if (inFlight) {
3468
3575
  if (queuedResolvers) {
3469
3576
  queuedResolvers.push(resolver);
@@ -3756,6 +3863,55 @@ var init_branch_policy = __esm({
3756
3863
  }
3757
3864
  });
3758
3865
 
3866
+ // spex-console/src/lib/git-export/branch-resolver.ts
3867
+ function isValidSessionBranch(name, defaultBranch) {
3868
+ const b = name.trim();
3869
+ if (!b) return { ok: false, reason: "branch name is empty" };
3870
+ if (b === defaultBranch) {
3871
+ return { ok: false, reason: `\`${b}\` is the default branch; the drain never writes it` };
3872
+ }
3873
+ if (PROTECTED_SESSION_BRANCHES.includes(b)) {
3874
+ return { ok: false, reason: `\`${b}\` is a protected ref` };
3875
+ }
3876
+ const hasControlChar = [...b].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127);
3877
+ if (hasControlChar || /[\s~^:?*[\\]/.test(b)) {
3878
+ return { ok: false, reason: "branch name contains characters git refs do not allow" };
3879
+ }
3880
+ if (b.includes("..") || b.includes("@{") || b.includes("//")) {
3881
+ return { ok: false, reason: "branch name contains a forbidden sequence (.. @{ //)" };
3882
+ }
3883
+ if (b.startsWith("/") || b.endsWith("/") || b.startsWith("-") || b.endsWith(".") || b === "@") {
3884
+ return { ok: false, reason: "branch name has a forbidden leading or trailing character" };
3885
+ }
3886
+ if (b.split("/").some((seg) => seg.startsWith(".") || seg.endsWith(".lock"))) {
3887
+ return {
3888
+ ok: false,
3889
+ reason: "branch name has a path segment starting with . or ending in .lock"
3890
+ };
3891
+ }
3892
+ return { ok: true };
3893
+ }
3894
+ function resolveSessionTargetBranch(input) {
3895
+ const dated = resolveTargetBranch({
3896
+ user: input.user,
3897
+ date: input.date,
3898
+ defaultBranch: input.defaultBranch
3899
+ });
3900
+ const declared = input.sessionBranch?.trim();
3901
+ if (!declared) return { branch: dated, source: "dated" };
3902
+ const check = isValidSessionBranch(declared, input.defaultBranch);
3903
+ if (!check.ok) return { branch: dated, source: "dated", reason: check.reason };
3904
+ return { branch: declared, source: "session" };
3905
+ }
3906
+ var PROTECTED_SESSION_BRANCHES;
3907
+ var init_branch_resolver = __esm({
3908
+ "spex-console/src/lib/git-export/branch-resolver.ts"() {
3909
+ "use strict";
3910
+ init_branch_policy();
3911
+ PROTECTED_SESSION_BRANCHES = ["main", "master", "HEAD", "trunk"];
3912
+ }
3913
+ });
3914
+
3759
3915
  // spex-console/src/lib/git-export/config.ts
3760
3916
  function parseGithubRemote(githubRemote) {
3761
3917
  const match = githubRemote.match(/^([^/\s]+)\/([^/\s]+)$/);
@@ -3820,6 +3976,9 @@ ${fm}
3820
3976
  ---
3821
3977
  ${body}`;
3822
3978
  }
3979
+ function parseSpecMarkdown(raw) {
3980
+ return (0, import_gray_matter.default)(raw);
3981
+ }
3823
3982
  var import_gray_matter, YAML_STRINGIFY_OPTIONS;
3824
3983
  var init_vault_frontmatter = __esm({
3825
3984
  "spex-console/src/lib/vault-frontmatter.ts"() {
@@ -3843,10 +4002,10 @@ function trashPathFor(relPath, now) {
3843
4002
  const normalized = relPath.split("\\").join("/");
3844
4003
  const slashIdx = normalized.lastIndexOf("/");
3845
4004
  const folder = slashIdx === -1 ? "" : normalized.slice(0, slashIdx);
3846
- const basename = slashIdx === -1 ? normalized : normalized.slice(slashIdx + 1);
4005
+ const basename4 = slashIdx === -1 ? normalized : normalized.slice(slashIdx + 1);
3847
4006
  const stamp = now.toISOString().replace(/[:.]/g, "-");
3848
4007
  const trashDir = folder ? `${folder}/.trash` : ".trash";
3849
- return `${trashDir}/${stamp}-${basename}`;
4008
+ return `${trashDir}/${stamp}-${basename4}`;
3850
4009
  }
3851
4010
  function materializeSpecMarkdown(bundle) {
3852
4011
  return stringifySpecMarkdown(bundle.bodyText, bundle.frontmatter);
@@ -4155,6 +4314,39 @@ var init_github_client = __esm({
4155
4314
  }
4156
4315
  });
4157
4316
 
4317
+ // spex-console/src/lib/git-export/session-branch-store.ts
4318
+ async function readActiveDeclaration(db, vaultId, actorKey, now) {
4319
+ const rows = await db.select().from(sessionBranchDeclarations).where(
4320
+ and(
4321
+ eq(sessionBranchDeclarations.vaultId, vaultId),
4322
+ eq(sessionBranchDeclarations.actorKey, actorKey),
4323
+ gt(sessionBranchDeclarations.expiresAt, now)
4324
+ )
4325
+ ).orderBy(desc(sessionBranchDeclarations.declaredAt)).limit(1);
4326
+ const row = rows[0];
4327
+ if (!row) return null;
4328
+ return {
4329
+ vaultId: row.vaultId,
4330
+ actorKey: row.actorKey,
4331
+ sessionId: row.sessionId,
4332
+ branch: row.branch,
4333
+ declaredAt: row.declaredAt.toISOString(),
4334
+ expiresAt: row.expiresAt.toISOString()
4335
+ };
4336
+ }
4337
+ async function readActiveSessionBranch(db, vaultId, actorKey, now) {
4338
+ return (await readActiveDeclaration(db, vaultId, actorKey, now))?.branch ?? null;
4339
+ }
4340
+ var SESSION_BRANCH_TTL_MS;
4341
+ var init_session_branch_store = __esm({
4342
+ "spex-console/src/lib/git-export/session-branch-store.ts"() {
4343
+ "use strict";
4344
+ init_sql_orm_inert_cli_shim();
4345
+ init_postgres();
4346
+ SESSION_BRANCH_TTL_MS = 12 * 60 * 60 * 1e3;
4347
+ }
4348
+ });
4349
+
4158
4350
  // spex-console/src/lib/git-export/settlement-store.ts
4159
4351
  async function recordSettlement(tx, input) {
4160
4352
  await tx.insert(specSettlements).values({
@@ -4385,6 +4577,7 @@ async function processVault(db, vaultId, env, now, deps, batchLimit, jobTimeoutM
4385
4577
  result: { status: "error", message: target.message }
4386
4578
  };
4387
4579
  }
4580
+ const lookupSessionBranch = deps.lookupSessionBranch ?? ((vId, actor) => readActiveSessionBranch(tx, vId, actor, now()));
4388
4581
  const { config, tokenSource, installationId } = target;
4389
4582
  const provider = createPatCommitProvider(() => config.token);
4390
4583
  let pendingSettlement = null;
@@ -4398,11 +4591,18 @@ async function processVault(db, vaultId, env, now, deps, batchLimit, jobTimeoutM
4398
4591
  fetchBundles: (specIds) => fetchDrainBundles(tx, vaultId, specIds),
4399
4592
  resolveActorIdentity,
4400
4593
  commit: async (plan, message, coAuthor) => {
4401
- const targetBranch = resolveTargetBranch({
4594
+ let sessionBranch = null;
4595
+ try {
4596
+ sessionBranch = await lookupSessionBranch(vaultId, coAuthor.name);
4597
+ } catch {
4598
+ sessionBranch = null;
4599
+ }
4600
+ const targetBranch = resolveSessionTargetBranch({
4402
4601
  user: coAuthor.name,
4403
4602
  date: now(),
4404
- defaultBranch: config.branch
4405
- });
4603
+ defaultBranch: config.branch,
4604
+ sessionBranch
4605
+ }).branch;
4406
4606
  const outcome = await commitPlanToGitHub(
4407
4607
  provider,
4408
4608
  config,
@@ -4543,12 +4743,13 @@ var init_drain_runner = __esm({
4543
4743
  init_postgres();
4544
4744
  init_github_app();
4545
4745
  init_advisory_lock();
4546
- init_branch_policy();
4746
+ init_branch_resolver();
4547
4747
  init_config();
4548
4748
  init_drain();
4549
4749
  init_drain_skip();
4550
4750
  init_export_status();
4551
4751
  init_github_client();
4752
+ init_session_branch_store();
4552
4753
  init_settlement_store();
4553
4754
  BOT_NAME = "heyspex-bot";
4554
4755
  BOT_EMAIL = "bot@heyspex.app";
@@ -4806,7 +5007,7 @@ var init_diagnose_spawned = __esm({
4806
5007
  });
4807
5008
 
4808
5009
  // spex-console/src/lib/guardrail-naming.ts
4809
- import fs3 from "node:fs";
5010
+ import fs5 from "node:fs";
4810
5011
  import path7 from "node:path";
4811
5012
  var init_guardrail_naming = __esm({
4812
5013
  "spex-console/src/lib/guardrail-naming.ts"() {
@@ -4815,8 +5016,8 @@ var init_guardrail_naming = __esm({
4815
5016
  });
4816
5017
 
4817
5018
  // spex-console/src/lib/indexer/parser.ts
4818
- import { createHash as createHash2 } from "node:crypto";
4819
- import { promises as fs4 } from "node:fs";
5019
+ import { createHash as createHash3 } from "node:crypto";
5020
+ import { promises as fs6 } from "node:fs";
4820
5021
  import path8 from "node:path";
4821
5022
  var import_gray_matter2;
4822
5023
  var init_parser = __esm({
@@ -4923,7 +5124,7 @@ var init_scoped_context = __esm({
4923
5124
 
4924
5125
  // spex-console/src/lib/indexer/indexer-service.ts
4925
5126
  import { spawnSync } from "node:child_process";
4926
- import { promises as fs5 } from "node:fs";
5127
+ import { promises as fs7 } from "node:fs";
4927
5128
  import path9 from "node:path";
4928
5129
  var init_indexer_service = __esm({
4929
5130
  "spex-console/src/lib/indexer/indexer-service.ts"() {
@@ -4942,7 +5143,7 @@ var init_indexer_service = __esm({
4942
5143
  });
4943
5144
 
4944
5145
  // spex-console/src/lib/indexer/outbox-index-feeder.ts
4945
- import { promises as fs6 } from "node:fs";
5146
+ import { promises as fs8 } from "node:fs";
4946
5147
  import path10 from "node:path";
4947
5148
  var init_outbox_index_feeder = __esm({
4948
5149
  "spex-console/src/lib/indexer/outbox-index-feeder.ts"() {
@@ -5322,8 +5523,8 @@ var init_search_body_hit = __esm({
5322
5523
 
5323
5524
  // spex-console/src/lib/vault.ts
5324
5525
  import { spawn } from "node:child_process";
5325
- import { readdirSync, readFileSync as readFileSync11, statSync } from "node:fs";
5326
- import { promises as fs7 } from "node:fs";
5526
+ import { readdirSync as readdirSync2, readFileSync as readFileSync12, statSync } from "node:fs";
5527
+ import { promises as fs9 } from "node:fs";
5327
5528
  import path13 from "node:path";
5328
5529
  import yaml3 from "js-yaml";
5329
5530
  function normalizeDateField(value) {
@@ -5365,18 +5566,18 @@ function extractBodyLinks(body) {
5365
5566
  return links;
5366
5567
  }
5367
5568
  function toSummary(absPath, root, fm, body) {
5368
- const basename = path13.basename(absPath).replace(/\.md$/i, "");
5569
+ const basename4 = path13.basename(absPath).replace(/\.md$/i, "");
5369
5570
  const relPath = path13.relative(root, absPath).split(path13.sep).join("/");
5370
5571
  return {
5371
- id: typeof fm.id === "string" && fm.id ? fm.id : basename,
5372
- name: typeof fm.name === "string" && fm.name ? fm.name : basename,
5572
+ id: typeof fm.id === "string" && fm.id ? fm.id : basename4,
5573
+ name: typeof fm.name === "string" && fm.name ? fm.name : basename4,
5373
5574
  type: typeof fm.type === "string" ? fm.type : "unknown",
5374
5575
  status: typeof fm.status === "string" ? fm.status : "unknown",
5375
5576
  effort: typeof fm.effort === "number" ? fm.effort : null,
5376
5577
  owner: typeof fm.owner === "string" ? fm.owner : null,
5377
5578
  tags: Array.isArray(fm.tags) ? fm.tags.map(String) : [],
5378
5579
  archived: fm.archived === true,
5379
- basename,
5580
+ basename: basename4,
5380
5581
  relPath,
5381
5582
  links: extractBodyLinks(body),
5382
5583
  created: normalizeDateField(fm.created),
@@ -5462,9 +5663,9 @@ function invalidateVaultCacheForFile(absPath) {
5462
5663
  else evictProcessVaultHotRows();
5463
5664
  }
5464
5665
  function specIdFromAbsPath(absPath) {
5465
- const basename = path13.basename(absPath).replace(/\.md$/i, "");
5466
- if (!basename) return null;
5467
- const parsed = parseSpecId(basename);
5666
+ const basename4 = path13.basename(absPath).replace(/\.md$/i, "");
5667
+ if (!basename4) return null;
5668
+ const parsed = parseSpecId(basename4);
5468
5669
  if (parsed?.era === 3 && parsed.fileKey) return `HS-${parsed.fileKey.toUpperCase()}`;
5469
5670
  return null;
5470
5671
  }
@@ -5555,8 +5756,8 @@ async function writeSpecBytesToVault(relPath, content) {
5555
5756
  try {
5556
5757
  const absPath = path13.join(vaultRoot(), relPath);
5557
5758
  const dir = path13.dirname(absPath);
5558
- await fs7.mkdir(dir, { recursive: true });
5559
- await fs7.writeFile(absPath, content, "utf8");
5759
+ await fs9.mkdir(dir, { recursive: true });
5760
+ await fs9.writeFile(absPath, content, "utf8");
5560
5761
  } catch (err) {
5561
5762
  const message = err instanceof Error ? err.message : String(err);
5562
5763
  if (isLocalVaultMode()) {
@@ -5568,7 +5769,7 @@ async function writeSpecBytesToVault(relPath, content) {
5568
5769
  async function parseLoadedSpec(absPath, root) {
5569
5770
  let raw;
5570
5771
  try {
5571
- raw = await fs7.readFile(absPath, "utf8");
5772
+ raw = await fs9.readFile(absPath, "utf8");
5572
5773
  } catch {
5573
5774
  return null;
5574
5775
  }
@@ -5718,45 +5919,1761 @@ var init_vault = __esm({
5718
5919
  }
5719
5920
  });
5720
5921
 
5721
- // spex-console/bin/hs-vault-pull.ts
5722
- import { readFileSync as readFileSync12 } from "node:fs";
5723
- import path14 from "node:path";
5724
- import process4 from "node:process";
5922
+ // node_modules/readdirp/esm/index.js
5923
+ import { stat, lstat, readdir, realpath } from "node:fs/promises";
5924
+ import { Readable } from "node:stream";
5925
+ import { resolve as presolve, relative as prelative, join as pjoin, sep as psep } from "node:path";
5926
+ function readdirp(root, options2 = {}) {
5927
+ let type = options2.entryType || options2.type;
5928
+ if (type === "both")
5929
+ type = EntryTypes.FILE_DIR_TYPE;
5930
+ if (type)
5931
+ options2.type = type;
5932
+ if (!root) {
5933
+ throw new Error("readdirp: root argument is required. Usage: readdirp(root, options)");
5934
+ } else if (typeof root !== "string") {
5935
+ throw new TypeError("readdirp: root argument must be a string. Usage: readdirp(root, options)");
5936
+ } else if (type && !ALL_TYPES.includes(type)) {
5937
+ throw new Error(`readdirp: Invalid type passed. Use one of ${ALL_TYPES.join(", ")}`);
5938
+ }
5939
+ options2.root = root;
5940
+ return new ReaddirpStream(options2);
5941
+ }
5942
+ var EntryTypes, defaultOptions, RECURSIVE_ERROR_CODE, NORMAL_FLOW_ERRORS, ALL_TYPES, DIR_TYPES, FILE_TYPES, isNormalFlowError, wantBigintFsStats, emptyFn, normalizeFilter, ReaddirpStream;
5943
+ var init_esm = __esm({
5944
+ "node_modules/readdirp/esm/index.js"() {
5945
+ "use strict";
5946
+ EntryTypes = {
5947
+ FILE_TYPE: "files",
5948
+ DIR_TYPE: "directories",
5949
+ FILE_DIR_TYPE: "files_directories",
5950
+ EVERYTHING_TYPE: "all"
5951
+ };
5952
+ defaultOptions = {
5953
+ root: ".",
5954
+ fileFilter: (_entryInfo) => true,
5955
+ directoryFilter: (_entryInfo) => true,
5956
+ type: EntryTypes.FILE_TYPE,
5957
+ lstat: false,
5958
+ depth: 2147483648,
5959
+ alwaysStat: false,
5960
+ highWaterMark: 4096
5961
+ };
5962
+ Object.freeze(defaultOptions);
5963
+ RECURSIVE_ERROR_CODE = "READDIRP_RECURSIVE_ERROR";
5964
+ NORMAL_FLOW_ERRORS = /* @__PURE__ */ new Set(["ENOENT", "EPERM", "EACCES", "ELOOP", RECURSIVE_ERROR_CODE]);
5965
+ ALL_TYPES = [
5966
+ EntryTypes.DIR_TYPE,
5967
+ EntryTypes.EVERYTHING_TYPE,
5968
+ EntryTypes.FILE_DIR_TYPE,
5969
+ EntryTypes.FILE_TYPE
5970
+ ];
5971
+ DIR_TYPES = /* @__PURE__ */ new Set([
5972
+ EntryTypes.DIR_TYPE,
5973
+ EntryTypes.EVERYTHING_TYPE,
5974
+ EntryTypes.FILE_DIR_TYPE
5975
+ ]);
5976
+ FILE_TYPES = /* @__PURE__ */ new Set([
5977
+ EntryTypes.EVERYTHING_TYPE,
5978
+ EntryTypes.FILE_DIR_TYPE,
5979
+ EntryTypes.FILE_TYPE
5980
+ ]);
5981
+ isNormalFlowError = (error) => NORMAL_FLOW_ERRORS.has(error.code);
5982
+ wantBigintFsStats = process.platform === "win32";
5983
+ emptyFn = (_entryInfo) => true;
5984
+ normalizeFilter = (filter) => {
5985
+ if (filter === void 0)
5986
+ return emptyFn;
5987
+ if (typeof filter === "function")
5988
+ return filter;
5989
+ if (typeof filter === "string") {
5990
+ const fl = filter.trim();
5991
+ return (entry) => entry.basename === fl;
5992
+ }
5993
+ if (Array.isArray(filter)) {
5994
+ const trItems = filter.map((item) => item.trim());
5995
+ return (entry) => trItems.some((f) => entry.basename === f);
5996
+ }
5997
+ return emptyFn;
5998
+ };
5999
+ ReaddirpStream = class extends Readable {
6000
+ constructor(options2 = {}) {
6001
+ super({
6002
+ objectMode: true,
6003
+ autoDestroy: true,
6004
+ highWaterMark: options2.highWaterMark
6005
+ });
6006
+ const opts = { ...defaultOptions, ...options2 };
6007
+ const { root, type } = opts;
6008
+ this._fileFilter = normalizeFilter(opts.fileFilter);
6009
+ this._directoryFilter = normalizeFilter(opts.directoryFilter);
6010
+ const statMethod = opts.lstat ? lstat : stat;
6011
+ if (wantBigintFsStats) {
6012
+ this._stat = (path15) => statMethod(path15, { bigint: true });
6013
+ } else {
6014
+ this._stat = statMethod;
6015
+ }
6016
+ this._maxDepth = opts.depth ?? defaultOptions.depth;
6017
+ this._wantsDir = type ? DIR_TYPES.has(type) : false;
6018
+ this._wantsFile = type ? FILE_TYPES.has(type) : false;
6019
+ this._wantsEverything = type === EntryTypes.EVERYTHING_TYPE;
6020
+ this._root = presolve(root);
6021
+ this._isDirent = !opts.alwaysStat;
6022
+ this._statsProp = this._isDirent ? "dirent" : "stats";
6023
+ this._rdOptions = { encoding: "utf8", withFileTypes: this._isDirent };
6024
+ this.parents = [this._exploreDir(root, 1)];
6025
+ this.reading = false;
6026
+ this.parent = void 0;
6027
+ }
6028
+ async _read(batch) {
6029
+ if (this.reading)
6030
+ return;
6031
+ this.reading = true;
6032
+ try {
6033
+ while (!this.destroyed && batch > 0) {
6034
+ const par = this.parent;
6035
+ const fil = par && par.files;
6036
+ if (fil && fil.length > 0) {
6037
+ const { path: path15, depth } = par;
6038
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path15));
6039
+ const awaited = await Promise.all(slice);
6040
+ for (const entry of awaited) {
6041
+ if (!entry)
6042
+ continue;
6043
+ if (this.destroyed)
6044
+ return;
6045
+ const entryType = await this._getEntryType(entry);
6046
+ if (entryType === "directory" && this._directoryFilter(entry)) {
6047
+ if (depth <= this._maxDepth) {
6048
+ this.parents.push(this._exploreDir(entry.fullPath, depth + 1));
6049
+ }
6050
+ if (this._wantsDir) {
6051
+ this.push(entry);
6052
+ batch--;
6053
+ }
6054
+ } else if ((entryType === "file" || this._includeAsFile(entry)) && this._fileFilter(entry)) {
6055
+ if (this._wantsFile) {
6056
+ this.push(entry);
6057
+ batch--;
6058
+ }
6059
+ }
6060
+ }
6061
+ } else {
6062
+ const parent = this.parents.pop();
6063
+ if (!parent) {
6064
+ this.push(null);
6065
+ break;
6066
+ }
6067
+ this.parent = await parent;
6068
+ if (this.destroyed)
6069
+ return;
6070
+ }
6071
+ }
6072
+ } catch (error) {
6073
+ this.destroy(error);
6074
+ } finally {
6075
+ this.reading = false;
6076
+ }
6077
+ }
6078
+ async _exploreDir(path15, depth) {
6079
+ let files;
6080
+ try {
6081
+ files = await readdir(path15, this._rdOptions);
6082
+ } catch (error) {
6083
+ this._onError(error);
6084
+ }
6085
+ return { files, depth, path: path15 };
6086
+ }
6087
+ async _formatEntry(dirent, path15) {
6088
+ let entry;
6089
+ const basename4 = this._isDirent ? dirent.name : dirent;
6090
+ try {
6091
+ const fullPath = presolve(pjoin(path15, basename4));
6092
+ entry = { path: prelative(this._root, fullPath), fullPath, basename: basename4 };
6093
+ entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
6094
+ } catch (err) {
6095
+ this._onError(err);
6096
+ return;
6097
+ }
6098
+ return entry;
6099
+ }
6100
+ _onError(err) {
6101
+ if (isNormalFlowError(err) && !this.destroyed) {
6102
+ this.emit("warn", err);
6103
+ } else {
6104
+ this.destroy(err);
6105
+ }
6106
+ }
6107
+ async _getEntryType(entry) {
6108
+ if (!entry && this._statsProp in entry) {
6109
+ return "";
6110
+ }
6111
+ const stats = entry[this._statsProp];
6112
+ if (stats.isFile())
6113
+ return "file";
6114
+ if (stats.isDirectory())
6115
+ return "directory";
6116
+ if (stats && stats.isSymbolicLink()) {
6117
+ const full = entry.fullPath;
6118
+ try {
6119
+ const entryRealPath = await realpath(full);
6120
+ const entryRealPathStats = await lstat(entryRealPath);
6121
+ if (entryRealPathStats.isFile()) {
6122
+ return "file";
6123
+ }
6124
+ if (entryRealPathStats.isDirectory()) {
6125
+ const len = entryRealPath.length;
6126
+ if (full.startsWith(entryRealPath) && full.substr(len, 1) === psep) {
6127
+ const recursiveError = new Error(`Circular symlink detected: "${full}" points to "${entryRealPath}"`);
6128
+ recursiveError.code = RECURSIVE_ERROR_CODE;
6129
+ return this._onError(recursiveError);
6130
+ }
6131
+ return "directory";
6132
+ }
6133
+ } catch (error) {
6134
+ this._onError(error);
6135
+ return "";
6136
+ }
6137
+ }
6138
+ }
6139
+ _includeAsFile(entry) {
6140
+ const stats = entry && entry[this._statsProp];
6141
+ return stats && this._wantsEverything && !stats.isDirectory();
6142
+ }
6143
+ };
6144
+ }
6145
+ });
5725
6146
 
5726
- // spex-console/src/lib/cli/control-shared.ts
5727
- init_hs_command();
5728
- init_console_fetch();
5729
- init_credentials();
5730
- init_session_owner();
5731
- init_vault_less_endpoints();
5732
- init_registry_file();
5733
- import path from "node:path";
5734
- var VAULT_ID_HEADER = "x-heyspex-vault-id";
5735
- var ACCOUNT_ID_HEADER = "x-heyspex-account-id";
5736
- function scopeHeadersFor(scope, method, path15) {
5737
- if (scope === "none") {
5738
- if (!isVaultLessEndpoint(method, path15)) {
5739
- throw new Error(
5740
- `controlRequest: scope "none" is only allowed for the vault-less endpoints (${VAULT_LESS_ENDPOINTS.join(", ")}); ${method.toUpperCase()} ${path15} is vault-scoped and must name { accountId, vaultId }.`
5741
- );
6147
+ // node_modules/chokidar/esm/handler.js
6148
+ import { watchFile, unwatchFile, watch as fs_watch } from "fs";
6149
+ import { open, stat as stat2, lstat as lstat2, realpath as fsrealpath } from "fs/promises";
6150
+ import * as sysPath from "path";
6151
+ import { type as osType } from "os";
6152
+ function createFsWatchInstance(path15, options2, listener, errHandler, emitRaw) {
6153
+ const handleEvent = (rawEvent, evPath) => {
6154
+ listener(path15);
6155
+ emitRaw(rawEvent, evPath, { watchedPath: path15 });
6156
+ if (evPath && path15 !== evPath) {
6157
+ fsWatchBroadcast(sysPath.resolve(path15, evPath), KEY_LISTENERS, sysPath.join(path15, evPath));
5742
6158
  }
5743
- return {};
5744
- }
5745
- return { [VAULT_ID_HEADER]: scope.vaultId, [ACCOUNT_ID_HEADER]: scope.accountId };
5746
- }
5747
- function cloudRequestScope(scope) {
5748
- const named = { accountId: scope.accountId, vaultId: scope.vaultId };
5749
- return {
5750
- ...named,
5751
- headersFor: (method, path15) => isVaultLessEndpoint(method, path15) ? {} : scopeHeadersFor(named, method, path15)
5752
6159
  };
6160
+ try {
6161
+ return fs_watch(path15, {
6162
+ persistent: options2.persistent
6163
+ }, handleEvent);
6164
+ } catch (error) {
6165
+ errHandler(error);
6166
+ return void 0;
6167
+ }
5753
6168
  }
6169
+ var STR_DATA, STR_END, STR_CLOSE, EMPTY_FN, pl, isWindows, isMacos, isLinux, isFreeBSD, isIBMi, EVENTS, EV, THROTTLE_MODE_WATCH, statMethods, KEY_LISTENERS, KEY_ERR, KEY_RAW, HANDLER_KEYS, binaryExtensions, isBinaryPath, foreach, addAndConvert, clearItem, delFromSet, isEmptySet, FsWatchInstances, fsWatchBroadcast, setFsWatchListener, FsWatchFileInstances, setFsWatchFileListener, NodeFsHandler;
6170
+ var init_handler = __esm({
6171
+ "node_modules/chokidar/esm/handler.js"() {
6172
+ "use strict";
6173
+ STR_DATA = "data";
6174
+ STR_END = "end";
6175
+ STR_CLOSE = "close";
6176
+ EMPTY_FN = () => {
6177
+ };
6178
+ pl = process.platform;
6179
+ isWindows = pl === "win32";
6180
+ isMacos = pl === "darwin";
6181
+ isLinux = pl === "linux";
6182
+ isFreeBSD = pl === "freebsd";
6183
+ isIBMi = osType() === "OS400";
6184
+ EVENTS = {
6185
+ ALL: "all",
6186
+ READY: "ready",
6187
+ ADD: "add",
6188
+ CHANGE: "change",
6189
+ ADD_DIR: "addDir",
6190
+ UNLINK: "unlink",
6191
+ UNLINK_DIR: "unlinkDir",
6192
+ RAW: "raw",
6193
+ ERROR: "error"
6194
+ };
6195
+ EV = EVENTS;
6196
+ THROTTLE_MODE_WATCH = "watch";
6197
+ statMethods = { lstat: lstat2, stat: stat2 };
6198
+ KEY_LISTENERS = "listeners";
6199
+ KEY_ERR = "errHandlers";
6200
+ KEY_RAW = "rawEmitters";
6201
+ HANDLER_KEYS = [KEY_LISTENERS, KEY_ERR, KEY_RAW];
6202
+ binaryExtensions = /* @__PURE__ */ new Set([
6203
+ "3dm",
6204
+ "3ds",
6205
+ "3g2",
6206
+ "3gp",
6207
+ "7z",
6208
+ "a",
6209
+ "aac",
6210
+ "adp",
6211
+ "afdesign",
6212
+ "afphoto",
6213
+ "afpub",
6214
+ "ai",
6215
+ "aif",
6216
+ "aiff",
6217
+ "alz",
6218
+ "ape",
6219
+ "apk",
6220
+ "appimage",
6221
+ "ar",
6222
+ "arj",
6223
+ "asf",
6224
+ "au",
6225
+ "avi",
6226
+ "bak",
6227
+ "baml",
6228
+ "bh",
6229
+ "bin",
6230
+ "bk",
6231
+ "bmp",
6232
+ "btif",
6233
+ "bz2",
6234
+ "bzip2",
6235
+ "cab",
6236
+ "caf",
6237
+ "cgm",
6238
+ "class",
6239
+ "cmx",
6240
+ "cpio",
6241
+ "cr2",
6242
+ "cur",
6243
+ "dat",
6244
+ "dcm",
6245
+ "deb",
6246
+ "dex",
6247
+ "djvu",
6248
+ "dll",
6249
+ "dmg",
6250
+ "dng",
6251
+ "doc",
6252
+ "docm",
6253
+ "docx",
6254
+ "dot",
6255
+ "dotm",
6256
+ "dra",
6257
+ "DS_Store",
6258
+ "dsk",
6259
+ "dts",
6260
+ "dtshd",
6261
+ "dvb",
6262
+ "dwg",
6263
+ "dxf",
6264
+ "ecelp4800",
6265
+ "ecelp7470",
6266
+ "ecelp9600",
6267
+ "egg",
6268
+ "eol",
6269
+ "eot",
6270
+ "epub",
6271
+ "exe",
6272
+ "f4v",
6273
+ "fbs",
6274
+ "fh",
6275
+ "fla",
6276
+ "flac",
6277
+ "flatpak",
6278
+ "fli",
6279
+ "flv",
6280
+ "fpx",
6281
+ "fst",
6282
+ "fvt",
6283
+ "g3",
6284
+ "gh",
6285
+ "gif",
6286
+ "graffle",
6287
+ "gz",
6288
+ "gzip",
6289
+ "h261",
6290
+ "h263",
6291
+ "h264",
6292
+ "icns",
6293
+ "ico",
6294
+ "ief",
6295
+ "img",
6296
+ "ipa",
6297
+ "iso",
6298
+ "jar",
6299
+ "jpeg",
6300
+ "jpg",
6301
+ "jpgv",
6302
+ "jpm",
6303
+ "jxr",
6304
+ "key",
6305
+ "ktx",
6306
+ "lha",
6307
+ "lib",
6308
+ "lvp",
6309
+ "lz",
6310
+ "lzh",
6311
+ "lzma",
6312
+ "lzo",
6313
+ "m3u",
6314
+ "m4a",
6315
+ "m4v",
6316
+ "mar",
6317
+ "mdi",
6318
+ "mht",
6319
+ "mid",
6320
+ "midi",
6321
+ "mj2",
6322
+ "mka",
6323
+ "mkv",
6324
+ "mmr",
6325
+ "mng",
6326
+ "mobi",
6327
+ "mov",
6328
+ "movie",
6329
+ "mp3",
6330
+ "mp4",
6331
+ "mp4a",
6332
+ "mpeg",
6333
+ "mpg",
6334
+ "mpga",
6335
+ "mxu",
6336
+ "nef",
6337
+ "npx",
6338
+ "numbers",
6339
+ "nupkg",
6340
+ "o",
6341
+ "odp",
6342
+ "ods",
6343
+ "odt",
6344
+ "oga",
6345
+ "ogg",
6346
+ "ogv",
6347
+ "otf",
6348
+ "ott",
6349
+ "pages",
6350
+ "pbm",
6351
+ "pcx",
6352
+ "pdb",
6353
+ "pdf",
6354
+ "pea",
6355
+ "pgm",
6356
+ "pic",
6357
+ "png",
6358
+ "pnm",
6359
+ "pot",
6360
+ "potm",
6361
+ "potx",
6362
+ "ppa",
6363
+ "ppam",
6364
+ "ppm",
6365
+ "pps",
6366
+ "ppsm",
6367
+ "ppsx",
6368
+ "ppt",
6369
+ "pptm",
6370
+ "pptx",
6371
+ "psd",
6372
+ "pya",
6373
+ "pyc",
6374
+ "pyo",
6375
+ "pyv",
6376
+ "qt",
6377
+ "rar",
6378
+ "ras",
6379
+ "raw",
6380
+ "resources",
6381
+ "rgb",
6382
+ "rip",
6383
+ "rlc",
6384
+ "rmf",
6385
+ "rmvb",
6386
+ "rpm",
6387
+ "rtf",
6388
+ "rz",
6389
+ "s3m",
6390
+ "s7z",
6391
+ "scpt",
6392
+ "sgi",
6393
+ "shar",
6394
+ "snap",
6395
+ "sil",
6396
+ "sketch",
6397
+ "slk",
6398
+ "smv",
6399
+ "snk",
6400
+ "so",
6401
+ "stl",
6402
+ "suo",
6403
+ "sub",
6404
+ "swf",
6405
+ "tar",
6406
+ "tbz",
6407
+ "tbz2",
6408
+ "tga",
6409
+ "tgz",
6410
+ "thmx",
6411
+ "tif",
6412
+ "tiff",
6413
+ "tlz",
6414
+ "ttc",
6415
+ "ttf",
6416
+ "txz",
6417
+ "udf",
6418
+ "uvh",
6419
+ "uvi",
6420
+ "uvm",
6421
+ "uvp",
6422
+ "uvs",
6423
+ "uvu",
6424
+ "viv",
6425
+ "vob",
6426
+ "war",
6427
+ "wav",
6428
+ "wax",
6429
+ "wbmp",
6430
+ "wdp",
6431
+ "weba",
6432
+ "webm",
6433
+ "webp",
6434
+ "whl",
6435
+ "wim",
6436
+ "wm",
6437
+ "wma",
6438
+ "wmv",
6439
+ "wmx",
6440
+ "woff",
6441
+ "woff2",
6442
+ "wrm",
6443
+ "wvx",
6444
+ "xbm",
6445
+ "xif",
6446
+ "xla",
6447
+ "xlam",
6448
+ "xls",
6449
+ "xlsb",
6450
+ "xlsm",
6451
+ "xlsx",
6452
+ "xlt",
6453
+ "xltm",
6454
+ "xltx",
6455
+ "xm",
6456
+ "xmind",
6457
+ "xpi",
6458
+ "xpm",
6459
+ "xwd",
6460
+ "xz",
6461
+ "z",
6462
+ "zip",
6463
+ "zipx"
6464
+ ]);
6465
+ isBinaryPath = (filePath) => binaryExtensions.has(sysPath.extname(filePath).slice(1).toLowerCase());
6466
+ foreach = (val, fn) => {
6467
+ if (val instanceof Set) {
6468
+ val.forEach(fn);
6469
+ } else {
6470
+ fn(val);
6471
+ }
6472
+ };
6473
+ addAndConvert = (main2, prop, item) => {
6474
+ let container = main2[prop];
6475
+ if (!(container instanceof Set)) {
6476
+ main2[prop] = container = /* @__PURE__ */ new Set([container]);
6477
+ }
6478
+ container.add(item);
6479
+ };
6480
+ clearItem = (cont) => (key) => {
6481
+ const set = cont[key];
6482
+ if (set instanceof Set) {
6483
+ set.clear();
6484
+ } else {
6485
+ delete cont[key];
6486
+ }
6487
+ };
6488
+ delFromSet = (main2, prop, item) => {
6489
+ const container = main2[prop];
6490
+ if (container instanceof Set) {
6491
+ container.delete(item);
6492
+ } else if (container === item) {
6493
+ delete main2[prop];
6494
+ }
6495
+ };
6496
+ isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
6497
+ FsWatchInstances = /* @__PURE__ */ new Map();
6498
+ fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
6499
+ const cont = FsWatchInstances.get(fullPath);
6500
+ if (!cont)
6501
+ return;
6502
+ foreach(cont[listenerType], (listener) => {
6503
+ listener(val1, val2, val3);
6504
+ });
6505
+ };
6506
+ setFsWatchListener = (path15, fullPath, options2, handlers) => {
6507
+ const { listener, errHandler, rawEmitter } = handlers;
6508
+ let cont = FsWatchInstances.get(fullPath);
6509
+ let watcher;
6510
+ if (!options2.persistent) {
6511
+ watcher = createFsWatchInstance(path15, options2, listener, errHandler, rawEmitter);
6512
+ if (!watcher)
6513
+ return;
6514
+ return watcher.close.bind(watcher);
6515
+ }
6516
+ if (cont) {
6517
+ addAndConvert(cont, KEY_LISTENERS, listener);
6518
+ addAndConvert(cont, KEY_ERR, errHandler);
6519
+ addAndConvert(cont, KEY_RAW, rawEmitter);
6520
+ } else {
6521
+ watcher = createFsWatchInstance(
6522
+ path15,
6523
+ options2,
6524
+ fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
6525
+ errHandler,
6526
+ // no need to use broadcast here
6527
+ fsWatchBroadcast.bind(null, fullPath, KEY_RAW)
6528
+ );
6529
+ if (!watcher)
6530
+ return;
6531
+ watcher.on(EV.ERROR, async (error) => {
6532
+ const broadcastErr = fsWatchBroadcast.bind(null, fullPath, KEY_ERR);
6533
+ if (cont)
6534
+ cont.watcherUnusable = true;
6535
+ if (isWindows && error.code === "EPERM") {
6536
+ try {
6537
+ const fd = await open(path15, "r");
6538
+ await fd.close();
6539
+ broadcastErr(error);
6540
+ } catch (err) {
6541
+ }
6542
+ } else {
6543
+ broadcastErr(error);
6544
+ }
6545
+ });
6546
+ cont = {
6547
+ listeners: listener,
6548
+ errHandlers: errHandler,
6549
+ rawEmitters: rawEmitter,
6550
+ watcher
6551
+ };
6552
+ FsWatchInstances.set(fullPath, cont);
6553
+ }
6554
+ return () => {
6555
+ delFromSet(cont, KEY_LISTENERS, listener);
6556
+ delFromSet(cont, KEY_ERR, errHandler);
6557
+ delFromSet(cont, KEY_RAW, rawEmitter);
6558
+ if (isEmptySet(cont.listeners)) {
6559
+ cont.watcher.close();
6560
+ FsWatchInstances.delete(fullPath);
6561
+ HANDLER_KEYS.forEach(clearItem(cont));
6562
+ cont.watcher = void 0;
6563
+ Object.freeze(cont);
6564
+ }
6565
+ };
6566
+ };
6567
+ FsWatchFileInstances = /* @__PURE__ */ new Map();
6568
+ setFsWatchFileListener = (path15, fullPath, options2, handlers) => {
6569
+ const { listener, rawEmitter } = handlers;
6570
+ let cont = FsWatchFileInstances.get(fullPath);
6571
+ const copts = cont && cont.options;
6572
+ if (copts && (copts.persistent < options2.persistent || copts.interval > options2.interval)) {
6573
+ unwatchFile(fullPath);
6574
+ cont = void 0;
6575
+ }
6576
+ if (cont) {
6577
+ addAndConvert(cont, KEY_LISTENERS, listener);
6578
+ addAndConvert(cont, KEY_RAW, rawEmitter);
6579
+ } else {
6580
+ cont = {
6581
+ listeners: listener,
6582
+ rawEmitters: rawEmitter,
6583
+ options: options2,
6584
+ watcher: watchFile(fullPath, options2, (curr, prev) => {
6585
+ foreach(cont.rawEmitters, (rawEmitter2) => {
6586
+ rawEmitter2(EV.CHANGE, fullPath, { curr, prev });
6587
+ });
6588
+ const currmtime = curr.mtimeMs;
6589
+ if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
6590
+ foreach(cont.listeners, (listener2) => listener2(path15, curr));
6591
+ }
6592
+ })
6593
+ };
6594
+ FsWatchFileInstances.set(fullPath, cont);
6595
+ }
6596
+ return () => {
6597
+ delFromSet(cont, KEY_LISTENERS, listener);
6598
+ delFromSet(cont, KEY_RAW, rawEmitter);
6599
+ if (isEmptySet(cont.listeners)) {
6600
+ FsWatchFileInstances.delete(fullPath);
6601
+ unwatchFile(fullPath);
6602
+ cont.options = cont.watcher = void 0;
6603
+ Object.freeze(cont);
6604
+ }
6605
+ };
6606
+ };
6607
+ NodeFsHandler = class {
6608
+ constructor(fsW) {
6609
+ this.fsw = fsW;
6610
+ this._boundHandleError = (error) => fsW._handleError(error);
6611
+ }
6612
+ /**
6613
+ * Watch file for changes with fs_watchFile or fs_watch.
6614
+ * @param path to file or dir
6615
+ * @param listener on fs change
6616
+ * @returns closer for the watcher instance
6617
+ */
6618
+ _watchWithNodeFs(path15, listener) {
6619
+ const opts = this.fsw.options;
6620
+ const directory = sysPath.dirname(path15);
6621
+ const basename4 = sysPath.basename(path15);
6622
+ const parent = this.fsw._getWatchedDir(directory);
6623
+ parent.add(basename4);
6624
+ const absolutePath = sysPath.resolve(path15);
6625
+ const options2 = {
6626
+ persistent: opts.persistent
6627
+ };
6628
+ if (!listener)
6629
+ listener = EMPTY_FN;
6630
+ let closer;
6631
+ if (opts.usePolling) {
6632
+ const enableBin = opts.interval !== opts.binaryInterval;
6633
+ options2.interval = enableBin && isBinaryPath(basename4) ? opts.binaryInterval : opts.interval;
6634
+ closer = setFsWatchFileListener(path15, absolutePath, options2, {
6635
+ listener,
6636
+ rawEmitter: this.fsw._emitRaw
6637
+ });
6638
+ } else {
6639
+ closer = setFsWatchListener(path15, absolutePath, options2, {
6640
+ listener,
6641
+ errHandler: this._boundHandleError,
6642
+ rawEmitter: this.fsw._emitRaw
6643
+ });
6644
+ }
6645
+ return closer;
6646
+ }
6647
+ /**
6648
+ * Watch a file and emit add event if warranted.
6649
+ * @returns closer for the watcher instance
6650
+ */
6651
+ _handleFile(file, stats, initialAdd) {
6652
+ if (this.fsw.closed) {
6653
+ return;
6654
+ }
6655
+ const dirname7 = sysPath.dirname(file);
6656
+ const basename4 = sysPath.basename(file);
6657
+ const parent = this.fsw._getWatchedDir(dirname7);
6658
+ let prevStats = stats;
6659
+ if (parent.has(basename4))
6660
+ return;
6661
+ const listener = async (path15, newStats) => {
6662
+ if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
6663
+ return;
6664
+ if (!newStats || newStats.mtimeMs === 0) {
6665
+ try {
6666
+ const newStats2 = await stat2(file);
6667
+ if (this.fsw.closed)
6668
+ return;
6669
+ const at = newStats2.atimeMs;
6670
+ const mt = newStats2.mtimeMs;
6671
+ if (!at || at <= mt || mt !== prevStats.mtimeMs) {
6672
+ this.fsw._emit(EV.CHANGE, file, newStats2);
6673
+ }
6674
+ if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
6675
+ this.fsw._closeFile(path15);
6676
+ prevStats = newStats2;
6677
+ const closer2 = this._watchWithNodeFs(file, listener);
6678
+ if (closer2)
6679
+ this.fsw._addPathCloser(path15, closer2);
6680
+ } else {
6681
+ prevStats = newStats2;
6682
+ }
6683
+ } catch (error) {
6684
+ this.fsw._remove(dirname7, basename4);
6685
+ }
6686
+ } else if (parent.has(basename4)) {
6687
+ const at = newStats.atimeMs;
6688
+ const mt = newStats.mtimeMs;
6689
+ if (!at || at <= mt || mt !== prevStats.mtimeMs) {
6690
+ this.fsw._emit(EV.CHANGE, file, newStats);
6691
+ }
6692
+ prevStats = newStats;
6693
+ }
6694
+ };
6695
+ const closer = this._watchWithNodeFs(file, listener);
6696
+ if (!(initialAdd && this.fsw.options.ignoreInitial) && this.fsw._isntIgnored(file)) {
6697
+ if (!this.fsw._throttle(EV.ADD, file, 0))
6698
+ return;
6699
+ this.fsw._emit(EV.ADD, file, stats);
6700
+ }
6701
+ return closer;
6702
+ }
6703
+ /**
6704
+ * Handle symlinks encountered while reading a dir.
6705
+ * @param entry returned by readdirp
6706
+ * @param directory path of dir being read
6707
+ * @param path of this item
6708
+ * @param item basename of this item
6709
+ * @returns true if no more processing is needed for this entry.
6710
+ */
6711
+ async _handleSymlink(entry, directory, path15, item) {
6712
+ if (this.fsw.closed) {
6713
+ return;
6714
+ }
6715
+ const full = entry.fullPath;
6716
+ const dir = this.fsw._getWatchedDir(directory);
6717
+ if (!this.fsw.options.followSymlinks) {
6718
+ this.fsw._incrReadyCount();
6719
+ let linkPath;
6720
+ try {
6721
+ linkPath = await fsrealpath(path15);
6722
+ } catch (e) {
6723
+ this.fsw._emitReady();
6724
+ return true;
6725
+ }
6726
+ if (this.fsw.closed)
6727
+ return;
6728
+ if (dir.has(item)) {
6729
+ if (this.fsw._symlinkPaths.get(full) !== linkPath) {
6730
+ this.fsw._symlinkPaths.set(full, linkPath);
6731
+ this.fsw._emit(EV.CHANGE, path15, entry.stats);
6732
+ }
6733
+ } else {
6734
+ dir.add(item);
6735
+ this.fsw._symlinkPaths.set(full, linkPath);
6736
+ this.fsw._emit(EV.ADD, path15, entry.stats);
6737
+ }
6738
+ this.fsw._emitReady();
6739
+ return true;
6740
+ }
6741
+ if (this.fsw._symlinkPaths.has(full)) {
6742
+ return true;
6743
+ }
6744
+ this.fsw._symlinkPaths.set(full, true);
6745
+ }
6746
+ _handleRead(directory, initialAdd, wh, target, dir, depth, throttler) {
6747
+ directory = sysPath.join(directory, "");
6748
+ throttler = this.fsw._throttle("readdir", directory, 1e3);
6749
+ if (!throttler)
6750
+ return;
6751
+ const previous = this.fsw._getWatchedDir(wh.path);
6752
+ const current = /* @__PURE__ */ new Set();
6753
+ let stream = this.fsw._readdirp(directory, {
6754
+ fileFilter: (entry) => wh.filterPath(entry),
6755
+ directoryFilter: (entry) => wh.filterDir(entry)
6756
+ });
6757
+ if (!stream)
6758
+ return;
6759
+ stream.on(STR_DATA, async (entry) => {
6760
+ if (this.fsw.closed) {
6761
+ stream = void 0;
6762
+ return;
6763
+ }
6764
+ const item = entry.path;
6765
+ let path15 = sysPath.join(directory, item);
6766
+ current.add(item);
6767
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path15, item)) {
6768
+ return;
6769
+ }
6770
+ if (this.fsw.closed) {
6771
+ stream = void 0;
6772
+ return;
6773
+ }
6774
+ if (item === target || !target && !previous.has(item)) {
6775
+ this.fsw._incrReadyCount();
6776
+ path15 = sysPath.join(dir, sysPath.relative(dir, path15));
6777
+ this._addToNodeFs(path15, initialAdd, wh, depth + 1);
6778
+ }
6779
+ }).on(EV.ERROR, this._boundHandleError);
6780
+ return new Promise((resolve7, reject) => {
6781
+ if (!stream)
6782
+ return reject();
6783
+ stream.once(STR_END, () => {
6784
+ if (this.fsw.closed) {
6785
+ stream = void 0;
6786
+ return;
6787
+ }
6788
+ const wasThrottled = throttler ? throttler.clear() : false;
6789
+ resolve7(void 0);
6790
+ previous.getChildren().filter((item) => {
6791
+ return item !== directory && !current.has(item);
6792
+ }).forEach((item) => {
6793
+ this.fsw._remove(directory, item);
6794
+ });
6795
+ stream = void 0;
6796
+ if (wasThrottled)
6797
+ this._handleRead(directory, false, wh, target, dir, depth, throttler);
6798
+ });
6799
+ });
6800
+ }
6801
+ /**
6802
+ * Read directory to add / remove files from `@watched` list and re-read it on change.
6803
+ * @param dir fs path
6804
+ * @param stats
6805
+ * @param initialAdd
6806
+ * @param depth relative to user-supplied path
6807
+ * @param target child path targeted for watch
6808
+ * @param wh Common watch helpers for this path
6809
+ * @param realpath
6810
+ * @returns closer for the watcher instance.
6811
+ */
6812
+ async _handleDir(dir, stats, initialAdd, depth, target, wh, realpath2) {
6813
+ const parentDir = this.fsw._getWatchedDir(sysPath.dirname(dir));
6814
+ const tracked = parentDir.has(sysPath.basename(dir));
6815
+ if (!(initialAdd && this.fsw.options.ignoreInitial) && !target && !tracked) {
6816
+ this.fsw._emit(EV.ADD_DIR, dir, stats);
6817
+ }
6818
+ parentDir.add(sysPath.basename(dir));
6819
+ this.fsw._getWatchedDir(dir);
6820
+ let throttler;
6821
+ let closer;
6822
+ const oDepth = this.fsw.options.depth;
6823
+ if ((oDepth == null || depth <= oDepth) && !this.fsw._symlinkPaths.has(realpath2)) {
6824
+ if (!target) {
6825
+ await this._handleRead(dir, initialAdd, wh, target, dir, depth, throttler);
6826
+ if (this.fsw.closed)
6827
+ return;
6828
+ }
6829
+ closer = this._watchWithNodeFs(dir, (dirPath, stats2) => {
6830
+ if (stats2 && stats2.mtimeMs === 0)
6831
+ return;
6832
+ this._handleRead(dirPath, false, wh, target, dir, depth, throttler);
6833
+ });
6834
+ }
6835
+ return closer;
6836
+ }
6837
+ /**
6838
+ * Handle added file, directory, or glob pattern.
6839
+ * Delegates call to _handleFile / _handleDir after checks.
6840
+ * @param path to file or ir
6841
+ * @param initialAdd was the file added at watch instantiation?
6842
+ * @param priorWh depth relative to user-supplied path
6843
+ * @param depth Child path actually targeted for watch
6844
+ * @param target Child path actually targeted for watch
6845
+ */
6846
+ async _addToNodeFs(path15, initialAdd, priorWh, depth, target) {
6847
+ const ready = this.fsw._emitReady;
6848
+ if (this.fsw._isIgnored(path15) || this.fsw.closed) {
6849
+ ready();
6850
+ return false;
6851
+ }
6852
+ const wh = this.fsw._getWatchHelpers(path15);
6853
+ if (priorWh) {
6854
+ wh.filterPath = (entry) => priorWh.filterPath(entry);
6855
+ wh.filterDir = (entry) => priorWh.filterDir(entry);
6856
+ }
6857
+ try {
6858
+ const stats = await statMethods[wh.statMethod](wh.watchPath);
6859
+ if (this.fsw.closed)
6860
+ return;
6861
+ if (this.fsw._isIgnored(wh.watchPath, stats)) {
6862
+ ready();
6863
+ return false;
6864
+ }
6865
+ const follow = this.fsw.options.followSymlinks;
6866
+ let closer;
6867
+ if (stats.isDirectory()) {
6868
+ const absPath = sysPath.resolve(path15);
6869
+ const targetPath = follow ? await fsrealpath(path15) : path15;
6870
+ if (this.fsw.closed)
6871
+ return;
6872
+ closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
6873
+ if (this.fsw.closed)
6874
+ return;
6875
+ if (absPath !== targetPath && targetPath !== void 0) {
6876
+ this.fsw._symlinkPaths.set(absPath, targetPath);
6877
+ }
6878
+ } else if (stats.isSymbolicLink()) {
6879
+ const targetPath = follow ? await fsrealpath(path15) : path15;
6880
+ if (this.fsw.closed)
6881
+ return;
6882
+ const parent = sysPath.dirname(wh.watchPath);
6883
+ this.fsw._getWatchedDir(parent).add(wh.watchPath);
6884
+ this.fsw._emit(EV.ADD, wh.watchPath, stats);
6885
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path15, wh, targetPath);
6886
+ if (this.fsw.closed)
6887
+ return;
6888
+ if (targetPath !== void 0) {
6889
+ this.fsw._symlinkPaths.set(sysPath.resolve(path15), targetPath);
6890
+ }
6891
+ } else {
6892
+ closer = this._handleFile(wh.watchPath, stats, initialAdd);
6893
+ }
6894
+ ready();
6895
+ if (closer)
6896
+ this.fsw._addPathCloser(path15, closer);
6897
+ return false;
6898
+ } catch (error) {
6899
+ if (this.fsw._handleError(error)) {
6900
+ ready();
6901
+ return path15;
6902
+ }
6903
+ }
6904
+ }
6905
+ };
6906
+ }
6907
+ });
5754
6908
 
5755
- // spex-console/src/lib/cli/init.ts
5756
- init_console_url_resolver();
5757
- import { randomUUID } from "node:crypto";
5758
- function parseVaultId(content) {
5759
- const m = content.match(/^vault_id:\s*(.+?)\s*$/m);
6909
+ // node_modules/chokidar/esm/index.js
6910
+ var esm_exports = {};
6911
+ __export(esm_exports, {
6912
+ FSWatcher: () => FSWatcher,
6913
+ WatchHelper: () => WatchHelper,
6914
+ default: () => esm_default,
6915
+ watch: () => watch
6916
+ });
6917
+ import { stat as statcb } from "fs";
6918
+ import { stat as stat3, readdir as readdir2 } from "fs/promises";
6919
+ import { EventEmitter } from "events";
6920
+ import * as sysPath2 from "path";
6921
+ function arrify(item) {
6922
+ return Array.isArray(item) ? item : [item];
6923
+ }
6924
+ function createPattern(matcher) {
6925
+ if (typeof matcher === "function")
6926
+ return matcher;
6927
+ if (typeof matcher === "string")
6928
+ return (string) => matcher === string;
6929
+ if (matcher instanceof RegExp)
6930
+ return (string) => matcher.test(string);
6931
+ if (typeof matcher === "object" && matcher !== null) {
6932
+ return (string) => {
6933
+ if (matcher.path === string)
6934
+ return true;
6935
+ if (matcher.recursive) {
6936
+ const relative3 = sysPath2.relative(matcher.path, string);
6937
+ if (!relative3) {
6938
+ return false;
6939
+ }
6940
+ return !relative3.startsWith("..") && !sysPath2.isAbsolute(relative3);
6941
+ }
6942
+ return false;
6943
+ };
6944
+ }
6945
+ return () => false;
6946
+ }
6947
+ function normalizePath(path15) {
6948
+ if (typeof path15 !== "string")
6949
+ throw new Error("string expected");
6950
+ path15 = sysPath2.normalize(path15);
6951
+ path15 = path15.replace(/\\/g, "/");
6952
+ let prepend = false;
6953
+ if (path15.startsWith("//"))
6954
+ prepend = true;
6955
+ const DOUBLE_SLASH_RE2 = /\/\//;
6956
+ while (path15.match(DOUBLE_SLASH_RE2))
6957
+ path15 = path15.replace(DOUBLE_SLASH_RE2, "/");
6958
+ if (prepend)
6959
+ path15 = "/" + path15;
6960
+ return path15;
6961
+ }
6962
+ function matchPatterns(patterns, testString, stats) {
6963
+ const path15 = normalizePath(testString);
6964
+ for (let index2 = 0; index2 < patterns.length; index2++) {
6965
+ const pattern = patterns[index2];
6966
+ if (pattern(path15, stats)) {
6967
+ return true;
6968
+ }
6969
+ }
6970
+ return false;
6971
+ }
6972
+ function anymatch(matchers, testString) {
6973
+ if (matchers == null) {
6974
+ throw new TypeError("anymatch: specify first argument");
6975
+ }
6976
+ const matchersArray = arrify(matchers);
6977
+ const patterns = matchersArray.map((matcher) => createPattern(matcher));
6978
+ if (testString == null) {
6979
+ return (testString2, stats) => {
6980
+ return matchPatterns(patterns, testString2, stats);
6981
+ };
6982
+ }
6983
+ return matchPatterns(patterns, testString);
6984
+ }
6985
+ function watch(paths, options2 = {}) {
6986
+ const watcher = new FSWatcher(options2);
6987
+ watcher.add(paths);
6988
+ return watcher;
6989
+ }
6990
+ var SLASH, SLASH_SLASH, ONE_DOT, TWO_DOTS, STRING_TYPE, BACK_SLASH_RE, DOUBLE_SLASH_RE, DOT_RE, REPLACER_RE, isMatcherObject, unifyPaths, toUnix, normalizePathToUnix, normalizeIgnored, getAbsolutePath, EMPTY_SET, DirEntry, STAT_METHOD_F, STAT_METHOD_L, WatchHelper, FSWatcher, esm_default;
6991
+ var init_esm2 = __esm({
6992
+ "node_modules/chokidar/esm/index.js"() {
6993
+ "use strict";
6994
+ init_esm();
6995
+ init_handler();
6996
+ SLASH = "/";
6997
+ SLASH_SLASH = "//";
6998
+ ONE_DOT = ".";
6999
+ TWO_DOTS = "..";
7000
+ STRING_TYPE = "string";
7001
+ BACK_SLASH_RE = /\\/g;
7002
+ DOUBLE_SLASH_RE = /\/\//;
7003
+ DOT_RE = /\..*\.(sw[px])$|~$|\.subl.*\.tmp/;
7004
+ REPLACER_RE = /^\.[/\\]/;
7005
+ isMatcherObject = (matcher) => typeof matcher === "object" && matcher !== null && !(matcher instanceof RegExp);
7006
+ unifyPaths = (paths_) => {
7007
+ const paths = arrify(paths_).flat();
7008
+ if (!paths.every((p) => typeof p === STRING_TYPE)) {
7009
+ throw new TypeError(`Non-string provided as watch path: ${paths}`);
7010
+ }
7011
+ return paths.map(normalizePathToUnix);
7012
+ };
7013
+ toUnix = (string) => {
7014
+ let str2 = string.replace(BACK_SLASH_RE, SLASH);
7015
+ let prepend = false;
7016
+ if (str2.startsWith(SLASH_SLASH)) {
7017
+ prepend = true;
7018
+ }
7019
+ while (str2.match(DOUBLE_SLASH_RE)) {
7020
+ str2 = str2.replace(DOUBLE_SLASH_RE, SLASH);
7021
+ }
7022
+ if (prepend) {
7023
+ str2 = SLASH + str2;
7024
+ }
7025
+ return str2;
7026
+ };
7027
+ normalizePathToUnix = (path15) => toUnix(sysPath2.normalize(toUnix(path15)));
7028
+ normalizeIgnored = (cwd = "") => (path15) => {
7029
+ if (typeof path15 === "string") {
7030
+ return normalizePathToUnix(sysPath2.isAbsolute(path15) ? path15 : sysPath2.join(cwd, path15));
7031
+ } else {
7032
+ return path15;
7033
+ }
7034
+ };
7035
+ getAbsolutePath = (path15, cwd) => {
7036
+ if (sysPath2.isAbsolute(path15)) {
7037
+ return path15;
7038
+ }
7039
+ return sysPath2.join(cwd, path15);
7040
+ };
7041
+ EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
7042
+ DirEntry = class {
7043
+ constructor(dir, removeWatcher) {
7044
+ this.path = dir;
7045
+ this._removeWatcher = removeWatcher;
7046
+ this.items = /* @__PURE__ */ new Set();
7047
+ }
7048
+ add(item) {
7049
+ const { items } = this;
7050
+ if (!items)
7051
+ return;
7052
+ if (item !== ONE_DOT && item !== TWO_DOTS)
7053
+ items.add(item);
7054
+ }
7055
+ async remove(item) {
7056
+ const { items } = this;
7057
+ if (!items)
7058
+ return;
7059
+ items.delete(item);
7060
+ if (items.size > 0)
7061
+ return;
7062
+ const dir = this.path;
7063
+ try {
7064
+ await readdir2(dir);
7065
+ } catch (err) {
7066
+ if (this._removeWatcher) {
7067
+ this._removeWatcher(sysPath2.dirname(dir), sysPath2.basename(dir));
7068
+ }
7069
+ }
7070
+ }
7071
+ has(item) {
7072
+ const { items } = this;
7073
+ if (!items)
7074
+ return;
7075
+ return items.has(item);
7076
+ }
7077
+ getChildren() {
7078
+ const { items } = this;
7079
+ if (!items)
7080
+ return [];
7081
+ return [...items.values()];
7082
+ }
7083
+ dispose() {
7084
+ this.items.clear();
7085
+ this.path = "";
7086
+ this._removeWatcher = EMPTY_FN;
7087
+ this.items = EMPTY_SET;
7088
+ Object.freeze(this);
7089
+ }
7090
+ };
7091
+ STAT_METHOD_F = "stat";
7092
+ STAT_METHOD_L = "lstat";
7093
+ WatchHelper = class {
7094
+ constructor(path15, follow, fsw) {
7095
+ this.fsw = fsw;
7096
+ const watchPath = path15;
7097
+ this.path = path15 = path15.replace(REPLACER_RE, "");
7098
+ this.watchPath = watchPath;
7099
+ this.fullWatchPath = sysPath2.resolve(watchPath);
7100
+ this.dirParts = [];
7101
+ this.dirParts.forEach((parts) => {
7102
+ if (parts.length > 1)
7103
+ parts.pop();
7104
+ });
7105
+ this.followSymlinks = follow;
7106
+ this.statMethod = follow ? STAT_METHOD_F : STAT_METHOD_L;
7107
+ }
7108
+ entryPath(entry) {
7109
+ return sysPath2.join(this.watchPath, sysPath2.relative(this.watchPath, entry.fullPath));
7110
+ }
7111
+ filterPath(entry) {
7112
+ const { stats } = entry;
7113
+ if (stats && stats.isSymbolicLink())
7114
+ return this.filterDir(entry);
7115
+ const resolvedPath = this.entryPath(entry);
7116
+ return this.fsw._isntIgnored(resolvedPath, stats) && this.fsw._hasReadPermissions(stats);
7117
+ }
7118
+ filterDir(entry) {
7119
+ return this.fsw._isntIgnored(this.entryPath(entry), entry.stats);
7120
+ }
7121
+ };
7122
+ FSWatcher = class extends EventEmitter {
7123
+ // Not indenting methods for history sake; for now.
7124
+ constructor(_opts = {}) {
7125
+ super();
7126
+ this.closed = false;
7127
+ this._closers = /* @__PURE__ */ new Map();
7128
+ this._ignoredPaths = /* @__PURE__ */ new Set();
7129
+ this._throttled = /* @__PURE__ */ new Map();
7130
+ this._streams = /* @__PURE__ */ new Set();
7131
+ this._symlinkPaths = /* @__PURE__ */ new Map();
7132
+ this._watched = /* @__PURE__ */ new Map();
7133
+ this._pendingWrites = /* @__PURE__ */ new Map();
7134
+ this._pendingUnlinks = /* @__PURE__ */ new Map();
7135
+ this._readyCount = 0;
7136
+ this._readyEmitted = false;
7137
+ const awf = _opts.awaitWriteFinish;
7138
+ const DEF_AWF = { stabilityThreshold: 2e3, pollInterval: 100 };
7139
+ const opts = {
7140
+ // Defaults
7141
+ persistent: true,
7142
+ ignoreInitial: false,
7143
+ ignorePermissionErrors: false,
7144
+ interval: 100,
7145
+ binaryInterval: 300,
7146
+ followSymlinks: true,
7147
+ usePolling: false,
7148
+ // useAsync: false,
7149
+ atomic: true,
7150
+ // NOTE: overwritten later (depends on usePolling)
7151
+ ..._opts,
7152
+ // Change format
7153
+ ignored: _opts.ignored ? arrify(_opts.ignored) : arrify([]),
7154
+ awaitWriteFinish: awf === true ? DEF_AWF : typeof awf === "object" ? { ...DEF_AWF, ...awf } : false
7155
+ };
7156
+ if (isIBMi)
7157
+ opts.usePolling = true;
7158
+ if (opts.atomic === void 0)
7159
+ opts.atomic = !opts.usePolling;
7160
+ const envPoll = process.env.CHOKIDAR_USEPOLLING;
7161
+ if (envPoll !== void 0) {
7162
+ const envLower = envPoll.toLowerCase();
7163
+ if (envLower === "false" || envLower === "0")
7164
+ opts.usePolling = false;
7165
+ else if (envLower === "true" || envLower === "1")
7166
+ opts.usePolling = true;
7167
+ else
7168
+ opts.usePolling = !!envLower;
7169
+ }
7170
+ const envInterval = process.env.CHOKIDAR_INTERVAL;
7171
+ if (envInterval)
7172
+ opts.interval = Number.parseInt(envInterval, 10);
7173
+ let readyCalls = 0;
7174
+ this._emitReady = () => {
7175
+ readyCalls++;
7176
+ if (readyCalls >= this._readyCount) {
7177
+ this._emitReady = EMPTY_FN;
7178
+ this._readyEmitted = true;
7179
+ process.nextTick(() => this.emit(EVENTS.READY));
7180
+ }
7181
+ };
7182
+ this._emitRaw = (...args) => this.emit(EVENTS.RAW, ...args);
7183
+ this._boundRemove = this._remove.bind(this);
7184
+ this.options = opts;
7185
+ this._nodeFsHandler = new NodeFsHandler(this);
7186
+ Object.freeze(opts);
7187
+ }
7188
+ _addIgnoredPath(matcher) {
7189
+ if (isMatcherObject(matcher)) {
7190
+ for (const ignored of this._ignoredPaths) {
7191
+ if (isMatcherObject(ignored) && ignored.path === matcher.path && ignored.recursive === matcher.recursive) {
7192
+ return;
7193
+ }
7194
+ }
7195
+ }
7196
+ this._ignoredPaths.add(matcher);
7197
+ }
7198
+ _removeIgnoredPath(matcher) {
7199
+ this._ignoredPaths.delete(matcher);
7200
+ if (typeof matcher === "string") {
7201
+ for (const ignored of this._ignoredPaths) {
7202
+ if (isMatcherObject(ignored) && ignored.path === matcher) {
7203
+ this._ignoredPaths.delete(ignored);
7204
+ }
7205
+ }
7206
+ }
7207
+ }
7208
+ // Public methods
7209
+ /**
7210
+ * Adds paths to be watched on an existing FSWatcher instance.
7211
+ * @param paths_ file or file list. Other arguments are unused
7212
+ */
7213
+ add(paths_, _origAdd, _internal) {
7214
+ const { cwd } = this.options;
7215
+ this.closed = false;
7216
+ this._closePromise = void 0;
7217
+ let paths = unifyPaths(paths_);
7218
+ if (cwd) {
7219
+ paths = paths.map((path15) => {
7220
+ const absPath = getAbsolutePath(path15, cwd);
7221
+ return absPath;
7222
+ });
7223
+ }
7224
+ paths.forEach((path15) => {
7225
+ this._removeIgnoredPath(path15);
7226
+ });
7227
+ this._userIgnored = void 0;
7228
+ if (!this._readyCount)
7229
+ this._readyCount = 0;
7230
+ this._readyCount += paths.length;
7231
+ Promise.all(paths.map(async (path15) => {
7232
+ const res = await this._nodeFsHandler._addToNodeFs(path15, !_internal, void 0, 0, _origAdd);
7233
+ if (res)
7234
+ this._emitReady();
7235
+ return res;
7236
+ })).then((results) => {
7237
+ if (this.closed)
7238
+ return;
7239
+ results.forEach((item) => {
7240
+ if (item)
7241
+ this.add(sysPath2.dirname(item), sysPath2.basename(_origAdd || item));
7242
+ });
7243
+ });
7244
+ return this;
7245
+ }
7246
+ /**
7247
+ * Close watchers or start ignoring events from specified paths.
7248
+ */
7249
+ unwatch(paths_) {
7250
+ if (this.closed)
7251
+ return this;
7252
+ const paths = unifyPaths(paths_);
7253
+ const { cwd } = this.options;
7254
+ paths.forEach((path15) => {
7255
+ if (!sysPath2.isAbsolute(path15) && !this._closers.has(path15)) {
7256
+ if (cwd)
7257
+ path15 = sysPath2.join(cwd, path15);
7258
+ path15 = sysPath2.resolve(path15);
7259
+ }
7260
+ this._closePath(path15);
7261
+ this._addIgnoredPath(path15);
7262
+ if (this._watched.has(path15)) {
7263
+ this._addIgnoredPath({
7264
+ path: path15,
7265
+ recursive: true
7266
+ });
7267
+ }
7268
+ this._userIgnored = void 0;
7269
+ });
7270
+ return this;
7271
+ }
7272
+ /**
7273
+ * Close watchers and remove all listeners from watched paths.
7274
+ */
7275
+ close() {
7276
+ if (this._closePromise) {
7277
+ return this._closePromise;
7278
+ }
7279
+ this.closed = true;
7280
+ this.removeAllListeners();
7281
+ const closers = [];
7282
+ this._closers.forEach((closerList) => closerList.forEach((closer) => {
7283
+ const promise = closer();
7284
+ if (promise instanceof Promise)
7285
+ closers.push(promise);
7286
+ }));
7287
+ this._streams.forEach((stream) => stream.destroy());
7288
+ this._userIgnored = void 0;
7289
+ this._readyCount = 0;
7290
+ this._readyEmitted = false;
7291
+ this._watched.forEach((dirent) => dirent.dispose());
7292
+ this._closers.clear();
7293
+ this._watched.clear();
7294
+ this._streams.clear();
7295
+ this._symlinkPaths.clear();
7296
+ this._throttled.clear();
7297
+ this._closePromise = closers.length ? Promise.all(closers).then(() => void 0) : Promise.resolve();
7298
+ return this._closePromise;
7299
+ }
7300
+ /**
7301
+ * Expose list of watched paths
7302
+ * @returns for chaining
7303
+ */
7304
+ getWatched() {
7305
+ const watchList = {};
7306
+ this._watched.forEach((entry, dir) => {
7307
+ const key = this.options.cwd ? sysPath2.relative(this.options.cwd, dir) : dir;
7308
+ const index2 = key || ONE_DOT;
7309
+ watchList[index2] = entry.getChildren().sort();
7310
+ });
7311
+ return watchList;
7312
+ }
7313
+ emitWithAll(event, args) {
7314
+ this.emit(event, ...args);
7315
+ if (event !== EVENTS.ERROR)
7316
+ this.emit(EVENTS.ALL, event, ...args);
7317
+ }
7318
+ // Common helpers
7319
+ // --------------
7320
+ /**
7321
+ * Normalize and emit events.
7322
+ * Calling _emit DOES NOT MEAN emit() would be called!
7323
+ * @param event Type of event
7324
+ * @param path File or directory path
7325
+ * @param stats arguments to be passed with event
7326
+ * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
7327
+ */
7328
+ async _emit(event, path15, stats) {
7329
+ if (this.closed)
7330
+ return;
7331
+ const opts = this.options;
7332
+ if (isWindows)
7333
+ path15 = sysPath2.normalize(path15);
7334
+ if (opts.cwd)
7335
+ path15 = sysPath2.relative(opts.cwd, path15);
7336
+ const args = [path15];
7337
+ if (stats != null)
7338
+ args.push(stats);
7339
+ const awf = opts.awaitWriteFinish;
7340
+ let pw;
7341
+ if (awf && (pw = this._pendingWrites.get(path15))) {
7342
+ pw.lastChange = /* @__PURE__ */ new Date();
7343
+ return this;
7344
+ }
7345
+ if (opts.atomic) {
7346
+ if (event === EVENTS.UNLINK) {
7347
+ this._pendingUnlinks.set(path15, [event, ...args]);
7348
+ setTimeout(() => {
7349
+ this._pendingUnlinks.forEach((entry, path16) => {
7350
+ this.emit(...entry);
7351
+ this.emit(EVENTS.ALL, ...entry);
7352
+ this._pendingUnlinks.delete(path16);
7353
+ });
7354
+ }, typeof opts.atomic === "number" ? opts.atomic : 100);
7355
+ return this;
7356
+ }
7357
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path15)) {
7358
+ event = EVENTS.CHANGE;
7359
+ this._pendingUnlinks.delete(path15);
7360
+ }
7361
+ }
7362
+ if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
7363
+ const awfEmit = (err, stats2) => {
7364
+ if (err) {
7365
+ event = EVENTS.ERROR;
7366
+ args[0] = err;
7367
+ this.emitWithAll(event, args);
7368
+ } else if (stats2) {
7369
+ if (args.length > 1) {
7370
+ args[1] = stats2;
7371
+ } else {
7372
+ args.push(stats2);
7373
+ }
7374
+ this.emitWithAll(event, args);
7375
+ }
7376
+ };
7377
+ this._awaitWriteFinish(path15, awf.stabilityThreshold, event, awfEmit);
7378
+ return this;
7379
+ }
7380
+ if (event === EVENTS.CHANGE) {
7381
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path15, 50);
7382
+ if (isThrottled)
7383
+ return this;
7384
+ }
7385
+ if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
7386
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path15) : path15;
7387
+ let stats2;
7388
+ try {
7389
+ stats2 = await stat3(fullPath);
7390
+ } catch (err) {
7391
+ }
7392
+ if (!stats2 || this.closed)
7393
+ return;
7394
+ args.push(stats2);
7395
+ }
7396
+ this.emitWithAll(event, args);
7397
+ return this;
7398
+ }
7399
+ /**
7400
+ * Common handler for errors
7401
+ * @returns The error if defined, otherwise the value of the FSWatcher instance's `closed` flag
7402
+ */
7403
+ _handleError(error) {
7404
+ const code = error && error.code;
7405
+ if (error && code !== "ENOENT" && code !== "ENOTDIR" && (!this.options.ignorePermissionErrors || code !== "EPERM" && code !== "EACCES")) {
7406
+ this.emit(EVENTS.ERROR, error);
7407
+ }
7408
+ return error || this.closed;
7409
+ }
7410
+ /**
7411
+ * Helper utility for throttling
7412
+ * @param actionType type being throttled
7413
+ * @param path being acted upon
7414
+ * @param timeout duration of time to suppress duplicate actions
7415
+ * @returns tracking object or false if action should be suppressed
7416
+ */
7417
+ _throttle(actionType, path15, timeout) {
7418
+ if (!this._throttled.has(actionType)) {
7419
+ this._throttled.set(actionType, /* @__PURE__ */ new Map());
7420
+ }
7421
+ const action = this._throttled.get(actionType);
7422
+ if (!action)
7423
+ throw new Error("invalid throttle");
7424
+ const actionPath = action.get(path15);
7425
+ if (actionPath) {
7426
+ actionPath.count++;
7427
+ return false;
7428
+ }
7429
+ let timeoutObject;
7430
+ const clear = () => {
7431
+ const item = action.get(path15);
7432
+ const count2 = item ? item.count : 0;
7433
+ action.delete(path15);
7434
+ clearTimeout(timeoutObject);
7435
+ if (item)
7436
+ clearTimeout(item.timeoutObject);
7437
+ return count2;
7438
+ };
7439
+ timeoutObject = setTimeout(clear, timeout);
7440
+ const thr = { timeoutObject, clear, count: 0 };
7441
+ action.set(path15, thr);
7442
+ return thr;
7443
+ }
7444
+ _incrReadyCount() {
7445
+ return this._readyCount++;
7446
+ }
7447
+ /**
7448
+ * Awaits write operation to finish.
7449
+ * Polls a newly created file for size variations. When files size does not change for 'threshold' milliseconds calls callback.
7450
+ * @param path being acted upon
7451
+ * @param threshold Time in milliseconds a file size must be fixed before acknowledging write OP is finished
7452
+ * @param event
7453
+ * @param awfEmit Callback to be called when ready for event to be emitted.
7454
+ */
7455
+ _awaitWriteFinish(path15, threshold, event, awfEmit) {
7456
+ const awf = this.options.awaitWriteFinish;
7457
+ if (typeof awf !== "object")
7458
+ return;
7459
+ const pollInterval = awf.pollInterval;
7460
+ let timeoutHandler;
7461
+ let fullPath = path15;
7462
+ if (this.options.cwd && !sysPath2.isAbsolute(path15)) {
7463
+ fullPath = sysPath2.join(this.options.cwd, path15);
7464
+ }
7465
+ const now = /* @__PURE__ */ new Date();
7466
+ const writes = this._pendingWrites;
7467
+ function awaitWriteFinishFn(prevStat) {
7468
+ statcb(fullPath, (err, curStat) => {
7469
+ if (err || !writes.has(path15)) {
7470
+ if (err && err.code !== "ENOENT")
7471
+ awfEmit(err);
7472
+ return;
7473
+ }
7474
+ const now2 = Number(/* @__PURE__ */ new Date());
7475
+ if (prevStat && curStat.size !== prevStat.size) {
7476
+ writes.get(path15).lastChange = now2;
7477
+ }
7478
+ const pw = writes.get(path15);
7479
+ const df = now2 - pw.lastChange;
7480
+ if (df >= threshold) {
7481
+ writes.delete(path15);
7482
+ awfEmit(void 0, curStat);
7483
+ } else {
7484
+ timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
7485
+ }
7486
+ });
7487
+ }
7488
+ if (!writes.has(path15)) {
7489
+ writes.set(path15, {
7490
+ lastChange: now,
7491
+ cancelWait: () => {
7492
+ writes.delete(path15);
7493
+ clearTimeout(timeoutHandler);
7494
+ return event;
7495
+ }
7496
+ });
7497
+ timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval);
7498
+ }
7499
+ }
7500
+ /**
7501
+ * Determines whether user has asked to ignore this path.
7502
+ */
7503
+ _isIgnored(path15, stats) {
7504
+ if (this.options.atomic && DOT_RE.test(path15))
7505
+ return true;
7506
+ if (!this._userIgnored) {
7507
+ const { cwd } = this.options;
7508
+ const ign = this.options.ignored;
7509
+ const ignored = (ign || []).map(normalizeIgnored(cwd));
7510
+ const ignoredPaths = [...this._ignoredPaths];
7511
+ const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
7512
+ this._userIgnored = anymatch(list, void 0);
7513
+ }
7514
+ return this._userIgnored(path15, stats);
7515
+ }
7516
+ _isntIgnored(path15, stat4) {
7517
+ return !this._isIgnored(path15, stat4);
7518
+ }
7519
+ /**
7520
+ * Provides a set of common helpers and properties relating to symlink handling.
7521
+ * @param path file or directory pattern being watched
7522
+ */
7523
+ _getWatchHelpers(path15) {
7524
+ return new WatchHelper(path15, this.options.followSymlinks, this);
7525
+ }
7526
+ // Directory helpers
7527
+ // -----------------
7528
+ /**
7529
+ * Provides directory tracking objects
7530
+ * @param directory path of the directory
7531
+ */
7532
+ _getWatchedDir(directory) {
7533
+ const dir = sysPath2.resolve(directory);
7534
+ if (!this._watched.has(dir))
7535
+ this._watched.set(dir, new DirEntry(dir, this._boundRemove));
7536
+ return this._watched.get(dir);
7537
+ }
7538
+ // File helpers
7539
+ // ------------
7540
+ /**
7541
+ * Check for read permissions: https://stackoverflow.com/a/11781404/1358405
7542
+ */
7543
+ _hasReadPermissions(stats) {
7544
+ if (this.options.ignorePermissionErrors)
7545
+ return true;
7546
+ return Boolean(Number(stats.mode) & 256);
7547
+ }
7548
+ /**
7549
+ * Handles emitting unlink events for
7550
+ * files and directories, and via recursion, for
7551
+ * files and directories within directories that are unlinked
7552
+ * @param directory within which the following item is located
7553
+ * @param item base path of item/directory
7554
+ */
7555
+ _remove(directory, item, isDirectory) {
7556
+ const path15 = sysPath2.join(directory, item);
7557
+ const fullPath = sysPath2.resolve(path15);
7558
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path15) || this._watched.has(fullPath);
7559
+ if (!this._throttle("remove", path15, 100))
7560
+ return;
7561
+ if (!isDirectory && this._watched.size === 1) {
7562
+ this.add(directory, item, true);
7563
+ }
7564
+ const wp = this._getWatchedDir(path15);
7565
+ const nestedDirectoryChildren = wp.getChildren();
7566
+ nestedDirectoryChildren.forEach((nested) => this._remove(path15, nested));
7567
+ const parent = this._getWatchedDir(directory);
7568
+ const wasTracked = parent.has(item);
7569
+ parent.remove(item);
7570
+ if (this._symlinkPaths.has(fullPath)) {
7571
+ this._symlinkPaths.delete(fullPath);
7572
+ }
7573
+ let relPath = path15;
7574
+ if (this.options.cwd)
7575
+ relPath = sysPath2.relative(this.options.cwd, path15);
7576
+ if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
7577
+ const event = this._pendingWrites.get(relPath).cancelWait();
7578
+ if (event === EVENTS.ADD)
7579
+ return;
7580
+ }
7581
+ this._watched.delete(path15);
7582
+ this._watched.delete(fullPath);
7583
+ const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
7584
+ if (wasTracked && !this._isIgnored(path15))
7585
+ this._emit(eventName, path15);
7586
+ this._closePath(path15);
7587
+ }
7588
+ /**
7589
+ * Closes all watchers for a path
7590
+ */
7591
+ _closePath(path15) {
7592
+ this._closeFile(path15);
7593
+ const dir = sysPath2.dirname(path15);
7594
+ this._getWatchedDir(dir).remove(sysPath2.basename(path15));
7595
+ }
7596
+ /**
7597
+ * Closes only file-specific watchers
7598
+ */
7599
+ _closeFile(path15) {
7600
+ const closers = this._closers.get(path15);
7601
+ if (!closers)
7602
+ return;
7603
+ closers.forEach((closer) => closer());
7604
+ this._closers.delete(path15);
7605
+ }
7606
+ _addPathCloser(path15, closer) {
7607
+ if (!closer)
7608
+ return;
7609
+ let list = this._closers.get(path15);
7610
+ if (!list) {
7611
+ list = [];
7612
+ this._closers.set(path15, list);
7613
+ }
7614
+ list.push(closer);
7615
+ }
7616
+ _readdirp(root, opts) {
7617
+ if (this.closed)
7618
+ return;
7619
+ const options2 = { type: EVENTS.ALL, alwaysStat: true, lstat: true, ...opts, depth: 0 };
7620
+ let stream = readdirp(root, options2);
7621
+ this._streams.add(stream);
7622
+ stream.once(STR_CLOSE, () => {
7623
+ stream = void 0;
7624
+ });
7625
+ stream.once(STR_END, () => {
7626
+ if (stream) {
7627
+ this._streams.delete(stream);
7628
+ stream = void 0;
7629
+ }
7630
+ });
7631
+ return stream;
7632
+ }
7633
+ };
7634
+ esm_default = { watch, FSWatcher };
7635
+ }
7636
+ });
7637
+
7638
+ // spex-console/bin/hs-vault-pull.ts
7639
+ import { readFileSync as readFileSync13 } from "node:fs";
7640
+ import path14 from "node:path";
7641
+ import process4 from "node:process";
7642
+
7643
+ // spex-console/src/lib/cli/control-shared.ts
7644
+ init_hs_command();
7645
+ init_console_fetch();
7646
+ init_credentials();
7647
+ init_session_owner();
7648
+ init_vault_less_endpoints();
7649
+ init_registry_file();
7650
+ import path from "node:path";
7651
+ var VAULT_ID_HEADER = "x-heyspex-vault-id";
7652
+ var ACCOUNT_ID_HEADER = "x-heyspex-account-id";
7653
+ function scopeHeadersFor(scope, method, path15) {
7654
+ if (scope === "none") {
7655
+ if (!isVaultLessEndpoint(method, path15)) {
7656
+ throw new Error(
7657
+ `controlRequest: scope "none" is only allowed for the vault-less endpoints (${VAULT_LESS_ENDPOINTS.join(", ")}); ${method.toUpperCase()} ${path15} is vault-scoped and must name { accountId, vaultId }.`
7658
+ );
7659
+ }
7660
+ return {};
7661
+ }
7662
+ return { [VAULT_ID_HEADER]: scope.vaultId, [ACCOUNT_ID_HEADER]: scope.accountId };
7663
+ }
7664
+ function cloudRequestScope(scope) {
7665
+ const named = { accountId: scope.accountId, vaultId: scope.vaultId };
7666
+ return {
7667
+ ...named,
7668
+ headersFor: (method, path15) => isVaultLessEndpoint(method, path15) ? {} : scopeHeadersFor(named, method, path15)
7669
+ };
7670
+ }
7671
+
7672
+ // spex-console/src/lib/cli/init.ts
7673
+ init_console_url_resolver();
7674
+ import { randomUUID } from "node:crypto";
7675
+ function parseVaultId(content) {
7676
+ const m = content.match(/^vault_id:\s*(.+?)\s*$/m);
5760
7677
  if (!m) return null;
5761
7678
  const raw = m[1].trim();
5762
7679
  const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
@@ -5822,6 +7739,7 @@ var PULL_HELP = [
5822
7739
  "Usage:",
5823
7740
  " hs pull [--apply] [--only <id|basename>[,...]] [--overwrite]",
5824
7741
  " [--from <console-url>] [--detail] [--json]",
7742
+ " hs pull --apply --watch [--interval <seconds>] [--push [--force]] [--from <console-url>]",
5825
7743
  "",
5826
7744
  "Options:",
5827
7745
  " --apply Actually write. WITHOUT it, hs pull only REPORTS a plan.",
@@ -5831,6 +7749,16 @@ var PULL_HELP = [
5831
7749
  " --from <url> Source console (default: the cloud console, app.heyspex.com).",
5832
7750
  " --detail List every skipped spec instead of a bounded sample.",
5833
7751
  " --json Emit the raw result object.",
7752
+ " --watch Keep running: pull now, then every --interval seconds,",
7753
+ " backing off while idle. Requires --apply. One line per",
7754
+ " tick that wrote something, silence otherwise. This is",
7755
+ " what the Companion runs (SpexRelay's cloud \u2192 local leg).",
7756
+ " --interval <s> Base tick for --watch (default 60).",
7757
+ " --push With --watch: also run the disk -> cloud leg. File edits under",
7758
+ " specs/ are pushed (1 s debounce, never status, never creating",
7759
+ " specs, never over a newer cloud copy). A file with no sync",
7760
+ " record is only SEEDED, never pushed. See `hs push`.",
7761
+ " --force With --push: let a disk edit overwrite a newer cloud copy.",
5834
7762
  "",
5835
7763
  "Which vault: --vault <id> (global flag) > this repo's heyspex.yaml vault_id >",
5836
7764
  "the account's active vault read from the console. The registry.json pointer",
@@ -5843,11 +7771,18 @@ var PULL_HELP = [
5843
7771
  " 2 Usage error",
5844
7772
  " 11 No vault could be resolved, or --apply would write another vault into this repo",
5845
7773
  "",
5846
- "One direction only: cloud \u2192 disk. Never pushes, never deletes. A spec that",
5847
- "already exists on disk is skipped and reported, never silently overwritten.",
7774
+ "Without --push: cloud \u2192 disk only; never deletes. A spec already on disk is",
7775
+ "skipped and reported, except one the last sync wrote and nobody edited since",
7776
+ "(sync ledger), which fast-forwards to a newer cloud copy.",
5848
7777
  "Fixes the custody gap in [[HS-OQNH-21DO-DIAG-cloud-written-specs-are-invisible-to]]."
5849
7778
  ].join("\n");
5850
- var FLAGS_WITH_VALUES = /* @__PURE__ */ new Set(["--only", "--from"]);
7779
+ var DEFAULT_WATCH_INTERVAL_SEC = 60;
7780
+ var FLAGS_WITH_VALUES = /* @__PURE__ */ new Set(["--only", "--from", "--interval"]);
7781
+ function parseIntervalSec(value) {
7782
+ if (!/^\d+$/.test(value)) return null;
7783
+ const n = Number(value);
7784
+ return n >= 1 ? n : null;
7785
+ }
5851
7786
  function parsePullArgs(argv) {
5852
7787
  const tokens = argv[0] === "pull" ? argv.slice(1) : [...argv];
5853
7788
  if (tokens.includes("--help") || tokens.includes("-h")) return { help: true };
@@ -5857,7 +7792,27 @@ function parsePullArgs(argv) {
5857
7792
  detail: false,
5858
7793
  json: false,
5859
7794
  only: [],
5860
- from: null
7795
+ from: null,
7796
+ watch: false,
7797
+ intervalSec: DEFAULT_WATCH_INTERVAL_SEC,
7798
+ push: false,
7799
+ force: false
7800
+ };
7801
+ const applyValued = (flag, value) => {
7802
+ if (flag === "--only") {
7803
+ args.only.push(
7804
+ ...value.split(",").map((s) => s.trim()).filter((s) => s.length > 0)
7805
+ );
7806
+ return null;
7807
+ }
7808
+ if (flag === "--interval") {
7809
+ const n = parseIntervalSec(value);
7810
+ if (n === null) return `--interval must be a whole number of seconds >= 1 (got \`${value}\`)`;
7811
+ args.intervalSec = n;
7812
+ return null;
7813
+ }
7814
+ args.from = value;
7815
+ return null;
5861
7816
  };
5862
7817
  for (let i = 0; i < tokens.length; i++) {
5863
7818
  const tok = tokens[i];
@@ -5867,13 +7822,8 @@ function parsePullArgs(argv) {
5867
7822
  return { error: { message: `${tok} requires a value`, code: PULL_EXIT.USAGE } };
5868
7823
  }
5869
7824
  i++;
5870
- if (tok === "--only") {
5871
- args.only.push(
5872
- ...value.split(",").map((s) => s.trim()).filter((s) => s.length > 0)
5873
- );
5874
- } else {
5875
- args.from = value.trim();
5876
- }
7825
+ const problem = applyValued(tok, value.trim());
7826
+ if (problem) return { error: { message: problem, code: PULL_EXIT.USAGE } };
5877
7827
  continue;
5878
7828
  }
5879
7829
  const eq2 = tok.indexOf("=");
@@ -5883,13 +7833,8 @@ function parsePullArgs(argv) {
5883
7833
  if (value.length === 0) {
5884
7834
  return { error: { message: `${flag} requires a value`, code: PULL_EXIT.USAGE } };
5885
7835
  }
5886
- if (flag === "--only") {
5887
- args.only.push(
5888
- ...value.split(",").map((s) => s.trim()).filter((s) => s.length > 0)
5889
- );
5890
- } else {
5891
- args.from = value;
5892
- }
7836
+ const problem = applyValued(flag, value);
7837
+ if (problem) return { error: { message: problem, code: PULL_EXIT.USAGE } };
5893
7838
  continue;
5894
7839
  }
5895
7840
  switch (tok) {
@@ -5905,10 +7850,31 @@ function parsePullArgs(argv) {
5905
7850
  case "--json":
5906
7851
  args.json = true;
5907
7852
  break;
7853
+ case "--watch":
7854
+ args.watch = true;
7855
+ break;
7856
+ case "--push":
7857
+ args.push = true;
7858
+ break;
7859
+ case "--force":
7860
+ args.force = true;
7861
+ break;
5908
7862
  default:
5909
7863
  return { error: { message: `unknown option \`${tok}\``, code: PULL_EXIT.USAGE } };
5910
7864
  }
5911
7865
  }
7866
+ if (args.watch && !args.apply) {
7867
+ return { error: { message: "--watch requires --apply", code: PULL_EXIT.USAGE } };
7868
+ }
7869
+ if (args.push && !args.watch) {
7870
+ return { error: { message: "--push requires --watch --apply", code: PULL_EXIT.USAGE } };
7871
+ }
7872
+ if (args.force && !args.push) {
7873
+ return { error: { message: "--force requires --push", code: PULL_EXIT.USAGE } };
7874
+ }
7875
+ if (args.watch && args.json) {
7876
+ return { error: { message: "--watch cannot be combined with --json", code: PULL_EXIT.USAGE } };
7877
+ }
5912
7878
  return { args };
5913
7879
  }
5914
7880
 
@@ -5950,7 +7916,7 @@ function resolveSyncVaultTarget(input) {
5950
7916
  detail: `heyspex.yaml vault_id${drift}`
5951
7917
  };
5952
7918
  }
5953
- if (input.pointer.status === "read" && input.pointer.vaultId) {
7919
+ if (input.allowPointer && input.pointer.status === "read" && input.pointer.vaultId) {
5954
7920
  return {
5955
7921
  ok: true,
5956
7922
  vaultId: input.pointer.vaultId,
@@ -5958,9 +7924,9 @@ function resolveSyncVaultTarget(input) {
5958
7924
  detail: "this account's active vault, read from the console (heyspex.yaml names none)"
5959
7925
  };
5960
7926
  }
5961
- const why = input.pointer.status === "unreadable" ? `the console could not answer for the active vault (${input.pointer.detail})` : "the console reports no active vault for this account";
7927
+ const why = !input.allowPointer ? "the account-wide active vault is not a binding and was not consulted (set HEYSPEX_ALLOW_POINTER_VAULT=1 to follow it on purpose)" : input.pointer.status === "unreadable" ? `the console could not answer for the active vault (${input.pointer.detail})` : "the console reports no active vault for this account";
5962
7928
  return refuse(
5963
- `cannot tell which vault to ${verb === "pull" ? "pull into" : verb === "index reconcile" ? "reconcile" : "rebuild"}: heyspex.yaml names no vault_id and ${why}. Nothing was changed. The registry.json pointer copy was deliberately not used (it can be stale and target the wrong vault). Name the vault: \`hs --vault <id> ${verb}\`; check this repo's wiring with \`hs connect --check\`; or set the account pointer with \`hs vault switch <id>\`.`
7929
+ `cannot tell which vault to ${verb === "pull" ? "pull into" : verb === "push" ? "push from" : verb === "import" ? "import into" : verb === "index reconcile" ? "reconcile" : "rebuild"}: heyspex.yaml names no vault_id and ${why}. Nothing was changed. The registry.json pointer copy was deliberately not used (it can be stale and target the wrong vault). Name the vault: \`hs --vault <id> ${verb}\`; check this repo's wiring with \`hs connect --check\`; or bind this repo with a heyspex.yaml vault_id (a committed satellite marker is portable).`
5964
7930
  );
5965
7931
  }
5966
7932
  async function resolveSyncVaultTargetFromEnv(deps) {
@@ -5982,8 +7948,11 @@ async function resolveSyncVaultTargetFromEnv(deps) {
5982
7948
  } catch {
5983
7949
  registryRowId = null;
5984
7950
  }
7951
+ const allowPointer = ["1", "true", "yes"].includes(
7952
+ (deps.env.HEYSPEX_ALLOW_POINTER_VAULT ?? "").trim().toLowerCase()
7953
+ );
5985
7954
  let pointer = { status: "unreadable", detail: "not read" };
5986
- if (!override && !markerVaultId) {
7955
+ if (!override && !markerVaultId && allowPointer) {
5987
7956
  const http = {
5988
7957
  fetchImpl: deps.fetchImpl ?? fetch,
5989
7958
  consoleUrl: deps.consoleUrl,
@@ -6005,13 +7974,157 @@ async function resolveSyncVaultTargetFromEnv(deps) {
6005
7974
  markerVaultId,
6006
7975
  registryRowId,
6007
7976
  pointer,
6008
- verb: deps.verb
7977
+ verb: deps.verb,
7978
+ allowPointer
6009
7979
  });
6010
7980
  }
6011
7981
 
6012
7982
  // spex-console/bin/hs-vault-pull.ts
6013
7983
  init_vault_workspace_shared();
6014
7984
 
7985
+ // spex-console/src/lib/vault-sync/dirty.ts
7986
+ import { promises as fs3, watch as fsWatch } from "node:fs";
7987
+ import nodePath2 from "node:path";
7988
+
7989
+ // spex-console/src/lib/vault-sync/ledger.ts
7990
+ import { createHash } from "node:crypto";
7991
+ import { promises as fs2 } from "node:fs";
7992
+ import nodePath from "node:path";
7993
+ var VAULT_RUNTIME_DIR = ".heyspex";
7994
+ var LEDGER_FILE = "vault-sync-ledger.json";
7995
+ function ledgerPath(vaultRoot2) {
7996
+ return nodePath.join(vaultRoot2, VAULT_RUNTIME_DIR, LEDGER_FILE);
7997
+ }
7998
+ function hashContent(content) {
7999
+ return createHash("sha256").update(content, "utf8").digest("hex");
8000
+ }
8001
+ function parseLedger(raw) {
8002
+ let json;
8003
+ try {
8004
+ json = JSON.parse(raw);
8005
+ } catch {
8006
+ return {};
8007
+ }
8008
+ if (!json || typeof json !== "object" || Array.isArray(json)) return {};
8009
+ const out = {};
8010
+ for (const [k, v] of Object.entries(json)) {
8011
+ if (!v || typeof v !== "object") continue;
8012
+ const e = v;
8013
+ if (typeof e.hash !== "string" || e.hash.length === 0) continue;
8014
+ out[k] = {
8015
+ hash: e.hash,
8016
+ remoteUpdatedAt: typeof e.remoteUpdatedAt === "string" ? e.remoteUpdatedAt : null,
8017
+ at: typeof e.at === "string" ? e.at : "",
8018
+ by: e.by === "push" ? "push" : "pull"
8019
+ };
8020
+ }
8021
+ return out;
8022
+ }
8023
+ function createLedgerStore(vaultRoot2, { now = () => /* @__PURE__ */ new Date() } = {}) {
8024
+ const file = ledgerPath(vaultRoot2);
8025
+ const pending = /* @__PURE__ */ new Map();
8026
+ let chain = Promise.resolve();
8027
+ async function readDisk() {
8028
+ try {
8029
+ return parseLedger(await fs2.readFile(file, "utf8"));
8030
+ } catch {
8031
+ return {};
8032
+ }
8033
+ }
8034
+ async function writeAtomic(ledger) {
8035
+ await fs2.mkdir(nodePath.dirname(file), { recursive: true });
8036
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
8037
+ await fs2.writeFile(tmp, `${JSON.stringify(ledger)}
8038
+ `, "utf8");
8039
+ let lastErr;
8040
+ for (let attempt = 0; attempt < 5; attempt += 1) {
8041
+ try {
8042
+ await fs2.rename(tmp, file);
8043
+ return;
8044
+ } catch (err) {
8045
+ lastErr = err;
8046
+ await new Promise((r) => setTimeout(r, 25 * (attempt + 1)));
8047
+ }
8048
+ }
8049
+ await fs2.rm(tmp, { force: true }).catch(() => {
8050
+ });
8051
+ throw lastErr;
8052
+ }
8053
+ return {
8054
+ async snapshot() {
8055
+ const disk = await readDisk();
8056
+ for (const [k, v] of pending) disk[k] = v;
8057
+ return disk;
8058
+ },
8059
+ record(relPath, entry) {
8060
+ pending.set(relPath, { ...entry, at: entry.at ?? now().toISOString() });
8061
+ },
8062
+ flush() {
8063
+ chain = chain.then(async () => {
8064
+ if (pending.size === 0) return;
8065
+ const batch = new Map(pending);
8066
+ const merged = await readDisk();
8067
+ for (const [k, v] of batch) merged[k] = v;
8068
+ await writeAtomic(merged);
8069
+ for (const [k, v] of batch) if (pending.get(k) === v) pending.delete(k);
8070
+ });
8071
+ return chain;
8072
+ }
8073
+ };
8074
+ }
8075
+
8076
+ // spex-console/src/lib/vault-sync/dirty.ts
8077
+ var DIRTY_FILE = "vault-dirty";
8078
+ function dirtyMarkerPath(vaultRoot2) {
8079
+ return nodePath2.join(vaultRoot2, VAULT_RUNTIME_DIR, DIRTY_FILE);
8080
+ }
8081
+ async function writeDirtyMarker(vaultRoot2, id, now = () => /* @__PURE__ */ new Date()) {
8082
+ const file = dirtyMarkerPath(vaultRoot2);
8083
+ await fs3.mkdir(nodePath2.dirname(file), { recursive: true });
8084
+ await fs3.writeFile(file, JSON.stringify({ id, at: now().toISOString() }), "utf8");
8085
+ }
8086
+ function parseDirtyMarker(raw) {
8087
+ try {
8088
+ const j = JSON.parse(raw);
8089
+ return typeof j.id === "string" && j.id.trim() ? { id: j.id.trim(), at: typeof j.at === "string" ? j.at : "" } : null;
8090
+ } catch {
8091
+ return null;
8092
+ }
8093
+ }
8094
+ function watchDirtyMarker(vaultRoot2, onDirty, { debounceMs = 100 } = {}) {
8095
+ const file = dirtyMarkerPath(vaultRoot2);
8096
+ let timer = null;
8097
+ let closed = false;
8098
+ let watcher = null;
8099
+ try {
8100
+ void fs3.mkdir(nodePath2.dirname(file), { recursive: true }).then(() => {
8101
+ if (closed) return;
8102
+ try {
8103
+ watcher = fsWatch(nodePath2.dirname(file), (_evt, name) => {
8104
+ if (name && String(name) !== DIRTY_FILE) return;
8105
+ if (timer) clearTimeout(timer);
8106
+ timer = setTimeout(async () => {
8107
+ try {
8108
+ const m = parseDirtyMarker(await fs3.readFile(file, "utf8"));
8109
+ if (m) onDirty(m.id);
8110
+ } catch {
8111
+ }
8112
+ }, debounceMs);
8113
+ });
8114
+ watcher.on("error", () => {
8115
+ });
8116
+ } catch {
8117
+ }
8118
+ });
8119
+ } catch {
8120
+ }
8121
+ return () => {
8122
+ closed = true;
8123
+ if (timer) clearTimeout(timer);
8124
+ watcher?.close();
8125
+ };
8126
+ }
8127
+
6015
8128
  // spex-console/src/lib/vault-sync/pull.ts
6016
8129
  function normalizeSpecRelPath(raw) {
6017
8130
  if (typeof raw !== "string") return null;
@@ -6029,6 +8142,10 @@ function normalizeSpecRelPath(raw) {
6029
8142
  if (!/\.md$/i.test(p)) return null;
6030
8143
  return p;
6031
8144
  }
8145
+ function hasDotSegment(relPath) {
8146
+ const segments = relPath.split("/");
8147
+ return segments.slice(0, -1).some((s) => s.startsWith("."));
8148
+ }
6032
8149
  function basenameOfRelPath(relPath) {
6033
8150
  const last = relPath.split("/").pop() ?? relPath;
6034
8151
  return last.replace(/\.md$/i, "");
@@ -6040,11 +8157,11 @@ function selectRemoteRefs(remote, only) {
6040
8157
  const selected = [];
6041
8158
  for (const ref of remote) {
6042
8159
  const id = (ref.id ?? "").toLowerCase();
6043
- const basename = (ref.basename ?? "").toLowerCase();
8160
+ const basename4 = (ref.basename ?? "").toLowerCase();
6044
8161
  let hit = null;
6045
8162
  for (const sel of selectors) {
6046
8163
  const s = sel.toLowerCase();
6047
- if (id === s || basename === s || id.startsWith(`${s}-`) || basename.startsWith(`${s}-`)) {
8164
+ if (id === s || basename4 === s || id.startsWith(`${s}-`) || basename4.startsWith(`${s}-`)) {
6048
8165
  hit = sel;
6049
8166
  break;
6050
8167
  }
@@ -6056,6 +8173,18 @@ function selectRemoteRefs(remote, only) {
6056
8173
  }
6057
8174
  return { selected, unmatched: selectors.filter((s) => !matched.has(s)) };
6058
8175
  }
8176
+ function instantMs(value) {
8177
+ return typeof value === "string" && value ? Date.parse(value) : Number.NaN;
8178
+ }
8179
+ function canFastForward(ref, relPath, local) {
8180
+ const entry = local.ledger?.[relPath];
8181
+ const disk = local.hashes?.get(relPath);
8182
+ if (!entry || !disk || disk !== entry.hash) return null;
8183
+ const remote = instantMs(ref.updatedAt);
8184
+ const seen = instantMs(entry.remoteUpdatedAt);
8185
+ if (Number.isNaN(remote) || Number.isNaN(seen) || remote <= seen) return null;
8186
+ return entry.hash;
8187
+ }
6059
8188
  function decidePullPlan({
6060
8189
  remote,
6061
8190
  local,
@@ -6071,11 +8200,11 @@ function decidePullPlan({
6071
8200
  if (!localByBasename.has(base)) localByBasename.set(base, norm);
6072
8201
  }
6073
8202
  const pull = [];
6074
- const skip = [];
8203
+ const skip2 = [];
6075
8204
  for (const ref of remote) {
6076
8205
  const relPath = normalizeSpecRelPath(ref.relPath ?? null);
6077
8206
  if (!relPath) {
6078
- skip.push({
8207
+ skip2.push({
6079
8208
  id: ref.id,
6080
8209
  relPath: null,
6081
8210
  reason: "unsafe-path",
@@ -6083,18 +8212,43 @@ function decidePullPlan({
6083
8212
  });
6084
8213
  continue;
6085
8214
  }
6086
- const basename = basenameOfRelPath(relPath);
8215
+ const basename4 = basenameOfRelPath(relPath);
8216
+ if (hasDotSegment(relPath)) {
8217
+ skip2.push({
8218
+ id: ref.id,
8219
+ relPath,
8220
+ reason: "trashed-path",
8221
+ detail: "soft-deleted on the remote \u2014 the pull never resurrects a .trash/ row"
8222
+ });
8223
+ continue;
8224
+ }
6087
8225
  if (localRelPaths.has(relPath)) {
8226
+ const ffHash = overwrite ? null : canFastForward(ref, relPath, local);
6088
8227
  if (overwrite) {
6089
- pull.push({ id: ref.id, relPath, basename, overwrites: true });
8228
+ pull.push({
8229
+ id: ref.id,
8230
+ relPath,
8231
+ basename: basename4,
8232
+ overwrites: true,
8233
+ remoteUpdatedAt: ref.updatedAt ?? void 0
8234
+ });
8235
+ } else if (ffHash) {
8236
+ pull.push({
8237
+ id: ref.id,
8238
+ relPath,
8239
+ basename: basename4,
8240
+ overwrites: true,
8241
+ remoteUpdatedAt: ref.updatedAt ?? void 0,
8242
+ fastForwardFromHash: ffHash
8243
+ });
6090
8244
  } else {
6091
- skip.push({ id: ref.id, relPath, reason: "present-on-disk" });
8245
+ skip2.push({ id: ref.id, relPath, reason: "present-on-disk" });
6092
8246
  }
6093
8247
  continue;
6094
8248
  }
6095
- const elsewhere = localByBasename.get(basename.toLowerCase());
8249
+ const elsewhere = localByBasename.get(basename4.toLowerCase());
6096
8250
  if (elsewhere) {
6097
- skip.push({
8251
+ skip2.push({
6098
8252
  id: ref.id,
6099
8253
  relPath,
6100
8254
  reason: "basename-exists-elsewhere",
@@ -6102,19 +8256,54 @@ function decidePullPlan({
6102
8256
  });
6103
8257
  continue;
6104
8258
  }
6105
- pull.push({ id: ref.id, relPath, basename, overwrites: false });
8259
+ pull.push({
8260
+ id: ref.id,
8261
+ relPath,
8262
+ basename: basename4,
8263
+ overwrites: false,
8264
+ remoteUpdatedAt: ref.updatedAt ?? void 0
8265
+ });
6106
8266
  }
6107
- return { pull, skip };
8267
+ return { pull, skip: skip2 };
6108
8268
  }
6109
8269
  async function runVaultPull(deps, options2 = {}) {
6110
8270
  const dryRun = options2.dryRun !== false;
6111
8271
  const overwrite = options2.overwrite === true;
6112
- const remote = await deps.listRemote();
6113
- const { selected, unmatched } = selectRemoteRefs(remote, options2.only);
8272
+ let remote;
8273
+ let selected;
8274
+ let unmatched;
8275
+ if (options2.direct && deps.getRemoteRef && options2.only && options2.only.length > 0) {
8276
+ selected = [];
8277
+ unmatched = [];
8278
+ for (const sel of options2.only) {
8279
+ const ref = await deps.getRemoteRef(sel);
8280
+ if (ref) selected.push(ref);
8281
+ else unmatched.push(sel);
8282
+ }
8283
+ remote = selected;
8284
+ } else {
8285
+ remote = await deps.listRemote();
8286
+ ({ selected, unmatched } = selectRemoteRefs(remote, options2.only));
8287
+ }
6114
8288
  const localRelPaths = await deps.listLocal();
8289
+ const ledger = deps.readLedger && !overwrite ? await deps.readLedger() : void 0;
8290
+ const hashes = /* @__PURE__ */ new Map();
8291
+ if (ledger && deps.hashLocal) {
8292
+ const onDisk = new Set(localRelPaths.map((p) => normalizeSpecRelPath(p) ?? p));
8293
+ for (const ref of selected) {
8294
+ const rel = normalizeSpecRelPath(ref.relPath ?? null);
8295
+ if (!rel || !onDisk.has(rel)) continue;
8296
+ const entry = ledger[rel];
8297
+ const rem = instantMs(ref.updatedAt);
8298
+ const seen = instantMs(entry?.remoteUpdatedAt);
8299
+ if (!entry || Number.isNaN(rem) || Number.isNaN(seen) || rem <= seen) continue;
8300
+ const h = await deps.hashLocal(rel);
8301
+ if (h) hashes.set(rel, h);
8302
+ }
8303
+ }
6115
8304
  const plan = decidePullPlan({
6116
8305
  remote: selected,
6117
- local: { relPaths: localRelPaths },
8306
+ local: { relPaths: localRelPaths, ledger, hashes },
6118
8307
  overwrite
6119
8308
  });
6120
8309
  const written = [];
@@ -6132,7 +8321,19 @@ async function runVaultPull(deps, options2 = {}) {
6132
8321
  continue;
6133
8322
  }
6134
8323
  const bytes = deps.serialize(content.body, content.frontmatter);
6135
- await deps.writeSpec(item.relPath, bytes);
8324
+ if (item.fastForwardFromHash && deps.hashLocal) {
8325
+ const nowHash = await deps.hashLocal(item.relPath);
8326
+ if (nowHash !== item.fastForwardFromHash) {
8327
+ plan.skip.push({
8328
+ id: item.id,
8329
+ relPath: item.relPath,
8330
+ reason: "present-on-disk",
8331
+ detail: "edited locally while the pull ran - not overwritten"
8332
+ });
8333
+ continue;
8334
+ }
8335
+ }
8336
+ await deps.writeSpec(item.relPath, bytes, { overwrite: item.overwrites });
6136
8337
  written.push({
6137
8338
  id: item.id,
6138
8339
  relPath: item.relPath,
@@ -6141,7 +8342,11 @@ async function runVaultPull(deps, options2 = {}) {
6141
8342
  });
6142
8343
  if (deps.afterWrite) {
6143
8344
  try {
6144
- await deps.afterWrite(item.relPath);
8345
+ await deps.afterWrite(item.relPath, {
8346
+ id: item.id,
8347
+ content: bytes,
8348
+ remoteUpdatedAt: item.remoteUpdatedAt ?? null
8349
+ });
6145
8350
  } catch {
6146
8351
  }
6147
8352
  }
@@ -6154,6 +8359,12 @@ async function runVaultPull(deps, options2 = {}) {
6154
8359
  }
6155
8360
  }
6156
8361
  }
8362
+ if (!dryRun && deps.finish) {
8363
+ try {
8364
+ await deps.finish();
8365
+ } catch {
8366
+ }
8367
+ }
6157
8368
  return {
6158
8369
  dryRun,
6159
8370
  remoteCount: remote.length,
@@ -6165,9 +8376,10 @@ async function runVaultPull(deps, options2 = {}) {
6165
8376
  };
6166
8377
  }
6167
8378
  var SKIP_LABELS = {
6168
- "present-on-disk": "already on disk (use --overwrite to replace)",
8379
+ "present-on-disk": "already on disk (use --overwrite to replace; files unmodified since the last sync fast-forward on their own)",
6169
8380
  "basename-exists-elsewhere": "same basename lives at another path \u2014 not touched",
6170
- "unsafe-path": "remote row has no safe vault-relative .md path"
8381
+ "unsafe-path": "remote row has no safe vault-relative .md path",
8382
+ "trashed-path": "soft-deleted on the remote (.trash/) \u2014 never resurrected"
6171
8383
  };
6172
8384
  function formatPullSummary(result, { detail = false, sampleSize = 5 } = {}) {
6173
8385
  const lines = [];
@@ -6222,23 +8434,22 @@ function formatPullSummary(result, { detail = false, sampleSize = 5 } = {}) {
6222
8434
  }
6223
8435
 
6224
8436
  // spex-console/src/lib/vault-sync/pull-io.ts
6225
- import { promises as fs8 } from "node:fs";
6226
- import { homedir as homedir9 } from "node:os";
6227
- import nodePath from "node:path";
8437
+ import { promises as fs10 } from "node:fs";
8438
+ import nodePath3 from "node:path";
6228
8439
 
6229
8440
  // spex-console/src/lib/dev-mode.ts
8441
+ init_heyspex_home2();
6230
8442
  init_credentials();
6231
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
6232
- import { homedir as homedir8 } from "node:os";
6233
- import { join as join9 } from "node:path";
8443
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
8444
+ import { join as join10 } from "node:path";
6234
8445
  var CLOUD_CONSOLE_URL2 = "https://app.heyspex.com";
6235
- var defaultReadFile = (p) => readFileSync9(p, "utf8");
8446
+ var defaultReadFile = (p) => readFileSync10(p, "utf8");
6236
8447
  function readCredentialsMcpToken({
6237
- homeDir = homedir8(),
8448
+ homeDir = heyspexHome2(),
6238
8449
  readFile = defaultReadFile
6239
8450
  } = {}) {
6240
8451
  try {
6241
- const raw = readFile(join9(homeDir, ".heyspex", "credentials.yaml"));
8452
+ const raw = readFile(join10(homeDir, ".heyspex", "credentials.yaml"));
6242
8453
  const match = raw.match(/^mcp_token:\s*["']?([^"'\r\n#]+)/m);
6243
8454
  return match?.[1]?.trim() || null;
6244
8455
  } catch {
@@ -6247,6 +8458,7 @@ function readCredentialsMcpToken({
6247
8458
  }
6248
8459
 
6249
8460
  // spex-console/src/lib/vault-sync/pull-io.ts
8461
+ init_heyspex_home2();
6250
8462
  init_vault();
6251
8463
  init_vault_frontmatter();
6252
8464
  var LIST_FIELDS = "id,basename,relPath,type,status,updated,updatedAt";
@@ -6262,7 +8474,7 @@ var VaultPullSourceError = class extends Error {
6262
8474
  };
6263
8475
  function resolvePullToken({
6264
8476
  env = process.env,
6265
- homeDir = homedir9()
8477
+ homeDir = heyspexHome2()
6266
8478
  } = {}) {
6267
8479
  const fromEnv = env.HEYSPEX_MCP_TOKEN?.trim();
6268
8480
  if (fromEnv) return fromEnv;
@@ -6361,22 +8573,68 @@ async function fetchRemoteSpecContent(http, item) {
6361
8573
  }
6362
8574
  return { body: spec.body, frontmatter };
6363
8575
  }
8576
+ function remoteStamp(spec) {
8577
+ if (!spec) return null;
8578
+ const fm = spec.frontmatter;
8579
+ for (const candidate of [fm?.updated, spec.updatedAt, spec.updated]) {
8580
+ const raw = candidate instanceof Date ? candidate.toISOString() : candidate;
8581
+ if (typeof raw === "string" && raw.trim()) {
8582
+ const ms = Date.parse(raw);
8583
+ if (!Number.isNaN(ms)) return /T/.test(raw) ? new Date(ms).toISOString() : raw.trim();
8584
+ }
8585
+ }
8586
+ return null;
8587
+ }
8588
+ async function fetchRemoteRefAndContent(http, selector) {
8589
+ const { status, json, text: text2 } = await getJson(http, `api/specs/${encodeURIComponent(selector)}`);
8590
+ if (status === 404) return null;
8591
+ if (status !== 200) {
8592
+ throw new VaultPullSourceError(
8593
+ `GET /api/specs/${selector} returned ${status}: ${text2.slice(0, 200)}`,
8594
+ status
8595
+ );
8596
+ }
8597
+ const spec = json?.spec;
8598
+ const fm = spec?.frontmatter && typeof spec.frontmatter === "object" ? spec.frontmatter : null;
8599
+ if (!spec || typeof spec.body !== "string" || !fm || Object.keys(fm).length === 0) {
8600
+ throw new VaultPullSourceError(`GET /api/specs/${selector} returned no usable spec`, status);
8601
+ }
8602
+ return {
8603
+ ref: {
8604
+ id: String(spec.id ?? fm.id ?? ""),
8605
+ basename: typeof spec.basename === "string" ? spec.basename : null,
8606
+ relPath: typeof spec.relPath === "string" ? spec.relPath : null,
8607
+ type: typeof spec.type === "string" ? spec.type : null,
8608
+ status: typeof spec.status === "string" ? spec.status : null,
8609
+ updated: typeof spec.updated === "string" ? spec.updated : null,
8610
+ updatedAt: remoteStamp(spec)
8611
+ },
8612
+ content: { body: spec.body, frontmatter: fm }
8613
+ };
8614
+ }
8615
+ async function hashFile(abs) {
8616
+ try {
8617
+ return hashContent(await fs10.readFile(abs, "utf8"));
8618
+ } catch {
8619
+ return null;
8620
+ }
8621
+ }
6364
8622
  async function listLocalSpecRelPaths(root = vaultRoot()) {
6365
8623
  const out = [];
6366
8624
  async function walk(dir) {
6367
8625
  let entries;
6368
8626
  try {
6369
- entries = await fs8.readdir(dir, { withFileTypes: true });
8627
+ entries = await fs10.readdir(dir, { withFileTypes: true });
6370
8628
  } catch {
6371
8629
  return;
6372
8630
  }
6373
8631
  for (const entry of entries) {
6374
- const full = nodePath.join(dir, entry.name);
8632
+ const full = nodePath3.join(dir, entry.name);
6375
8633
  if (entry.isDirectory()) {
6376
8634
  if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
6377
8635
  await walk(full);
6378
8636
  } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
6379
- out.push(nodePath.relative(root, full).split(nodePath.sep).join("/"));
8637
+ out.push(nodePath3.relative(root, full).split(nodePath3.sep).join("/"));
6380
8638
  }
6381
8639
  }
6382
8640
  }
@@ -6388,21 +8646,659 @@ function createVaultPullDeps({
6388
8646
  token,
6389
8647
  fetchImpl,
6390
8648
  localRoot,
6391
- scopeHeaders
8649
+ scopeHeaders,
8650
+ ledger,
8651
+ onLocalWrite
6392
8652
  }) {
6393
8653
  const http = { baseUrl, token, fetchImpl, scopeHeaders };
8654
+ const prefetched = /* @__PURE__ */ new Map();
6394
8655
  return {
6395
8656
  listRemote: () => listRemoteSpecs(http),
6396
- fetchRemoteSpec: (item) => fetchRemoteSpecContent(http, item),
8657
+ getRemoteRef: async (selector) => {
8658
+ const hit = await fetchRemoteRefAndContent(http, selector);
8659
+ if (!hit) return null;
8660
+ if (hit.ref.basename) prefetched.set(hit.ref.basename, hit.content);
8661
+ return hit.ref;
8662
+ },
8663
+ fetchRemoteSpec: async (item) => {
8664
+ const cached = prefetched.get(item.basename);
8665
+ if (cached) {
8666
+ prefetched.delete(item.basename);
8667
+ return cached;
8668
+ }
8669
+ return fetchRemoteSpecContent(http, item);
8670
+ },
8671
+ readLedger: ledger ? () => ledger.snapshot() : void 0,
8672
+ hashLocal: (relPath) => hashFile(nodePath3.join(localRoot ?? vaultRoot(), relPath)),
8673
+ finish: ledger ? () => ledger.flush() : void 0,
6397
8674
  listLocal: () => listLocalSpecRelPaths(localRoot ?? vaultRoot()),
6398
8675
  serialize: stringifySpecMarkdown,
6399
- writeSpec: localRoot ? async (relPath, content) => {
6400
- const abs = nodePath.join(localRoot, relPath);
6401
- await fs8.mkdir(nodePath.dirname(abs), { recursive: true });
6402
- await fs8.writeFile(abs, content, "utf8");
8676
+ writeSpec: localRoot ? async (relPath, content, opts) => {
8677
+ const abs = nodePath3.join(localRoot, relPath);
8678
+ await fs10.mkdir(nodePath3.dirname(abs), { recursive: true });
8679
+ onLocalWrite?.(relPath);
8680
+ await fs10.writeFile(abs, content, {
8681
+ encoding: "utf8",
8682
+ flag: opts?.overwrite === true ? "w" : "wx"
8683
+ });
6403
8684
  } : (relPath, content) => writeSpecBytesToVault(relPath, content),
6404
- afterWrite: (relPath) => updateVaultCacheEntry(nodePath.join(localRoot ?? vaultRoot(), relPath))
8685
+ afterWrite: async (relPath, info) => {
8686
+ if (ledger && info) {
8687
+ ledger.record(relPath, {
8688
+ hash: hashContent(info.content),
8689
+ remoteUpdatedAt: info.remoteUpdatedAt,
8690
+ by: "pull"
8691
+ });
8692
+ }
8693
+ await updateVaultCacheEntry(nodePath3.join(localRoot ?? vaultRoot(), relPath));
8694
+ }
8695
+ };
8696
+ }
8697
+
8698
+ // spex-console/src/lib/vault-sync/pull-watch.ts
8699
+ var DEFAULT_WATCH_OPTIONS = {
8700
+ baseIntervalMs: 6e4,
8701
+ // 120 s: a cloud-side console edit lands within ~2 minutes worst case. A tick
8702
+ // is ~14 list GETs for ~7,000 specs (500 rows/page) + one GET per changed
8703
+ // spec, so an idle vault costs ~7 req/min at the ceiling - well inside the
8704
+ // list route's budget. Local writes use the dirty-marker fast path instead.
8705
+ maxIntervalMs: 12e4,
8706
+ backoffFactor: 1.5
8707
+ };
8708
+ function nextWatchDelay(previousMs, outcome, opts = DEFAULT_WATCH_OPTIONS) {
8709
+ if (outcome === "wrote") return opts.baseIntervalMs;
8710
+ const prev = previousMs ?? opts.baseIntervalMs;
8711
+ return Math.min(opts.maxIntervalMs, Math.round(prev * opts.backoffFactor));
8712
+ }
8713
+ function classifyTick(result) {
8714
+ if (result.written.length > 0) return "wrote";
8715
+ if (result.failed.length > 0) return "error";
8716
+ return "idle";
8717
+ }
8718
+ function formatWatchTick(result, sourceLabel) {
8719
+ const lines = [];
8720
+ if (result.written.length > 0) {
8721
+ lines.push({
8722
+ level: "info",
8723
+ line: `relay: pulled ${result.written.length} spec(s) from ${sourceLabel}`
8724
+ });
8725
+ for (const w of result.written) {
8726
+ lines.push({ level: "info", line: ` ${w.overwrote ? "~" : "+"} ${w.relPath} (${w.id})` });
8727
+ }
8728
+ }
8729
+ for (const f of result.failed) {
8730
+ lines.push({
8731
+ level: "warn",
8732
+ line: `relay: failed to pull ${f.relPath} (${f.id}) \u2014 ${f.error}`
8733
+ });
8734
+ }
8735
+ return lines;
8736
+ }
8737
+ async function runPullWatch(deps, opts = DEFAULT_WATCH_OPTIONS) {
8738
+ const summary = { ticks: 0, written: 0, failed: 0, errors: 0 };
8739
+ let delay = null;
8740
+ while (!deps.signal.aborted) {
8741
+ const dirty = deps.takeDirty?.() ?? [];
8742
+ if (dirty.length > 0 && deps.pullDirty && delay !== null) {
8743
+ try {
8744
+ const result = await deps.pullDirty(dirty);
8745
+ summary.written += result.written.length;
8746
+ summary.failed += result.failed.length;
8747
+ for (const { level, line } of formatWatchTick(result, deps.sourceLabel)) {
8748
+ deps.log(level, line);
8749
+ }
8750
+ } catch (err) {
8751
+ const message = err instanceof Error ? err.message : String(err);
8752
+ deps.log("warn", `relay: refresh of ${dirty.join(", ")} failed - ${message}`);
8753
+ }
8754
+ if (deps.signal.aborted) break;
8755
+ await deps.sleep(delay, deps.signal);
8756
+ continue;
8757
+ }
8758
+ summary.ticks += 1;
8759
+ let outcome;
8760
+ try {
8761
+ const result = await deps.pullOnce();
8762
+ summary.written += result.written.length;
8763
+ summary.failed += result.failed.length;
8764
+ for (const { level, line } of formatWatchTick(result, deps.sourceLabel)) {
8765
+ deps.log(level, line);
8766
+ }
8767
+ outcome = classifyTick(result);
8768
+ } catch (err) {
8769
+ summary.errors += 1;
8770
+ const message = err instanceof Error ? err.message : String(err);
8771
+ deps.log("warn", `relay: pull from ${deps.sourceLabel} failed \u2014 ${message}`);
8772
+ outcome = "error";
8773
+ }
8774
+ if (deps.signal.aborted) break;
8775
+ delay = nextWatchDelay(delay, outcome, opts);
8776
+ await deps.sleep(delay, deps.signal);
8777
+ }
8778
+ return summary;
8779
+ }
8780
+
8781
+ // spex-console/src/lib/vault-sync/push.ts
8782
+ init_vault_frontmatter();
8783
+ var skip = (reason, detail) => ({
8784
+ reason,
8785
+ push: false,
8786
+ ...detail ? { detail } : {}
8787
+ });
8788
+ function readSpecIdentity(content) {
8789
+ try {
8790
+ const data = parseSpecMarkdown(content).data ?? {};
8791
+ const id = typeof data.id === "string" ? data.id.trim() : "";
8792
+ const type = typeof data.type === "string" ? data.type.trim() : "";
8793
+ return id && type ? { id, type } : null;
8794
+ } catch {
8795
+ return null;
8796
+ }
8797
+ }
8798
+ function precheckPush(local, ledger, explicit = false) {
8799
+ const rel = normalizeSpecRelPath(local.relPath);
8800
+ if (!rel) return skip("unsafe-path", local.relPath);
8801
+ if (hasDotSegment(rel))
8802
+ return skip("trashed-path", "files under a dot-folder (.trash/) never sync");
8803
+ if (!explicit && ledger && ledger.hash === local.hash)
8804
+ return skip("unchanged", "disk matches the last sync");
8805
+ if (!readSpecIdentity(local.content)) {
8806
+ return skip("no-id", "no id:/type: in the frontmatter - not a spec the cloud can address");
8807
+ }
8808
+ return null;
8809
+ }
8810
+ function decidePushPlan({
8811
+ local,
8812
+ ledger,
8813
+ remote,
8814
+ force = false,
8815
+ allowUnledgered = false
8816
+ }) {
8817
+ const entry = isLedgerEntry(ledger) ? ledger : ledger?.[local.relPath];
8818
+ const pre = precheckPush(local, entry, allowUnledgered);
8819
+ if (pre) return pre;
8820
+ if (!remote)
8821
+ return skip("new-on-disk", "no remote row - hs never creates cloud specs from a disk file");
8822
+ if (!entry && !allowUnledgered) {
8823
+ return skip("seeded", "no sync record yet - recorded as the baseline, nothing sent");
8824
+ }
8825
+ if (remote.bytes === local.content) {
8826
+ return skip("identical", "disk already equals the remote serialization");
8827
+ }
8828
+ const remoteMs = instantMs(remote.updatedAt);
8829
+ let newer = false;
8830
+ let why = "";
8831
+ if (entry) {
8832
+ const seen = instantMs(entry.remoteUpdatedAt);
8833
+ newer = !Number.isNaN(remoteMs) && !Number.isNaN(seen) && remoteMs > seen;
8834
+ why = `remote updated ${remote.updatedAt} > last synced ${entry.remoteUpdatedAt}`;
8835
+ } else if (!Number.isNaN(remoteMs)) {
8836
+ newer = local.mtimeMs === void 0 || local.mtimeMs < remoteMs;
8837
+ why = `no sync record; remote updated ${remote.updatedAt} is not older than the disk file`;
8838
+ }
8839
+ if (newer && !force) return skip("remote-newer", why);
8840
+ return { reason: "push", push: true, ...newer ? { forced: true, detail: why } : {} };
8841
+ }
8842
+ function isLedgerEntry(v) {
8843
+ return !!v && typeof v === "object" && typeof v.hash === "string";
8844
+ }
8845
+ function jsonSafe(value) {
8846
+ if (value instanceof Date) {
8847
+ const iso = value.toISOString();
8848
+ return iso.endsWith("T00:00:00.000Z") ? iso.slice(0, 10) : iso;
8849
+ }
8850
+ if (Array.isArray(value)) return value.map(jsonSafe);
8851
+ if (value && typeof value === "object") {
8852
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, jsonSafe(v)]));
8853
+ }
8854
+ return value;
8855
+ }
8856
+ function buildPushPayload(content) {
8857
+ const parsed = parseSpecMarkdown(content);
8858
+ const frontmatter = jsonSafe({ ...parsed.data ?? {} });
8859
+ delete frontmatter.status;
8860
+ return { id: String(frontmatter.id ?? ""), frontmatter, body: parsed.content };
8861
+ }
8862
+ function matchesOnly(relPath, id, only) {
8863
+ const base = basenameOfRelPath(relPath).toLowerCase();
8864
+ const lid = (id ?? "").toLowerCase();
8865
+ return only.some((sel) => {
8866
+ const s = sel.toLowerCase();
8867
+ return base === s || lid === s || base.startsWith(`${s}-`) || lid.startsWith(`${s}-`);
8868
+ });
8869
+ }
8870
+ async function runVaultPush(deps, options2 = {}) {
8871
+ const dryRun = options2.dryRun !== false;
8872
+ const only = (options2.only ?? []).map((s) => s.trim()).filter(Boolean);
8873
+ const allowUnledgered = options2.allowUnledgered === true || only.length > 0;
8874
+ const result = {
8875
+ dryRun,
8876
+ scanned: 0,
8877
+ pushed: [],
8878
+ skipped: [],
8879
+ conflicts: [],
8880
+ warnings: [],
8881
+ failed: [],
8882
+ unmatchedSelectors: [],
8883
+ deferred: []
8884
+ };
8885
+ const ledger = await deps.readLedger();
8886
+ let relPaths = options2.paths ? [...options2.paths] : await deps.listLocal();
8887
+ relPaths = relPaths.map((p) => normalizeSpecRelPath(p) ?? p);
8888
+ let remoteIndex = null;
8889
+ if (deps.listRemote) {
8890
+ remoteIndex = /* @__PURE__ */ new Map();
8891
+ for (const r of await deps.listRemote()) {
8892
+ if (r.basename) remoteIndex.set(r.basename.toLowerCase(), r.updatedAt ?? null);
8893
+ }
8894
+ }
8895
+ const matchedSelectors = /* @__PURE__ */ new Set();
8896
+ let puts = 0;
8897
+ for (const relPath of relPaths) {
8898
+ const local = await deps.readLocal(relPath);
8899
+ if (!local) continue;
8900
+ const identity = readSpecIdentity(local.content);
8901
+ if (only.length > 0) {
8902
+ if (!matchesOnly(relPath, identity?.id ?? null, only)) continue;
8903
+ for (const sel of only) {
8904
+ if (matchesOnly(relPath, identity?.id ?? null, [sel])) matchedSelectors.add(sel);
8905
+ }
8906
+ }
8907
+ result.scanned += 1;
8908
+ const entry = ledger[relPath];
8909
+ const pl2 = {
8910
+ relPath,
8911
+ content: local.content,
8912
+ hash: local.hash,
8913
+ mtimeMs: local.mtimeMs
8914
+ };
8915
+ const item = (reason, detail) => ({
8916
+ relPath,
8917
+ id: identity?.id ?? null,
8918
+ reason,
8919
+ ...detail ? { detail } : {}
8920
+ });
8921
+ const pre = precheckPush(pl2, entry, allowUnledgered);
8922
+ if (pre) {
8923
+ result.skipped.push(item(pre.reason, pre.detail));
8924
+ continue;
8925
+ }
8926
+ const basename4 = basenameOfRelPath(relPath);
8927
+ if (remoteIndex) {
8928
+ const stamp = remoteIndex.get(basename4.toLowerCase());
8929
+ if (stamp === void 0) {
8930
+ result.skipped.push(
8931
+ item("new-on-disk", "no remote row - hs never creates cloud specs from a disk file")
8932
+ );
8933
+ continue;
8934
+ }
8935
+ if (!entry && !allowUnledgered) {
8936
+ if (!dryRun) {
8937
+ deps.recordLedger(relPath, { hash: local.hash, remoteUpdatedAt: stamp, by: "push" });
8938
+ }
8939
+ result.skipped.push(
8940
+ item("seeded", "no sync record yet - recorded as the baseline, nothing sent")
8941
+ );
8942
+ continue;
8943
+ }
8944
+ }
8945
+ let remote;
8946
+ try {
8947
+ remote = await deps.fetchRemote(basename4);
8948
+ } catch (err) {
8949
+ result.failed.push({
8950
+ relPath,
8951
+ id: identity?.id ?? null,
8952
+ error: err instanceof Error ? err.message : String(err)
8953
+ });
8954
+ continue;
8955
+ }
8956
+ const decision = decidePushPlan({
8957
+ local: pl2,
8958
+ ledger: entry,
8959
+ remote: remote ? { updatedAt: remote.updatedAt, bytes: deps.serialize(remote.body, remote.frontmatter) } : null,
8960
+ force: options2.force === true,
8961
+ allowUnledgered
8962
+ });
8963
+ if (decision.reason === "seeded" && remote && !dryRun) {
8964
+ deps.recordLedger(relPath, {
8965
+ hash: local.hash,
8966
+ remoteUpdatedAt: remote.updatedAt,
8967
+ by: "push"
8968
+ });
8969
+ }
8970
+ if (decision.reason === "identical" && remote && !dryRun) {
8971
+ deps.recordLedger(relPath, {
8972
+ hash: local.hash,
8973
+ remoteUpdatedAt: remote.updatedAt,
8974
+ by: "push"
8975
+ });
8976
+ }
8977
+ if (!decision.push) {
8978
+ (decision.reason === "remote-newer" ? result.conflicts : result.skipped).push(
8979
+ item(decision.reason, decision.detail)
8980
+ );
8981
+ continue;
8982
+ }
8983
+ if (dryRun) {
8984
+ result.pushed.push(item("push", decision.detail));
8985
+ continue;
8986
+ }
8987
+ if (options2.maxPuts !== void 0 && puts >= options2.maxPuts) {
8988
+ result.deferred.push(item("push", "rate cap for this run"));
8989
+ continue;
8990
+ }
8991
+ const payload = buildPushPayload(local.content);
8992
+ puts += 1;
8993
+ let put;
8994
+ try {
8995
+ put = await deps.putRemote(basename4, {
8996
+ frontmatter: payload.frontmatter,
8997
+ body: payload.body
8998
+ });
8999
+ } catch (err) {
9000
+ result.failed.push({
9001
+ relPath,
9002
+ id: payload.id,
9003
+ error: err instanceof Error ? err.message : String(err)
9004
+ });
9005
+ continue;
9006
+ }
9007
+ if (!put.ok) {
9008
+ result.failed.push({
9009
+ relPath,
9010
+ id: payload.id,
9011
+ error: put.error ?? `PUT returned ${put.status}`
9012
+ });
9013
+ if (put.status === 429) {
9014
+ result.retryAfterSec = put.retryAfterSec ?? 60;
9015
+ break;
9016
+ }
9017
+ continue;
9018
+ }
9019
+ if (put.statusIgnored) {
9020
+ result.warnings.push(
9021
+ `${relPath}: status is owned by PATCH /api/specs/${payload.id}/status - the disk status was not pushed`
9022
+ );
9023
+ }
9024
+ deps.recordLedger(relPath, {
9025
+ hash: local.hash,
9026
+ remoteUpdatedAt: put.updatedAt ?? null,
9027
+ by: "push"
9028
+ });
9029
+ result.pushed.push(item("push", decision.detail));
9030
+ try {
9031
+ await deps.markDirty?.(payload.id);
9032
+ } catch {
9033
+ }
9034
+ }
9035
+ result.unmatchedSelectors = only.filter((s) => !matchedSelectors.has(s));
9036
+ try {
9037
+ await deps.flushLedger();
9038
+ } catch {
9039
+ }
9040
+ return result;
9041
+ }
9042
+
9043
+ // spex-console/src/lib/vault-sync/push-io.ts
9044
+ import { promises as fs11 } from "node:fs";
9045
+ import nodePath4 from "node:path";
9046
+ init_vault_frontmatter();
9047
+ async function putRemoteSpec(http, id, payload) {
9048
+ const { baseUrl, token, fetchImpl = fetch } = http;
9049
+ const path15 = `/api/specs/${encodeURIComponent(id)}`;
9050
+ const scope = http.accountId && http.vaultId ? cloudRequestScope({ accountId: http.accountId, vaultId: http.vaultId }).headersFor(
9051
+ "PUT",
9052
+ "/api/specs/:id"
9053
+ ) : http.scopeHeaders ?? {};
9054
+ const headers = {
9055
+ Accept: "application/json",
9056
+ "Content-Type": "application/json",
9057
+ ...scope
9058
+ };
9059
+ if (token) headers.Authorization = `Bearer ${token}`;
9060
+ const res = await fetchImpl(`${baseUrl}${path15}`, {
9061
+ method: "PUT",
9062
+ headers,
9063
+ body: JSON.stringify({ frontmatter: payload.frontmatter, body: payload.body })
9064
+ });
9065
+ const text2 = await res.text();
9066
+ let json = null;
9067
+ try {
9068
+ json = JSON.parse(text2);
9069
+ } catch {
9070
+ }
9071
+ if (res.status !== 200) {
9072
+ const retry = Number(res.headers.get("retry-after"));
9073
+ return {
9074
+ ok: false,
9075
+ status: res.status,
9076
+ error: `PUT ${path15} returned ${res.status}: ${text2.slice(0, 200)}`,
9077
+ ...res.status === 429 && Number.isFinite(retry) && retry > 0 ? { retryAfterSec: retry } : {}
9078
+ };
9079
+ }
9080
+ const spec = json?.spec ?? null;
9081
+ return {
9082
+ ok: true,
9083
+ status: 200,
9084
+ updatedAt: remoteStamp(spec),
9085
+ statusIgnored: json?.status_ignored === true
9086
+ };
9087
+ }
9088
+ async function fetchPushRemote(http, basename4) {
9089
+ const { status, json, text: text2 } = await getJson(http, `api/specs/${encodeURIComponent(basename4)}`);
9090
+ if (status === 404) return null;
9091
+ if (status !== 200) {
9092
+ throw new VaultPullSourceError(
9093
+ `GET /api/specs/${basename4} returned ${status}: ${text2.slice(0, 200)}`,
9094
+ status
9095
+ );
9096
+ }
9097
+ const spec = json?.spec;
9098
+ const fm = spec?.frontmatter && typeof spec.frontmatter === "object" ? spec.frontmatter : null;
9099
+ if (!spec || typeof spec.body !== "string" || !fm) {
9100
+ throw new VaultPullSourceError(`GET /api/specs/${basename4} returned no usable spec`, status);
9101
+ }
9102
+ return {
9103
+ id: String(spec.id ?? fm.id ?? ""),
9104
+ updatedAt: remoteStamp(spec),
9105
+ body: spec.body,
9106
+ frontmatter: fm
9107
+ };
9108
+ }
9109
+ function createVaultPushDeps({
9110
+ baseUrl,
9111
+ token,
9112
+ fetchImpl,
9113
+ vaultRoot: vaultRoot2,
9114
+ scopeHeaders,
9115
+ accountId,
9116
+ vaultId,
9117
+ ledger = createLedgerStore(vaultRoot2),
9118
+ withListIndex = false,
9119
+ markDirty
9120
+ }) {
9121
+ const http = {
9122
+ baseUrl,
9123
+ token,
9124
+ fetchImpl,
9125
+ scopeHeaders,
9126
+ accountId,
9127
+ vaultId
9128
+ };
9129
+ const specsRoot = nodePath4.join(vaultRoot2, "specs");
9130
+ return {
9131
+ listLocal: () => listLocalSpecRelPaths(specsRoot),
9132
+ readLocal: async (relPath) => {
9133
+ const abs = nodePath4.join(specsRoot, relPath);
9134
+ try {
9135
+ const [content, stat4] = await Promise.all([fs11.readFile(abs, "utf8"), fs11.stat(abs)]);
9136
+ return { content, hash: hashContent(content), mtimeMs: stat4.mtimeMs };
9137
+ } catch {
9138
+ return null;
9139
+ }
9140
+ },
9141
+ readLedger: () => ledger.snapshot(),
9142
+ fetchRemote: (basename4) => fetchPushRemote(http, basename4),
9143
+ listRemote: withListIndex ? () => listRemoteSpecs(http) : void 0,
9144
+ serialize: stringifySpecMarkdown,
9145
+ putRemote: (basename4, payload) => putRemoteSpec(http, basename4, payload),
9146
+ recordLedger: (relPath, entry) => ledger.record(relPath, entry),
9147
+ flushLedger: () => ledger.flush(),
9148
+ markDirty
9149
+ };
9150
+ }
9151
+
9152
+ // spex-console/src/lib/vault-sync/push-watch.ts
9153
+ var realClock = {
9154
+ now: () => Date.now(),
9155
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
9156
+ clearTimeout: (h) => clearTimeout(h)
9157
+ };
9158
+ var DEFAULT_PUSH_OPTIONS = {
9159
+ debounceMs: 1e3,
9160
+ recentPullWriteMs: 2e3,
9161
+ maxPutsPerWindow: 20,
9162
+ windowMs: 36e5
9163
+ };
9164
+ function formatPushTick(result) {
9165
+ const out = [];
9166
+ for (const p of result.pushed) {
9167
+ out.push({ level: "info", line: `relay: pushed ${p.relPath} (${p.id ?? "?"}) to the cloud` });
9168
+ }
9169
+ for (const c of result.conflicts) {
9170
+ const ref = c.id ?? c.relPath;
9171
+ out.push({
9172
+ level: "warn",
9173
+ line: `relay: conflict ${c.relPath} (${c.id ?? "?"}) - the cloud copy changed since the last sync; kept both. hs pull --overwrite --only ${ref} | hs push --force --only ${ref} --apply`
9174
+ });
9175
+ }
9176
+ for (const f of result.failed) {
9177
+ out.push({
9178
+ level: "warn",
9179
+ line: `relay: push failed ${f.relPath} (${f.id ?? "?"}) - ${f.error}`
9180
+ });
9181
+ }
9182
+ for (const w of result.warnings) out.push({ level: "warn", line: `relay: push warning ${w}` });
9183
+ return out;
9184
+ }
9185
+ function createPushScheduler(deps, opts = DEFAULT_PUSH_OPTIONS) {
9186
+ const { clock } = deps;
9187
+ const timers = /* @__PURE__ */ new Map();
9188
+ const ready = /* @__PURE__ */ new Set();
9189
+ const pullWrites = /* @__PURE__ */ new Map();
9190
+ const putTimes = [];
9191
+ let running = null;
9192
+ let retryHandle = null;
9193
+ let blockedUntil = 0;
9194
+ let disposed = false;
9195
+ const budget = () => {
9196
+ const cutoff = clock.now() - opts.windowMs;
9197
+ while (putTimes.length > 0 && putTimes[0] <= cutoff) putTimes.shift();
9198
+ return Math.max(0, opts.maxPutsPerWindow - putTimes.length);
9199
+ };
9200
+ function scheduleRetry(ms) {
9201
+ if (retryHandle || disposed) return;
9202
+ retryHandle = clock.setTimeout(() => {
9203
+ retryHandle = null;
9204
+ void drain();
9205
+ }, ms);
9206
+ }
9207
+ async function runDrain() {
9208
+ while (ready.size > 0 && !disposed) {
9209
+ const wait = blockedUntil - clock.now();
9210
+ if (wait > 0) return scheduleRetry(wait);
9211
+ const room = budget();
9212
+ if (room === 0) {
9213
+ deps.log(
9214
+ "warn",
9215
+ `relay: push deferred - hourly write cap (${opts.maxPutsPerWindow}) reached; ${ready.size} pending`
9216
+ );
9217
+ return scheduleRetry(Math.max(1e3, putTimes[0] + opts.windowMs - clock.now()));
9218
+ }
9219
+ const batch = [...ready];
9220
+ ready.clear();
9221
+ let result;
9222
+ try {
9223
+ result = await deps.pushPaths(batch, room);
9224
+ } catch (err) {
9225
+ deps.log(
9226
+ "warn",
9227
+ `relay: push failed - ${err instanceof Error ? err.message : String(err)}`
9228
+ );
9229
+ continue;
9230
+ }
9231
+ for (const { level, line } of formatPushTick(result)) deps.log(level, line);
9232
+ for (const p of result.pushed) if (!result.dryRun) putTimes.push(clock.now());
9233
+ for (const d of result.deferred) ready.add(d.relPath);
9234
+ if (result.retryAfterSec) {
9235
+ blockedUntil = clock.now() + result.retryAfterSec * 1e3;
9236
+ for (const f of result.failed) ready.add(f.relPath);
9237
+ }
9238
+ }
9239
+ }
9240
+ function drain() {
9241
+ if (!running) {
9242
+ running = runDrain().finally(() => {
9243
+ running = null;
9244
+ if (ready.size > 0 && !disposed && blockedUntil <= clock.now() && budget() > 0)
9245
+ void drain();
9246
+ });
9247
+ }
9248
+ return running;
9249
+ }
9250
+ return {
9251
+ notify(relPath) {
9252
+ if (disposed) return;
9253
+ const wrote = pullWrites.get(relPath);
9254
+ if (wrote !== void 0 && clock.now() - wrote < opts.recentPullWriteMs) return;
9255
+ const existing = timers.get(relPath);
9256
+ if (existing) clock.clearTimeout(existing);
9257
+ timers.set(
9258
+ relPath,
9259
+ clock.setTimeout(() => {
9260
+ timers.delete(relPath);
9261
+ ready.add(relPath);
9262
+ void drain();
9263
+ }, opts.debounceMs)
9264
+ );
9265
+ },
9266
+ notePullWrite(relPath) {
9267
+ pullWrites.set(relPath, clock.now());
9268
+ const pending = timers.get(relPath);
9269
+ if (pending) {
9270
+ clock.clearTimeout(pending);
9271
+ timers.delete(relPath);
9272
+ }
9273
+ },
9274
+ async idle() {
9275
+ while (running) await running;
9276
+ },
9277
+ dispose() {
9278
+ disposed = true;
9279
+ for (const h of timers.values()) clock.clearTimeout(h);
9280
+ timers.clear();
9281
+ if (retryHandle) clock.clearTimeout(retryHandle);
9282
+ }
9283
+ };
9284
+ }
9285
+ async function startPushFileWatch(specsRoot, scheduler) {
9286
+ const chokidar = await Promise.resolve().then(() => (init_esm2(), esm_exports));
9287
+ const nodePath5 = await import("node:path");
9288
+ const watcher = chokidar.watch(specsRoot, {
9289
+ ignoreInitial: true,
9290
+ awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
9291
+ ignored: (p) => {
9292
+ const rel = nodePath5.relative(specsRoot, p).split(nodePath5.sep);
9293
+ return rel.some((seg) => seg.startsWith(".") && seg !== "");
9294
+ }
9295
+ });
9296
+ const onFile = (abs) => {
9297
+ if (!/\.md$/i.test(abs)) return;
9298
+ scheduler.notify(nodePath5.relative(specsRoot, abs).split(nodePath5.sep).join("/"));
6405
9299
  };
9300
+ watcher.on("add", onFile).on("change", onFile);
9301
+ return () => watcher.close();
6406
9302
  }
6407
9303
 
6408
9304
  // spex-console/bin/run-entry.ts
@@ -6434,9 +9330,25 @@ function runEntry(name, main2, options2 = {}) {
6434
9330
  }
6435
9331
 
6436
9332
  // spex-console/bin/hs-vault-pull.ts
9333
+ function makeSleeper() {
9334
+ let wakeNow = null;
9335
+ const sleep = (ms, signal) => new Promise((resolve7) => {
9336
+ if (signal.aborted) return resolve7();
9337
+ const done = () => {
9338
+ clearTimeout(timer);
9339
+ signal.removeEventListener("abort", done);
9340
+ wakeNow = null;
9341
+ resolve7();
9342
+ };
9343
+ const timer = setTimeout(done, ms);
9344
+ wakeNow = done;
9345
+ signal.addEventListener("abort", done, { once: true });
9346
+ });
9347
+ return { sleep, wake: () => wakeNow?.() };
9348
+ }
6437
9349
  function readMarkerVaultId(vaultRoot2) {
6438
9350
  try {
6439
- return parseVaultId(readFileSync12(path14.join(vaultRoot2, "heyspex.yaml"), "utf8"));
9351
+ return parseVaultId(readFileSync13(path14.join(vaultRoot2, "heyspex.yaml"), "utf8"));
6440
9352
  } catch {
6441
9353
  return null;
6442
9354
  }
@@ -6513,20 +9425,118 @@ ${PULL_HELP}
6513
9425
  accountId: account.accountId,
6514
9426
  vaultId: target.vaultId
6515
9427
  }).headersFor("GET", "/api/specs");
6516
- try {
6517
- const result = await runVaultPull(
6518
- createVaultPullDeps({
9428
+ const ledger = createLedgerStore(vaultRoot2);
9429
+ let pushScheduler = null;
9430
+ const deps = createVaultPullDeps({
9431
+ baseUrl,
9432
+ token,
9433
+ scopeHeaders,
9434
+ localRoot: path14.join(vaultRoot2, "specs"),
9435
+ ledger,
9436
+ onLocalWrite: (relPath) => pushScheduler?.notePullWrite(relPath)
9437
+ });
9438
+ if (args.watch) {
9439
+ const controller = new AbortController();
9440
+ const stop = () => controller.abort();
9441
+ process4.once("SIGINT", stop);
9442
+ process4.once("SIGTERM", stop);
9443
+ process4.stdout.write(
9444
+ `vault: ${target.vaultId} (source: ${target.source} \u2014 ${target.detail})
9445
+ source: ${baseUrl}
9446
+ watch: every ${args.intervalSec}s while active, backing off to ${Math.round(Math.max(DEFAULT_WATCH_OPTIONS.maxIntervalMs, args.intervalSec * 1e3) / 1e3)}s while idle
9447
+ `
9448
+ );
9449
+ const log = (level, line) => (level === "warn" ? process4.stderr : process4.stdout).write(`${line}
9450
+ `);
9451
+ const { sleep, wake } = makeSleeper();
9452
+ const dirtyIds = /* @__PURE__ */ new Set();
9453
+ const closeDirty = watchDirtyMarker(vaultRoot2, (id) => {
9454
+ dirtyIds.add(id);
9455
+ wake();
9456
+ });
9457
+ process4.stdout.write(`dirty-trigger: ${dirtyMarkerPath(vaultRoot2)}
9458
+ `);
9459
+ let closePushWatch = null;
9460
+ if (args.push) {
9461
+ const common = {
6519
9462
  baseUrl,
6520
9463
  token,
9464
+ vaultRoot: vaultRoot2,
6521
9465
  scopeHeaders,
6522
- localRoot: path14.join(vaultRoot2, "specs")
6523
- }),
9466
+ accountId: account.accountId,
9467
+ vaultId: target.vaultId,
9468
+ ledger,
9469
+ markDirty: (id) => writeDirtyMarker(vaultRoot2, id)
9470
+ };
9471
+ const eventDeps = createVaultPushDeps(common);
9472
+ const sweepDeps = createVaultPushDeps({ ...common, withListIndex: true });
9473
+ const cap = DEFAULT_PUSH_OPTIONS.maxPutsPerWindow;
9474
+ pushScheduler = createPushScheduler({
9475
+ clock: realClock,
9476
+ log,
9477
+ pushPaths: (paths, maxPuts) => runVaultPush(eventDeps, {
9478
+ dryRun: false,
9479
+ force: args.force,
9480
+ paths,
9481
+ maxPuts
9482
+ })
9483
+ });
9484
+ try {
9485
+ const sweep = await runVaultPush(sweepDeps, {
9486
+ dryRun: false,
9487
+ force: args.force,
9488
+ maxPuts: cap
9489
+ });
9490
+ const seeded = sweep.skipped.filter((i) => i.reason === "seeded").length;
9491
+ for (const { level, line } of formatPushTick(sweep)) log(level, line);
9492
+ process4.stdout.write(
9493
+ `push: ready \u2014 ${seeded} spec(s) newly baselined, ${sweep.pushed.length} pushed, ${sweep.conflicts.length} conflict(s); watching specs/ (cap ${cap} writes/hour)
9494
+ `
9495
+ );
9496
+ } catch (err) {
9497
+ log(
9498
+ "warn",
9499
+ `relay: push startup sweep failed \u2014 ${err instanceof Error ? err.message : String(err)}`
9500
+ );
9501
+ }
9502
+ closePushWatch = await startPushFileWatch(path14.join(vaultRoot2, "specs"), pushScheduler);
9503
+ }
9504
+ const summary = await runPullWatch(
9505
+ {
9506
+ pullOnce: () => runVaultPull(deps, { dryRun: false, overwrite: args.overwrite, only: args.only }),
9507
+ sleep,
9508
+ log,
9509
+ signal: controller.signal,
9510
+ sourceLabel: baseUrl,
9511
+ takeDirty: () => {
9512
+ const ids = [...dirtyIds];
9513
+ dirtyIds.clear();
9514
+ return ids;
9515
+ },
9516
+ pullDirty: (ids) => runVaultPull(deps, { dryRun: false, only: ids, direct: true })
9517
+ },
6524
9518
  {
6525
- dryRun: !args.apply,
6526
- overwrite: args.overwrite,
6527
- only: args.only
9519
+ ...DEFAULT_WATCH_OPTIONS,
9520
+ baseIntervalMs: args.intervalSec * 1e3,
9521
+ maxIntervalMs: Math.max(DEFAULT_WATCH_OPTIONS.maxIntervalMs, args.intervalSec * 1e3)
6528
9522
  }
6529
9523
  );
9524
+ closeDirty();
9525
+ pushScheduler?.dispose();
9526
+ await closePushWatch?.();
9527
+ process4.stdout.write(
9528
+ `watch stopped after ${summary.ticks} tick(s): ${summary.written} pulled, ${summary.failed} failed
9529
+ `
9530
+ );
9531
+ process4.exitCode = PULL_EXIT.OK;
9532
+ return;
9533
+ }
9534
+ try {
9535
+ const result = await runVaultPull(deps, {
9536
+ dryRun: !args.apply,
9537
+ overwrite: args.overwrite,
9538
+ only: args.only
9539
+ });
6530
9540
  if (args.json) {
6531
9541
  process4.stdout.write(
6532
9542
  `${JSON.stringify(