@indigoai-us/hq-cli 5.77.13 → 5.78.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 +34 -0
- package/assets/scaffold/core/scripts/archive-old-threads.sh +86 -0
- package/assets/scaffold/core/scripts/backfill-company-skill-mirrors.sh +73 -0
- package/assets/scaffold/core/scripts/backfill-workspace-mirror.sh +51 -0
- package/assets/scaffold/core/scripts/codex-skill-bridge.sh +550 -0
- package/assets/scaffold/core/scripts/detect-stale-core-policy-mirror.sh +185 -0
- package/assets/scaffold/core/scripts/hq-status-summary.sh +187 -0
- package/assets/scaffold/core/scripts/ontology-readme-drift.sh +264 -0
- package/assets/scaffold/core/scripts/qmd-reindex-after-sync.sh +93 -0
- package/assets/scaffold/core/scripts/rebuild-all-indexes.sh +69 -0
- package/assets/scaffold/core/scripts/rebuild-companies-index.sh +206 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +127 -0
- package/assets/scaffold/core/scripts/rebuild-orchestrator-index.sh +86 -0
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +105 -0
- package/assets/scaffold/core/scripts/rebuild-public-knowledge-index.sh +83 -0
- package/assets/scaffold/core/scripts/rebuild-reports-index.sh +114 -0
- package/assets/scaffold/core/scripts/rebuild-social-drafts-index.sh +135 -0
- package/assets/scaffold/core/scripts/rebuild-threads-index.sh +82 -0
- package/assets/scaffold/core/scripts/rebuild-workers-index.sh +97 -0
- package/assets/scaffold/core/scripts/resize-screenshot.sh +47 -0
- package/assets/scaffold/core/scripts/token-usage-report.sh +254 -0
- package/assets/scaffold/core/scripts/worktree.sh +136 -0
- package/dist/commands/agents.js +28 -12
- package/dist/commands/api-keys.js +100 -20
- package/dist/commands/core.d.ts +90 -0
- package/dist/commands/core.js +279 -0
- package/dist/commands/files-browse.d.ts +21 -0
- package/dist/commands/files-browse.js +145 -24
- package/dist/commands/files.js +33 -7
- package/dist/commands/members.js +16 -5
- package/dist/commands/pack-install.d.ts +14 -0
- package/dist/commands/pack-install.js +51 -2
- package/dist/commands/pkg-install.js +43 -0
- package/dist/commands/run.js +8 -0
- package/dist/commands/secrets.js +125 -65
- package/dist/main.js +6 -0
- package/dist/utils/api-key-command-gate.d.ts +45 -0
- package/dist/utils/api-key-command-gate.js +51 -0
- package/dist/utils/hq-roots.d.ts +91 -0
- package/dist/utils/hq-roots.js +212 -0
- package/dist/utils/resolve-vault-credential.d.ts +63 -3
- package/dist/utils/resolve-vault-credential.js +152 -6
- package/dist/utils/run-bundled-script.d.ts +66 -0
- package/dist/utils/run-bundled-script.js +80 -0
- package/dist/utils/vault-api.d.ts +21 -0
- package/dist/utils/vault-api.js +74 -3
- package/package.json +7 -1
- package/.claude/policies/hq-cli-interactive-reads-use-sts-vend.md +0 -49
- package/.github/workflows/ci.yml +0 -46
- package/.github/workflows/publish.yml +0 -36
- package/.github/workflows/scripts/smoke-test-pkg.sh +0 -97
- package/e2e/cli.test.ts +0 -128
- package/eslint.config.js +0 -37
- package/pnpm-workspace.yaml +0 -4
- package/scripts/chmod-bins.mjs +0 -49
- package/scripts/generate-dsn.mjs +0 -26
- package/scripts/generate-scan-packages-table.mjs +0 -113
- package/scripts/smoke-sources-signals.sh +0 -103
- package/src/bin/hq-auth-refresh.ts +0 -43
- package/src/cli-version.ts +0 -13
- package/src/commands/__fixtures__/make-tar.ts +0 -126
- package/src/commands/_patterns.ts +0 -10
- package/src/commands/add.ts +0 -75
- package/src/commands/agents.test.ts +0 -527
- package/src/commands/agents.ts +0 -923
- package/src/commands/api-keys.test.ts +0 -291
- package/src/commands/api-keys.ts +0 -382
- package/src/commands/artifact-verify.test.ts +0 -177
- package/src/commands/auth.test.ts +0 -82
- package/src/commands/auth.ts +0 -213
- package/src/commands/billing.test.ts +0 -158
- package/src/commands/billing.ts +0 -146
- package/src/commands/channels.test.ts +0 -74
- package/src/commands/channels.ts +0 -91
- package/src/commands/cloud-demote.test.ts +0 -401
- package/src/commands/cloud-demote.ts +0 -278
- package/src/commands/cloud-provision.test.ts +0 -1117
- package/src/commands/cloud-provision.ts +0 -942
- package/src/commands/cloud.pull-all.test.ts +0 -711
- package/src/commands/cloud.pull-per-company.test.ts +0 -188
- package/src/commands/cloud.push-all.test.ts +0 -485
- package/src/commands/cloud.scope-excluded-warning.test.ts +0 -22
- package/src/commands/cloud.selectors.test.ts +0 -208
- package/src/commands/cloud.status.test.ts +0 -160
- package/src/commands/cloud.test.ts +0 -41
- package/src/commands/cloud.ts +0 -2130
- package/src/commands/company.test.ts +0 -202
- package/src/commands/company.ts +0 -148
- package/src/commands/creators.test.ts +0 -60
- package/src/commands/creators.ts +0 -117
- package/src/commands/crm.test.ts +0 -179
- package/src/commands/crm.ts +0 -236
- package/src/commands/db-migrate.ts +0 -55
- package/src/commands/db-provision.ts +0 -114
- package/src/commands/db-sql.ts +0 -124
- package/src/commands/db-status.ts +0 -108
- package/src/commands/db.ts +0 -26
- package/src/commands/dm.test.ts +0 -748
- package/src/commands/dm.ts +0 -1226
- package/src/commands/feedback.test.ts +0 -413
- package/src/commands/feedback.ts +0 -169
- package/src/commands/files-browse.test.ts +0 -1249
- package/src/commands/files-browse.ts +0 -1506
- package/src/commands/files-delete.test.ts +0 -484
- package/src/commands/files-recovery.test.ts +0 -370
- package/src/commands/files.test.ts +0 -722
- package/src/commands/files.ts +0 -1446
- package/src/commands/group-grants.test.ts +0 -330
- package/src/commands/group-grants.ts +0 -455
- package/src/commands/groups.test.ts +0 -44
- package/src/commands/groups.ts +0 -368
- package/src/commands/integrations.test.ts +0 -633
- package/src/commands/integrations.ts +0 -507
- package/src/commands/list.ts +0 -85
- package/src/commands/login.ts +0 -52
- package/src/commands/logout.ts +0 -30
- package/src/commands/marketplace-install.test.ts +0 -543
- package/src/commands/marketplace-security.test.ts +0 -646
- package/src/commands/mcp-registration.test.ts +0 -2787
- package/src/commands/mcp-registration.ts +0 -2612
- package/src/commands/mcp-status.test.ts +0 -483
- package/src/commands/mcp-status.ts +0 -575
- package/src/commands/mcp-status.us011.test.ts +0 -243
- package/src/commands/meetings.test.ts +0 -522
- package/src/commands/meetings.ts +0 -789
- package/src/commands/members.test.ts +0 -1474
- package/src/commands/members.ts +0 -1181
- package/src/commands/onboard-identity-guard.test.ts +0 -99
- package/src/commands/onboard-identity-guard.ts +0 -103
- package/src/commands/onboard-join.test.ts +0 -96
- package/src/commands/onboard-join.ts +0 -101
- package/src/commands/onboard-warning.test.ts +0 -26
- package/src/commands/onboard-warning.ts +0 -12
- package/src/commands/onboard.ts +0 -267
- package/src/commands/outposts-heartbeat.test.ts +0 -299
- package/src/commands/outposts-heartbeat.ts +0 -310
- package/src/commands/outposts-replica-sync.test.ts +0 -272
- package/src/commands/outposts-self-deploy.test.ts +0 -243
- package/src/commands/outposts.test.ts +0 -1039
- package/src/commands/outposts.ts +0 -1935
- package/src/commands/pack-install-secret-authorization.test.ts +0 -115
- package/src/commands/pack-install.test.ts +0 -1469
- package/src/commands/pack-install.ts +0 -2247
- package/src/commands/pack-update-cache.test.ts +0 -149
- package/src/commands/packs.test.ts +0 -88
- package/src/commands/packs.ts +0 -622
- package/src/commands/people.test.ts +0 -807
- package/src/commands/people.ts +0 -422
- package/src/commands/pkg-install.dispatch.test.ts +0 -71
- package/src/commands/pkg-install.ts +0 -215
- package/src/commands/pkg-list.test.ts +0 -79
- package/src/commands/pkg-list.ts +0 -13
- package/src/commands/pkg-remove.ts +0 -71
- package/src/commands/pkg-update.ts +0 -189
- package/src/commands/publish.test.ts +0 -650
- package/src/commands/publish.ts +0 -580
- package/src/commands/reindex.test.ts +0 -259
- package/src/commands/reindex.ts +0 -274
- package/src/commands/rescue.test.ts +0 -39
- package/src/commands/rescue.ts +0 -210
- package/src/commands/run.env-local.test.ts +0 -88
- package/src/commands/run.test.ts +0 -45
- package/src/commands/run.ts +0 -184
- package/src/commands/safe-extract.test.ts +0 -459
- package/src/commands/safe-extract.ts +0 -444
- package/src/commands/secrets-scope.test.ts +0 -56
- package/src/commands/secrets-scope.ts +0 -32
- package/src/commands/secrets.parse-destination.test.ts +0 -38
- package/src/commands/secrets.test.ts +0 -2037
- package/src/commands/secrets.ts +0 -2240
- package/src/commands/signals.ts +0 -351
- package/src/commands/skill.test.ts +0 -770
- package/src/commands/skill.ts +0 -796
- package/src/commands/sources.ts +0 -362
- package/src/commands/sync-mode.test.ts +0 -366
- package/src/commands/sync-mode.ts +0 -387
- package/src/commands/sync-narrow.test.ts +0 -633
- package/src/commands/sync-narrow.ts +0 -555
- package/src/commands/sync.ts +0 -165
- package/src/commands/team-sync.ts +0 -628
- package/src/commands/update.ts +0 -71
- package/src/commands/whoami.ts +0 -83
- package/src/commands/workers.test.ts +0 -158
- package/src/commands/workers.ts +0 -301
- package/src/index.test.ts +0 -32
- package/src/index.ts +0 -20
- package/src/lib/db/control-plane.test.ts +0 -59
- package/src/lib/db/control-plane.ts +0 -113
- package/src/lib/db/local.test.ts +0 -81
- package/src/lib/db/local.ts +0 -148
- package/src/lib/db/migrate.test.ts +0 -133
- package/src/lib/db/migrate.ts +0 -137
- package/src/lib/db/paths.test.ts +0 -112
- package/src/lib/db/paths.ts +0 -128
- package/src/lib/db/remote-engine.test.ts +0 -44
- package/src/lib/db/remote-engine.ts +0 -148
- package/src/lib/db/remote-sql.test.ts +0 -32
- package/src/lib/db/remote-sql.ts +0 -62
- package/src/lib/db/sql.test.ts +0 -106
- package/src/lib/db/sql.ts +0 -192
- package/src/lib/hq-cloud-dep.smoke.test.ts +0 -75
- package/src/lib/local-tree-diff.test.ts +0 -301
- package/src/lib/local-tree-diff.ts +0 -362
- package/src/lib/narrow-hint-banner.test.ts +0 -235
- package/src/lib/narrow-hint-banner.ts +0 -212
- package/src/lib/plan-limit-nag.test.ts +0 -317
- package/src/lib/plan-limit-nag.ts +0 -264
- package/src/main.ts +0 -371
- package/src/node-preflight.test.ts +0 -60
- package/src/node-preflight.ts +0 -67
- package/src/outpost/session-heartbeat-bounds.test.ts +0 -195
- package/src/outpost/session-heartbeat-guard.test.ts +0 -105
- package/src/outpost/session-heartbeat-publisher.test.ts +0 -178
- package/src/outpost/session-heartbeat-publisher.ts +0 -186
- package/src/outpost/session-heartbeat-retain-guard.test.ts +0 -126
- package/src/outpost/session-heartbeat.test.ts +0 -459
- package/src/outpost/session-heartbeat.ts +0 -877
- package/src/packaging.test.ts +0 -109
- package/src/run/__fixtures__/discover-schemas/example.env.schema +0 -4
- package/src/run/discover-schemas.test.ts +0 -153
- package/src/run/discover-schemas.ts +0 -79
- package/src/run/hq-plugin.test.ts +0 -371
- package/src/run/hq-plugin.ts +0 -277
- package/src/run/varlock-shape.test.ts +0 -57
- package/src/schemas/hq-package.schema.json +0 -155
- package/src/sentry-before-send.test.ts +0 -359
- package/src/sentry-before-send.ts +0 -221
- package/src/sentry-epipe.test.ts +0 -37
- package/src/sentry-release.test.ts +0 -54
- package/src/sentry.test.ts +0 -24
- package/src/sentry.ts +0 -54
- package/src/strategies/link.ts +0 -62
- package/src/strategies/merge.ts +0 -142
- package/src/types.ts +0 -146
- package/src/utils/__fixtures__/scan-packages.generated-block.sh +0 -23
- package/src/utils/auth-error.test.ts +0 -40
- package/src/utils/auth-error.ts +0 -42
- package/src/utils/billing-gate.test.ts +0 -95
- package/src/utils/billing-gate.ts +0 -228
- package/src/utils/breadcrumb-buffer.ts +0 -18
- package/src/utils/cli-telemetry.test.ts +0 -153
- package/src/utils/cli-telemetry.ts +0 -61
- package/src/utils/cognito-session.machine.test.ts +0 -109
- package/src/utils/cognito-session.refresh.test.ts +0 -84
- package/src/utils/cognito-session.test.ts +0 -328
- package/src/utils/cognito-session.ts +0 -448
- package/src/utils/company-selection-error.test.ts +0 -42
- package/src/utils/company-selection-error.ts +0 -45
- package/src/utils/contribution-table.ts +0 -83
- package/src/utils/environmental-error.test.ts +0 -45
- package/src/utils/environmental-error.ts +0 -39
- package/src/utils/epipe.test.ts +0 -28
- package/src/utils/epipe.ts +0 -29
- package/src/utils/expected-cli-error.test.ts +0 -28
- package/src/utils/expected-cli-error.ts +0 -39
- package/src/utils/feedback-diagnostics.test.ts +0 -183
- package/src/utils/feedback-diagnostics.ts +0 -123
- package/src/utils/feedback-screenshots.test.ts +0 -134
- package/src/utils/feedback-screenshots.ts +0 -124
- package/src/utils/feedback-versions.test.ts +0 -98
- package/src/utils/feedback-versions.ts +0 -68
- package/src/utils/git.ts +0 -74
- package/src/utils/integrity.ts +0 -59
- package/src/utils/intercepted-process-exit.test.ts +0 -37
- package/src/utils/intercepted-process-exit.ts +0 -36
- package/src/utils/login-provider.test.ts +0 -42
- package/src/utils/login-provider.ts +0 -30
- package/src/utils/manifest.test.ts +0 -167
- package/src/utils/manifest.ts +0 -131
- package/src/utils/pack-contributions.test.ts +0 -603
- package/src/utils/pack-contributions.ts +0 -450
- package/src/utils/people.ts +0 -215
- package/src/utils/registry-client.ts +0 -204
- package/src/utils/registry.ts +0 -90
- package/src/utils/resolve-vault-credential.test.ts +0 -69
- package/src/utils/resolve-vault-credential.ts +0 -60
- package/src/utils/sandbox-runner-client.test.ts +0 -279
- package/src/utils/sandbox-runner-client.ts +0 -260
- package/src/utils/secrets-cache.ts +0 -183
- package/src/utils/sentry-identity.ts +0 -45
- package/src/utils/settle-with-timeout.test.ts +0 -21
- package/src/utils/settle-with-timeout.ts +0 -22
- package/src/utils/user-yaml-error.test.ts +0 -24
- package/src/utils/user-yaml-error.ts +0 -30
- package/src/utils/vault-api.test.ts +0 -536
- package/src/utils/vault-api.ts +0 -382
- package/src/utils/version-check.test.ts +0 -176
- package/src/utils/version-check.ts +0 -155
- package/src/utils/version-gate.test.ts +0 -1024
- package/src/utils/version-gate.ts +0 -702
- package/test/commands/db-tenant-isolation.test.ts +0 -94
- package/test/commands/db.test.ts +0 -85
- package/test/commands/signals.test.ts +0 -207
- package/test/commands/sources.test.ts +0 -231
- package/test/e2e/smoke-install-mcp.sh +0 -113
- package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +0 -1
- package/test/fixtures/hq-pack-smoke-mcp/package.yaml +0 -11
- package/test/fixtures/signals/action_item/sample.md +0 -16
- package/test/fixtures/signals/summary/sample.md +0 -12
- package/test/fixtures/sources/meetings/sample.md +0 -25
- package/test/helpers/cli-runner.ts +0 -150
- package/test/helpers/s3-list-mock.ts +0 -79
- package/test/helpers/vault-service-mock.ts +0 -295
- package/test/sources-signals/smoke.test.ts +0 -228
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -11
- package/vitest.e2e.config.ts +0 -7
|
@@ -1,555 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `hq sync narrow` (US-007) — explicit migration ritual to flip a company's
|
|
3
|
-
* membership from `syncMode: all` to `syncMode: shared`, with a dry-run
|
|
4
|
-
* preview and a dirty-file safety gate.
|
|
5
|
-
*
|
|
6
|
-
* Two call shapes:
|
|
7
|
-
*
|
|
8
|
-
* hq sync narrow --dry-run [--company <slug>]
|
|
9
|
-
* Resolves the membership for <slug> (or the active company), fetches
|
|
10
|
-
* the caller's explicit grants, coalesces them, walks the local
|
|
11
|
-
* `companies/<slug>/` tree, and prints a table:
|
|
12
|
-
*
|
|
13
|
-
* files staying: N
|
|
14
|
-
* clean orphans: N (size)
|
|
15
|
-
* dirty orphans: N (size)
|
|
16
|
-
*
|
|
17
|
-
* Writes nothing — no journal, no local file, no server call beyond
|
|
18
|
-
* the read-only GET on grants + sync-config.
|
|
19
|
-
*
|
|
20
|
-
* hq sync narrow --apply [--yes] [--force] [--company <slug>]
|
|
21
|
-
* Same computation, then:
|
|
22
|
-
* - if `dirty > 0` and `--force` NOT set → abort with conflict report
|
|
23
|
-
* and a recovery message.
|
|
24
|
-
* - else delete clean orphans on disk, tombstone their journal
|
|
25
|
-
* entries, optionally delete dirty orphans too (with `--force`),
|
|
26
|
-
* then PUT sync-config to `shared`. The server's PUT writes the
|
|
27
|
-
* authoritative `MEMBERSHIP_SYNC_CONFIG_CHANGED` audit row; the
|
|
28
|
-
* CLI prints a local audit line as well.
|
|
29
|
-
*
|
|
30
|
-
* Refuses on already-`shared` or `custom` memberships (acceptance 5) —
|
|
31
|
-
* narrowing makes sense only as `all → shared`.
|
|
32
|
-
*
|
|
33
|
-
* Cross-package note: depends on hq-cloud helpers shipped in US-005
|
|
34
|
-
* (`coalescePrefixes`, `readJournal`/`writeJournal`, `tombstoneEntry`) and
|
|
35
|
-
* the VaultClient sync-config endpoints from US-003. While that hq-cloud
|
|
36
|
-
* release is unpublished, hq-cli pins `@indigoai-us/hq-cloud` to
|
|
37
|
-
* `file:../hq-cloud` via `pnpm.overrides`.
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
import { Command } from "commander";
|
|
41
|
-
import chalk from "chalk";
|
|
42
|
-
import * as readline from "node:readline";
|
|
43
|
-
import * as fs from "node:fs";
|
|
44
|
-
|
|
45
|
-
import {
|
|
46
|
-
VaultClient,
|
|
47
|
-
coalescePrefixes,
|
|
48
|
-
grantPathToPrefix,
|
|
49
|
-
readJournal,
|
|
50
|
-
writeJournal,
|
|
51
|
-
tombstoneEntry,
|
|
52
|
-
type ExplicitGrant,
|
|
53
|
-
type Membership,
|
|
54
|
-
type MembershipSyncConfig,
|
|
55
|
-
type SyncMode,
|
|
56
|
-
type SyncJournal,
|
|
57
|
-
} from "@indigoai-us/hq-cloud";
|
|
58
|
-
|
|
59
|
-
import {
|
|
60
|
-
DEFAULT_HQ_ROOT,
|
|
61
|
-
ensureCognitoToken,
|
|
62
|
-
buildVaultConfig,
|
|
63
|
-
} from "../utils/cognito-session.js";
|
|
64
|
-
import { readActiveCompanySlug } from "./sync-mode.js";
|
|
65
|
-
import {
|
|
66
|
-
buildNarrowPlan,
|
|
67
|
-
formatBytes,
|
|
68
|
-
formatNarrowPlanSummary,
|
|
69
|
-
type NarrowPlan,
|
|
70
|
-
type NarrowDirtyFile,
|
|
71
|
-
} from "../lib/local-tree-diff.js";
|
|
72
|
-
|
|
73
|
-
// ── Types ───────────────────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
/** Subset of VaultClient surface this command exercises (test seam). */
|
|
76
|
-
export interface SyncNarrowVaultClient {
|
|
77
|
-
listMyMemberships(): Promise<Membership[]>;
|
|
78
|
-
getMembershipSyncConfig(membershipId: string): Promise<MembershipSyncConfig>;
|
|
79
|
-
setMembershipSyncConfig(
|
|
80
|
-
membershipId: string,
|
|
81
|
-
partial: { syncMode: SyncMode; customPaths?: string[] },
|
|
82
|
-
): Promise<MembershipSyncConfig>;
|
|
83
|
-
listMyExplicitGrants(companyUid: string): Promise<ExplicitGrant[]>;
|
|
84
|
-
entity: {
|
|
85
|
-
get(uid: string): Promise<{ uid: string; slug: string; name?: string }>;
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Journal I/O seam — defaults to the real `readJournal`/`writeJournal`. */
|
|
90
|
-
export interface JournalIO {
|
|
91
|
-
read(slug: string): SyncJournal;
|
|
92
|
-
write(slug: string, journal: SyncJournal): void;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const realJournalIO: JournalIO = {
|
|
96
|
-
read: readJournal,
|
|
97
|
-
write: writeJournal,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/** Filesystem seam — defaults to real `fs` (only `unlinkSync` is exercised). */
|
|
101
|
-
export interface FsIO {
|
|
102
|
-
unlinkSync(p: string): void;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const realFsIO: FsIO = {
|
|
106
|
-
unlinkSync: fs.unlinkSync,
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
/** Confirmation prompt seam — defaults to a `readline` y/N gate. */
|
|
110
|
-
export type ConfirmFn = (message: string) => Promise<boolean>;
|
|
111
|
-
|
|
112
|
-
const realConfirm: ConfirmFn = async (message) => {
|
|
113
|
-
const rl = readline.createInterface({
|
|
114
|
-
input: process.stdin,
|
|
115
|
-
output: process.stdout,
|
|
116
|
-
});
|
|
117
|
-
return new Promise((resolve) => {
|
|
118
|
-
rl.question(`${message} [y/N] `, (answer) => {
|
|
119
|
-
rl.close();
|
|
120
|
-
resolve(/^y(es)?$/i.test(answer.trim()));
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
export interface ResolveTargetInput {
|
|
126
|
-
companySlug: string;
|
|
127
|
-
vaultClient: SyncNarrowVaultClient;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export interface ResolveTargetResult {
|
|
131
|
-
membership: Membership;
|
|
132
|
-
companyUid: string;
|
|
133
|
-
companySlug: string;
|
|
134
|
-
syncConfig: MembershipSyncConfig;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Resolve the caller's membership for `companySlug` and read its current
|
|
139
|
-
* sync-config. Throws if the slug doesn't match any membership or if the
|
|
140
|
-
* current mode isn't eligible for narrowing (`all` → `shared` only).
|
|
141
|
-
*/
|
|
142
|
-
export async function resolveNarrowTarget(
|
|
143
|
-
input: ResolveTargetInput,
|
|
144
|
-
): Promise<ResolveTargetResult> {
|
|
145
|
-
const { companySlug, vaultClient } = input;
|
|
146
|
-
const memberships = await vaultClient.listMyMemberships();
|
|
147
|
-
const enriched = await Promise.all(
|
|
148
|
-
memberships.map(async (m) => {
|
|
149
|
-
try {
|
|
150
|
-
const ent = await vaultClient.entity.get(m.companyUid);
|
|
151
|
-
return { membership: m, slug: ent.slug, name: ent.name };
|
|
152
|
-
} catch {
|
|
153
|
-
return { membership: m, slug: undefined, name: undefined };
|
|
154
|
-
}
|
|
155
|
-
}),
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
const match = enriched.find((row) => row.slug === companySlug);
|
|
159
|
-
if (!match) {
|
|
160
|
-
const known = enriched
|
|
161
|
-
.map((r) => r.slug)
|
|
162
|
-
.filter((s): s is string => !!s)
|
|
163
|
-
.join(", ");
|
|
164
|
-
throw new Error(
|
|
165
|
-
`No membership found for company '${companySlug}'. Memberships visible to you: ${
|
|
166
|
-
known || "(none)"
|
|
167
|
-
}.`,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const membership = match.membership;
|
|
172
|
-
const syncConfig = await vaultClient.getMembershipSyncConfig(
|
|
173
|
-
membership.membershipKey,
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
if (syncConfig.syncMode !== "all") {
|
|
177
|
-
throw new Error(
|
|
178
|
-
`Refusing to narrow: membership for '${companySlug}' is currently ` +
|
|
179
|
-
`syncMode='${syncConfig.syncMode}'. The narrow ritual only runs on ` +
|
|
180
|
-
`syncMode='all' memberships (shared → shared is a no-op; ` +
|
|
181
|
-
`custom requires \`hq sync mode shared\` directly).`,
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
membership,
|
|
187
|
-
companyUid: membership.companyUid,
|
|
188
|
-
companySlug,
|
|
189
|
-
syncConfig,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export interface ComputePlanInput {
|
|
194
|
-
hqRoot: string;
|
|
195
|
-
companySlug: string;
|
|
196
|
-
companyUid: string;
|
|
197
|
-
vaultClient: SyncNarrowVaultClient;
|
|
198
|
-
journalIO?: JournalIO;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export interface ComputePlanResult {
|
|
202
|
-
plan: NarrowPlan;
|
|
203
|
-
prospectivePrefixSet: string[];
|
|
204
|
-
grants: ExplicitGrant[];
|
|
205
|
-
journal: SyncJournal;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Compute the narrow plan for one company: fetch explicit grants, coalesce,
|
|
210
|
-
* read the per-company journal, walk the local tree, classify.
|
|
211
|
-
*
|
|
212
|
-
* Reads only — never mutates the journal or hits a destructive endpoint.
|
|
213
|
-
* `apply()` consumes this and performs the destructive steps.
|
|
214
|
-
*/
|
|
215
|
-
export async function computeNarrowPlan(
|
|
216
|
-
input: ComputePlanInput,
|
|
217
|
-
): Promise<ComputePlanResult> {
|
|
218
|
-
const { hqRoot, companySlug, companyUid, vaultClient } = input;
|
|
219
|
-
const io = input.journalIO ?? realJournalIO;
|
|
220
|
-
|
|
221
|
-
const grants = await vaultClient.listMyExplicitGrants(companyUid);
|
|
222
|
-
// Normalize each grant into a company-relative, startsWith-friendly prefix
|
|
223
|
-
// (grantPathToPrefix, hq-cloud ≥5.42.0): real grants are anchored
|
|
224
|
-
// (`companies/<slug>/x/*`, `<slug>/x/*`) and glob-style (`x/*`, bare `*`),
|
|
225
|
-
// none of which startsWith-match the company-relative local-tree keys
|
|
226
|
-
// buildNarrowPlan emits. A wildcard grant normalizes to "" (everything);
|
|
227
|
-
// coalescePrefixes drops empties, so guard it explicitly to `[""]` (which
|
|
228
|
-
// isCoveredByAny treats as covering everything → nothing orphaned) rather
|
|
229
|
-
// than letting it collapse to "nothing" and propose deleting the tree.
|
|
230
|
-
const normalizedPrefixes = grants.map((g) =>
|
|
231
|
-
grantPathToPrefix(g.path, companySlug),
|
|
232
|
-
);
|
|
233
|
-
const prospectivePrefixSet = normalizedPrefixes.some((p) => p === "")
|
|
234
|
-
? [""]
|
|
235
|
-
: coalescePrefixes(normalizedPrefixes);
|
|
236
|
-
const journal = io.read(companySlug);
|
|
237
|
-
|
|
238
|
-
const plan = buildNarrowPlan({
|
|
239
|
-
hqRoot,
|
|
240
|
-
companySlug,
|
|
241
|
-
prospectivePrefixSet,
|
|
242
|
-
journal,
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
return { plan, prospectivePrefixSet, grants, journal };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export interface ApplyNarrowInput {
|
|
249
|
-
companySlug: string;
|
|
250
|
-
membershipId: string;
|
|
251
|
-
plan: NarrowPlan;
|
|
252
|
-
journal: SyncJournal;
|
|
253
|
-
vaultClient: SyncNarrowVaultClient;
|
|
254
|
-
/** If true, delete dirty orphans too (still tombstoned). */
|
|
255
|
-
force: boolean;
|
|
256
|
-
journalIO?: JournalIO;
|
|
257
|
-
fsIO?: FsIO;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export interface ApplyNarrowResult {
|
|
261
|
-
cleanDeleted: number;
|
|
262
|
-
cleanBytes: number;
|
|
263
|
-
dirtyDeleted: number;
|
|
264
|
-
dirtyBytes: number;
|
|
265
|
-
tombstoned: number;
|
|
266
|
-
newConfig: MembershipSyncConfig;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Apply the destructive side of a narrow:
|
|
271
|
-
* 1. Delete clean orphans (best-effort — ENOENT is ignored).
|
|
272
|
-
* 2. Tombstone their journal entries.
|
|
273
|
-
* 3. If `force`, also delete + tombstone dirty orphans.
|
|
274
|
-
* 4. Persist the journal.
|
|
275
|
-
* 5. PUT sync-config → `shared` (server writes the audit row).
|
|
276
|
-
*
|
|
277
|
-
* Order is fail-safe: if a delete throws partway, the journal write below
|
|
278
|
-
* won't run, but the in-progress deletions are already on disk — that's the
|
|
279
|
-
* "don't roll back I/O" contract from the spec. The CLI surfaces a
|
|
280
|
-
* recovery message when this throws.
|
|
281
|
-
*
|
|
282
|
-
* Pure-ish: no prompting (the CLI wrapper handles that), no console
|
|
283
|
-
* output. Just disk + network.
|
|
284
|
-
*/
|
|
285
|
-
export async function applyNarrow(
|
|
286
|
-
input: ApplyNarrowInput,
|
|
287
|
-
): Promise<ApplyNarrowResult> {
|
|
288
|
-
const { plan, journal, force, companySlug, membershipId, vaultClient } = input;
|
|
289
|
-
const journalIO = input.journalIO ?? realJournalIO;
|
|
290
|
-
const fsIO = input.fsIO ?? realFsIO;
|
|
291
|
-
|
|
292
|
-
let cleanDeleted = 0;
|
|
293
|
-
let dirtyDeleted = 0;
|
|
294
|
-
let tombstoned = 0;
|
|
295
|
-
const cleanBytes = plan.clean.reduce((sum, f) => sum + f.bytes, 0);
|
|
296
|
-
const dirtyBytes = force
|
|
297
|
-
? plan.dirty.reduce((sum, f) => sum + f.bytes, 0)
|
|
298
|
-
: 0;
|
|
299
|
-
|
|
300
|
-
// 1. Delete clean orphans + tombstone.
|
|
301
|
-
for (const file of plan.clean) {
|
|
302
|
-
try {
|
|
303
|
-
fsIO.unlinkSync(file.absPath);
|
|
304
|
-
cleanDeleted++;
|
|
305
|
-
} catch (err) {
|
|
306
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
307
|
-
if (code !== "ENOENT") throw err;
|
|
308
|
-
}
|
|
309
|
-
tombstoneEntry(journal, file.relPath, "narrow_apply");
|
|
310
|
-
tombstoned++;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// 2. With --force, delete dirty orphans too.
|
|
314
|
-
if (force) {
|
|
315
|
-
for (const file of plan.dirty) {
|
|
316
|
-
try {
|
|
317
|
-
fsIO.unlinkSync(file.absPath);
|
|
318
|
-
dirtyDeleted++;
|
|
319
|
-
} catch (err) {
|
|
320
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
321
|
-
if (code !== "ENOENT") throw err;
|
|
322
|
-
}
|
|
323
|
-
// tombstone only if a journal entry exists — `not-in-journal` files
|
|
324
|
-
// never had one to tombstone.
|
|
325
|
-
if (journal.files[file.relPath]) {
|
|
326
|
-
tombstoneEntry(journal, file.relPath, "narrow_apply");
|
|
327
|
-
tombstoned++;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// 3. Persist journal.
|
|
333
|
-
journalIO.write(companySlug, journal);
|
|
334
|
-
|
|
335
|
-
// 4. PUT sync-config → shared. Server writes the authoritative
|
|
336
|
-
// MEMBERSHIP_SYNC_CONFIG_CHANGED audit row.
|
|
337
|
-
const newConfig = await vaultClient.setMembershipSyncConfig(membershipId, {
|
|
338
|
-
syncMode: "shared",
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
return {
|
|
342
|
-
cleanDeleted,
|
|
343
|
-
cleanBytes,
|
|
344
|
-
dirtyDeleted,
|
|
345
|
-
dirtyBytes,
|
|
346
|
-
tombstoned,
|
|
347
|
-
newConfig,
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// ── Dirty-file conflict report (operator-facing) ───────────────────────────
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Format the dirty-file conflict report shown on `--apply` when dirty files
|
|
355
|
-
* are present and `--force` is NOT set. The CLI prints this and exits
|
|
356
|
-
* non-zero so the operator can resolve before retrying.
|
|
357
|
-
*/
|
|
358
|
-
export function formatDirtyConflictReport(
|
|
359
|
-
dirty: NarrowDirtyFile[],
|
|
360
|
-
companySlug: string,
|
|
361
|
-
): string {
|
|
362
|
-
const lines: string[] = [];
|
|
363
|
-
lines.push(
|
|
364
|
-
`Aborting narrow for '${companySlug}': ${dirty.length} locally-modified file(s) ` +
|
|
365
|
-
`would be removed by this narrow.`,
|
|
366
|
-
);
|
|
367
|
-
lines.push("");
|
|
368
|
-
lines.push("Dirty files:");
|
|
369
|
-
const cap = 25;
|
|
370
|
-
for (const f of dirty.slice(0, cap)) {
|
|
371
|
-
lines.push(` ${f.relPath} (${formatBytes(f.bytes)}, ${f.reason})`);
|
|
372
|
-
}
|
|
373
|
-
if (dirty.length > cap) {
|
|
374
|
-
lines.push(` ... ${dirty.length - cap} more`);
|
|
375
|
-
}
|
|
376
|
-
lines.push("");
|
|
377
|
-
lines.push("Resolve before retrying:");
|
|
378
|
-
lines.push(" - commit or push the modifications via the normal sync flow;");
|
|
379
|
-
lines.push(" - move them outside the company folder; or");
|
|
380
|
-
lines.push(" - delete them locally if they really should be gone.");
|
|
381
|
-
lines.push("");
|
|
382
|
-
lines.push(
|
|
383
|
-
"Alternatively, re-run with --force to remove the dirty files anyway " +
|
|
384
|
-
"(destructive — there is no undo).",
|
|
385
|
-
);
|
|
386
|
-
return lines.join("\n");
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// ── CLI registration ────────────────────────────────────────────────────────
|
|
390
|
-
|
|
391
|
-
interface SyncNarrowCliOptions {
|
|
392
|
-
dryRun?: boolean;
|
|
393
|
-
apply?: boolean;
|
|
394
|
-
yes?: boolean;
|
|
395
|
-
force?: boolean;
|
|
396
|
-
company?: string;
|
|
397
|
-
hqRoot: string;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Wire `hq sync narrow` onto the existing `sync` Commander group. Called
|
|
402
|
-
* from `src/index.ts` after `registerSyncModeCommand`.
|
|
403
|
-
*/
|
|
404
|
-
export function registerSyncNarrowCommand(syncCmd: Command): void {
|
|
405
|
-
syncCmd
|
|
406
|
-
.command("narrow")
|
|
407
|
-
.description(
|
|
408
|
-
"Migrate a company's membership from syncMode='all' to 'shared' — " +
|
|
409
|
-
"dry-run previews diff, --apply performs the prune + flip",
|
|
410
|
-
)
|
|
411
|
-
.option(
|
|
412
|
-
"--dry-run",
|
|
413
|
-
"Compute + print the narrow plan; write nothing (default if neither --dry-run nor --apply is passed)",
|
|
414
|
-
)
|
|
415
|
-
.option("--apply", "Execute the narrow — delete clean orphans, flip sync-mode to 'shared'")
|
|
416
|
-
.option("--yes", "Skip the interactive confirmation prompt on --apply")
|
|
417
|
-
.option(
|
|
418
|
-
"--force",
|
|
419
|
-
"On --apply, also remove locally-modified (dirty) files (destructive — no undo)",
|
|
420
|
-
)
|
|
421
|
-
.option(
|
|
422
|
-
"--company <slug>",
|
|
423
|
-
"Company slug (defaults to the active company in <hq-root>/.hq/config.json)",
|
|
424
|
-
)
|
|
425
|
-
.option(
|
|
426
|
-
"--hq-root <path>",
|
|
427
|
-
`Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`,
|
|
428
|
-
DEFAULT_HQ_ROOT,
|
|
429
|
-
)
|
|
430
|
-
.action(async (options: SyncNarrowCliOptions) => {
|
|
431
|
-
try {
|
|
432
|
-
if (options.dryRun && options.apply) {
|
|
433
|
-
throw new Error("--dry-run and --apply are mutually exclusive.");
|
|
434
|
-
}
|
|
435
|
-
// Default to dry-run when nothing is specified — safer.
|
|
436
|
-
const apply = !!options.apply;
|
|
437
|
-
const dryRun = !apply;
|
|
438
|
-
|
|
439
|
-
const companySlug =
|
|
440
|
-
options.company ?? readActiveCompanySlug(options.hqRoot);
|
|
441
|
-
if (!companySlug) {
|
|
442
|
-
throw new Error(
|
|
443
|
-
"No company specified. Pass --company <slug> or set activeCompany in <hq-root>/.hq/config.json.",
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const accessToken = await ensureCognitoToken();
|
|
448
|
-
const vaultConfig = buildVaultConfig(accessToken);
|
|
449
|
-
const client = new VaultClient(vaultConfig);
|
|
450
|
-
|
|
451
|
-
const target = await resolveNarrowTarget({
|
|
452
|
-
companySlug,
|
|
453
|
-
vaultClient: client,
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
const { plan, prospectivePrefixSet, grants } = await computeNarrowPlan({
|
|
457
|
-
hqRoot: options.hqRoot,
|
|
458
|
-
companySlug: target.companySlug,
|
|
459
|
-
companyUid: target.companyUid,
|
|
460
|
-
vaultClient: client,
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
// Always print the summary.
|
|
464
|
-
console.log(
|
|
465
|
-
chalk.bold(
|
|
466
|
-
`${dryRun ? "Dry-run" : "Apply"}: narrow '${target.companySlug}' (${grants.length} explicit grant${
|
|
467
|
-
grants.length === 1 ? "" : "s"
|
|
468
|
-
}, ${prospectivePrefixSet.length} coalesced prefix${
|
|
469
|
-
prospectivePrefixSet.length === 1 ? "" : "es"
|
|
470
|
-
})`,
|
|
471
|
-
),
|
|
472
|
-
);
|
|
473
|
-
console.log(formatNarrowPlanSummary(plan));
|
|
474
|
-
|
|
475
|
-
if (dryRun) {
|
|
476
|
-
if (plan.totalDirtyCount > 0) {
|
|
477
|
-
console.log("");
|
|
478
|
-
console.log(
|
|
479
|
-
chalk.yellow(
|
|
480
|
-
`Heads-up: ${plan.totalDirtyCount} dirty file(s) would block --apply unless --force is passed.`,
|
|
481
|
-
),
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// --apply path
|
|
488
|
-
if (plan.totalDirtyCount > 0 && !options.force) {
|
|
489
|
-
console.error(chalk.red(formatDirtyConflictReport(plan.dirty, target.companySlug)));
|
|
490
|
-
process.exit(2);
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (!options.yes) {
|
|
495
|
-
const force = !!options.force;
|
|
496
|
-
const dirtyTail = force
|
|
497
|
-
? ` AND remove ${plan.totalDirtyCount} dirty file(s) (--force)`
|
|
498
|
-
: "";
|
|
499
|
-
const confirmed = await realConfirm(
|
|
500
|
-
`About to remove ${plan.totalCleanCount} clean file(s)${dirtyTail} and flip '${target.companySlug}' to syncMode='shared'. Proceed?`,
|
|
501
|
-
);
|
|
502
|
-
if (!confirmed) {
|
|
503
|
-
console.log("Cancelled. No changes applied.");
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Re-read the journal at apply time to avoid races with concurrent syncs.
|
|
509
|
-
const journal = readJournal(target.companySlug);
|
|
510
|
-
const result = await applyNarrow({
|
|
511
|
-
companySlug: target.companySlug,
|
|
512
|
-
membershipId: target.membership.membershipKey,
|
|
513
|
-
plan,
|
|
514
|
-
journal,
|
|
515
|
-
vaultClient: client,
|
|
516
|
-
force: !!options.force,
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
console.log(
|
|
520
|
-
chalk.green("✓"),
|
|
521
|
-
`Narrow applied for ${chalk.bold(target.companySlug)}:`,
|
|
522
|
-
);
|
|
523
|
-
console.log(
|
|
524
|
-
chalk.dim(
|
|
525
|
-
` removed: ${result.cleanDeleted} clean (${formatBytes(result.cleanBytes)})${
|
|
526
|
-
result.dirtyDeleted > 0
|
|
527
|
-
? ` + ${result.dirtyDeleted} dirty (${formatBytes(result.dirtyBytes)})`
|
|
528
|
-
: ""
|
|
529
|
-
}`,
|
|
530
|
-
),
|
|
531
|
-
);
|
|
532
|
-
console.log(
|
|
533
|
-
chalk.dim(` tombstoned: ${result.tombstoned} journal entr${result.tombstoned === 1 ? "y" : "ies"}`),
|
|
534
|
-
);
|
|
535
|
-
console.log(
|
|
536
|
-
chalk.dim(
|
|
537
|
-
` syncMode: all → ${result.newConfig.syncMode}` +
|
|
538
|
-
(result.newConfig.updatedAt ? ` (updatedAt: ${result.newConfig.updatedAt})` : ""),
|
|
539
|
-
),
|
|
540
|
-
);
|
|
541
|
-
console.log(
|
|
542
|
-
chalk.dim(
|
|
543
|
-
` audit: server-side MEMBERSHIP_SYNC_CONFIG_CHANGED row written by PUT /v1/memberships/${target.membership.membershipKey}/sync-config`,
|
|
544
|
-
),
|
|
545
|
-
);
|
|
546
|
-
} catch (err) {
|
|
547
|
-
console.error(
|
|
548
|
-
chalk.red("✗ sync narrow failed:"),
|
|
549
|
-
err instanceof Error ? err.message : String(err),
|
|
550
|
-
);
|
|
551
|
-
process.exit(1);
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
|