@indigoai-us/hq-cli 5.33.1 → 5.34.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.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * hq master-sync — surface namespaced skills, mirror the personal overlay into
3
+ * core/, and regenerate the workers registry.
4
+ *
5
+ * Thin wrapper over @indigoai-us/hq-cloud's masterSync(), which execs the
6
+ * bundled scripts/master-sync.sh against the HQ root. This command is what the
7
+ * hq-core master-sync hook shim calls on Stop / PostToolUse.
8
+ */
9
+ import { Command } from 'commander';
10
+ export declare function registerMasterSyncCommand(program: Command): void;
11
+ //# sourceMappingURL=master-sync.d.ts.map
@@ -0,0 +1,15 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c5486cda-a7fa-5274-ab0a-a61e1958aac7")}catch(e){}}();
3
+ import { masterSync } from '@indigoai-us/hq-cloud';
4
+ export function registerMasterSyncCommand(program) {
5
+ program
6
+ .command('master-sync')
7
+ .description('Surface namespaced skills, mirror the personal overlay into core/, and regenerate the workers registry')
8
+ .option('--repo-root <path>', 'HQ root to operate on (defaults to the current directory)')
9
+ .action((opts) => {
10
+ const { status } = masterSync({ repoRoot: opts.repoRoot });
11
+ process.exit(status);
12
+ });
13
+ }
14
+ //# sourceMappingURL=master-sync.js.map
15
+ //# debugId=c5486cda-a7fa-5274-ab0a-a61e1958aac7
@@ -0,0 +1,33 @@
1
+ /**
2
+ * hq rescue — re-sync the local HQ core to an upstream hq-core release (or the
3
+ * staging branch) WITHOUT destroying local edits ("drift").
4
+ *
5
+ * Thin driver over @indigoai-us/hq-cloud's `rescue()`, which execs the bundled
6
+ * scripts/replace-rescue.sh against the HQ root. This is the CLI sibling of the
7
+ * HQ Sync menubar app's "Update / Restore" pill — both drive the exact same
8
+ * rescue script, just resolved from the shared hq-cloud package.
9
+ *
10
+ * Prod (default): resolves the latest `indigoai-us/hq-core` release tag and
11
+ * pins the three-way history floor to the commit of the user's currently
12
+ * installed version (read from core/core.yaml). Staging (`--staging`): targets
13
+ * `indigoai-us/hq-core-staging@main` and lets the script read its on-disk
14
+ * sync stamp for the floor.
15
+ */
16
+ import { Command } from 'commander';
17
+ export interface RescueTarget {
18
+ source: string;
19
+ /** undefined → let the script apply its own default ref (`main`). */
20
+ ref?: string;
21
+ }
22
+ /**
23
+ * Resolve the source repo + ref from the user's flags. Pure + exported for
24
+ * tests. `latestTag` is the resolved latest release tag (prod only); ignored
25
+ * for staging and when an explicit `--ref` is given.
26
+ */
27
+ export declare function resolveRescueTarget(opts: {
28
+ staging?: boolean;
29
+ source?: string;
30
+ ref?: string;
31
+ }, latestTag?: string): RescueTarget;
32
+ export declare function registerRescueCommand(program: Command): void;
33
+ //# sourceMappingURL=rescue.d.ts.map
@@ -0,0 +1,161 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e0da62c9-7fd5-5514-b2d0-1b5b76ebcd21")}catch(e){}}();
3
+ import { spawnSync } from 'child_process';
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import * as yaml from 'js-yaml';
7
+ import chalk from 'chalk';
8
+ import { rescue } from '@indigoai-us/hq-cloud';
9
+ import { findHqRoot } from '../utils/manifest.js';
10
+ const PROD_SOURCE = 'indigoai-us/hq-core';
11
+ const STAGING_SOURCE = 'indigoai-us/hq-core-staging';
12
+ /**
13
+ * Resolve the source repo + ref from the user's flags. Pure + exported for
14
+ * tests. `latestTag` is the resolved latest release tag (prod only); ignored
15
+ * for staging and when an explicit `--ref` is given.
16
+ */
17
+ export function resolveRescueTarget(opts, latestTag) {
18
+ if (opts.staging) {
19
+ return { source: opts.source ?? STAGING_SOURCE, ref: opts.ref };
20
+ }
21
+ return { source: opts.source ?? PROD_SOURCE, ref: opts.ref ?? latestTag };
22
+ }
23
+ /** Resolve a GitHub token: prefer `gh auth token`, fall back to env. */
24
+ function resolveGhToken() {
25
+ try {
26
+ const res = spawnSync('gh', ['auth', 'token'], { encoding: 'utf8' });
27
+ if (res.status === 0) {
28
+ const tok = res.stdout.trim();
29
+ if (tok)
30
+ return tok;
31
+ }
32
+ }
33
+ catch {
34
+ // gh not installed — fall through to env.
35
+ }
36
+ return process.env.GH_TOKEN || process.env.GITHUB_TOKEN || undefined;
37
+ }
38
+ function ghHeaders(token) {
39
+ const headers = {
40
+ Accept: 'application/vnd.github+json',
41
+ 'User-Agent': 'hq-cli-rescue',
42
+ 'X-GitHub-Api-Version': '2022-11-28',
43
+ };
44
+ if (token)
45
+ headers.Authorization = `Bearer ${token}`;
46
+ return headers;
47
+ }
48
+ /** Latest release tag for a repo, e.g. `v12.3.0`. Throws on failure. */
49
+ async function getLatestReleaseTag(repo, token) {
50
+ const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
51
+ headers: ghHeaders(token),
52
+ });
53
+ if (!res.ok) {
54
+ throw new Error(`GitHub releases/latest for ${repo}: HTTP ${res.status}`);
55
+ }
56
+ const body = (await res.json());
57
+ if (!body.tag_name)
58
+ throw new Error(`no tag_name in latest release for ${repo}`);
59
+ return body.tag_name;
60
+ }
61
+ /** Resolve a tag to its commit SHA, dereferencing annotated tags. */
62
+ async function resolveTagSha(repo, tag, token) {
63
+ const refRes = await fetch(`https://api.github.com/repos/${repo}/git/ref/tags/${encodeURIComponent(tag)}`, { headers: ghHeaders(token) });
64
+ if (!refRes.ok)
65
+ throw new Error(`git/ref/tags/${tag} for ${repo}: HTTP ${refRes.status}`);
66
+ const ref = (await refRes.json());
67
+ const obj = ref.object;
68
+ if (!obj?.sha)
69
+ throw new Error(`no object.sha for tag ${tag} in ${repo}`);
70
+ if (obj.type !== 'tag')
71
+ return obj.sha; // lightweight tag → already a commit
72
+ // Annotated tag → dereference to the commit it points at.
73
+ const tagRes = await fetch(`https://api.github.com/repos/${repo}/git/tags/${obj.sha}`, {
74
+ headers: ghHeaders(token),
75
+ });
76
+ if (!tagRes.ok)
77
+ throw new Error(`git/tags/${obj.sha} for ${repo}: HTTP ${tagRes.status}`);
78
+ const annotated = (await tagRes.json());
79
+ if (!annotated.object?.sha)
80
+ throw new Error(`annotated tag ${tag} has no target sha`);
81
+ return annotated.object.sha;
82
+ }
83
+ /** Read the installed HQ core version from `{hqRoot}/core/core.yaml`. */
84
+ function readInstalledVersion(hqRoot) {
85
+ const file = path.join(hqRoot, 'core', 'core.yaml');
86
+ if (!fs.existsSync(file))
87
+ return undefined;
88
+ try {
89
+ const doc = yaml.load(fs.readFileSync(file, 'utf8'));
90
+ const v = doc?.hqVersion;
91
+ return typeof v === 'string' ? v : undefined;
92
+ }
93
+ catch {
94
+ return undefined;
95
+ }
96
+ }
97
+ export function registerRescueCommand(program) {
98
+ program
99
+ .command('rescue')
100
+ .description('Re-sync your HQ core to the latest release, preserving your local edits (drift)')
101
+ .option('--hq-root <path>', 'HQ root to operate on (defaults to auto-detected root)')
102
+ .option('--ref <ref>', 'Target tag/branch (default: latest hq-core release)')
103
+ .option('--source <repo>', 'Source repo (default: indigoai-us/hq-core)')
104
+ .option('--staging', 'Use the staging channel (indigoai-us/hq-core-staging@main)')
105
+ .option('--floor-sha <sha>', 'Pin the three-way history floor to a 40-char commit SHA')
106
+ .option('--paths <list>', 'Comma-separated top-level paths to narrow the rescue to')
107
+ .option('--check', 'Plan only — classify and report, change nothing on disk (--dry-run)')
108
+ .option('-y, --yes', 'Skip the confirmation prompt')
109
+ .option('--no-backup', 'Skip the pre-op safety snapshot under ~/.hq/backups')
110
+ .option('--cloud-update', 'Cloud-update mode')
111
+ .action(async (opts) => {
112
+ try {
113
+ const hqRoot = opts.hqRoot ?? findHqRoot();
114
+ const token = resolveGhToken();
115
+ let { source, ref } = resolveRescueTarget(opts);
116
+ let floorSha = opts.floorSha;
117
+ if (!opts.staging) {
118
+ // Prod: resolve the latest release tag when no explicit ref, and
119
+ // pin the floor to the installed version's commit when possible.
120
+ if (!ref) {
121
+ ref = await getLatestReleaseTag(source, token);
122
+ }
123
+ if (!floorSha) {
124
+ const installed = readInstalledVersion(hqRoot);
125
+ if (installed) {
126
+ try {
127
+ floorSha = await resolveTagSha(source, `v${installed}`, token);
128
+ }
129
+ catch (err) {
130
+ console.warn(chalk.yellow(`Warning: couldn't resolve the floor commit for v${installed} ` +
131
+ `(${err instanceof Error ? err.message : 'unknown error'}); ` +
132
+ `falling back to the on-disk sync stamp.`));
133
+ }
134
+ }
135
+ }
136
+ }
137
+ console.log(chalk.dim(`Rescuing ${hqRoot} from ${source}${ref ? `@${ref}` : ''}` +
138
+ `${floorSha ? ` (floor ${floorSha.slice(0, 12)})` : ''}` +
139
+ `${opts.check ? ' [dry-run]' : ''}`));
140
+ const { status } = rescue({
141
+ hqRoot,
142
+ source,
143
+ ref,
144
+ floorSha,
145
+ paths: opts.paths ? opts.paths.split(',').map((p) => p.trim()).filter(Boolean) : undefined,
146
+ dryRun: opts.check,
147
+ assumeYes: opts.yes,
148
+ noBackup: opts.backup === false,
149
+ cloudUpdate: opts.cloudUpdate,
150
+ ghToken: token,
151
+ });
152
+ process.exit(status);
153
+ }
154
+ catch (error) {
155
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error');
156
+ process.exit(1);
157
+ }
158
+ });
159
+ }
160
+ //# sourceMappingURL=rescue.js.map
161
+ //# debugId=e0da62c9-7fd5-5514-b2d0-1b5b76ebcd21
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * HQ CLI - Module management, package management, and cloud sync for HQ
4
4
  */
