@indigoai-us/hq-cloud 5.32.0 → 5.34.0

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 (60) hide show
  1. package/dist/bin/sync-runner.d.ts +9 -0
  2. package/dist/bin/sync-runner.d.ts.map +1 -1
  3. package/dist/bin/sync-runner.js +53 -27
  4. package/dist/bin/sync-runner.js.map +1 -1
  5. package/dist/bin/sync-runner.test.js +69 -0
  6. package/dist/bin/sync-runner.test.js.map +1 -1
  7. package/dist/cli/share.d.ts +60 -4
  8. package/dist/cli/share.d.ts.map +1 -1
  9. package/dist/cli/share.js +129 -8
  10. package/dist/cli/share.js.map +1 -1
  11. package/dist/cli/share.test.js +104 -6
  12. package/dist/cli/share.test.js.map +1 -1
  13. package/dist/cli/sync.d.ts +20 -0
  14. package/dist/cli/sync.d.ts.map +1 -1
  15. package/dist/cli/sync.js +260 -7
  16. package/dist/cli/sync.js.map +1 -1
  17. package/dist/cli/sync.test.js +469 -0
  18. package/dist/cli/sync.test.js.map +1 -1
  19. package/dist/ignore.d.ts.map +1 -1
  20. package/dist/ignore.js +7 -1
  21. package/dist/ignore.js.map +1 -1
  22. package/dist/ignore.test.js +19 -3
  23. package/dist/ignore.test.js.map +1 -1
  24. package/dist/lib/conflict-file.d.ts +7 -6
  25. package/dist/lib/conflict-file.d.ts.map +1 -1
  26. package/dist/lib/conflict-file.js +7 -27
  27. package/dist/lib/conflict-file.js.map +1 -1
  28. package/dist/lib/conflict.test.d.ts +4 -3
  29. package/dist/lib/conflict.test.d.ts.map +1 -1
  30. package/dist/lib/conflict.test.js +5 -33
  31. package/dist/lib/conflict.test.js.map +1 -1
  32. package/dist/lib/machine-id.d.ts +108 -0
  33. package/dist/lib/machine-id.d.ts.map +1 -0
  34. package/dist/lib/machine-id.js +170 -0
  35. package/dist/lib/machine-id.js.map +1 -0
  36. package/dist/lib/machine-id.test.d.ts +8 -0
  37. package/dist/lib/machine-id.test.d.ts.map +1 -0
  38. package/dist/lib/machine-id.test.js +195 -0
  39. package/dist/lib/machine-id.test.js.map +1 -0
  40. package/dist/s3.d.ts +21 -0
  41. package/dist/s3.d.ts.map +1 -1
  42. package/dist/s3.js +69 -2
  43. package/dist/s3.js.map +1 -1
  44. package/dist/s3.test.js +129 -2
  45. package/dist/s3.test.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/bin/sync-runner.test.ts +85 -0
  48. package/src/bin/sync-runner.ts +62 -25
  49. package/src/cli/share.test.ts +115 -6
  50. package/src/cli/share.ts +149 -9
  51. package/src/cli/sync.test.ts +529 -0
  52. package/src/cli/sync.ts +295 -8
  53. package/src/ignore.test.ts +20 -3
  54. package/src/ignore.ts +7 -1
  55. package/src/lib/conflict-file.ts +7 -27
  56. package/src/lib/conflict.test.ts +4 -40
  57. package/src/lib/machine-id.test.ts +221 -0
  58. package/src/lib/machine-id.ts +175 -0
  59. package/src/s3.test.ts +142 -2
  60. package/src/s3.ts +71 -2
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Machine-ID provisioning — owns the per-host identity used to attribute
3
+ * conflict mirrors, telemetry rows, and `.hq-conflicts/index.json` entries.
4
+ *
5
+ * Historically the menubar app (`indigoai-us/hq-sync`) was the sole writer
6
+ * of `machineId` via `~/.hq/menubar.json`, and every other sync caller
7
+ * best-effort read from there. That arrangement is backwards: hq-cloud is
8
+ * the engine that runs on every sync host (macOS-with-menubar, macOS CLI,
9
+ * Linux HQ Pro Outposts, future Windows), while the menubar is an optional
10
+ * macOS-only UI. Linux outposts therefore had no menubar.json, so
11
+ * `readShortMachineId()` returned the literal string `"unknown"` and
12
+ * every conflict file on those hosts was tagged `-unknown` (which then
13
+ * also slipped past `EPHEMERAL_PATH_PATTERN` in `src/cli/share.ts` — see
14
+ * Fix 2 — and rode S3 round-trips as a regular file).
15
+ *
16
+ * This module flips ownership: hq-cloud provisions a UUID on first call
17
+ * and persists it to `<hqRoot>/.hq/machine-id` (one line, plain text).
18
+ * Every subsequent call hits the persisted file. Existing macOS installs
19
+ * with menubar-written IDs are migrated forward on first call: tier 3
20
+ * picks up the menubar.json value, writes it to `<hqRoot>/.hq/machine-id`,
21
+ * and returns it — so the id is stable across the migration window.
22
+ *
23
+ * Resolution order (first hit wins, every miss falls through):
24
+ * 1. `process.env.HQ_MACHINE_ID` — explicit override for CI / tests.
25
+ * 2. `<hqRoot>/.hq/machine-id` — source-of-truth on the host.
26
+ * 3. `~/.hq/menubar.json` `machineId` field — back-compat for existing
27
+ * macOS installs; migrated forward to tier 2 on first read.
28
+ * 4. Autogen — write a fresh UUID to `<hqRoot>/.hq/machine-id` and return.
29
+ *
30
+ * Concurrent autogen race is benign: two writers each pick a fresh UUID,
31
+ * last-writer-wins on disk, both processes re-read the now-stable file on
32
+ * their next call. The window is narrow (single sync run startup) and the
33
+ * downside (one extra conflict-file rename across a single race window) is
34
+ * trivial compared to the litter-ratchet bug it replaces.
35
+ */
36
+ /**
37
+ * Path to `~/.hq/menubar.json`. Evaluated lazily at call time (not module
38
+ * load) so tests overriding `HOME` post-import see the right file. Going
39
+ * through `os.homedir()` rather than `process.env.HOME` keeps the Windows
40
+ * USERPROFILE fallback intact.
41
+ */
42
+ declare function menubarJsonPath(): string;
43
+ /**
44
+ * Path to `<hqRoot>/.hq/machine-id` — the source-of-truth file.
45
+ */
46
+ declare function hqRootMachineIdPath(hqRoot: string): string;
47
+ /**
48
+ * Read the persisted id from `<hqRoot>/.hq/machine-id`, or undefined if
49
+ * absent/unreadable/empty. Trims trailing whitespace so manual edits with
50
+ * a final newline don't break attribution.
51
+ */
52
+ declare function readHqRootMachineId(hqRoot: string): string | undefined;
53
+ /**
54
+ * Read the menubar-written id from `~/.hq/menubar.json`, or undefined if
55
+ * the file is missing / unreadable / doesn't contain a string `machineId`.
56
+ */
57
+ declare function readMenubarMachineId(): string | undefined;
58
+ /**
59
+ * Persist `id` to `<hqRoot>/.hq/machine-id`. Best-effort — failures are
60
+ * silent so a read-only hqRoot (e.g. a CI mount) still gets a working id
61
+ * for the current process, even if it can't be persisted for the next run.
62
+ */
63
+ declare function persistMachineId(hqRoot: string, id: string): void;
64
+ /**
65
+ * Resolve or provision the machine id for this host, persisting it to
66
+ * `<hqRoot>/.hq/machine-id` so the result is stable across sync runs.
67
+ *
68
+ * Returns the full id (UUID-shaped on first generation, free-form when
69
+ * migrated from a menubar.json that wrote something non-UUID). Use
70
+ * {@link readShortMachineId} for the 6-char prefix used in conflict
71
+ * filenames.
72
+ */
73
+ export declare function getOrCreateMachineId(hqRoot: string): string;
74
+ /**
75
+ * Short form (six hex chars) for use in conflict filenames. The short
76
+ * token is what gets stamped into `<orig>.conflict-<ts>-<short>.<ext>` —
77
+ * see `buildConflictPath` in `./conflict-file.ts`.
78
+ *
79
+ * **Always returns `[a-f0-9]{6}`** so the resulting filename matches the
80
+ * `EPHEMERAL_PATH_PATTERN` in `src/cli/share.ts`. Tier 1 (`HQ_MACHINE_ID`)
81
+ * and tier 3 (legacy menubar values) can return arbitrary non-hex strings
82
+ * — e.g. an env override of `"ci-runner-42"` or a menubar-written
83
+ * `"menubar-legacy-id"`. Slicing those raw would produce `ci-run` or
84
+ * `menuba`, which the ephemeral filter would refuse and the push walker
85
+ * would round-trip to S3 — the exact litter-ratchet bug this module
86
+ * exists to close.
87
+ *
88
+ * Normalization: if the first 6 chars of the resolved id are all hex
89
+ * (the typical UUID / hex-id case), use them as-is so the short token
90
+ * remains an intuitive prefix of the full id. Otherwise derive a
91
+ * deterministic SHA-1 hash of the full id and take the first 6 chars —
92
+ * stable across calls, attributable to the same machine, always hex.
93
+ */
94
+ export declare function readShortMachineId(hqRoot: string): string;
95
+ /**
96
+ * Test-only exports. Mirrors the `_testing` namespace pattern used by
97
+ * `src/cli/share.ts` so regression-critical helpers can be pinned by
98
+ * direct unit tests without round-tripping through the public API.
99
+ */
100
+ export declare const _testing: {
101
+ menubarJsonPath: typeof menubarJsonPath;
102
+ hqRootMachineIdPath: typeof hqRootMachineIdPath;
103
+ readHqRootMachineId: typeof readHqRootMachineId;
104
+ readMenubarMachineId: typeof readMenubarMachineId;
105
+ persistMachineId: typeof persistMachineId;
106
+ };
107
+ export {};
108
+ //# sourceMappingURL=machine-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine-id.d.ts","sourceRoot":"","sources":["../../src/lib/machine-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAOH;;;;;GAKG;AACH,iBAAS,eAAe,IAAI,MAAM,CAEjC;AAED;;GAEG;AACH,iBAAS,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;GAIG;AACH,iBAAS,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO/D;AAED;;;GAGG;AACH,iBAAS,oBAAoB,IAAI,MAAM,GAAG,SAAS,CAWlD;AAED;;;;GAIG;AACH,iBAAS,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAQ1D;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAqB3D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKzD;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ;;;;;;CAMpB,CAAC"}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Machine-ID provisioning — owns the per-host identity used to attribute
3
+ * conflict mirrors, telemetry rows, and `.hq-conflicts/index.json` entries.
4
+ *
5
+ * Historically the menubar app (`indigoai-us/hq-sync`) was the sole writer
6
+ * of `machineId` via `~/.hq/menubar.json`, and every other sync caller
7
+ * best-effort read from there. That arrangement is backwards: hq-cloud is
8
+ * the engine that runs on every sync host (macOS-with-menubar, macOS CLI,
9
+ * Linux HQ Pro Outposts, future Windows), while the menubar is an optional
10
+ * macOS-only UI. Linux outposts therefore had no menubar.json, so
11
+ * `readShortMachineId()` returned the literal string `"unknown"` and
12
+ * every conflict file on those hosts was tagged `-unknown` (which then
13
+ * also slipped past `EPHEMERAL_PATH_PATTERN` in `src/cli/share.ts` — see
14
+ * Fix 2 — and rode S3 round-trips as a regular file).
15
+ *
16
+ * This module flips ownership: hq-cloud provisions a UUID on first call
17
+ * and persists it to `<hqRoot>/.hq/machine-id` (one line, plain text).
18
+ * Every subsequent call hits the persisted file. Existing macOS installs
19
+ * with menubar-written IDs are migrated forward on first call: tier 3
20
+ * picks up the menubar.json value, writes it to `<hqRoot>/.hq/machine-id`,
21
+ * and returns it — so the id is stable across the migration window.
22
+ *
23
+ * Resolution order (first hit wins, every miss falls through):
24
+ * 1. `process.env.HQ_MACHINE_ID` — explicit override for CI / tests.
25
+ * 2. `<hqRoot>/.hq/machine-id` — source-of-truth on the host.
26
+ * 3. `~/.hq/menubar.json` `machineId` field — back-compat for existing
27
+ * macOS installs; migrated forward to tier 2 on first read.
28
+ * 4. Autogen — write a fresh UUID to `<hqRoot>/.hq/machine-id` and return.
29
+ *
30
+ * Concurrent autogen race is benign: two writers each pick a fresh UUID,
31
+ * last-writer-wins on disk, both processes re-read the now-stable file on
32
+ * their next call. The window is narrow (single sync run startup) and the
33
+ * downside (one extra conflict-file rename across a single race window) is
34
+ * trivial compared to the litter-ratchet bug it replaces.
35
+ */
36
+ import { createHash, randomUUID } from "node:crypto";
37
+ import * as fs from "node:fs";
38
+ import * as os from "node:os";
39
+ import * as path from "node:path";
40
+ /**
41
+ * Path to `~/.hq/menubar.json`. Evaluated lazily at call time (not module
42
+ * load) so tests overriding `HOME` post-import see the right file. Going
43
+ * through `os.homedir()` rather than `process.env.HOME` keeps the Windows
44
+ * USERPROFILE fallback intact.
45
+ */
46
+ function menubarJsonPath() {
47
+ return path.join(os.homedir(), ".hq", "menubar.json");
48
+ }
49
+ /**
50
+ * Path to `<hqRoot>/.hq/machine-id` — the source-of-truth file.
51
+ */
52
+ function hqRootMachineIdPath(hqRoot) {
53
+ return path.join(hqRoot, ".hq", "machine-id");
54
+ }
55
+ /**
56
+ * Read the persisted id from `<hqRoot>/.hq/machine-id`, or undefined if
57
+ * absent/unreadable/empty. Trims trailing whitespace so manual edits with
58
+ * a final newline don't break attribution.
59
+ */
60
+ function readHqRootMachineId(hqRoot) {
61
+ try {
62
+ const raw = fs.readFileSync(hqRootMachineIdPath(hqRoot), "utf-8").trim();
63
+ return raw.length > 0 ? raw : undefined;
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ /**
70
+ * Read the menubar-written id from `~/.hq/menubar.json`, or undefined if
71
+ * the file is missing / unreadable / doesn't contain a string `machineId`.
72
+ */
73
+ function readMenubarMachineId() {
74
+ try {
75
+ const raw = fs.readFileSync(menubarJsonPath(), "utf-8");
76
+ const parsed = JSON.parse(raw);
77
+ if (typeof parsed.machineId === "string" && parsed.machineId.length > 0) {
78
+ return parsed.machineId;
79
+ }
80
+ return undefined;
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ }
86
+ /**
87
+ * Persist `id` to `<hqRoot>/.hq/machine-id`. Best-effort — failures are
88
+ * silent so a read-only hqRoot (e.g. a CI mount) still gets a working id
89
+ * for the current process, even if it can't be persisted for the next run.
90
+ */
91
+ function persistMachineId(hqRoot, id) {
92
+ try {
93
+ fs.mkdirSync(path.join(hqRoot, ".hq"), { recursive: true });
94
+ fs.writeFileSync(hqRootMachineIdPath(hqRoot), `${id}\n`);
95
+ }
96
+ catch {
97
+ // Read-only filesystem or permission issue — caller gets the id back
98
+ // anyway. Next sync run will retry the persist.
99
+ }
100
+ }
101
+ /**
102
+ * Resolve or provision the machine id for this host, persisting it to
103
+ * `<hqRoot>/.hq/machine-id` so the result is stable across sync runs.
104
+ *
105
+ * Returns the full id (UUID-shaped on first generation, free-form when
106
+ * migrated from a menubar.json that wrote something non-UUID). Use
107
+ * {@link readShortMachineId} for the 6-char prefix used in conflict
108
+ * filenames.
109
+ */
110
+ export function getOrCreateMachineId(hqRoot) {
111
+ // Tier 1: env override.
112
+ const fromEnv = process.env.HQ_MACHINE_ID;
113
+ if (fromEnv && fromEnv.length > 0)
114
+ return fromEnv;
115
+ // Tier 2: persisted source-of-truth.
116
+ const persisted = readHqRootMachineId(hqRoot);
117
+ if (persisted)
118
+ return persisted;
119
+ // Tier 3: back-compat read of menubar.json. Migrate forward on first hit
120
+ // so subsequent calls take tier 2 and the menubar dependency drops out.
121
+ const fromMenubar = readMenubarMachineId();
122
+ if (fromMenubar) {
123
+ persistMachineId(hqRoot, fromMenubar);
124
+ return fromMenubar;
125
+ }
126
+ // Tier 4: autogen + persist.
127
+ const fresh = randomUUID();
128
+ persistMachineId(hqRoot, fresh);
129
+ return fresh;
130
+ }
131
+ /**
132
+ * Short form (six hex chars) for use in conflict filenames. The short
133
+ * token is what gets stamped into `<orig>.conflict-<ts>-<short>.<ext>` —
134
+ * see `buildConflictPath` in `./conflict-file.ts`.
135
+ *
136
+ * **Always returns `[a-f0-9]{6}`** so the resulting filename matches the
137
+ * `EPHEMERAL_PATH_PATTERN` in `src/cli/share.ts`. Tier 1 (`HQ_MACHINE_ID`)
138
+ * and tier 3 (legacy menubar values) can return arbitrary non-hex strings
139
+ * — e.g. an env override of `"ci-runner-42"` or a menubar-written
140
+ * `"menubar-legacy-id"`. Slicing those raw would produce `ci-run` or
141
+ * `menuba`, which the ephemeral filter would refuse and the push walker
142
+ * would round-trip to S3 — the exact litter-ratchet bug this module
143
+ * exists to close.
144
+ *
145
+ * Normalization: if the first 6 chars of the resolved id are all hex
146
+ * (the typical UUID / hex-id case), use them as-is so the short token
147
+ * remains an intuitive prefix of the full id. Otherwise derive a
148
+ * deterministic SHA-1 hash of the full id and take the first 6 chars —
149
+ * stable across calls, attributable to the same machine, always hex.
150
+ */
151
+ export function readShortMachineId(hqRoot) {
152
+ const full = getOrCreateMachineId(hqRoot);
153
+ const head = full.slice(0, 6);
154
+ if (/^[a-f0-9]{6}$/.test(head))
155
+ return head;
156
+ return createHash("sha1").update(full).digest("hex").slice(0, 6);
157
+ }
158
+ /**
159
+ * Test-only exports. Mirrors the `_testing` namespace pattern used by
160
+ * `src/cli/share.ts` so regression-critical helpers can be pinned by
161
+ * direct unit tests without round-tripping through the public API.
162
+ */
163
+ export const _testing = {
164
+ menubarJsonPath,
165
+ hqRootMachineIdPath,
166
+ readHqRootMachineId,
167
+ readMenubarMachineId,
168
+ persistMachineId,
169
+ };
170
+ //# sourceMappingURL=machine-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine-id.js","sourceRoot":"","sources":["../../src/lib/machine-id.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Machine-ID resolver tests. Pins the four-tier fallback contract so a
3
+ * regression in tier ordering, the migration-forward behavior, or the
4
+ * "unknown sentinel is no longer reachable" invariant is caught at build
5
+ * time rather than re-litigating it on a user's Lightsail outpost.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=machine-id.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine-id.test.d.ts","sourceRoot":"","sources":["../../src/lib/machine-id.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Machine-ID resolver tests. Pins the four-tier fallback contract so a
3
+ * regression in tier ordering, the migration-forward behavior, or the
4
+ * "unknown sentinel is no longer reachable" invariant is caught at build
5
+ * time rather than re-litigating it on a user's Lightsail outpost.
6
+ */
7
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
8
+ import * as fs from "fs";
9
+ import * as os from "os";
10
+ import * as path from "path";
11
+ import { getOrCreateMachineId, readShortMachineId } from "./machine-id.js";
12
+ function freshTmp(prefix) {
13
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
14
+ }
15
+ describe("getOrCreateMachineId (four-tier resolver)", () => {
16
+ let originalHome;
17
+ let originalEnvId;
18
+ let tmpHome;
19
+ let tmpHqRoot;
20
+ beforeEach(() => {
21
+ originalHome = process.env.HOME;
22
+ originalEnvId = process.env.HQ_MACHINE_ID;
23
+ delete process.env.HQ_MACHINE_ID;
24
+ tmpHome = freshTmp("hq-machineid-home-");
25
+ tmpHqRoot = freshTmp("hq-machineid-root-");
26
+ process.env.HOME = tmpHome;
27
+ });
28
+ afterEach(() => {
29
+ if (originalHome)
30
+ process.env.HOME = originalHome;
31
+ else
32
+ delete process.env.HOME;
33
+ if (originalEnvId !== undefined)
34
+ process.env.HQ_MACHINE_ID = originalEnvId;
35
+ else
36
+ delete process.env.HQ_MACHINE_ID;
37
+ fs.rmSync(tmpHome, { recursive: true, force: true });
38
+ fs.rmSync(tmpHqRoot, { recursive: true, force: true });
39
+ });
40
+ // ── tier 1: HQ_MACHINE_ID env override ────────────────────────────────
41
+ it("tier 1: returns HQ_MACHINE_ID env when set, ignoring lower tiers", () => {
42
+ process.env.HQ_MACHINE_ID = "env-override-id";
43
+ // Even if a persisted file exists, env wins.
44
+ fs.mkdirSync(path.join(tmpHqRoot, ".hq"), { recursive: true });
45
+ fs.writeFileSync(path.join(tmpHqRoot, ".hq", "machine-id"), "persisted\n");
46
+ expect(getOrCreateMachineId(tmpHqRoot)).toBe("env-override-id");
47
+ // Env-only resolution must not clobber the on-disk source-of-truth.
48
+ expect(fs.readFileSync(path.join(tmpHqRoot, ".hq", "machine-id"), "utf-8").trim()).toBe("persisted");
49
+ });
50
+ // ── tier 2: <hqRoot>/.hq/machine-id ───────────────────────────────────
51
+ it("tier 2: returns the trimmed contents of <hqRoot>/.hq/machine-id", () => {
52
+ fs.mkdirSync(path.join(tmpHqRoot, ".hq"), { recursive: true });
53
+ fs.writeFileSync(path.join(tmpHqRoot, ".hq", "machine-id"), " abc-123-persisted \n\n");
54
+ expect(getOrCreateMachineId(tmpHqRoot)).toBe("abc-123-persisted");
55
+ });
56
+ it("tier 2: empty machine-id file falls through to autogen", () => {
57
+ fs.mkdirSync(path.join(tmpHqRoot, ".hq"), { recursive: true });
58
+ fs.writeFileSync(path.join(tmpHqRoot, ".hq", "machine-id"), "");
59
+ const id = getOrCreateMachineId(tmpHqRoot);
60
+ // UUID v4 shape.
61
+ expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
62
+ // Persisted on disk for next call.
63
+ expect(fs.readFileSync(path.join(tmpHqRoot, ".hq", "machine-id"), "utf-8").trim()).toBe(id);
64
+ });
65
+ // ── tier 3: ~/.hq/menubar.json (legacy, migrated forward) ─────────────
66
+ it("tier 3: reads menubar.json AND migrates the value into <hqRoot>/.hq/machine-id", () => {
67
+ fs.mkdirSync(path.join(tmpHome, ".hq"), { recursive: true });
68
+ fs.writeFileSync(path.join(tmpHome, ".hq", "menubar.json"), JSON.stringify({ machineId: "menubar-legacy-id" }));
69
+ expect(getOrCreateMachineId(tmpHqRoot)).toBe("menubar-legacy-id");
70
+ // Migrated forward — subsequent calls now hit tier 2.
71
+ expect(fs.readFileSync(path.join(tmpHqRoot, ".hq", "machine-id"), "utf-8").trim()).toBe("menubar-legacy-id");
72
+ });
73
+ it("tier 3: malformed menubar.json falls through to autogen", () => {
74
+ fs.mkdirSync(path.join(tmpHome, ".hq"), { recursive: true });
75
+ fs.writeFileSync(path.join(tmpHome, ".hq", "menubar.json"), "{not-json");
76
+ const id = getOrCreateMachineId(tmpHqRoot);
77
+ expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
78
+ });
79
+ it("tier 3: menubar.json without a machineId field falls through to autogen", () => {
80
+ fs.mkdirSync(path.join(tmpHome, ".hq"), { recursive: true });
81
+ fs.writeFileSync(path.join(tmpHome, ".hq", "menubar.json"), JSON.stringify({ telemetryEnabled: true }));
82
+ const id = getOrCreateMachineId(tmpHqRoot);
83
+ expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
84
+ });
85
+ // ── tier 4: autogen + persist ─────────────────────────────────────────
86
+ it("tier 4: generates a UUID and persists it for the next call", () => {
87
+ const id = getOrCreateMachineId(tmpHqRoot);
88
+ expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
89
+ expect(fs.readFileSync(path.join(tmpHqRoot, ".hq", "machine-id"), "utf-8").trim()).toBe(id);
90
+ // Stable across calls.
91
+ expect(getOrCreateMachineId(tmpHqRoot)).toBe(id);
92
+ });
93
+ it("tier 4: stays in-process even if hqRoot is read-only (best-effort persist)", () => {
94
+ // Pre-create .hq as a regular dir, then strip write perms.
95
+ fs.mkdirSync(path.join(tmpHqRoot, ".hq"), { recursive: true });
96
+ fs.chmodSync(path.join(tmpHqRoot, ".hq"), 0o500); // r-x only
97
+ try {
98
+ const id = getOrCreateMachineId(tmpHqRoot);
99
+ expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
100
+ }
101
+ finally {
102
+ fs.chmodSync(path.join(tmpHqRoot, ".hq"), 0o700);
103
+ }
104
+ });
105
+ // ── invariant: "unknown" sentinel is unreachable ──────────────────────
106
+ it("never returns the legacy 'unknown' sentinel — every host gets a real id", () => {
107
+ // No env, no persisted file, no menubar.json — pure tier-4 path.
108
+ const id = getOrCreateMachineId(tmpHqRoot);
109
+ expect(id).not.toBe("unknown");
110
+ expect(id.length).toBeGreaterThan(6);
111
+ });
112
+ });
113
+ describe("readShortMachineId", () => {
114
+ let originalHome;
115
+ let originalEnvId;
116
+ let tmpHome;
117
+ let tmpHqRoot;
118
+ beforeEach(() => {
119
+ originalHome = process.env.HOME;
120
+ originalEnvId = process.env.HQ_MACHINE_ID;
121
+ delete process.env.HQ_MACHINE_ID;
122
+ tmpHome = freshTmp("hq-machineid-short-home-");
123
+ tmpHqRoot = freshTmp("hq-machineid-short-root-");
124
+ process.env.HOME = tmpHome;
125
+ });
126
+ afterEach(() => {
127
+ if (originalHome)
128
+ process.env.HOME = originalHome;
129
+ else
130
+ delete process.env.HOME;
131
+ if (originalEnvId !== undefined)
132
+ process.env.HQ_MACHINE_ID = originalEnvId;
133
+ else
134
+ delete process.env.HQ_MACHINE_ID;
135
+ fs.rmSync(tmpHome, { recursive: true, force: true });
136
+ fs.rmSync(tmpHqRoot, { recursive: true, force: true });
137
+ });
138
+ it("returns the first 6 chars when the resolved id has a hex prefix", () => {
139
+ process.env.HQ_MACHINE_ID = "deadbeefcafe1234567890";
140
+ expect(readShortMachineId(tmpHqRoot)).toBe("deadbe");
141
+ });
142
+ it("returns the first 6 chars of an autogenerated UUID", () => {
143
+ const short = readShortMachineId(tmpHqRoot);
144
+ expect(short).toHaveLength(6);
145
+ expect(short).toMatch(/^[0-9a-f]{6}$/);
146
+ expect(short).not.toBe("unknow"); // legacy "unknown" prefix — must not reappear
147
+ });
148
+ it("reads the same hex prefix from <hqRoot>/.hq/machine-id when persisted", () => {
149
+ fs.mkdirSync(path.join(tmpHqRoot, ".hq"), { recursive: true });
150
+ fs.writeFileSync(path.join(tmpHqRoot, ".hq", "machine-id"), "abcdef-rest-can-be-anything");
151
+ expect(readShortMachineId(tmpHqRoot)).toBe("abcdef");
152
+ });
153
+ // ── normalization invariant: short token is ALWAYS [a-f0-9]{6} ────────
154
+ //
155
+ // Regression coverage for the Codex-flagged P2 — without this, a
156
+ // non-hex `HQ_MACHINE_ID` or legacy menubar value (e.g. "ci-runner-42",
157
+ // "menubar-legacy-id") would slice to a non-hex 6-char prefix, the
158
+ // conflict filename would carry that non-hex token, and the
159
+ // `EPHEMERAL_PATH_PATTERN` in `src/cli/share.ts` (which only accepts
160
+ // `[a-f0-9]+` or the literal `unknown`) would refuse it, restoring the
161
+ // exact litter-ratchet loop this module exists to close.
162
+ it.each([
163
+ // Tier-1 env override with non-hex characters.
164
+ ["ci-runner-42"],
165
+ ["env-override-id"],
166
+ // Tier-3 legacy menubar value with non-hex characters.
167
+ ["menubar-legacy-id"],
168
+ // Mixed-case that contains non-hex letters in the first 6 chars.
169
+ ["Gabc12-rest"],
170
+ // First 6 chars are hex but contain uppercase (regex is case-sensitive).
171
+ ["ABCDEF-rest"],
172
+ ])("normalizes non-hex source ids to a hex token: %s", (sourceId) => {
173
+ process.env.HQ_MACHINE_ID = sourceId;
174
+ const short = readShortMachineId(tmpHqRoot);
175
+ expect(short).toMatch(/^[a-f0-9]{6}$/);
176
+ expect(short).toHaveLength(6);
177
+ });
178
+ it("normalization is deterministic — same source id always yields same short token", () => {
179
+ process.env.HQ_MACHINE_ID = "menubar-legacy-id";
180
+ const a = readShortMachineId(tmpHqRoot);
181
+ const b = readShortMachineId(tmpHqRoot);
182
+ expect(a).toBe(b);
183
+ expect(a).toMatch(/^[a-f0-9]{6}$/);
184
+ });
185
+ it("normalization distinguishes different source ids", () => {
186
+ process.env.HQ_MACHINE_ID = "menubar-legacy-id";
187
+ const a = readShortMachineId(tmpHqRoot);
188
+ process.env.HQ_MACHINE_ID = "ci-runner-42";
189
+ const b = readShortMachineId(tmpHqRoot);
190
+ expect(a).not.toBe(b);
191
+ expect(a).toMatch(/^[a-f0-9]{6}$/);
192
+ expect(b).toMatch(/^[a-f0-9]{6}$/);
193
+ });
194
+ });
195
+ //# sourceMappingURL=machine-id.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine-id.test.js","sourceRoot":"","sources":["../../src/lib/machine-id.test.ts"],"names":[],"mappings":"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"}
package/dist/s3.d.ts CHANGED
@@ -87,6 +87,27 @@ export declare function decodeSymlinkMetadataValue(value: string): string;
87
87
  * extension can encode additional fields if needed.
88
88
  */
89
89
  export declare const SYMLINK_BODY_PREFIX = "hq-symlink:";
90
+ /**
91
+ * S3 user-metadata key carrying the source-side file mode (permission bits
92
+ * only — \`mode & 0o777\`) as an octal string ("755", "640", etc.). On
93
+ * download, downloadFile parses this with \`parseInt(value, 8)\` and chmods
94
+ * the file to the exact source mode after the byte write.
95
+ *
96
+ * Bug #5 in the 5.33.0 deep-test was originally reported as "exec bit lost
97
+ * on sync" but the verification report broadened it: ALL modes (0600 / 0640
98
+ * / 0700 / 0750 / 0755) collapsed to the receiver's umask default (0644)
99
+ * because no mode signal crossed the wire at all. Stamping the mode in
100
+ * metadata is the smallest schema change that preserves the full
101
+ * permission bitfield without a per-host umask negotiation.
102
+ *
103
+ * Symlinks: skipped at upload time (symlink mode is OS-controlled
104
+ * lrwxrwxrwx) and skipped on download (\`fs.chmodSync\` follows symlinks
105
+ * and would mutate the target's mode instead).
106
+ *
107
+ * Back-compat: legacy uploads have no \`hq-mode\` header — the receiver
108
+ * leaves the umask default in place, matching pre-fix behavior.
109
+ */
110
+ export declare const FILE_MODE_META_KEY = "hq-mode";
90
111
  /**
91
112
  * Encode/decode the symlink wire body. Kept as exported helpers so the
92
113
  * format is centrally defined and tests can probe both sides without
package/dist/s3.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAYH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAkBhD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf;AA6BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,uBAAuB,sBAAsB,CAAC;AAE3D;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUhE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,mBAAmB,gBAAgB,CAAC;AAEjD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAqC3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CA+C3B;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC,CAiHhD;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,eAAe,CACnC,GAAG,EAAE,aAAa,EAClB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,EAAE,CAAC,CAwDvB;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAAC,CAqBvG"}
1
+ {"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAYH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAkBhD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf;AA6BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,uBAAuB,sBAAsB,CAAC;AAE3D;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUhE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,mBAAmB,gBAAgB,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,aAAa,EAClB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAsD3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CA+C3B;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC,CA+IhD;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,eAAe,CACnC,GAAG,EAAE,aAAa,EAClB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,EAAE,CAAC,CAwDvB;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAAC,CAqBvG"}