@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.
- package/dist/commands/master-sync.d.ts +11 -0
- package/dist/commands/master-sync.js +15 -0
- package/dist/commands/rescue.d.ts +33 -0
- package/dist/commands/rescue.js +161 -0
- package/dist/index.js +12 -2
- package/package.json +2 -2
- package/src/commands/master-sync.ts +23 -0
- package/src/commands/rescue.test.ts +39 -0
- package/src/commands/rescue.ts +210 -0
- package/src/index.ts +12 -0
|
@@ -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]="
|
|
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=
|
|
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.
|
|
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.
|
|
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({
|