5
5
 
6
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="83d8729b-5b5c-50b8-8785-372d61feb425")}catch(e){}}();
6
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="004dc716-c660-5745-94db-988ac49d102e")}catch(e){}}();
7
7
  import { Command } from "commander";
8
8
  import { initSentry, Sentry } from "./sentry.js";
9
9
  import { registerAddCommand } from "./commands/add.js";
@@ -39,6 +39,8 @@ import { registerFeedbackCommand } from "./commands/feedback.js";
39
39
  import { registerMeetingsCommand } from "./commands/meetings.js";
40
40
  import { registerSourcesCommand } from "./commands/sources.js";
41
41
  import { registerSignalsCommand } from "./commands/signals.js";
42
+ import { registerMasterSyncCommand } from "./commands/master-sync.js";
43
+ import { registerRescueCommand } from "./commands/rescue.js";
42
44
  import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
43
45
  import { maybeWarnNewVersion, refreshVersionCache, } from "./utils/version-check.js";
44
46
  import { enforceVersionGate, shouldSkipGate, } from "./utils/version-gate.js";
@@ -137,6 +139,14 @@ registerMeetingsCommand(program);
137
139
  registerSourcesCommand(program);
138
140
  // Signals read surface (subcommand group — hq signals list|get|types|entities)
