@indigoai-us/hq-cli 5.289.0 → 5.291.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 +61 -0
- package/assets/scaffold/core/scripts/handoff-finalize.sh +670 -0
- package/assets/scaffold/core/scripts/handoff-open-steps.sh +81 -0
- package/assets/scaffold/core/scripts/handoff-post.sh +187 -0
- package/assets/scaffold/core/scripts/hook-lib.sh +345 -50
- package/assets/scaffold/core/scripts/hq-session.sh +182 -21
- package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +310 -0
- package/assets/scaffold/core/scripts/jobs-validate.sh +451 -0
- package/assets/scaffold/core/scripts/lib/session-id.sh +3 -2
- package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +2 -1
- package/assets/scaffold/core/scripts/qmd-reindex-bg.sh +40 -0
- package/dist/command-catalog.generated.d.ts +110 -0
- package/dist/command-catalog.generated.js +136 -0
- package/dist/commands/cloud.d.ts +26 -14
- package/dist/commands/cloud.js +207 -56
- package/dist/commands/core-handoff.d.ts +22 -0
- package/dist/commands/core-handoff.js +1240 -0
- package/dist/commands/core-jobs-native.d.ts +8 -0
- package/dist/commands/core-jobs-native.js +624 -0
- package/dist/commands/core-native-jobs-flag.d.ts +4 -0
- package/dist/commands/core-native-jobs-flag.js +7 -0
- package/dist/commands/core-ontology.d.ts +6 -1
- package/dist/commands/core-ontology.js +42 -7
- package/dist/commands/core-policy.js +24 -5
- package/dist/commands/core.js +15 -1
- package/dist/commands/integrations-core.js +7 -0
- package/dist/commands/lanes.js +111 -35
- package/dist/commands/members.js +5 -0
- package/dist/commands/mesh.js +46 -25
- package/dist/commands/personal-targeted-push-flag.d.ts +4 -0
- package/dist/commands/personal-targeted-push-flag.js +7 -0
- package/dist/commands/personal-targeted-push-paths.d.ts +3 -0
- package/dist/commands/personal-targeted-push-paths.js +67 -0
- package/dist/commands/secrets.js +9 -0
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.js +28 -0
- package/dist/fast-core.js +1 -0
- package/dist/lib/billing/free-month.d.ts +18 -0
- package/dist/lib/billing/free-month.js +31 -0
- package/dist/lib/billing/plan-limit-refusals.d.ts +69 -0
- package/dist/lib/billing/plan-limit-refusals.js +187 -0
- package/dist/lib/billing/plan-lock.d.ts +2 -6
- package/dist/lib/billing/plan-lock.js +5 -16
- package/dist/lib/checkpoint-ontology/entity-file.d.ts +21 -0
- package/dist/lib/checkpoint-ontology/entity-file.js +100 -0
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +34 -2
- package/dist/lib/checkpoint-ontology/merge-copies.js +541 -27
- package/dist/lib/core-utils/secret-patterns.d.ts +3 -1
- package/dist/lib/core-utils/secret-patterns.js +3 -2
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/autopilot.d.ts +69 -0
- package/dist/lib/lanes/autopilot.js +418 -0
- package/dist/lib/lanes/mesh-events.js +2 -0
- package/dist/lib/lanes/store.d.ts +4 -2
- package/dist/lib/lanes/store.js +18 -9
- package/dist/lib/mesh/api.js +4 -0
- package/dist/lib/mesh/live/daemon/state.d.ts +7 -1
- package/dist/lib/mesh/live/daemon/state.js +34 -2
- package/dist/main.js +23 -12
- package/dist/utils/plan-gate-error.d.ts +24 -0
- package/dist/utils/plan-gate-error.js +219 -9
- package/dist/utils/self-update.d.ts +3 -0
- package/dist/utils/self-update.js +232 -1
- package/package.json +2 -2
package/dist/commands/cloud.js
CHANGED
|
@@ -22,6 +22,10 @@ import { resolveUploadAuthor } from "../utils/upload-author.js";
|
|
|
22
22
|
import { companyFolderExceedsThreshold, emitNarrowHint, isStrictRefusal, resolveBannerLevel, resolveNarrowHintPresentationLevel, resolveNarrowHintMinBytes, } from "../lib/narrow-hint-banner.js";
|
|
23
23
|
import { beginSyncHealthReport, } from "../utils/client-health.js";
|
|
24
24
|
import { recordFirstWorkBearingSync } from "../lib/first-sync-next-step.js";
|
|
25
|
+
import { createPlanLimitRefusalTracker, describeRefusedCount, describeRefusedFiles, mergePlanLimitRefusals, renderPlanLimitRefusalLines, trackingProgressRecorder, } from "../lib/billing/plan-limit-refusals.js";
|
|
26
|
+
import { formatPlanGateError, planGateErrorFromVaultError, } from "../utils/plan-gate-error.js";
|
|
27
|
+
import { personalTargetedPushPathsEnabled } from "./personal-targeted-push-flag.js";
|
|
28
|
+
import { resolveExplicitPersonalPushPaths } from "./personal-targeted-push-paths.js";
|
|
25
29
|
/**
|
|
26
30
|
* Terminal-failure signal for the sync fanout runners. The runners
|
|
27
31
|
* (`runPushAll` / `runPullAll` / `runPullPersonal` / `runNowSingle` /
|
|
@@ -35,10 +39,18 @@ import { recordFirstWorkBearingSync } from "../lib/first-sync-next-step.js";
|
|
|
35
39
|
*/
|
|
36
40
|
export class SyncExitError extends Error {
|
|
37
41
|
code;
|
|
38
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The client-health outcome to record for this exit. A run whose only
|
|
44
|
+
* problem was a plan-limit refusal exits non-zero (it did not upload what
|
|
45
|
+
* was asked) but is not a sync failure: the engine worked and the refusal
|
|
46
|
+
* is plan state, so it must not count toward `consecutiveFailures`.
|
|
47
|
+
*/
|
|
48
|
+
health;
|
|
49
|
+
constructor(code, opts = {}) {
|
|
39
50
|
super(`sync command exit ${code}`);
|
|
40
51
|
this.code = code;
|
|
41
52
|
this.name = "SyncExitError";
|
|
53
|
+
this.health = opts.health ?? "failed";
|
|
42
54
|
}
|
|
43
55
|
}
|
|
44
56
|
/**
|
|
@@ -47,6 +59,16 @@ export class SyncExitError extends Error {
|
|
|
47
59
|
* CLI are released independently.
|
|
48
60
|
*/
|
|
49
61
|
export function formatSyncFailure(err) {
|
|
62
|
+
// A plan-limit refusal the runner threw (a 402 on the whole request) renders
|
|
63
|
+
// with the same reason, fix options and upgrade link as every other refusal.
|
|
64
|
+
const planGate = planGateErrorFromVaultError(err);
|
|
65
|
+
if (planGate) {
|
|
66
|
+
const refusedPath = typeof err.path === "string"
|
|
67
|
+
? err.path
|
|
68
|
+
: undefined;
|
|
69
|
+
return (formatPlanGateError(planGate) +
|
|
70
|
+
(refusedPath ? `\nNot uploaded: ${refusedPath}` : ""));
|
|
71
|
+
}
|
|
50
72
|
if (isAccessDeniedError(err)) {
|
|
51
73
|
const key = accessDeniedKeyOf(err);
|
|
52
74
|
const base = err instanceof Error ? err.message : String(err);
|
|
@@ -92,7 +114,12 @@ export async function withSyncHealthReport(run) {
|
|
|
92
114
|
return result;
|
|
93
115
|
}
|
|
94
116
|
catch (err) {
|
|
95
|
-
|
|
117
|
+
if (err instanceof SyncExitError && err.health === "succeeded") {
|
|
118
|
+
await health.succeeded();
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
await health.failed();
|
|
122
|
+
}
|
|
96
123
|
if (err instanceof SyncExitError)
|
|
97
124
|
process.exit(err.code);
|
|
98
125
|
throw err;
|
|
@@ -418,6 +445,7 @@ export async function pushAll(options, deps) {
|
|
|
418
445
|
companyUid: m.companyUid,
|
|
419
446
|
shareOptions: {
|
|
420
447
|
company: m.companyUid,
|
|
448
|
+
targetSlug: slug,
|
|
421
449
|
hqRoot: options.hqRoot,
|
|
422
450
|
paths: [path.join(options.hqRoot, "companies", slug)],
|
|
423
451
|
skipUnchanged: true,
|
|
@@ -526,21 +554,15 @@ export async function resolveCanonicalPersonUid(vaultClient) {
|
|
|
526
554
|
return pick.uid;
|
|
527
555
|
}
|
|
528
556
|
/**
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
* `
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
* hand-rolled S3 sweep. Closes hq-cli#25.
|
|
536
|
-
*
|
|
537
|
-
* Refusal — not silent filtering — is intentional: explicit is better than
|
|
538
|
-
* implicit guesswork, and the legitimate "I want to push a subset of my
|
|
539
|
-
* personal vault" use case has a clean workaround (drop `--personal`, the
|
|
540
|
-
* subset upload targets the active company via standard semantics).
|
|
557
|
+
* Keep `hq sync push --personal <path>` refused by default. Positional paths
|
|
558
|
+
* bypass `computePersonalVaultPaths` and can send excluded `companies/`,
|
|
559
|
+
* `repos/`, `workspace/`, or `.git/` content to the personal vault. The
|
|
560
|
+
* default-off targeted-paths gate allows the combination only after every
|
|
561
|
+
* path is realpath-normalized and checked against the computed personal scope
|
|
562
|
+
* and its file exclusions. Closes hq-cli#25.
|
|
541
563
|
*/
|
|
542
|
-
export function assertNoPersonalPositionalPaths(opts, paths) {
|
|
543
|
-
if (opts.personal && paths && paths.length > 0) {
|
|
564
|
+
export function assertNoPersonalPositionalPaths(opts, paths, targetedPersonalPathsEnabled = false) {
|
|
565
|
+
if (opts.personal && paths && paths.length > 0 && !targetedPersonalPathsEnabled) {
|
|
544
566
|
throw new Error("`--personal` cannot be combined with explicit [paths]: " +
|
|
545
567
|
"positional paths bypass the PERSONAL_VAULT_EXCLUDED_TOP_LEVEL " +
|
|
546
568
|
"guard (skips .git/, companies/, repos/, workspace/), risking " +
|
|
@@ -670,6 +692,32 @@ async function resolveCliPullScope(client, companyRef, hqRoot) {
|
|
|
670
692
|
}
|
|
671
693
|
return resolvePullScope(client, companyUid, slug, hqRoot);
|
|
672
694
|
}
|
|
695
|
+
/**
|
|
696
|
+
* The progress-snapshot label share()'s default recorder would use: the
|
|
697
|
+
* target's slug, or null for a personal-vault push. A pre-vended context
|
|
698
|
+
* carries its slug; otherwise the caller's company reference is the best the
|
|
699
|
+
* CLI knows before share() resolves it.
|
|
700
|
+
*/
|
|
701
|
+
function shareProgressCompany(input) {
|
|
702
|
+
if (input.personalMode)
|
|
703
|
+
return null;
|
|
704
|
+
if (input.entityContext) {
|
|
705
|
+
return input.entityContext.uid.startsWith("prs_")
|
|
706
|
+
? null
|
|
707
|
+
: input.entityContext.slug;
|
|
708
|
+
}
|
|
709
|
+
return input.companyRef ?? null;
|
|
710
|
+
}
|
|
711
|
+
/** Only a real slug builds a company billing link; a raw uid does not. */
|
|
712
|
+
function companySlugForBilling(label) {
|
|
713
|
+
return label && !/^(?:cmp|prs)_/.test(label) ? { companySlug: label } : {};
|
|
714
|
+
}
|
|
715
|
+
/** Print one push leg's refusal block under its row. */
|
|
716
|
+
function printPlanLimitRefusal(summary, indent = " ") {
|
|
717
|
+
for (const line of renderPlanLimitRefusalLines(summary, indent)) {
|
|
718
|
+
console.log(chalk.yellow(line));
|
|
719
|
+
}
|
|
720
|
+
}
|
|
673
721
|
export function registerCloudCommands(program) {
|
|
674
722
|
program
|
|
675
723
|
.command("push")
|
|
@@ -736,7 +784,13 @@ export function registerCloudCommands(program) {
|
|
|
736
784
|
// the fanout" — wired through to `pushAll.skipPersonal`. Outside
|
|
737
785
|
// `--all` the flag has no effect (logged above as part of the
|
|
738
786
|
// option's help text).
|
|
739
|
-
await withSyncHealthReport(() =>
|
|
787
|
+
await withSyncHealthReport(async () => {
|
|
788
|
+
const refused = await runPushAll(options.hqRoot, options.message, options.onConflict, options.personal === false);
|
|
789
|
+
// A plan-limit refusal is not a sync failure, but the push did
|
|
790
|
+
// not upload everything it was asked to: exit non-zero.
|
|
791
|
+
if (refused)
|
|
792
|
+
throw new SyncExitError(1, { health: "succeeded" });
|
|
793
|
+
});
|
|
740
794
|
return;
|
|
741
795
|
}
|
|
742
796
|
const jsonMode = options.json === true;
|
|
@@ -752,6 +806,7 @@ export function registerCloudCommands(program) {
|
|
|
752
806
|
// Assigned right before the sync engine runs, so option-validation
|
|
753
807
|
// failures never count as a sync attempt (US-003).
|
|
754
808
|
let syncHealth;
|
|
809
|
+
let explicitPersonalPaths;
|
|
755
810
|
try {
|
|
756
811
|
if (options.personal && options.credsFromStdin) {
|
|
757
812
|
throw new Error("`--personal` cannot be combined with --creds-from-stdin: " +
|
|
@@ -760,7 +815,11 @@ export function registerCloudCommands(program) {
|
|
|
760
815
|
"to have already resolved entity + credentials. Pick one.");
|
|
761
816
|
}
|
|
762
817
|
// Closes hq-cli#25 — see `assertNoPersonalPositionalPaths` doc-block.
|
|
763
|
-
|
|
818
|
+
const targetedPersonalPathsEnabled = Boolean(options.personal && paths && paths.length > 0 && personalTargetedPushPathsEnabled());
|
|
819
|
+
assertNoPersonalPositionalPaths(options, paths, targetedPersonalPathsEnabled);
|
|
820
|
+
if (targetedPersonalPathsEnabled) {
|
|
821
|
+
explicitPersonalPaths = resolveExplicitPersonalPushPaths(options.hqRoot, paths);
|
|
822
|
+
}
|
|
764
823
|
log(chalk.bold("\nHQ Sync — Push"));
|
|
765
824
|
log(` HQ root: ${options.hqRoot}`);
|
|
766
825
|
// Resolve credentials. Two paths:
|
|
@@ -818,11 +877,13 @@ export function registerCloudCommands(program) {
|
|
|
818
877
|
// Otherwise preserve the historical "default to cwd" semantics so
|
|
819
878
|
// `hq sync push <file>` and bare `hq sync push` from inside the
|
|
820
879
|
// company tree both work as before.
|
|
821
|
-
const targetPaths =
|
|
822
|
-
?
|
|
823
|
-
:
|
|
824
|
-
?
|
|
825
|
-
:
|
|
880
|
+
const targetPaths = explicitPersonalPaths
|
|
881
|
+
? explicitPersonalPaths
|
|
882
|
+
: paths && paths.length > 0
|
|
883
|
+
? paths
|
|
884
|
+
: options.personal
|
|
885
|
+
? computePersonalVaultPaths(options.hqRoot)
|
|
886
|
+
: [process.cwd()];
|
|
826
887
|
log(` Company: ${options.personal
|
|
827
888
|
? `(personal: ${targetCompany})`
|
|
828
889
|
: (options.company ?? "(from .hq/config.json or stdin)")}`);
|
|
@@ -846,6 +907,16 @@ export function registerCloudCommands(program) {
|
|
|
846
907
|
const scope = await resolveCliPullScope(scopeClient, targetCompany, options.hqRoot);
|
|
847
908
|
pushPrefixSet = scope?.prefixSet;
|
|
848
909
|
}
|
|
910
|
+
// Collect plan-limit refusals from the runner's events through the
|
|
911
|
+
// progress-recorder seam, which (unlike onEvent) leaves share()'s own
|
|
912
|
+
// console output untouched. The snapshot label mirrors share()'s
|
|
913
|
+
// default recorder: the target slug, or null for the personal vault.
|
|
914
|
+
const progressCompany = shareProgressCompany({
|
|
915
|
+
personalMode,
|
|
916
|
+
entityContext,
|
|
917
|
+
companyRef: targetCompany ?? readActiveCompany(options.hqRoot),
|
|
918
|
+
});
|
|
919
|
+
const planLimitTracker = createPlanLimitRefusalTracker(companySlugForBilling(progressCompany));
|
|
849
920
|
syncHealth = beginSyncHealthReport();
|
|
850
921
|
const result = await share({
|
|
851
922
|
paths: targetPaths,
|
|
@@ -859,16 +930,20 @@ export function registerCloudCommands(program) {
|
|
|
859
930
|
}),
|
|
860
931
|
hqRoot: options.hqRoot,
|
|
861
932
|
onEvent,
|
|
933
|
+
progressRecorder: trackingProgressRecorder(planLimitTracker, progressCompany),
|
|
862
934
|
...(personalMode ? { personalMode: true } : {}),
|
|
863
935
|
...(journalSlug !== undefined ? { journalSlug } : {}),
|
|
864
936
|
...(author ? { author } : {}),
|
|
865
937
|
...(pushPrefixSet !== undefined ? { prefixSet: pushPrefixSet } : {}),
|
|
866
938
|
});
|
|
939
|
+
const planLimitRefused = planLimitTracker.summary();
|
|
867
940
|
if (jsonMode) {
|
|
868
941
|
// Synthetic terminal event so subprocess consumers can read final
|
|
869
942
|
// counts without summing per-file events. Distinguished from
|
|
870
943
|
// SyncProgressEvent by `type:"complete"` (not in the share()
|
|
871
|
-
// event schema — added at the CLI seam).
|
|
944
|
+
// event schema — added at the CLI seam). `planLimitRefused` is
|
|
945
|
+
// additive: present only when a plan limit refused new files, so a
|
|
946
|
+
// consumer never has to infer a refusal from the counts.
|
|
872
947
|
emitJson({
|
|
873
948
|
type: "complete",
|
|
874
949
|
filesUploaded: result.filesUploaded,
|
|
@@ -876,6 +951,7 @@ export function registerCloudCommands(program) {
|
|
|
876
951
|
filesSkipped: result.filesSkipped,
|
|
877
952
|
conflictPaths: result.conflictPaths,
|
|
878
953
|
aborted: result.aborted,
|
|
954
|
+
...(planLimitRefused ? { planLimitRefused } : {}),
|
|
879
955
|
});
|
|
880
956
|
}
|
|
881
957
|
if (result.aborted) {
|
|
@@ -888,6 +964,26 @@ export function registerCloudCommands(program) {
|
|
|
888
964
|
filesDeleted: result.filesDeleted,
|
|
889
965
|
aborted: result.aborted,
|
|
890
966
|
});
|
|
967
|
+
if (planLimitRefused) {
|
|
968
|
+
// Never a success line over a refusal: say what was pushed, what
|
|
969
|
+
// was refused and how to fix it, then exit non-zero. The sync
|
|
970
|
+
// itself worked, so client health records a success. `--json`
|
|
971
|
+
// callers read `planLimitRefused` on the complete event instead
|
|
972
|
+
// and keep their exit status.
|
|
973
|
+
log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
|
|
974
|
+
`${result.filesSkipped} skipped), but ${describeRefusedFiles(planLimitRefused)} ` +
|
|
975
|
+
"refused by your plan limit."));
|
|
976
|
+
for (const line of renderPlanLimitRefusalLines(planLimitRefused)) {
|
|
977
|
+
log(chalk.yellow(line));
|
|
978
|
+
}
|
|
979
|
+
for (const line of scopeExcludedLines(result.filesExcludedByScope, pushPrefixSet)) {
|
|
980
|
+
log(chalk.yellow(line));
|
|
981
|
+
}
|
|
982
|
+
await syncHealth.succeeded();
|
|
983
|
+
if (!jsonMode)
|
|
984
|
+
process.exit(1);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
891
987
|
if (result.filesExcludedByScope > 0) {
|
|
892
988
|
log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
|
|
893
989
|
`${result.filesSkipped} skipped, ${result.filesExcludedByScope} scope-excluded)`));
|
|
@@ -1398,6 +1494,11 @@ export async function runPullPersonal(hqRoot, onConflict) {
|
|
|
1398
1494
|
throw new SyncExitError(1);
|
|
1399
1495
|
}
|
|
1400
1496
|
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Run `push --all` and print its per-target report. Returns the files a plan
|
|
1499
|
+
* limit refused across every target (null when none were), so the caller can
|
|
1500
|
+
* refuse to report success; hard failures still throw {@link SyncExitError}.
|
|
1501
|
+
*/
|
|
1401
1502
|
async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
|
|
1402
1503
|
console.log(chalk.bold("\nHQ Sync — Push (all)"));
|
|
1403
1504
|
console.log(` HQ root: ${hqRoot}`);
|
|
@@ -1432,31 +1533,39 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
|
|
|
1432
1533
|
}, {
|
|
1433
1534
|
vaultClient: adapter,
|
|
1434
1535
|
resolveScope: (companyUid, slug) => resolvePullScope(realClient, companyUid, slug, hqRoot),
|
|
1435
|
-
share: (opts) =>
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
: {}),
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
: {}),
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1536
|
+
share: async (opts) => {
|
|
1537
|
+
const progressCompany = opts.personalMode
|
|
1538
|
+
? null
|
|
1539
|
+
: (opts.targetSlug ?? opts.company);
|
|
1540
|
+
const tracker = createPlanLimitRefusalTracker(companySlugForBilling(progressCompany));
|
|
1541
|
+
const shareResult = await share({
|
|
1542
|
+
paths: opts.paths,
|
|
1543
|
+
company: opts.company,
|
|
1544
|
+
vaultConfig,
|
|
1545
|
+
hqRoot: opts.hqRoot,
|
|
1546
|
+
progressRecorder: trackingProgressRecorder(tracker, progressCompany),
|
|
1547
|
+
...(opts.onConflict ? { onConflict: opts.onConflict } : {}),
|
|
1548
|
+
...(opts.personalMode !== undefined
|
|
1549
|
+
? { personalMode: opts.personalMode }
|
|
1550
|
+
: {}),
|
|
1551
|
+
...(opts.journalSlug !== undefined
|
|
1552
|
+
? { journalSlug: opts.journalSlug }
|
|
1553
|
+
: {}),
|
|
1554
|
+
...(opts.message !== undefined ? { message: opts.message } : {}),
|
|
1555
|
+
...(opts.skipUnchanged !== undefined
|
|
1556
|
+
? { skipUnchanged: opts.skipUnchanged }
|
|
1557
|
+
: {}),
|
|
1558
|
+
...(opts.propagateDeletes !== undefined
|
|
1559
|
+
? { propagateDeletes: opts.propagateDeletes }
|
|
1560
|
+
: {}),
|
|
1561
|
+
...(opts.propagateDeletePolicy !== undefined
|
|
1562
|
+
? { propagateDeletePolicy: opts.propagateDeletePolicy }
|
|
1563
|
+
: {}),
|
|
1564
|
+
...(opts.prefixSet !== undefined ? { prefixSet: opts.prefixSet } : {}),
|
|
1565
|
+
...(author ? { author } : {}),
|
|
1566
|
+
});
|
|
1567
|
+
return { ...shareResult, planLimit: tracker.summary() };
|
|
1568
|
+
},
|
|
1460
1569
|
});
|
|
1461
1570
|
}
|
|
1462
1571
|
catch (err) {
|
|
@@ -1465,28 +1574,42 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
|
|
|
1465
1574
|
// terminal sync_failure before exiting on our behalf.
|
|
1466
1575
|
throw new SyncExitError(1);
|
|
1467
1576
|
}
|
|
1577
|
+
let refused = null;
|
|
1578
|
+
let refusedTargets = 0;
|
|
1468
1579
|
for (const row of result.perCompany) {
|
|
1469
1580
|
if (row.error) {
|
|
1470
1581
|
console.log(chalk.red(` ✗ ${row.slug}: ${row.error}`));
|
|
1471
1582
|
}
|
|
1472
1583
|
else if (row.result) {
|
|
1473
1584
|
const r = row.result;
|
|
1474
|
-
const
|
|
1585
|
+
const legRefused = r.planLimit ?? null;
|
|
1586
|
+
const status = r.aborted || r.filesExcludedByScope > 0 || legRefused
|
|
1475
1587
|
? chalk.yellow("⚠")
|
|
1476
1588
|
: chalk.green("✓");
|
|
1477
1589
|
console.log(` ${status} ${row.slug}: ${r.filesUploaded} file(s), ` +
|
|
1478
1590
|
`${formatBytes(r.bytesUploaded)}, ${r.filesSkipped} skipped, ` +
|
|
1479
1591
|
`${r.filesDeleted} deleted, ${r.filesExcludedByScope} scope-excluded, ` +
|
|
1480
1592
|
`${r.conflictPaths.length} conflict(s)` +
|
|
1481
|
-
(r.aborted ? " — aborted" : "")
|
|
1593
|
+
(r.aborted ? " — aborted" : "") +
|
|
1594
|
+
(legRefused
|
|
1595
|
+
? ` — ${describeRefusedCount(legRefused)} refused by plan limit`
|
|
1596
|
+
: ""));
|
|
1597
|
+
if (legRefused) {
|
|
1598
|
+
printPlanLimitRefusal(legRefused);
|
|
1599
|
+
refused = mergePlanLimitRefusals(refused, legRefused);
|
|
1600
|
+
refusedTargets += 1;
|
|
1601
|
+
}
|
|
1482
1602
|
}
|
|
1483
1603
|
}
|
|
1484
1604
|
const errored = result.errors.length;
|
|
1485
1605
|
const summary = `\nPushed ${result.filesUploaded} file(s) ` +
|
|
1486
1606
|
`(${formatBytes(result.bytesUploaded)}) across ${result.attempted} ` +
|
|
1487
1607
|
`target(s); ${result.filesDeleted} deleted; ${errored} error(s); ` +
|
|
1488
|
-
`${result.filesExcludedByScope} scope-excluded
|
|
1489
|
-
|
|
1608
|
+
`${result.filesExcludedByScope} scope-excluded` +
|
|
1609
|
+
(refusedTargets > 0
|
|
1610
|
+
? `; new files refused by a plan limit on ${refusedTargets} target(s)`
|
|
1611
|
+
: "");
|
|
1612
|
+
console.log(errored > 0 || result.filesExcludedByScope > 0 || refusedTargets > 0
|
|
1490
1613
|
? chalk.yellow(summary)
|
|
1491
1614
|
: chalk.green(summary));
|
|
1492
1615
|
for (const row of result.perCompany) {
|
|
@@ -1506,6 +1629,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
|
|
|
1506
1629
|
}
|
|
1507
1630
|
if (errored > 0)
|
|
1508
1631
|
throw new SyncExitError(1);
|
|
1632
|
+
return refused;
|
|
1509
1633
|
}
|
|
1510
1634
|
async function runNowSingle(hqRoot, company, personal, message, onConflict, modeAllOverride, forceScopeShrink) {
|
|
1511
1635
|
console.log(chalk.bold("\nHQ Sync — Now"));
|
|
@@ -1523,6 +1647,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1523
1647
|
let personalMode = false;
|
|
1524
1648
|
let journalSlug;
|
|
1525
1649
|
let pushPaths;
|
|
1650
|
+
let pushSlug;
|
|
1526
1651
|
if (personal) {
|
|
1527
1652
|
const client = new VaultClient(vaultConfig);
|
|
1528
1653
|
targetCompany = await resolveCanonicalPersonUid({
|
|
@@ -1547,6 +1672,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1547
1672
|
"Use --company <slug>, --personal, or set up .hq/config.json.");
|
|
1548
1673
|
}
|
|
1549
1674
|
pushPaths = [path.join(hqRoot, "companies", slug)];
|
|
1675
|
+
pushSlug = slug;
|
|
1550
1676
|
}
|
|
1551
1677
|
// Push first so the subsequent pull doesn't redownload files we were
|
|
1552
1678
|
// about to broadcast (matches hq-sync-runner ordering).
|
|
@@ -1557,11 +1683,14 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1557
1683
|
const pushScope = await resolveCliPullScope(scopeClient, targetCompany, hqRoot);
|
|
1558
1684
|
nowPushPrefixSet = pushScope?.prefixSet;
|
|
1559
1685
|
}
|
|
1686
|
+
const pushProgressCompany = personalMode ? null : (pushSlug ?? null);
|
|
1687
|
+
const planLimitTracker = createPlanLimitRefusalTracker(companySlugForBilling(pushProgressCompany));
|
|
1560
1688
|
const pushResult = await share({
|
|
1561
1689
|
paths: pushPaths,
|
|
1562
1690
|
company: targetCompany,
|
|
1563
1691
|
vaultConfig,
|
|
1564
1692
|
hqRoot,
|
|
1693
|
+
progressRecorder: trackingProgressRecorder(planLimitTracker, pushProgressCompany),
|
|
1565
1694
|
skipUnchanged: true,
|
|
1566
1695
|
propagateDeletes: true,
|
|
1567
1696
|
propagateDeletePolicy: resolveDeletePolicy(),
|
|
@@ -1572,13 +1701,19 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1572
1701
|
...(author ? { author } : {}),
|
|
1573
1702
|
...(nowPushPrefixSet !== undefined ? { prefixSet: nowPushPrefixSet } : {}),
|
|
1574
1703
|
});
|
|
1575
|
-
const
|
|
1704
|
+
const pushRefused = planLimitTracker.summary();
|
|
1705
|
+
const pushStatus = pushResult.aborted || pushResult.filesExcludedByScope > 0 || pushRefused
|
|
1576
1706
|
? chalk.yellow("⚠")
|
|
1577
1707
|
: chalk.green("✓");
|
|
1578
1708
|
console.log(` ${pushStatus} ` +
|
|
1579
1709
|
`${pushResult.filesUploaded} uploaded, ${pushResult.filesSkipped} skipped, ` +
|
|
1580
1710
|
`${pushResult.filesDeleted} deleted, ${pushResult.filesExcludedByScope} scope-excluded` +
|
|
1581
|
-
(pushResult.aborted ? " — aborted" : "")
|
|
1711
|
+
(pushResult.aborted ? " — aborted" : "") +
|
|
1712
|
+
(pushRefused
|
|
1713
|
+
? ` — ${describeRefusedCount(pushRefused)} refused by plan limit`
|
|
1714
|
+
: ""));
|
|
1715
|
+
if (pushRefused)
|
|
1716
|
+
printPlanLimitRefusal(pushRefused, " ");
|
|
1582
1717
|
if (pushResult.filesExcludedByScope > 0) {
|
|
1583
1718
|
for (const line of scopeExcludedLines(pushResult.filesExcludedByScope, nowPushPrefixSet)) {
|
|
1584
1719
|
console.log(chalk.yellow(line));
|
|
@@ -1698,6 +1833,12 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1698
1833
|
level: bannerLevel,
|
|
1699
1834
|
});
|
|
1700
1835
|
}
|
|
1836
|
+
if (pushRefused) {
|
|
1837
|
+
// The pull ran, but the push did not upload everything: no success line.
|
|
1838
|
+
console.log(chalk.yellow(`\n⚠ Sync now finished, but ${describeRefusedFiles(pushRefused)} ` +
|
|
1839
|
+
"refused by your plan limit (listed above)."));
|
|
1840
|
+
throw new SyncExitError(1, { health: "succeeded" });
|
|
1841
|
+
}
|
|
1701
1842
|
console.log(chalk.green("\n✓ Sync now complete"));
|
|
1702
1843
|
}
|
|
1703
1844
|
catch (err) {
|
|
@@ -1719,12 +1860,22 @@ async function runNowAll(hqRoot, message, onConflict, modeAllOverride, skipPerso
|
|
|
1719
1860
|
// so the per-target rendering, error isolation, and exit codes are
|
|
1720
1861
|
// identical to running `push --all` then `pull --all` back-to-back.
|
|
1721
1862
|
console.log(chalk.dim("→ push --all"));
|
|
1722
|
-
await runPushAll(hqRoot, message, onConflict, skipPersonal);
|
|
1863
|
+
const refused = await runPushAll(hqRoot, message, onConflict, skipPersonal);
|
|
1723
1864
|
console.log(chalk.dim("\n→ pull --all"));
|
|
1724
1865
|
// US-011: forward --mode-all so the strict refusal applies to the
|
|
1725
1866
|
// pull leg (push doesn't need a narrow-hint — the narrow ritual is
|
|
1726
1867
|
// pull-side).
|
|
1727
1868
|
await runPullAll(hqRoot, onConflict, modeAllOverride, skipPersonal, forceScopeShrink);
|
|
1869
|
+
if (refused) {
|
|
1870
|
+
// A plan-limit refusal does not stop the pull, but the run must not end
|
|
1871
|
+
// looking like a clean sync: say so last and exit non-zero.
|
|
1872
|
+
console.log(chalk.yellow(`\n⚠ Sync finished, but ${describeRefusedFiles(refused)} refused ` +
|
|
1873
|
+
"by your plan limit (listed under push --all above)."));
|
|
1874
|
+
if (refused.upgradeUrl) {
|
|
1875
|
+
console.log(chalk.yellow(` Upgrade: ${refused.upgradeUrl}`));
|
|
1876
|
+
}
|
|
1877
|
+
throw new SyncExitError(1, { health: "succeeded" });
|
|
1878
|
+
}
|
|
1728
1879
|
}
|
|
1729
1880
|
/**
|
|
1730
1881
|
* Best-effort read of the active company slug from `<hqRoot>/.hq/config.json`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Native implementations of the handoff scaffold command family. */
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
type RunResult = {
|
|
4
|
+
code: number;
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
};
|
|
8
|
+
type HandoffContext = {
|
|
9
|
+
root: string;
|
|
10
|
+
cwd: string;
|
|
11
|
+
now?: () => Date;
|
|
12
|
+
/** Test-only override for the finalizer's historical fixed /tmp paths. */
|
|
13
|
+
tmpDir?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function runHandoffOpenSteps(args: string[], context: HandoffContext): RunResult;
|
|
16
|
+
export declare function runHandoffFinalize(args: string[], context: HandoffContext): RunResult;
|
|
17
|
+
export declare function runHandoffPost(args: string[], context: HandoffContext): RunResult | Promise<RunResult>;
|
|
18
|
+
export declare function runHandoffPostWorker(args: string[], context: HandoffContext): RunResult;
|
|
19
|
+
/** Register native handoff commands behind the shared default-off flag. */
|
|
20
|
+
export declare function registerCoreHandoffCommand(core: Command): void;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=core-handoff.d.ts.map
|