@indigoai-us/hq-cli 5.345.147 → 5.345.148

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 (122) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/assets/bot-workers/setup/worker.yaml +11 -0
  3. package/dist/command-catalog.generated.d.ts +297 -3
  4. package/dist/command-catalog.generated.js +368 -3
  5. package/dist/commands/agent-inbox.js +2 -1
  6. package/dist/commands/cloud.js +31 -1
  7. package/dist/commands/core-refresh-vault-access-native.d.ts +17 -0
  8. package/dist/commands/core-refresh-vault-access-native.js +214 -19
  9. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
  10. package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
  11. package/dist/commands/core.js +6 -0
  12. package/dist/commands/daemon.d.ts +7 -0
  13. package/dist/commands/daemon.js +37 -3
  14. package/dist/commands/dm.js +9 -2
  15. package/dist/commands/doctor.d.ts +11 -1
  16. package/dist/commands/doctor.js +56 -1
  17. package/dist/commands/install-global.d.ts +7 -0
  18. package/dist/commands/install-global.js +129 -35
  19. package/dist/commands/lanes-shared.d.ts +3 -1
  20. package/dist/commands/lanes.d.ts +4 -4
  21. package/dist/commands/lanes.js +380 -25
  22. package/dist/commands/pack-install.d.ts +2 -2
  23. package/dist/commands/pack-install.js +133 -35
  24. package/dist/commands/sync-manifest.d.ts +2 -9
  25. package/dist/commands/sync-manifest.js +4 -15
  26. package/dist/hook-engine-path.d.ts +4 -0
  27. package/dist/hook-engine-path.js +36 -0
  28. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2783 -2354
  29. package/dist/hq-cloud-sync-runner/package.json +1 -1
  30. package/dist/index.js +6 -0
  31. package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
  32. package/dist/lib/agent-kit/inbox-redact.js +361 -0
  33. package/dist/lib/agent-kit/inbox-state.js +11 -2
  34. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  35. package/dist/lib/agent-kit/run/inbox.js +36 -7
  36. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
  37. package/dist/lib/anywhere-runtime-flag-key.js +9 -0
  38. package/dist/lib/company-skill-wrapper.js +6 -4
  39. package/dist/lib/daemon/install.d.ts +47 -1
  40. package/dist/lib/daemon/install.js +181 -15
  41. package/dist/lib/daemon/lock.d.ts +32 -0
  42. package/dist/lib/daemon/lock.js +55 -2
  43. package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
  44. package/dist/lib/doctor/checks/anywhere.d.ts +3 -0
  45. package/dist/lib/doctor/checks/anywhere.js +23 -8
  46. package/dist/lib/doctor/checks/cli-path.d.ts +11 -0
  47. package/dist/lib/doctor/checks/cli-path.js +45 -0
  48. package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
  49. package/dist/lib/doctor/checks/hook-engine.js +16 -0
  50. package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
  51. package/dist/lib/doctor/checks/sync-health.js +38 -2
  52. package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
  53. package/dist/lib/doctor/checks/temp-homes.js +54 -0
  54. package/dist/lib/doctor/fix/apply.d.ts +90 -1
  55. package/dist/lib/doctor/fix/apply.js +176 -10
  56. package/dist/lib/doctor/fix/json-output.d.ts +4 -1
  57. package/dist/lib/doctor/fix/json-output.js +6 -0
  58. package/dist/lib/doctor/fix/remediation.d.ts +7 -1
  59. package/dist/lib/doctor/fix/remediation.js +23 -1
  60. package/dist/lib/doctor/registry.js +10 -0
  61. package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
  62. package/dist/lib/doctor/temp-home-sweeper.js +418 -0
  63. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  64. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  65. package/dist/lib/flag-registry.js +9 -1
  66. package/dist/lib/flag-snapshot-file.js +8 -3
  67. package/dist/lib/hook-engine/resolve.d.ts +31 -0
  68. package/dist/lib/hook-engine/resolve.js +134 -0
  69. package/dist/lib/lanes/admission-host.d.ts +1 -0
  70. package/dist/lib/lanes/admission-host.js +8 -1
  71. package/dist/lib/lanes/admission.d.ts +4 -0
  72. package/dist/lib/lanes/admission.js +6 -2
  73. package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
  74. package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
  75. package/dist/lib/lanes/index.d.ts +2 -2
  76. package/dist/lib/lanes/index.js +1 -1
  77. package/dist/lib/lanes/keep-alive.js +4 -1
  78. package/dist/lib/lanes/local-host-identity.d.ts +40 -0
  79. package/dist/lib/lanes/local-host-identity.js +314 -0
  80. package/dist/lib/lanes/reaper.d.ts +1 -1
  81. package/dist/lib/lanes/reaper.js +7 -0
  82. package/dist/lib/lanes/reconcile.d.ts +4 -0
  83. package/dist/lib/lanes/reconcile.js +16 -8
  84. package/dist/lib/lanes/spawn.d.ts +20 -0
  85. package/dist/lib/lanes/spawn.js +48 -2
  86. package/dist/lib/lanes/status-fields.js +6 -1
  87. package/dist/lib/lanes/store.js +8 -1
  88. package/dist/lib/lanes/throttle.d.ts +49 -6
  89. package/dist/lib/lanes/throttle.js +154 -17
  90. package/dist/lib/lanes/types.d.ts +22 -1
  91. package/dist/lib/lanes/types.js +27 -3
  92. package/dist/lib/lanes/watcher.js +27 -2
  93. package/dist/lib/lanes/worker-continuity.d.ts +2 -0
  94. package/dist/lib/lanes/worker-continuity.js +18 -11
  95. package/dist/lib/manifest-disabled.d.ts +13 -0
  96. package/dist/lib/manifest-disabled.js +19 -0
  97. package/dist/lib/service-manager/launchd.d.ts +1 -0
  98. package/dist/lib/service-manager/launchd.js +1 -1
  99. package/dist/lib/windows/user-path.d.ts +48 -0
  100. package/dist/lib/windows/user-path.js +114 -0
  101. package/dist/lib/work-context/auto-bind.d.ts +1 -1
  102. package/dist/lib/work-context/auto-bind.js +22 -3
  103. package/dist/lib/work-context/lane-bind.d.ts +31 -0
  104. package/dist/lib/work-context/lane-bind.js +101 -0
  105. package/dist/lib/work-context/organize.js +1 -0
  106. package/dist/lib/work-context/prd-sync.js +7 -0
  107. package/dist/lib/work-context/project.d.ts +36 -10
  108. package/dist/lib/work-context/project.js +130 -41
  109. package/dist/lib/work-context/reconcile.d.ts +6 -0
  110. package/dist/lib/work-context/reconcile.js +25 -5
  111. package/dist/lib/work-context/state.d.ts +6 -0
  112. package/dist/lib/work-context/state.js +29 -1
  113. package/dist/utils/copy-tree.d.ts +61 -0
  114. package/dist/utils/copy-tree.js +249 -0
  115. package/dist/utils/environmental-error.js +7 -3
  116. package/dist/utils/npm-invocation.d.ts +27 -0
  117. package/dist/utils/npm-invocation.js +54 -0
  118. package/dist/utils/safe-link.d.ts +50 -0
  119. package/dist/utils/safe-link.js +300 -0
  120. package/package.json +17 -5
  121. package/scripts/hook-engine-manifest.mjs +22 -0
  122. package/scripts/install-hook-engine.mjs +10 -0