139
141
  registerSignalsCommand(program);
142
+ // Skill/personal-overlay mirroring + workers-registry regen. Internal command
143
+ // invoked by the hq-core master-sync hook shim (formerly the master-sync.sh
144
+ // hook body). Implementation lives in @indigoai-us/hq-cloud.
145
+ registerMasterSyncCommand(program);
146
+ // Drift-preserving HQ-core re-sync (top-level — `hq rescue`). CLI sibling of
147
+ // the HQ Sync app's "Update / Restore" pill; drives the same replace-rescue.sh
148
+ // shipped from @indigoai-us/hq-cloud.
149
+ registerRescueCommand(program);
140
150
  (async () => {
141
151
  try {
142
152
  Sentry.addBreadcrumb({
@@ -163,4 +173,4 @@ registerSignalsCommand(program);
163
173
  }
164
174
  })();
165
175
  //# sourceMappingURL=index.js.map
166
- //# debugId=83d8729b-5b5c-50b8-8785-372d61feb425
176
+ //# debugId=004dc716-c660-5745-94db-988ac49d102e
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.33.1",
3
+ "version": "5.34.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "clean": "rm -rf dist"
16
16
  },
17
17
  "dependencies": {
18
- "@indigoai-us/hq-cloud": "~5.47.0",
18
+ "@indigoai-us/hq-cloud": "~5.48.0",
19
19
  "@indigoai-us/hq-onboarding": "^0.1.0",
20
20
  "@sentry/node": "^10.49.0",
21
21
  "chalk": "^5.3.0",
@@ -0,0 +1,23 @@
1
+ /**
2
+ * hq master-sync — surface namespaced skills, mirror the personal overlay into
3
+ * core/, and regenerate the workers registry.
4
+ *
5
+ * Thin wrapper over @indigoai-us/hq-cloud's masterSync(), which execs the
6
+ * bundled scripts/master-sync.sh against the HQ root. This command is what the
7
+ * hq-core master-sync hook shim calls on Stop / PostToolUse.
8
+ */
9
+ import { Command } from 'commander';
10
+ import { masterSync } from '@indigoai-us/hq-cloud';
11
+
12
+ export function registerMasterSyncCommand(program: Command): void {
13
+ program
14
+ .command('master-sync')
15
+ .description(
16
+ 'Surface namespaced skills, mirror the personal overlay into core/, and regenerate the workers registry'
17
+ )
18
+ .option('--repo-root <path>', 'HQ root to operate on (defaults to the current directory)')
19
+ .action((opts: { repoRoot?: string }) => {
20
+ const { status } = masterSync({ repoRoot: opts.repoRoot });
21
+ process.exit(status);
22
+ });
23
+ }
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { resolveRescueTarget } from './rescue.js';
3
+
4
+ describe('resolveRescueTarget', () => {
5
+ it('defaults to prod hq-core at the resolved latest tag', () => {
6
+ expect(resolveRescueTarget({}, 'v12.3.0')).toEqual({
7
+ source: 'indigoai-us/hq-core',
8
+ ref: 'v12.3.0',
9
+ });
10
+ });
11
+
12
+ it('lets an explicit --ref override the latest tag', () => {
13
+ expect(resolveRescueTarget({ ref: 'v11.0.0' }, 'v12.3.0')).toEqual({
14
+ source: 'indigoai-us/hq-core',
15
+ ref: 'v11.0.0',
16
+ });
17
+ });
18
+
19
+ it('uses the staging source and leaves ref to the script default', () => {
20
+ expect(resolveRescueTarget({ staging: true })).toEqual({
21
+ source: 'indigoai-us/hq-core-staging',
22
+ ref: undefined,
23
+ });
24
+ });
25
+
26
+ it('honors an explicit --source override', () => {
27
+ expect(resolveRescueTarget({ source: 'acme/hq-core-fork' }, 'v1.0.0')).toEqual({
28
+ source: 'acme/hq-core-fork',
29
+ ref: 'v1.0.0',
30
+ });
31
+ });
32
+
33
+ it('honors --ref on the staging channel', () => {
34
+ expect(resolveRescueTarget({ staging: true, ref: 'feature-branch' })).toEqual({
35
+ source: 'indigoai-us/hq-core-staging',
36
+ ref: 'feature-branch',
37
+ });
38
+ });
39
+ });
@@ -0,0 +1,210 @@
1
+ /**
2
+ * hq rescue — re-sync the local HQ core to an upstream hq-core release (or the
3
+ * staging branch) WITHOUT destroying local edits ("drift").
4
+ *
5
+ * Thin driver over @indigoai-us/hq-cloud's `rescue()`, which execs the bundled
6
+ * scripts/replace-rescue.sh against the HQ root. This is the CLI sibling of the
7
+ * HQ Sync menubar app's "Update / Restore" pill — both drive the exact same
8
+ * rescue script, just resolved from the shared hq-cloud package.
9
+ *
10
+ * Prod (default): resolves the latest `indigoai-us/hq-core` release tag and
11
+ * pins the three-way history floor to the commit of the user's currently
12
+ * installed version (read from core/core.yaml). Staging (`--staging`): targets
13
+ * `indigoai-us/hq-core-staging@main` and lets the script read its on-disk
14
+ * sync stamp for the floor.
15
+ */
16
+ import { Command } from 'commander';
17
+ import { spawnSync } from 'child_process';
18
+ import * as fs from 'fs';
19
+ import * as path from 'path';
20
+ import * as yaml from 'js-yaml';
21
+ import chalk from 'chalk';
22
+ import { rescue } from '@indigoai-us/hq-cloud';
23
+ import { findHqRoot } from '../utils/manifest.js';
24
+
25
+ const PROD_SOURCE = 'indigoai-us/hq-core';
26
+ const STAGING_SOURCE = 'indigoai-us/hq-core-staging';
27
+
28
+ export interface RescueTarget {
29
+ source: string;
30
+ /** undefined → let the script apply its own default ref (`main`). */
31
+ ref?: string;
32
+ }
33
+
34
+ /**
35
+ * Resolve the source repo + ref from the user's flags. Pure + exported for
36
+ * tests. `latestTag` is the resolved latest release tag (prod only); ignored
37
+ * for staging and when an explicit `--ref` is given.
38
+ */
39
+ export function resolveRescueTarget(
40
+ opts: { staging?: boolean; source?: string; ref?: string },
41
+ latestTag?: string
42
+ ): RescueTarget {
43
+ if (opts.staging) {
44
+ return { source: opts.source ?? STAGING_SOURCE, ref: opts.ref };
45
+ }
46
+ return { source: opts.source ?? PROD_SOURCE, ref: opts.ref ?? latestTag };
47
+ }
48
+
49
+ /** Resolve a GitHub token: prefer `gh auth token`, fall back to env. */
50
+ function resolveGhToken(): string | undefined {
51
+ try {
52
+ const res = spawnSync('gh', ['auth', 'token'], { encoding: 'utf8' });
53
+ if (res.status === 0) {
54
+ const tok = res.stdout.trim();
55
+ if (tok) return tok;
56
+ }
57
+ } catch {
58
+ // gh not installed — fall through to env.
59
+ }
60
+ return process.env.GH_TOKEN || process.env.GITHUB_TOKEN || undefined;
61
+ }
62
+
63
+ function ghHeaders(token?: string): Record<string, string> {
64
+ const headers: Record<string, string> = {
65
+ Accept: 'application/vnd.github+json',
66
+ 'User-Agent': 'hq-cli-rescue',
67
+ 'X-GitHub-Api-Version': '2022-11-28',
68
+ };
69
+ if (token) headers.Authorization = `Bearer ${token}`;
70
+ return headers;
71
+ }
72
+
73
+ /** Latest release tag for a repo, e.g. `v12.3.0`. Throws on failure. */
74
+ async function getLatestReleaseTag(repo: string, token?: string): Promise<string> {
75
+ const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
76
+ headers: ghHeaders(token),
77
+ });
78
+ if (!res.ok) {
79
+ throw new Error(`GitHub releases/latest for ${repo}: HTTP ${res.status}`);
80
+ }
81
+ const body = (await res.json()) as { tag_name?: string };
82
+ if (!body.tag_name) throw new Error(`no tag_name in latest release for ${repo}`);
83
+ return body.tag_name;
84
+ }
85
+
86
+ /** Resolve a tag to its commit SHA, dereferencing annotated tags. */
87
+ async function resolveTagSha(repo: string, tag: string, token?: string): Promise<string> {
88
+ const refRes = await fetch(
89
+ `https://api.github.com/repos/${repo}/git/ref/tags/${encodeURIComponent(tag)}`,
90
+ { headers: ghHeaders(token) }
91
+ );
92
+ if (!refRes.ok) throw new Error(`git/ref/tags/${tag} for ${repo}: HTTP ${refRes.status}`);
93
+ const ref = (await refRes.json()) as { object?: { type?: string; sha?: string } };
94
+ const obj = ref.object;
95
+ if (!obj?.sha) throw new Error(`no object.sha for tag ${tag} in ${repo}`);
96
+ if (obj.type !== 'tag') return obj.sha; // lightweight tag → already a commit
97
+ // Annotated tag → dereference to the commit it points at.
98
+ const tagRes = await fetch(`https://api.github.com/repos/${repo}/git/tags/${obj.sha}`, {
99
+ headers: ghHeaders(token),
100
+ });
101
+ if (!tagRes.ok) throw new Error(`git/tags/${obj.sha} for ${repo}: HTTP ${tagRes.status}`);
102
+ const annotated = (await tagRes.json()) as { object?: { sha?: string } };
103
+ if (!annotated.object?.sha) throw new Error(`annotated tag ${tag} has no target sha`);
104
+ return annotated.object.sha;
105
+ }
106
+
107
+ /** Read the installed HQ core version from `{hqRoot}/core/core.yaml`. */
108
+ function readInstalledVersion(hqRoot: string): string | undefined {
109
+ const file = path.join(hqRoot, 'core', 'core.yaml');
110
+ if (!fs.existsSync(file)) return undefined;
111
+ try {
112
+ const doc = yaml.load(fs.readFileSync(file, 'utf8')) as { hqVersion?: unknown } | null;
113
+ const v = doc?.hqVersion;
114
+ return typeof v === 'string' ? v : undefined;
115
+ } catch {
116
+ return undefined;
117
+ }
118
+ }
119
+
120
+ export function registerRescueCommand(program: Command): void {
121
+ program
122
+ .command('rescue')
123
+ .description(
124
+ 'Re-sync your HQ core to the latest release, preserving your local edits (drift)'
125
+ )
126
+ .option('--hq-root <path>', 'HQ root to operate on (defaults to auto-detected root)')
127
+ .option('--ref <ref>', 'Target tag/branch (default: latest hq-core release)')
128
+ .option('--source <repo>', 'Source repo (default: indigoai-us/hq-core)')
129
+ .option('--staging', 'Use the staging channel (indigoai-us/hq-core-staging@main)')
130
+ .option('--floor-sha <sha>', 'Pin the three-way history floor to a 40-char commit SHA')
131
+ .option('--paths <list>', 'Comma-separated top-level paths to narrow the rescue to')
132
+ .option('--check', 'Plan only — classify and report, change nothing on disk (--dry-run)')
133
+ .option('-y, --yes', 'Skip the confirmation prompt')
134
+ .option('--no-backup', 'Skip the pre-op safety snapshot under ~/.hq/backups')
135
+ .option('--cloud-update', 'Cloud-update mode')
136
+ .action(
137
+ async (opts: {
138
+ hqRoot?: string;
139
+ ref?: string;
140
+ source?: string;
141
+ staging?: boolean;
142
+ floorSha?: string;
143
+ paths?: string;
144
+ check?: boolean;
145
+ yes?: boolean;
146
+ backup?: boolean; // --no-backup → false
147
+ cloudUpdate?: boolean;
148
+ }) => {
149
+ try {
150
+ const hqRoot = opts.hqRoot ?? findHqRoot();
151
+ const token = resolveGhToken();
152
+
153
+ let { source, ref } = resolveRescueTarget(opts);
154
+ let floorSha = opts.floorSha;
155
+
156
+ if (!opts.staging) {
157
+ // Prod: resolve the latest release tag when no explicit ref, and
158
+ // pin the floor to the installed version's commit when possible.
159
+ if (!ref) {
160
+ ref = await getLatestReleaseTag(source, token);
161
+ }
162
+ if (!floorSha) {
163
+ const installed = readInstalledVersion(hqRoot);
164
+ if (installed) {
165
+ try {
166
+ floorSha = await resolveTagSha(source, `v${installed}`, token);
167
+ } catch (err) {
168
+ console.warn(
169
+ chalk.yellow(
170
+ `Warning: couldn't resolve the floor commit for v${installed} ` +
171
+ `(${err instanceof Error ? err.message : 'unknown error'}); ` +
172
+ `falling back to the on-disk sync stamp.`
173
+ )
174
+ );
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ console.log(
181
+ chalk.dim(
182
+ `Rescuing ${hqRoot} from ${source}${ref ? `@${ref}` : ''}` +
183
+ `${floorSha ? ` (floor ${floorSha.slice(0, 12)})` : ''}` +
184
+ `${opts.check ? ' [dry-run]' : ''}`
185
+ )
186
+ );
187
+
188
+ const { status } = rescue({
189
+ hqRoot,
190
+ source,
191
+ ref,
192
+ floorSha,
193
+ paths: opts.paths ? opts.paths.split(',').map((p) => p.trim()).filter(Boolean) : undefined,
194
+ dryRun: opts.check,
195
+ assumeYes: opts.yes,
196
+ noBackup: opts.backup === false,
197
+ cloudUpdate: opts.cloudUpdate,
198
+ ghToken: token,
199
+ });
200
+ process.exit(status);
201
+ } catch (error) {
202
+ console.error(
203
+ chalk.red('Error:'),
204
+ error instanceof Error ? error.message : 'Unknown error'
205
+ );
206
+ process.exit(1);
207
+ }
208
+ }
209
+ );
210
+ }
package/src/index.ts CHANGED
@@ -39,6 +39,8 @@ import { registerFeedbackCommand } from "./commands/feedback.js";
39
39
  import { registerMeetingsCommand } from "./commands/meetings.js";
40
40
  import { registerSourcesCommand } from "./commands/sources.js";
41
41
  import { registerSignalsCommand } from "./commands/signals.js";
42
+ import { registerMasterSyncCommand } from "./commands/master-sync.js";
43
+ import { registerRescueCommand } from "./commands/rescue.js";
42
44
  import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
43
45
  import {
44
46
  maybeWarnNewVersion,
@@ -174,6 +176,16 @@ registerSourcesCommand(program);
174
176
  // Signals read surface (subcommand group — hq signals list|get|types|entities)
175
177
  registerSignalsCommand(program);
176
178
 
179
+ // Skill/personal-overlay mirroring + workers-registry regen. Internal command
180
+ // invoked by the hq-core master-sync hook shim (formerly the master-sync.sh
181
+ // hook body). Implementation lives in @indigoai-us/hq-cloud.
182
+ registerMasterSyncCommand(program);
183
+
184
+ // Drift-preserving HQ-core re-sync (top-level — `hq rescue`). CLI sibling of
185
+ // the HQ Sync app's "Update / Restore" pill; drives the same replace-rescue.sh
186
+ // shipped from @indigoai-us/hq-cloud.
187
+ registerRescueCommand(program);
188
+
177
189
  (async () => {
178
190
  try {
179
191
  Sentry.addBreadcrumb({