@indigoai-us/hq-cli 5.78.0 → 5.80.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.
- package/CHANGELOG.md +8 -0
- package/dist/commands/cloud.d.ts +9 -0
- package/dist/commands/cloud.js +53 -1
- package/dist/commands/meetings.js +16 -9
- package/dist/commands/packs.js +1 -1
- package/dist/index.js +6 -2
- package/dist/utils/pack-contributions.d.ts +6 -1
- package/dist/utils/pack-contributions.js +49 -7
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.79.0]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Meeting commands now print a friendly `Meeting notetaker bots require the
|
|
10
|
+
$500/mo HQ Team plan` upgrade message instead of raw `API error (402)`;
|
|
11
|
+
`--json` continues to emit the raw body. (#281)
|
|
12
|
+
|
|
5
13
|
## [5.78.0]
|
|
6
14
|
|
|
7
15
|
### Added
|
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ import { type BannerLevel } from "../lib/narrow-hint-banner.js";
|
|
|
22
22
|
* succeed when files were dropped" guarantee unit-testable.
|
|
23
23
|
*/
|
|
24
24
|
export declare function scopeExcludedWarning(count: number): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Optional detail line listing the effective write prefixes when a push
|
|
27
|
+
* dropped files — helps debug ACL vs scope-excluded mismatches (feedback_7e9c535e).
|
|
28
|
+
*/
|
|
29
|
+
export declare function scopeExcludedPrefixDetail(prefixSet: readonly string[] | undefined): string | null;
|
|
25
30
|
/**
|
|
26
31
|
* Bound foreground waits for a watcher/manual sync that currently owns the
|
|
27
32
|
* per-root operation lock. The cloud engine reads this environment value on
|
|
@@ -159,6 +164,8 @@ export interface ShareCallOptions {
|
|
|
159
164
|
skipUnchanged?: boolean;
|
|
160
165
|
propagateDeletes?: boolean;
|
|
161
166
|
propagateDeletePolicy?: "owned-only" | "currency-gated" | "all";
|
|
167
|
+
/** DEV-1768 push parity: filter upload plan to granted ACL prefixes. */
|
|
168
|
+
prefixSet?: PullScope["prefixSet"];
|
|
162
169
|
}
|
|
163
170
|
export interface ShareCallResult {
|
|
164
171
|
filesUploaded: number;
|
|
@@ -172,6 +179,8 @@ export interface ShareCallResult {
|
|
|
172
179
|
export interface PushAllDeps {
|
|
173
180
|
vaultClient: PullAllVaultClient;
|
|
174
181
|
share: (options: ShareCallOptions) => Promise<ShareCallResult>;
|
|
182
|
+
/** DEV-1768: resolve membership push scope (same as pull). */
|
|
183
|
+
resolveScope?: (companyUid: string, slug: string) => Promise<PullScope>;
|
|
175
184
|
}
|
|
176
185
|
export interface PushAllOptions {
|
|
177
186
|
hqRoot: string;
|
package/dist/commands/cloud.js
CHANGED
|
@@ -32,6 +32,16 @@ export function scopeExcludedWarning(count) {
|
|
|
32
32
|
`They were skipped, not synced. Re-run with --json to list them, or ` +
|
|
33
33
|
`ask an admin to grant you write on those paths.`);
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Optional detail line listing the effective write prefixes when a push
|
|
37
|
+
* dropped files — helps debug ACL vs scope-excluded mismatches (feedback_7e9c535e).
|
|
38
|
+
*/
|
|
39
|
+
export function scopeExcludedPrefixDetail(prefixSet) {
|
|
40
|
+
if (!prefixSet || prefixSet.length === 0) {
|
|
41
|
+
return " Granted write prefixes: (none — shared-mode membership with no explicit grants)";
|
|
42
|
+
}
|
|
43
|
+
return ` Granted write prefixes: ${prefixSet.join(", ")}`;
|
|
44
|
+
}
|
|
35
45
|
/**
|
|
36
46
|
* Resolve the `propagateDeletePolicy` for share() calls.
|
|
37
47
|
*
|
|
@@ -268,6 +278,7 @@ export async function pushAll(options, deps) {
|
|
|
268
278
|
}
|
|
269
279
|
plan.push({
|
|
270
280
|
slug,
|
|
281
|
+
companyUid: m.companyUid,
|
|
271
282
|
shareOptions: {
|
|
272
283
|
company: m.companyUid,
|
|
273
284
|
hqRoot: options.hqRoot,
|
|
@@ -284,6 +295,7 @@ export async function pushAll(options, deps) {
|
|
|
284
295
|
if (personal) {
|
|
285
296
|
plan.push({
|
|
286
297
|
slug: "personal",
|
|
298
|
+
companyUid: personal.uid,
|
|
287
299
|
shareOptions: {
|
|
288
300
|
company: personal.uid,
|
|
289
301
|
hqRoot: options.hqRoot,
|
|
@@ -309,6 +321,17 @@ export async function pushAll(options, deps) {
|
|
|
309
321
|
};
|
|
310
322
|
for (const entry of plan) {
|
|
311
323
|
result.attempted += 1;
|
|
324
|
+
if (entry.slug !== "personal" && deps.resolveScope) {
|
|
325
|
+
try {
|
|
326
|
+
const scope = await deps.resolveScope(entry.companyUid, entry.slug);
|
|
327
|
+
if (scope.prefixSet !== undefined) {
|
|
328
|
+
entry.shareOptions.prefixSet = scope.prefixSet;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// Degrade to no explicit scope filter — share() treats undefined as full access.
|
|
333
|
+
}
|
|
334
|
+
}
|
|
312
335
|
try {
|
|
313
336
|
const r = await deps.share(entry.shareOptions);
|
|
314
337
|
result.filesUploaded += r.filesUploaded;
|
|
@@ -582,6 +605,8 @@ export function registerCloudCommands(program) {
|
|
|
582
605
|
// 2. default: vend via cached Cognito session (the human CLI path).
|
|
583
606
|
let entityContext;
|
|
584
607
|
let vaultConfig;
|
|
608
|
+
/** Vault API config for membership/grant scope lookups. Set even on the pre-vended `--creds-from-stdin` path — share() accepts entityContext OR vaultConfig, not both. */
|
|
609
|
+
let scopeVaultConfig;
|
|
585
610
|
if (options.credsFromStdin) {
|
|
586
611
|
if (process.stdin.isTTY) {
|
|
587
612
|
throw new Error("--creds-from-stdin requires JSON on stdin, but stdin is a " +
|
|
@@ -595,17 +620,22 @@ export function registerCloudCommands(program) {
|
|
|
595
620
|
catch (e) {
|
|
596
621
|
throw new Error(`--creds-from-stdin: failed to parse stdin as JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
597
622
|
}
|
|
623
|
+
// Upload creds are pre-vended in entityContext, but prefix filtering
|
|
624
|
+
// still needs vault-service membership/grant resolution.
|
|
625
|
+
const accessToken = await ensureCognitoToken();
|
|
626
|
+
scopeVaultConfig = buildVaultConfig(accessToken);
|
|
598
627
|
}
|
|
599
628
|
else {
|
|
600
629
|
const accessToken = await ensureCognitoToken();
|
|
601
630
|
vaultConfig = buildVaultConfig(accessToken);
|
|
631
|
+
scopeVaultConfig = vaultConfig;
|
|
602
632
|
}
|
|
603
633
|
// Resolve the target. For `--personal`, look up the caller's
|
|
604
634
|
// canonical person entity and force personalMode + journalSlug so
|
|
605
635
|
// share() lands files at hqRoot directly (no companies/<slug>/
|
|
606
636
|
// prefix). For everything else, the company is whatever the user
|
|
607
637
|
// passed or the active company from .hq/config.json.
|
|
608
|
-
let targetCompany = options.company;
|
|
638
|
+
let targetCompany = options.company ?? entityContext?.uid;
|
|
609
639
|
let personalMode = false;
|
|
610
640
|
let journalSlug;
|
|
611
641
|
if (options.personal) {
|
|
@@ -646,6 +676,12 @@ export function registerCloudCommands(program) {
|
|
|
646
676
|
// idToken — pre-vended `--creds-from-stdin` paths still get author
|
|
647
677
|
// attribution as long as the caller is logged in locally.
|
|
648
678
|
const author = resolveUploadAuthorFromCache();
|
|
679
|
+
let pushPrefixSet;
|
|
680
|
+
if (!personalMode && scopeVaultConfig) {
|
|
681
|
+
const scopeClient = new VaultClient(scopeVaultConfig);
|
|
682
|
+
const scope = await resolveCliPullScope(scopeClient, targetCompany, options.hqRoot);
|
|
683
|
+
pushPrefixSet = scope?.prefixSet;
|
|
684
|
+
}
|
|
649
685
|
const result = await share({
|
|
650
686
|
paths: targetPaths,
|
|
651
687
|
company: targetCompany,
|
|
@@ -658,6 +694,7 @@ export function registerCloudCommands(program) {
|
|
|
658
694
|
...(personalMode ? { personalMode: true } : {}),
|
|
659
695
|
...(journalSlug !== undefined ? { journalSlug } : {}),
|
|
660
696
|
...(author ? { author } : {}),
|
|
697
|
+
...(pushPrefixSet !== undefined ? { prefixSet: pushPrefixSet } : {}),
|
|
661
698
|
});
|
|
662
699
|
if (jsonMode) {
|
|
663
700
|
// Synthetic terminal event so subprocess consumers can read final
|
|
@@ -681,6 +718,9 @@ export function registerCloudCommands(program) {
|
|
|
681
718
|
log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
|
|
682
719
|
`${result.filesSkipped} skipped, ${result.filesExcludedByScope} scope-excluded)`));
|
|
683
720
|
log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)));
|
|
721
|
+
const prefixDetail = scopeExcludedPrefixDetail(pushPrefixSet);
|
|
722
|
+
if (prefixDetail)
|
|
723
|
+
log(chalk.yellow(prefixDetail));
|
|
684
724
|
}
|
|
685
725
|
else {
|
|
686
726
|
log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
|
|
@@ -1091,6 +1131,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
|
|
|
1091
1131
|
...(skipPersonal ? { skipPersonal: true } : {}),
|
|
1092
1132
|
}, {
|
|
1093
1133
|
vaultClient: adapter,
|
|
1134
|
+
resolveScope: (companyUid, slug) => resolvePullScope(realClient, companyUid, slug, hqRoot),
|
|
1094
1135
|
share: (opts) => share({
|
|
1095
1136
|
paths: opts.paths,
|
|
1096
1137
|
company: opts.company,
|
|
@@ -1113,6 +1154,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
|
|
|
1113
1154
|
...(opts.propagateDeletePolicy !== undefined
|
|
1114
1155
|
? { propagateDeletePolicy: opts.propagateDeletePolicy }
|
|
1115
1156
|
: {}),
|
|
1157
|
+
...(opts.prefixSet !== undefined ? { prefixSet: opts.prefixSet } : {}),
|
|
1116
1158
|
...(author ? { author } : {}),
|
|
1117
1159
|
}),
|
|
1118
1160
|
});
|
|
@@ -1195,6 +1237,12 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1195
1237
|
// Push first so the subsequent pull doesn't redownload files we were
|
|
1196
1238
|
// about to broadcast (matches hq-sync-runner ordering).
|
|
1197
1239
|
console.log(chalk.dim(" → push leg"));
|
|
1240
|
+
let nowPushPrefixSet;
|
|
1241
|
+
if (!personalMode) {
|
|
1242
|
+
const scopeClient = new VaultClient(vaultConfig);
|
|
1243
|
+
const pushScope = await resolveCliPullScope(scopeClient, targetCompany, hqRoot);
|
|
1244
|
+
nowPushPrefixSet = pushScope?.prefixSet;
|
|
1245
|
+
}
|
|
1198
1246
|
const pushResult = await share({
|
|
1199
1247
|
paths: pushPaths,
|
|
1200
1248
|
company: targetCompany,
|
|
@@ -1208,6 +1256,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1208
1256
|
...(personalMode ? { personalMode: true } : {}),
|
|
1209
1257
|
...(journalSlug !== undefined ? { journalSlug } : {}),
|
|
1210
1258
|
...(author ? { author } : {}),
|
|
1259
|
+
...(nowPushPrefixSet !== undefined ? { prefixSet: nowPushPrefixSet } : {}),
|
|
1211
1260
|
});
|
|
1212
1261
|
const pushStatus = pushResult.aborted || pushResult.filesExcludedByScope > 0
|
|
1213
1262
|
? chalk.yellow("⚠")
|
|
@@ -1218,6 +1267,9 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1218
1267
|
(pushResult.aborted ? " — aborted" : ""));
|
|
1219
1268
|
if (pushResult.filesExcludedByScope > 0) {
|
|
1220
1269
|
console.log(chalk.yellow(scopeExcludedWarning(pushResult.filesExcludedByScope)));
|
|
1270
|
+
const prefixDetail = scopeExcludedPrefixDetail(nowPushPrefixSet);
|
|
1271
|
+
if (prefixDetail)
|
|
1272
|
+
console.log(chalk.yellow(prefixDetail));
|
|
1221
1273
|
}
|
|
1222
1274
|
if (pushResult.aborted) {
|
|
1223
1275
|
console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
|
|
@@ -95,9 +95,12 @@ async function resolveShortId(token, prefix, query) {
|
|
|
95
95
|
console.error(chalk.red(`No meeting matches ID "${prefix}". It may be older than the meetings shown by \`hq meetings list\`, still processing, or attributed to a different company. Pass the full meeting id, add --company <slug>, or widen the list with \`hq meetings list --limit <n>\`.`));
|
|
96
96
|
process.exit(1);
|
|
97
97
|
}
|
|
98
|
-
async function handleApiError(res) {
|
|
98
|
+
async function handleApiError(res, json = false) {
|
|
99
99
|
const body = (await res.json().catch(() => ({})));
|
|
100
|
-
if (
|
|
100
|
+
if (json) {
|
|
101
|
+
console.log(JSON.stringify(body, null, 2));
|
|
102
|
+
}
|
|
103
|
+
else if (res.status === 401) {
|
|
101
104
|
console.error(chalk.red("Not authenticated — run `hq login` first"));
|
|
102
105
|
}
|
|
103
106
|
else if (res.status === 403) {
|
|
@@ -106,6 +109,10 @@ async function handleApiError(res) {
|
|
|
106
109
|
else if (res.status === 404) {
|
|
107
110
|
console.error(chalk.red(body.error ?? "Not found"));
|
|
108
111
|
}
|
|
112
|
+
else if (res.status === 402) {
|
|
113
|
+
console.error(chalk.red("Meeting notetaker bots require the $500/mo HQ Team plan."));
|
|
114
|
+
console.error(chalk.dim(body.message ?? body.error ?? "Upgrade your company to HQ Team to record meetings."));
|
|
115
|
+
}
|
|
109
116
|
else {
|
|
110
117
|
console.error(chalk.red(`API error (${res.status}): ${body.error ?? res.statusText}`));
|
|
111
118
|
}
|
|
@@ -190,7 +197,7 @@ export function registerMeetingsCommand(program) {
|
|
|
190
197
|
query.companyId = await getCompanyUid(token, companySlug);
|
|
191
198
|
const res = await vaultApiFetch({ token, path: "/v1/meetings", query });
|
|
192
199
|
if (!res.ok)
|
|
193
|
-
await handleApiError(res);
|
|
200
|
+
await handleApiError(res, meetings.opts().json);
|
|
194
201
|
const data = (await res.json());
|
|
195
202
|
if (meetings.opts().json) {
|
|
196
203
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -227,7 +234,7 @@ export function registerMeetingsCommand(program) {
|
|
|
227
234
|
body: { meetingUrl },
|
|
228
235
|
});
|
|
229
236
|
if (!res.ok)
|
|
230
|
-
await handleApiError(res);
|
|
237
|
+
await handleApiError(res, meetings.opts().json);
|
|
231
238
|
const data = (await res.json());
|
|
232
239
|
if (meetings.opts().json) {
|
|
233
240
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -263,7 +270,7 @@ export function registerMeetingsCommand(program) {
|
|
|
263
270
|
query,
|
|
264
271
|
});
|
|
265
272
|
if (!res.ok)
|
|
266
|
-
await handleApiError(res);
|
|
273
|
+
await handleApiError(res, meetings.opts().json);
|
|
267
274
|
const detail = (await res.json());
|
|
268
275
|
if (meetings.opts().json) {
|
|
269
276
|
console.log(JSON.stringify(detail, null, 2));
|
|
@@ -348,7 +355,7 @@ export function registerMeetingsCommand(program) {
|
|
|
348
355
|
body: { companyId, applyToSeries },
|
|
349
356
|
});
|
|
350
357
|
if (!res.ok)
|
|
351
|
-
await handleApiError(res);
|
|
358
|
+
await handleApiError(res, meetings.opts().json);
|
|
352
359
|
const data = (await res.json());
|
|
353
360
|
if (meetings.opts().json) {
|
|
354
361
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -389,7 +396,7 @@ export function registerMeetingsCommand(program) {
|
|
|
389
396
|
query: params,
|
|
390
397
|
});
|
|
391
398
|
if (!res.ok)
|
|
392
|
-
await handleApiError(res);
|
|
399
|
+
await handleApiError(res, meetings.opts().json);
|
|
393
400
|
const data = (await res.json());
|
|
394
401
|
if (meetings.opts().json) {
|
|
395
402
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -422,7 +429,7 @@ export function registerMeetingsCommand(program) {
|
|
|
422
429
|
query,
|
|
423
430
|
});
|
|
424
431
|
if (!res.ok)
|
|
425
|
-
await handleApiError(res);
|
|
432
|
+
await handleApiError(res, meetings.opts().json);
|
|
426
433
|
const detail = (await res.json());
|
|
427
434
|
if (isMarkdownShape(detail)) {
|
|
428
435
|
const documentUrl = detail.source.presigned_url;
|
|
@@ -492,7 +499,7 @@ export function registerMeetingsCommand(program) {
|
|
|
492
499
|
query,
|
|
493
500
|
});
|
|
494
501
|
if (!res.ok)
|
|
495
|
-
await handleApiError(res);
|
|
502
|
+
await handleApiError(res, meetings.opts().json);
|
|
496
503
|
const detail = (await res.json());
|
|
497
504
|
if (isMarkdownShape(detail)) {
|
|
498
505
|
if (hasSignals(detail.signals)) {
|
package/dist/commands/packs.js
CHANGED
|
@@ -78,7 +78,7 @@ export function buildInstalledView(hqRoot, hqVersion, pack, installedSources, ch
|
|
|
78
78
|
const counts = { live: 0, broken: 0, missing: 0, foreign: 0 };
|
|
79
79
|
const brokenLinks = [];
|
|
80
80
|
for (const link of links) {
|
|
81
|
-
const st = linkStatus(link);
|
|
81
|
+
const st = linkStatus(link, pack.dir);
|
|
82
82
|
counts[st]++;
|
|
83
83
|
if (st === 'broken')
|
|
84
84
|
brokenLinks.push({ key: link.key, item: link.item, dst: link.dst });
|
package/dist/index.js
CHANGED
|
@@ -11,8 +11,12 @@ if (isVersionRequest(process.argv)) {
|
|
|
11
11
|
process.stdout.write(`${CLI_VERSION}\n`);
|
|
12
12
|
}
|
|
13
13
|
else {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Keep the command lifecycle detached from module evaluation, as it was
|
|
15
|
+
// before the fast --version split. Some best-effort teardown work uses
|
|
16
|
+
// unref'd handles; top-level-awaiting runCli() makes Node turn an otherwise
|
|
17
|
+
// successful command into exit 13 when that teardown promise remains pending.
|
|
18
|
+
// Rejections still become unhandled and preserve a genuine non-zero failure.
|
|
19
|
+
void import("./main.js").then(({ runCli }) => runCli());
|
|
16
20
|
}
|
|
17
21
|
export const __test__ = { isVersionRequest };
|
|
18
22
|
//# sourceMappingURL=index.js.map
|
|
@@ -75,8 +75,13 @@ export type LinkStatus = 'live' | 'broken' | 'missing' | 'foreign';
|
|
|
75
75
|
* effect (a config merge) is handled separately (US-004/US-005).
|
|
76
76
|
*/
|
|
77
77
|
export declare function contributionLinks(hqRoot: string, packDir: string, contributes: Partial<Record<PackContributeKey, string[]>>): WiredLink[];
|
|
78
|
+
/**
|
|
79
|
+
* Canonical absolute path — resolves symlinks and platform aliases such as
|
|
80
|
+
* macOS `/var` ↔ `/private/var` so equivalent paths compare equal.
|
|
81
|
+
*/
|
|
82
|
+
export declare function canonicalPath(p: string): string;
|
|
78
83
|
/** Classify a host path against the link that should own it. */
|
|
79
|
-
export declare function linkStatus(link: WiredLink): LinkStatus;
|
|
84
|
+
export declare function linkStatus(link: WiredLink, packDir?: string): LinkStatus;
|
|
80
85
|
/** A content pack's manifest plus the install-time stamped source. */
|
|
81
86
|
export interface InstalledPackManifest extends PackManifest {
|
|
82
87
|
source?: string;
|
|
@@ -132,8 +132,42 @@ export function contributionLinks(hqRoot, packDir, contributes) {
|
|
|
132
132
|
}
|
|
133
133
|
return links;
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Canonical absolute path — resolves symlinks and platform aliases such as
|
|
137
|
+
* macOS `/var` ↔ `/private/var` so equivalent paths compare equal.
|
|
138
|
+
*/
|
|
139
|
+
export function canonicalPath(p) {
|
|
140
|
+
try {
|
|
141
|
+
return fs.realpathSync.native(p);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return path.resolve(p);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** True when `candidate` resolves under `packDir` (pack directory containment). */
|
|
148
|
+
function targetUnderPackDir(packDir, candidate) {
|
|
149
|
+
const root = canonicalPath(packDir);
|
|
150
|
+
const resolved = canonicalPath(candidate);
|
|
151
|
+
const prefix = root.endsWith(path.sep) ? root : root + path.sep;
|
|
152
|
+
return resolved === root || resolved.startsWith(prefix);
|
|
153
|
+
}
|
|
154
|
+
/** True when `a` and `b` resolve to the same on-disk file (canonical path or inode). */
|
|
155
|
+
function samePayloadFile(a, b) {
|
|
156
|
+
const canonA = canonicalPath(a);
|
|
157
|
+
const canonB = canonicalPath(b);
|
|
158
|
+
if (canonA === canonB)
|
|
159
|
+
return true;
|
|
160
|
+
try {
|
|
161
|
+
const stA = fs.statSync(a);
|
|
162
|
+
const stB = fs.statSync(b);
|
|
163
|
+
return stA.dev === stB.dev && stA.ino === stB.ino;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
135
169
|
/** Classify a host path against the link that should own it. */
|
|
136
|
-
export function linkStatus(link) {
|
|
170
|
+
export function linkStatus(link, packDir) {
|
|
137
171
|
let st;
|
|
138
172
|
try {
|
|
139
173
|
st = fs.lstatSync(link.dst);
|
|
@@ -151,13 +185,21 @@ export function linkStatus(link) {
|
|
|
151
185
|
catch {
|
|
152
186
|
return 'foreign';
|
|
153
187
|
}
|
|
154
|
-
// scan-packages.sh writes the symlink target as the absolute `src` path
|
|
155
|
-
//
|
|
188
|
+
// scan-packages.sh writes the symlink target as the absolute `src` path.
|
|
189
|
+
// Canonicalize both sides so `/var` vs `/private/var` (macOS) and symlink
|
|
190
|
+
// aliases classify as ours; optionally accept stale targets that still resolve
|
|
191
|
+
// inside this pack's directory.
|
|
156
192
|
const resolvedTarget = path.resolve(path.dirname(link.dst), target);
|
|
157
|
-
if (
|
|
158
|
-
return '
|
|
193
|
+
if (samePayloadFile(link.src, resolvedTarget)) {
|
|
194
|
+
return fs.existsSync(link.src) ? 'live' : 'broken';
|
|
195
|
+
}
|
|
196
|
+
if (packDir !== undefined && targetUnderPackDir(packDir, resolvedTarget)) {
|
|
197
|
+
// Stale or corrupted: symlink resolves inside this pack but not to the
|
|
198
|
+
// declared payload. Treat as broken (never live) so list does not report a
|
|
199
|
+
// healthy link; unwirePack still removes it on uninstall.
|
|
200
|
+
return 'broken';
|
|
159
201
|
}
|
|
160
|
-
return
|
|
202
|
+
return 'foreign'; // points at another pack / somewhere else
|
|
161
203
|
}
|
|
162
204
|
/** Absolute path to `<hqRoot>/core/packages`. */
|
|
163
205
|
export function packagesDir(hqRoot) {
|
|
@@ -230,7 +272,7 @@ export function findDependentPacks(installed, name) {
|
|
|
230
272
|
export function unwirePack(hqRoot, packDir, contributes) {
|
|
231
273
|
const result = { unlinked: [], skipped: [] };
|
|
232
274
|
for (const link of contributionLinks(hqRoot, packDir, contributes)) {
|
|
233
|
-
const status = linkStatus(link);
|
|
275
|
+
const status = linkStatus(link, packDir);
|
|
234
276
|
if (status === 'live' || status === 'broken') {
|
|
235
277
|
try {
|
|
236
278
|
fs.unlinkSync(link.dst);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.80.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
31
31
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
32
|
-
"@indigoai-us/hq-cloud": "^6.14.
|
|
32
|
+
"@indigoai-us/hq-cloud": "^6.14.37",
|
|
33
33
|
"@indigoai-us/hq-onboarding": "^0.1.0",
|
|
34
34
|
"@sentry/node": "^10.49.0",
|
|
35
35
|
"better-sqlite3": "^12.11.1",
|