@indigoai-us/hq-cli 5.291.0 → 5.292.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 (40) hide show
  1. package/CHANGELOG.md +27 -8
  2. package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
  3. package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
  4. package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
  7. package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
  8. package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
  9. package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
  10. package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
  11. package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
  12. package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
  13. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
  14. package/dist/command-catalog.generated.d.ts +106 -1
  15. package/dist/command-catalog.generated.js +132 -1
  16. package/dist/commands/core-delegate-w1b.d.ts +8 -0
  17. package/dist/commands/core-delegate-w1b.js +1049 -0
  18. package/dist/commands/core-delegate.d.ts +28 -0
  19. package/dist/commands/core-delegate.js +15 -13
  20. package/dist/commands/core-handoff.js +152 -23
  21. package/dist/commands/core-jobs-native.d.ts +15 -0
  22. package/dist/commands/core-jobs-native.js +709 -2
  23. package/dist/commands/core-ontology.d.ts +22 -4
  24. package/dist/commands/core-ontology.js +90 -8
  25. package/dist/commands/core.js +10 -0
  26. package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
  27. package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
  28. package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
  29. package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
  30. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
  31. package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
  32. package/dist/lib/daemon/flag.d.ts +2 -0
  33. package/dist/lib/daemon/flag.js +2 -0
  34. package/dist/utils/scaffold-sibling-references.js +4 -4
  35. package/dist/utils/self-update.d.ts +6 -0
  36. package/dist/utils/self-update.js +28 -6
  37. package/dist/utils/staged-install.d.ts +34 -0
  38. package/dist/utils/staged-install.js +296 -0
  39. package/package.json +1 -1
  40. package/scripts/mesh-daemon-if-stale.mjs +4 -0
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Staged npm self-update: install the new hq-cli beside the running package,
3
+ * check that it starts, then swap it in with two renames.
4
+ *
5
+ * The in-place update moves the live package aside for the whole
6
+ * `npm install -g`, so anything that starts `hq` during the install finds no
7
+ * CLI on disk ("hq: command not found", MODULE_NOT_FOUND; outposts primary and
8
+ * 2, 5.290.0 -> 5.291.0). Here the live package stays in place until the new
9
+ * one is complete and has answered `--version`.
10
+ *
11
+ * Only a plain npm global layout is staged: `<prefix>/lib/node_modules/<pkg>`
12
+ * as a real directory, with bin links in `<prefix>/bin`. pnpm, Bun, Windows
13
+ * and linked checkouts keep the in-place update.
14
+ */
15
+ import { spawnSync } from "node:child_process";
16
+ import { randomBytes } from "node:crypto";
17
+ import * as fs from "node:fs";
18
+ import * as path from "node:path";
19
+ import { CLI_NAME } from "../cli-version.js";
20
+ import { runUpdateCommand } from "./version-gate.js";
21
+ /** Set for the staging install; hq-cli's own postinstall skips itself on it. */
22
+ export const STAGED_INSTALL_ENV = "HQ_STAGED_INSTALL";
23
+ const PROBE_TIMEOUT_MS = 60_000;
24
+ const POSTINSTALL_TIMEOUT_MS = 120_000;
25
+ export function canStageInstall(install, platform = process.platform) {
26
+ if (platform === "win32" || install.manager !== "npm")
27
+ return false;
28
+ if (!install.prefix || !install.packageRoot)
29
+ return false;
30
+ if (path.resolve(install.packageRoot) !== path.resolve(install.prefix, "lib", "node_modules", CLI_NAME)) {
31
+ return false;
32
+ }
33
+ try {
34
+ const entry = fs.lstatSync(install.packageRoot);
35
+ return entry.isDirectory() && !entry.isSymbolicLink();
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ export function stagedNpmInstall(install, version, deps = {}) {
42
+ if (!canStageInstall(install, deps.platform)) {
43
+ return { ok: false, detail: "this hq-cli install is not a plain npm global install" };
44
+ }
45
+ const prefix = install.prefix;
46
+ const live = install.packageRoot;
47
+ const env = deps.env ?? process.env;
48
+ const run = deps.run ?? runUpdateCommand;
49
+ const rename = deps.rename ?? fs.renameSync;
50
+ const remove = deps.remove ?? ((target) => fs.rmSync(target, { recursive: true, force: true }));
51
+ const probe = deps.probeVersion ?? ((root) => probeHqVersion(root, env));
52
+ const postinstall = deps.runPostinstall ?? ((root) => runPackagePostinstall(root, env));
53
+ // Beside the live package, so both renames stay on one filesystem.
54
+ const tag = `${process.pid}-${randomBytes(8).toString("hex")}`;
55
+ const staging = path.join(path.dirname(live), `.hq-cli-staging-${tag}`);
56
+ const staged = path.join(staging, "lib", "node_modules", CLI_NAME);
57
+ const backup = path.join(path.dirname(live), `.hq-cli-backup-${tag}`);
58
+ let keepBackup = false;
59
+ try {
60
+ const result = run("npm", ["install", "-g", "--prefix", staging, `${CLI_NAME}@${version}`], { ...env, [STAGED_INSTALL_ENV]: "1" });
61
+ if (!result.ok)
62
+ return result;
63
+ const reported = probe(staged);
64
+ if (reported !== version) {
65
+ return {
66
+ ok: false,
67
+ detail: `the staged hq-cli reported ${reported ?? "no version"} instead of ${version}`,
68
+ };
69
+ }
70
+ try {
71
+ rename(live, backup);
72
+ }
73
+ catch (error) {
74
+ return { ok: false, detail: `could not move the running hq-cli aside: ${message(error)}` };
75
+ }
76
+ try {
77
+ rename(staged, live);
78
+ }
79
+ catch (error) {
80
+ try {
81
+ rename(backup, live);
82
+ }
83
+ catch (restoreError) {
84
+ keepBackup = true;
85
+ return {
86
+ ok: false,
87
+ detail: `could not move hq-cli ${version} into place (${message(error)}) or restore the previous install from ${backup} (${message(restoreError)})`,
88
+ };
89
+ }
90
+ return { ok: false, detail: `could not move hq-cli ${version} into place: ${message(error)}` };
91
+ }
92
+ if (!linkBins(prefix, live, readBins(backup), tag)) {
93
+ // `hq` does not reach the new package, so put the old one back.
94
+ try {
95
+ rename(live, staged);
96
+ rename(backup, live);
97
+ }
98
+ catch (error) {
99
+ keepBackup = true;
100
+ return {
101
+ ok: false,
102
+ detail: `could not link hq to hq-cli ${version} or restore the previous install from ${backup} (${message(error)})`,
103
+ };
104
+ }
105
+ return { ok: false, detail: `could not point ${path.join(prefix, "bin", "hq")} at hq-cli ${version}` };
106
+ }
107
+ try {
108
+ postinstall(live);
109
+ }
110
+ catch {
111
+ // npm runs this script as `... || true`; it never fails an install.
112
+ }
113
+ return { ok: true };
114
+ }
115
+ finally {
116
+ // Leftovers are hidden siblings npm ignores; they never change the result.
117
+ try {
118
+ remove(staging);
119
+ }
120
+ catch {
121
+ // best effort
122
+ }
123
+ if (!keepBackup) {
124
+ try {
125
+ remove(backup);
126
+ }
127
+ catch {
128
+ // best effort
129
+ }
130
+ }
131
+ }
132
+ }
133
+ /**
134
+ * Points `<prefix>/bin/<name>` at the live package for each bin it declares
135
+ * and removes hq-cli's links for bins the new version dropped. Existing npm
136
+ * links already point at the live path. A link into another package, or a
137
+ * regular file (someone's wrapper), is left alone.
138
+ *
139
+ * `hq` is settled first. If it does not resolve to a file, its link is put
140
+ * back as it was, no other bin is touched, and this returns false so the
141
+ * caller can restore the old package.
142
+ */
143
+ function linkBins(prefix, live, previousBins, tag) {
144
+ const binDir = path.join(prefix, "bin");
145
+ const ownedByHqCli = (link) => {
146
+ try {
147
+ const target = path.resolve(binDir, fs.readlinkSync(link));
148
+ return target.startsWith(`${live}${path.sep}`);
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ };
154
+ const pointAt = (link, want) => {
155
+ const temporary = `${link}.hq-cli-${tag}`;
156
+ try {
157
+ fs.mkdirSync(binDir, { recursive: true });
158
+ fs.symlinkSync(want, temporary);
159
+ fs.renameSync(temporary, link);
160
+ }
161
+ catch {
162
+ try {
163
+ fs.rmSync(temporary, { force: true });
164
+ }
165
+ catch {
166
+ // A stray temporary link is harmless.
167
+ }
168
+ }
169
+ };
170
+ const linkBin = (name, target) => {
171
+ const link = path.join(binDir, name);
172
+ const want = path.relative(binDir, path.join(live, target));
173
+ try {
174
+ const existing = fs.lstatSync(link, { throwIfNoEntry: false });
175
+ if (existing && !existing.isSymbolicLink())
176
+ return;
177
+ if (existing && fs.readlinkSync(link) === want)
178
+ return;
179
+ if (existing && !ownedByHqCli(link))
180
+ return;
181
+ }
182
+ catch {
183
+ return;
184
+ }
185
+ pointAt(link, want);
186
+ };
187
+ const bins = readBins(live);
188
+ const hqLink = path.join(binDir, "hq");
189
+ const hqBefore = symlinkTarget(hqLink);
190
+ if (bins.hq)
191
+ linkBin("hq", bins.hq);
192
+ if (!resolvesToFile(hqLink)) {
193
+ const hqAfter = symlinkTarget(hqLink);
194
+ if (hqAfter !== hqBefore) {
195
+ if (hqBefore === null) {
196
+ try {
197
+ fs.unlinkSync(hqLink);
198
+ }
199
+ catch {
200
+ // Nothing to undo.
201
+ }
202
+ }
203
+ else {
204
+ pointAt(hqLink, hqBefore);
205
+ }
206
+ }
207
+ return false;
208
+ }
209
+ for (const [name, target] of Object.entries(bins)) {
210
+ if (name !== "hq")
211
+ linkBin(name, target);
212
+ }
213
+ for (const name of Object.keys(previousBins)) {
214
+ if (name in bins)
215
+ continue;
216
+ const link = path.join(binDir, name);
217
+ try {
218
+ if (fs.lstatSync(link).isSymbolicLink() && ownedByHqCli(link))
219
+ fs.unlinkSync(link);
220
+ }
221
+ catch {
222
+ // Already gone, or not ours to remove.
223
+ }
224
+ }
225
+ return true;
226
+ }
227
+ function symlinkTarget(link) {
228
+ try {
229
+ return fs.lstatSync(link).isSymbolicLink() ? fs.readlinkSync(link) : null;
230
+ }
231
+ catch {
232
+ return null;
233
+ }
234
+ }
235
+ function resolvesToFile(link) {
236
+ try {
237
+ return fs.statSync(link).isFile();
238
+ }
239
+ catch {
240
+ return false;
241
+ }
242
+ }
243
+ function readBins(packageRoot) {
244
+ try {
245
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
246
+ if (typeof pkg.bin === "string")
247
+ return { hq: pkg.bin };
248
+ if (pkg.bin && typeof pkg.bin === "object") {
249
+ return Object.fromEntries(Object.entries(pkg.bin).filter(([, target]) => typeof target === "string"));
250
+ }
251
+ }
252
+ catch {
253
+ // An unreadable manifest declares no bins.
254
+ }
255
+ return {};
256
+ }
257
+ function probeHqVersion(packageRoot, env) {
258
+ const bin = readBins(packageRoot).hq ?? "dist/index.js";
259
+ const result = spawnSync(process.execPath, [path.join(packageRoot, bin), "--version"], {
260
+ encoding: "utf8",
261
+ env: { ...env, HQ_NO_UPDATE_CHECK: "1" },
262
+ stdio: ["ignore", "pipe", "pipe"],
263
+ timeout: PROBE_TIMEOUT_MS,
264
+ windowsHide: true,
265
+ });
266
+ if (result.status !== 0)
267
+ return null;
268
+ const lines = (result.stdout ?? "").trim().split(/\r?\n/);
269
+ return lines[lines.length - 1]?.trim() || null;
270
+ }
271
+ function runPackagePostinstall(packageRoot, env) {
272
+ let script;
273
+ try {
274
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
275
+ script = pkg.scripts?.postinstall;
276
+ }
277
+ catch {
278
+ return;
279
+ }
280
+ if (typeof script !== "string" || script.trim() === "")
281
+ return;
282
+ const childEnv = { ...env };
283
+ delete childEnv[STAGED_INSTALL_ENV];
284
+ spawnSync(script, {
285
+ cwd: packageRoot,
286
+ env: childEnv,
287
+ shell: true,
288
+ stdio: "ignore",
289
+ timeout: POSTINSTALL_TIMEOUT_MS,
290
+ windowsHide: true,
291
+ });
292
+ }
293
+ function message(error) {
294
+ return error instanceof Error ? error.message : String(error);
295
+ }
296
+ //# sourceMappingURL=staged-install.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.291.0",
3
+ "version": "5.292.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -7,6 +7,10 @@ import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
 
10
+ // A staged self-update installs into a directory it is about to rename; it
11
+ // runs this script again from the live path after the swap.
12
+ if (process.env.HQ_STAGED_INSTALL === "1") process.exit(0);
13
+
10
14
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
15
  const cli = path.join(root, "dist", "index.js");
12
16
  if (!fs.existsSync(cli)) process.exit(0);