@@ -0,0 +1,300 @@
1
+ // src/utils/safe-link.ts
2
+ //
3
+ // Directory links that degrade gracefully on Windows, and link removal that
4
+ // never touches the link target.
5
+ //
6
+ // A plain `fs.symlinkSync` on Windows needs SeCreateSymbolicLinkPrivilege
7
+ // (Developer Mode or an elevated shell). A standard user gets EPERM, which is
8
+ // what broke `hq install --global` on a cold Windows machine. A directory
9
+ // junction needs no privilege, so it is the fallback for directory targets.
10
+ // Node reports a junction as `isSymbolicLink()` and `readlinkSync` returns its
11
+ // absolute target, so code that recognises HQ links keeps working.
12
+ //
13
+ // A junction must point at an absolute path on a local volume. When the target
14
+ // is on a network share, or Windows refuses the junction too, the last resort
15
+ // is a copy of the folder, marked with a `.hq-skill-copy` file that records the
16
+ // source, the entry name and a hash of the copied files. Uninstall removes a
17
+ // skill folder only when it is a link or carries that marker, so a folder the
18
+ // user made by hand is never deleted. A marked copy whose files changed, or
19
+ // whose marker names a different entry, is kept with a printed notice.
20
+ //
21
+ // Removing a junction recursively (`rm -r`, `rmdir /s`, `fs.rmSync(..,
22
+ // { recursive: true })`) can delete the files of the folder it points to. Every
23
+ // HQ path that removes a skill link goes through `removeLinkOnly`, which removes
24
+ // the link entry and nothing else.
25
+ import * as crypto from 'crypto';
26
+ import * as fs from 'fs';
27
+ import * as path from 'path';
28
+ /** Marker file written into a skill folder that HQ copied instead of linked. */
29
+ export const SKILL_COPY_MARKER = '.hq-skill-copy';
30
+ /** Thrown when Windows refuses a directory symlink, a junction and a copy. */
31
+ export class LinkUnavailableError extends Error {
32
+ linkPath;
33
+ expected = true;
34
+ constructor(linkPath, cause) {
35
+ super(`Windows refused to create a link at ${linkPath}. ` +
36
+ 'Turn on Developer Mode (Settings > System > For developers) or run from an elevated terminal, then run the command again.', { cause });
37
+ this.linkPath = linkPath;
38
+ this.name = 'LinkUnavailableError';
39
+ }
40
+ }
41
+ const PRIVILEGE_CODES = new Set(['EPERM', 'EACCES']);
42
+ // A junction to a network path fails with EINVAL or ENOTSUP in libuv. EXDEV
43
+ // and the privilege codes cover the other refusals.
44
+ const JUNCTION_REFUSED_CODES = new Set([...PRIVILEGE_CODES, 'EINVAL', 'ENOTSUP', 'EXDEV']);
45
+ function hasCode(err, codes) {
46
+ const code = err?.code;
47
+ return typeof code === 'string' && codes.has(code);
48
+ }
49
+ /**
50
+ * Link `linkPath` to the directory `target`. On Windows a privilege refusal
51
+ * falls back to a junction (absolute target, no privilege needed), and a
52
+ * refused junction falls back to a marked copy unless `allowCopy` is false.
53
+ * Other platforms keep the plain symlink behaviour.
54
+ */
55
+ export function linkDirectory(target, linkPath, deps = {}) {
56
+ const platform = deps.platform ?? process.platform;
57
+ if (platform !== 'win32') {
58
+ fs.symlinkSync(target, linkPath);
59
+ return 'symlink';
60
+ }
61
+ try {
62
+ fs.symlinkSync(target, linkPath, 'dir');
63
+ return 'symlink';
64
+ }
65
+ catch (err) {
66
+ if (!hasCode(err, PRIVILEGE_CODES))
67
+ throw err;
68
+ }
69
+ // Junctions require an absolute target; resolve a relative one against the
70
+ // link's own folder, which is how the symlink would have resolved it.
71
+ const absolute = path.resolve(path.dirname(linkPath), target);
72
+ try {
73
+ fs.symlinkSync(absolute, linkPath, 'junction');
74
+ return 'junction';
75
+ }
76
+ catch (err) {
77
+ if (!hasCode(err, JUNCTION_REFUSED_CODES))
78
+ throw err;
79
+ if (deps.allowCopy === false)
80
+ throw new LinkUnavailableError(linkPath, err);
81
+ try {
82
+ copySkillFolder(absolute, linkPath);
83
+ }
84
+ catch (copyErr) {
85
+ throw new LinkUnavailableError(linkPath, copyErr);
86
+ }
87
+ return 'copy';
88
+ }
89
+ }
90
+ function copySkillFolder(source, dest) {
91
+ let created = false;
92
+ try {
93
+ fs.mkdirSync(dest);
94
+ created = true;
95
+ copyTreeDereferenced(source, dest, new Set([fs.realpathSync(source)]));
96
+ const marker = { source, name: path.basename(dest), hash: skillCopyHash(dest) };
97
+ fs.writeFileSync(path.join(dest, SKILL_COPY_MARKER), `${JSON.stringify(marker)}\n`);
98
+ }
99
+ catch (err) {
100
+ // Remove the partial copy, but only when this call created the folder.
101
+ if (created) {
102
+ try {
103
+ removeTreeNoFollow(dest);
104
+ }
105
+ catch { /* report the copy error */ }
106
+ }
107
+ throw err;
108
+ }
109
+ }
110
+ /**
111
+ * Copy the contents of `source` into the existing folder `dest`, following
112
+ * links. `ancestors` holds the real paths of the folders on the current path
113
+ * from the copy root down to `source`, so only a link back to one of them (a
114
+ * loop) is skipped. Two links to the same folder, or a link to a sibling
115
+ * folder, are each copied in full.
116
+ */
117
+ function copyTreeDereferenced(source, dest, ancestors) {
118
+ for (const entry of fs.readdirSync(source)) {
119
+ const from = path.join(source, entry);
120
+ const to = path.join(dest, entry);
121
+ const st = fs.statSync(from);
122
+ if (st.isDirectory()) {
123
+ const real = fs.realpathSync(from);
124
+ if (ancestors.has(real))
125
+ continue;
126
+ fs.mkdirSync(to);
127
+ ancestors.add(real);
128
+ try {
129
+ copyTreeDereferenced(from, to, ancestors);
130
+ }
131
+ finally {
132
+ ancestors.delete(real);
133
+ }
134
+ }
135
+ else if (st.isFile()) {
136
+ fs.copyFileSync(from, to, fs.constants.COPYFILE_EXCL);
137
+ }
138
+ }
139
+ }
140
+ /**
141
+ * A sha256 over the names and contents of everything in a skill copy, except
142
+ * the marker file itself. Links are hashed by their target text, not followed.
143
+ */
144
+ function skillCopyHash(dir) {
145
+ const hash = crypto.createHash('sha256');
146
+ const walk = (rel) => {
147
+ const abs = rel === '' ? dir : path.join(dir, rel);
148
+ for (const entry of fs.readdirSync(abs).sort()) {
149
+ if (rel === '' && entry === SKILL_COPY_MARKER)
150
+ continue;
151
+ const childRel = rel === '' ? entry : `${rel}/${entry}`;
152
+ const child = path.join(abs, entry);
153
+ const st = fs.lstatSync(child);
154
+ if (st.isSymbolicLink()) {
155
+ hash.update(`l:${childRel}\0${fs.readlinkSync(child)}\0`);
156
+ }
157
+ else if (st.isDirectory()) {
158
+ hash.update(`d:${childRel}\0`);
159
+ walk(childRel);
160
+ }
161
+ else {
162
+ const content = crypto.createHash('sha256').update(fs.readFileSync(child)).digest('hex');
163
+ hash.update(`f:${childRel}\0${content}\0`);
164
+ }
165
+ }
166
+ };
167
+ walk('');
168
+ return hash.digest('hex');
169
+ }
170
+ function readSkillCopyMarker(p) {
171
+ try {
172
+ if (!fs.lstatSync(p).isDirectory())
173
+ return null;
174
+ const parsed = JSON.parse(fs.readFileSync(path.join(p, SKILL_COPY_MARKER), 'utf-8'));
175
+ if (typeof parsed.source !== 'string' || parsed.source.length === 0)
176
+ return null;
177
+ return {
178
+ source: parsed.source,
179
+ name: typeof parsed.name === 'string' ? parsed.name : undefined,
180
+ hash: typeof parsed.hash === 'string' ? parsed.hash : undefined,
181
+ };
182
+ }
183
+ catch {
184
+ return null;
185
+ }
186
+ }
187
+ /**
188
+ * The source folder recorded in a skill copy's marker, or null when `p` is
189
+ * not a real folder carrying a valid marker.
190
+ */
191
+ export function skillCopySource(p) {
192
+ return readSkillCopyMarker(p)?.source ?? null;
193
+ }
194
+ /** Delete a real folder tree without following any link inside it. */
195
+ function removeTreeNoFollow(p) {
196
+ const st = fs.lstatSync(p);
197
+ if (st.isSymbolicLink()) {
198
+ removeLinkOnly(p);
199
+ return;
200
+ }
201
+ if (!st.isDirectory()) {
202
+ fs.unlinkSync(p);
203
+ return;
204
+ }
205
+ for (const entry of fs.readdirSync(p))
206
+ removeTreeNoFollow(path.join(p, entry));
207
+ fs.rmdirSync(p);
208
+ }
209
+ /**
210
+ * Remove an HQ skill entry: a link (never recursing into it) or a folder HQ
211
+ * copied and marked. Refuses an unmarked real folder. A marked copy is kept,
212
+ * with a notice through `onKept`, when its marker names a different entry
213
+ * (the user renamed or duplicated it) or its files changed since HQ copied
214
+ * them. Returns false when nothing exists at `p` or the copy was kept.
215
+ */
216
+ export function removeSkillEntry(p, onKept = (line) => console.warn(line)) {
217
+ let st;
218
+ try {
219
+ st = fs.lstatSync(p);
220
+ }
221
+ catch (err) {
222
+ if (err.code === 'ENOENT')
223
+ return false;
224
+ throw err;
225
+ }
226
+ if (st.isSymbolicLink())
227
+ return removeLinkOnly(p);
228
+ const marker = readSkillCopyMarker(p);
229
+ if (marker === null) {
230
+ throw new Error(`Refusing to remove ${p}: it is not a link or a skill copy made by HQ.`);
231
+ }
232
+ const name = path.basename(p);
233
+ if (marker.name === undefined || marker.hash === undefined) {
234
+ onKept(`Kept ${p}: HQ cannot confirm it is an unchanged copy it made. Remove it yourself if you do not need it.`);
235
+ return false;
236
+ }
237
+ if (marker.name !== name) {
238
+ onKept(`Kept ${p}: HQ made it as ${marker.name}, so it is a renamed or duplicated copy. Remove it yourself if you do not need it.`);
239
+ return false;
240
+ }
241
+ if (skillCopyHash(p) !== marker.hash) {
242
+ onKept(`Kept ${p}: its files changed since HQ copied them. Remove it yourself when you no longer need the changes.`);
243
+ return false;
244
+ }
245
+ removeTreeNoFollow(p);
246
+ return true;
247
+ }
248
+ /**
249
+ * Remove whatever is at `p` without following any link: a link at `p` goes
250
+ * through `removeLinkOnly`, and a real folder is removed entry by entry so a
251
+ * link nested inside it is removed as a link. With `force`, a missing path is
252
+ * not an error.
253
+ */
254
+ export function removeEntryNoFollow(p, opts = {}) {
255
+ let st;
256
+ try {
257
+ st = fs.lstatSync(p);
258
+ }
259
+ catch (err) {
260
+ if (opts.force && err.code === 'ENOENT')
261
+ return false;
262
+ throw err;
263
+ }
264
+ if (st.isSymbolicLink())
265
+ return removeLinkOnly(p);
266
+ removeTreeNoFollow(p);
267
+ return true;
268
+ }
269
+ /**
270
+ * Remove a symlink or junction without recursing into it. Refuses anything
271
+ * that is not a link, so a real folder is never deleted through this path.
272
+ * Returns false when nothing exists at `linkPath`.
273
+ */
274
+ export function removeLinkOnly(linkPath) {
275
+ let st;
276
+ try {
277
+ st = fs.lstatSync(linkPath);
278
+ }
279
+ catch (err) {
280
+ if (err.code === 'ENOENT')
281
+ return false;
282
+ throw err;
283
+ }
284
+ if (!st.isSymbolicLink()) {
285
+ throw new Error(`Refusing to remove ${linkPath}: it is not a link.`);
286
+ }
287
+ try {
288
+ fs.unlinkSync(linkPath);
289
+ }
290
+ catch (err) {
291
+ const code = err.code;
292
+ // Some Windows builds report a directory junction as a directory to
293
+ // unlink(). A non-recursive rmdir removes only the junction entry.
294
+ if (code !== 'EPERM' && code !== 'EISDIR')
295
+ throw err;
296
+ fs.rmdirSync(linkPath);
297
+ }
298
+ return true;
299
+ }
300
+ //# sourceMappingURL=safe-link.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.147",
3
+ "version": "5.345.148",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -21,7 +21,9 @@
21
21
  "CHANGELOG.md",
22
22
  "README.md",
23
23
  "contracts",
24
- "scripts/mesh-daemon-if-stale.mjs"
24
+ "scripts/mesh-daemon-if-stale.mjs",
25
+ "scripts/install-hook-engine.mjs",
26
+ "scripts/hook-engine-manifest.mjs"
25
27
  ],
26
28
  "scripts": {
27
29
  "build": "node scripts/generate-dsn.mjs && node scripts/build-flag-registry-worker.mjs && tsc && node scripts/generate-command-catalog.mjs --check && node scripts/copy-schemas.mjs && node scripts/chmod-bins.mjs && node scripts/bundle-sync-runner.mjs",
@@ -40,15 +42,17 @@
40
42
  "check:lanes-module-cycles": "node scripts/check-lanes-module-cycles.mjs",
41
43
  "vitest": "vitest",
42
44
  "clean": "rm -rf dist",
43
- "postinstall": "node scripts/mesh-daemon-if-stale.mjs || true",
45
+ "postinstall": "node scripts/install-hook-engine.mjs && node scripts/mesh-daemon-if-stale.mjs || node -e \"process.exit(0)\"",
44
46
  "prepare": "husky || true",
45
- "check:no-hermes": "bash scripts/check-no-hermes.sh"
47
+ "check:no-hermes": "bash scripts/check-no-hermes.sh",
48
+ "prepack": "node scripts/hook-engine-manifest.mjs",
49
+ "postpublish": "node scripts/hook-engine-manifest.mjs restore"
46
50
  },
47
51
  "dependencies": {
48
52
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
49
53
  "@aws-sdk/client-s3": "^3.1049.0",
50
54
  "@aws-sdk/dsql-signer": "^3.1146.0",
51
- "@indigoai-us/hq-cloud": "~6.18.87",
55
+ "@indigoai-us/hq-cloud": "~6.18.88",
52
56
  "@indigoai-us/hq-flags-client": "^0.1.2",
53
57
  "@modelcontextprotocol/sdk": "1.29.0",
54
58
  "@sentry/node": "^10.49.0",
@@ -108,5 +112,13 @@
108
112
  },
109
113
  "lint-staged": {
110
114
  "*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
115
+ },
116
+ "optionalDependencies": {
117
+ "@indigoai-us/hq-hook-win32-x64": "5.345.148",
118
+ "@indigoai-us/hq-hook-win32-arm64": "5.345.148",
119
+ "@indigoai-us/hq-hook-darwin-x64": "5.345.148",
120
+ "@indigoai-us/hq-hook-darwin-arm64": "5.345.148",
121
+ "@indigoai-us/hq-hook-linux-x64": "5.345.148",
122
+ "@indigoai-us/hq-hook-linux-arm64": "5.345.148"
111
123
  }
112
124
  }
@@ -0,0 +1,22 @@
1
+ // The six packages do not exist before the first approved release. Keep source
2
+ // installs reproducible; add exact-version optional dependencies to the tarball.
3
+ import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
4
+ const platforms = ['win32-x64', 'win32-arm64', 'darwin-x64', 'darwin-arm64', 'linux-x64', 'linux-arm64'];
5
+ const backup = '.hook-engine-package-backup.json';
6
+ if (process.argv[2] === 'restore') {
7
+ if (existsSync(backup)) { writeFileSync('package.json', readFileSync(backup)); unlinkSync(backup); }
8
+ } else {
9
+ const source = readFileSync('package.json', 'utf8');
10
+ const pkg = JSON.parse(source);
11
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(pkg.version)) throw new Error('Invalid package version');
12
+ // npm publish reads package.json again AFTER postpack. Do not restore it in
13
+ // postpack or registry metadata loses optionalDependencies. Restore only in
14
+ // postpublish (or explicitly after a local pack). Repeated smoke packs are safe.
15
+ // A backup is current only while package.json still carries the stamped deps.
16
+ // An unstamped package.json means the backup is left over from an earlier
17
+ // local pack, so replace it rather than restore stale content later.
18
+ const stamped = platforms.every(p => pkg.optionalDependencies?.[`@indigoai-us/hq-hook-${p}`] === pkg.version);
19
+ if (!existsSync(backup) || !stamped) writeFileSync(backup, source);
20
+ pkg.optionalDependencies = { ...pkg.optionalDependencies, ...Object.fromEntries(platforms.map(p => [`@indigoai-us/hq-hook-${p}`, pkg.version])) };
21
+ writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
22
+ }
@@ -0,0 +1,10 @@
1
+ import { existsSync } from 'node:fs';
2
+ // A source checkout has not built dist during pnpm install. Published tarballs
3
+ // never contain src; a broken tarball warns and keeps existing bash hooks active.
4
+ if (!existsSync(new URL('../dist/lib/hook-engine/resolve.js', import.meta.url)) && existsSync(new URL('../src/index.ts', import.meta.url))) process.exit(0);
5
+ try {
6
+ const { installHookEngine } = await import('../dist/lib/hook-engine/resolve.js');
7
+ if (!installHookEngine()) throw new Error('Optional platform package missing');
8
+ } catch (error) {
9
+ console.warn(`HQ native hook engine unavailable: ${error instanceof Error ? error.message : String(error)}. Existing bash hooks remain active. Repair: npm install -g @indigoai-us/hq-cli --include=optional`);
10
+ }