@indigoai-us/hq-cli 5.345.170 → 5.345.172
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 +84 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +206 -7
- package/dist/command-catalog.generated.js +252 -8
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/access.d.ts +32 -1
- package/dist/commands/access.js +112 -3
- package/dist/commands/agent-kit.d.ts +1 -1
- package/dist/commands/agent-kit.js +3 -1
- package/dist/commands/agents.js +2 -1
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +179 -18
- package/dist/commands/cloud.d.ts +58 -1
- package/dist/commands/cloud.js +228 -10
- package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
- package/dist/commands/core-company-index-scope-flag.js +9 -2
- package/dist/commands/core-delegate-w1b.js +5 -2
- package/dist/commands/core-dm-bind-native.js +19 -4
- package/dist/commands/core-rebuild-index-fast.js +24 -6
- package/dist/commands/core.js +18 -6
- package/dist/commands/daemon.js +16 -1
- package/dist/commands/dm.d.ts +17 -3
- package/dist/commands/dm.js +121 -26
- package/dist/commands/files.d.ts +1 -0
- package/dist/commands/files.js +22 -4
- package/dist/commands/integrations-api.d.ts +4 -0
- package/dist/commands/integrations-api.js +18 -2
- package/dist/commands/integrations-connect.d.ts +10 -3
- package/dist/commands/integrations-connect.js +70 -6
- package/dist/commands/integrations-core.d.ts +23 -0
- package/dist/commands/integrations-core.js +100 -0
- package/dist/commands/integrations-manage.js +8 -1
- package/dist/commands/integrations.js +49 -5
- package/dist/commands/lanes-loop.d.ts +20 -0
- package/dist/commands/lanes-loop.js +69 -11
- package/dist/commands/lanes.js +9 -1
- package/dist/commands/rescue.d.ts +9 -0
- package/dist/commands/rescue.js +31 -0
- package/dist/commands/run.js +115 -40
- package/dist/commands/scan.d.ts +5 -1
- package/dist/commands/scan.js +16 -4
- package/dist/commands/secrets.js +50 -9
- package/dist/commands/sync-core.d.ts +22 -0
- package/dist/commands/sync-core.js +124 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2447 -2135
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
- package/dist/lib/agent-kit/inbox-state.js +41 -20
- package/dist/lib/agent-kit/kit-config.d.ts +2 -0
- package/dist/lib/agent-kit/kit-config.js +6 -0
- package/dist/lib/agent-kit/mcp/tools.js +12 -5
- package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
- package/dist/lib/agent-kit/run/heartbeat.js +6 -3
- package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
- package/dist/lib/agent-kit/run/inbox.js +42 -10
- package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
- package/dist/lib/agent-kit/run/request-timeout.js +51 -0
- package/dist/lib/bot/api.d.ts +1 -1
- package/dist/lib/bot/config.d.ts +10 -0
- package/dist/lib/bot/config.js +17 -1
- package/dist/lib/bot/fleet-local.d.ts +142 -0
- package/dist/lib/bot/fleet-local.js +689 -0
- package/dist/lib/bot/fleet-python.d.ts +47 -0
- package/dist/lib/bot/fleet-python.js +218 -0
- package/dist/lib/bot/index.d.ts +2 -0
- package/dist/lib/bot/index.js +2 -0
- package/dist/lib/bot/local-config.d.ts +3 -2
- package/dist/lib/bot/local-config.js +5 -2
- package/dist/lib/bot/promote.d.ts +10 -0
- package/dist/lib/bot/promote.js +3 -2
- package/dist/lib/bot/prompt.d.ts +6 -1
- package/dist/lib/bot/prompt.js +8 -3
- package/dist/lib/bot/runtime/index.js +8 -4
- package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
- package/dist/lib/bot/runtime/scratch-dir.js +41 -0
- package/dist/lib/caller-principal.d.ts +4 -0
- package/dist/lib/caller-principal.js +40 -0
- package/dist/lib/capacity/collectors/grok.js +8 -2
- package/dist/lib/cli-flag-keys.d.ts +2 -0
- package/dist/lib/cli-flag-keys.js +2 -0
- package/dist/lib/daemon/catalog.d.ts +12 -4
- package/dist/lib/daemon/catalog.js +70 -24
- package/dist/lib/daemon/config.d.ts +0 -7
- package/dist/lib/daemon/config.js +0 -32
- package/dist/lib/daemon/run.d.ts +5 -1
- package/dist/lib/daemon/run.js +48 -11
- package/dist/lib/daemon/supervisor.js +3 -1
- package/dist/lib/doctor/checks/release-links.d.ts +62 -1
- package/dist/lib/doctor/checks/release-links.js +188 -2
- package/dist/lib/doctor/fix/apply.js +54 -0
- package/dist/lib/doctor/fix/remediation.d.ts +8 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/index-render/index.d.ts +9 -0
- package/dist/lib/index-render/index.js +65 -5
- package/dist/lib/index-render/session-scope.d.ts +69 -0
- package/dist/lib/index-render/session-scope.js +279 -0
- package/dist/lib/lanes/admission.js +7 -20
- package/dist/lib/lanes/reconcile.js +13 -15
- package/dist/lib/lanes/stale-lock.d.ts +50 -0
- package/dist/lib/lanes/stale-lock.js +105 -0
- package/dist/lib/lanes/store.js +11 -76
- package/dist/lib/notify-recipient.d.ts +10 -4
- package/dist/lib/notify-recipient.js +16 -4
- package/dist/lib/secret-input-link.d.ts +42 -0
- package/dist/lib/secret-input-link.js +93 -0
- package/dist/lib/secret-scan/range.d.ts +57 -0
- package/dist/lib/secret-scan/range.js +266 -0
- package/dist/lib/secret-scan/repo-config.d.ts +18 -3
- package/dist/lib/secret-scan/repo-config.js +135 -2
- package/dist/lib/sync-core/engine-home.d.ts +44 -0
- package/dist/lib/sync-core/engine-home.js +193 -0
- package/dist/lib/sync-core/flag.d.ts +17 -0
- package/dist/lib/sync-core/flag.js +47 -0
- package/dist/lib/sync-core/reconcile.d.ts +31 -0
- package/dist/lib/sync-core/reconcile.js +196 -0
- package/dist/lib/sync-core/release.d.ts +32 -0
- package/dist/lib/sync-core/release.js +140 -0
- package/dist/lib/sync-core/rollout-state.d.ts +24 -0
- package/dist/lib/sync-core/rollout-state.js +94 -0
- package/dist/lib/vault-sync-switches.d.ts +5 -0
- package/dist/lib/vault-sync-switches.js +43 -0
- package/dist/run/script-bound-schema.d.ts +27 -0
- package/dist/run/script-bound-schema.js +97 -0
- package/dist/secrets-env-action.d.ts +2 -0
- package/dist/secrets-env-action.js +7 -2
- package/dist/utils/cognito-session.d.ts +3 -30
- package/dist/utils/cognito-session.js +2 -100
- package/dist/utils/machine-principal-state.d.ts +38 -0
- package/dist/utils/machine-principal-state.js +110 -0
- package/dist/utils/remote-list-not-converging.d.ts +21 -0
- package/dist/utils/remote-list-not-converging.js +40 -0
- package/dist/utils/safe-link.js +9 -0
- package/dist/utils/script-bound-exec.d.ts +158 -0
- package/dist/utils/script-bound-exec.js +568 -0
- package/dist/utils/sync-watchdog.d.ts +92 -0
- package/dist/utils/sync-watchdog.js +250 -0
- package/package.json +8 -8
- package/dist/lib/daemon/sync-core-canary.d.ts +0 -9
- package/dist/lib/daemon/sync-core-canary.js +0 -56
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { parse as parseToml } from "smol-toml";
|
|
4
|
+
export const SYNC_CORE_REQUESTS_PER_DAY = 20_000;
|
|
5
|
+
export function syncCoreEngineHome(home, vaultUid) {
|
|
6
|
+
return path.join(home, ".hq", "sync-core", "engines", vaultUid);
|
|
7
|
+
}
|
|
8
|
+
export function syncCoreUnitId(vaultUid) {
|
|
9
|
+
return `sync-core:${vaultUid}`;
|
|
10
|
+
}
|
|
11
|
+
export function isSyncCoreVaultUid(value) {
|
|
12
|
+
return typeof value === "string" && /^(prs|cmp)_[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value);
|
|
13
|
+
}
|
|
14
|
+
export function engineConfigToml(vault, hqRoot, phase) {
|
|
15
|
+
const lines = [
|
|
16
|
+
"[sync]",
|
|
17
|
+
'engine = "rust-test"',
|
|
18
|
+
`mode = ${phase === "full" ? '"full"' : '"readonly"'}`,
|
|
19
|
+
...(phase === "full" ? ["full_vault = true"] : []),
|
|
20
|
+
`vaults = [${JSON.stringify(vault.uid)}]`,
|
|
21
|
+
`hq_root = ${JSON.stringify(hqRoot)}`,
|
|
22
|
+
`requests_per_day = ${SYNC_CORE_REQUESTS_PER_DAY}`,
|
|
23
|
+
];
|
|
24
|
+
if (vault.kind === "company") {
|
|
25
|
+
lines.push("[sync.roots]", `${vault.uid} = ${JSON.stringify(`companies/${vault.slug}`)}`);
|
|
26
|
+
}
|
|
27
|
+
return `${lines.join("\n")}\n`;
|
|
28
|
+
}
|
|
29
|
+
function replaceSymlink(link, target) {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.lstatSync(link).isSymbolicLink() && fs.readlinkSync(link) === target)
|
|
32
|
+
return;
|
|
33
|
+
fs.rmSync(link, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code !== "ENOENT")
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
fs.symlinkSync(target, link);
|
|
40
|
+
}
|
|
41
|
+
function writeIfChanged(file, text) {
|
|
42
|
+
try {
|
|
43
|
+
if (fs.readFileSync(file, "utf8") === text)
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code !== "ENOENT")
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
51
|
+
fs.writeFileSync(tmp, text, { mode: 0o600 });
|
|
52
|
+
fs.renameSync(tmp, file);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
export function writeEngineHome(opts) {
|
|
56
|
+
const engineHome = syncCoreEngineHome(opts.home, opts.vault.uid);
|
|
57
|
+
const hq = path.join(engineHome, ".hq");
|
|
58
|
+
const root = path.join(hq, "sync-core");
|
|
59
|
+
fs.mkdirSync(path.join(root, "owned"), { recursive: true, mode: 0o700 });
|
|
60
|
+
replaceSymlink(path.join(hq, "menubar.json"), path.join(opts.home, ".hq", "menubar.json"));
|
|
61
|
+
replaceSymlink(path.join(hq, "work-mesh"), path.join(opts.home, ".hq", "work-mesh"));
|
|
62
|
+
replaceSymlink(path.join(hq, "cognito-tokens.json"), opts.tokenFile);
|
|
63
|
+
const configChanged = writeIfChanged(path.join(root, "config.toml"), engineConfigToml(opts.vault, opts.hqRoot, opts.phase));
|
|
64
|
+
const ownedPath = path.join(root, "owned", `${opts.vault.uid}.json`);
|
|
65
|
+
let ownedChanged = false;
|
|
66
|
+
if (opts.phase === "full") {
|
|
67
|
+
ownedChanged = writeIfChanged(ownedPath, `${JSON.stringify({ vault: opts.vault.uid })}\n`);
|
|
68
|
+
}
|
|
69
|
+
else if (fs.existsSync(ownedPath)) {
|
|
70
|
+
fs.rmSync(ownedPath, { force: true });
|
|
71
|
+
ownedChanged = true;
|
|
72
|
+
}
|
|
73
|
+
return { engineHome, changed: configChanged || ownedChanged };
|
|
74
|
+
}
|
|
75
|
+
function count(value) {
|
|
76
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
77
|
+
}
|
|
78
|
+
export function engineReadOnlyPlan(engineHome, vaultUid) {
|
|
79
|
+
let status;
|
|
80
|
+
try {
|
|
81
|
+
status = record(JSON.parse(fs.readFileSync(path.join(engineHome, ".hq", "sync-core", "status.json"), "utf8")));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (record(status?.mode)?.value !== "readonly")
|
|
87
|
+
return null;
|
|
88
|
+
const lastPlan = record(status?.last_plan);
|
|
89
|
+
if (!lastPlan || typeof lastPlan.at_ms !== "number" || lastPlan.at_ms <= 0)
|
|
90
|
+
return null;
|
|
91
|
+
const vaults = Array.isArray(lastPlan.vaults) ? lastPlan.vaults : [];
|
|
92
|
+
const run = vaults.map(record).find((v) => v?.vault === vaultUid);
|
|
93
|
+
if (!run)
|
|
94
|
+
return null;
|
|
95
|
+
const errors = Array.isArray(lastPlan.errors) ? lastPlan.errors : [];
|
|
96
|
+
if (errors.some((e) => {
|
|
97
|
+
const vault = record(e)?.vault;
|
|
98
|
+
return vault === vaultUid || typeof vault !== "string";
|
|
99
|
+
}))
|
|
100
|
+
return null;
|
|
101
|
+
const ops = record(record(run.plan)?.ops_by_action) ?? {};
|
|
102
|
+
return {
|
|
103
|
+
at: new Date(lastPlan.at_ms).toISOString(),
|
|
104
|
+
uploads: count(ops.upload),
|
|
105
|
+
downloads: count(ops.download),
|
|
106
|
+
deletes: count(ops.delete_local) + count(ops.delete_remote),
|
|
107
|
+
remoteRows: count(record(run.remote)?.rows),
|
|
108
|
+
localFiles: count(record(run.local)?.files),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function clearEngineStatus(home, vaultUid) {
|
|
112
|
+
fs.rmSync(path.join(syncCoreEngineHome(home, vaultUid), ".hq", "sync-core", "status.json"), { force: true });
|
|
113
|
+
}
|
|
114
|
+
export function removeEngineOwnership(home, vaultUid) {
|
|
115
|
+
fs.rmSync(path.join(syncCoreEngineHome(home, vaultUid), ".hq", "sync-core", "owned", `${vaultUid}.json`), { force: true });
|
|
116
|
+
}
|
|
117
|
+
function record(value) {
|
|
118
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
119
|
+
}
|
|
120
|
+
function blocked(plan, blockedReason) {
|
|
121
|
+
return { ...plan, blockedReason };
|
|
122
|
+
}
|
|
123
|
+
export function inspectEngineHome(home, hqRoot, plan, platform) {
|
|
124
|
+
if (platform !== "darwin" && platform !== "linux")
|
|
125
|
+
return blocked(plan, "the sync engine requires macOS or Linux");
|
|
126
|
+
try {
|
|
127
|
+
if (!fs.statSync(plan.binaryPath).isFile())
|
|
128
|
+
return blocked(plan, "the sync engine binary is missing");
|
|
129
|
+
fs.accessSync(plan.binaryPath, fs.constants.X_OK);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return blocked(plan, "the sync engine binary is missing or not executable");
|
|
133
|
+
}
|
|
134
|
+
if (plan.engineHome !== syncCoreEngineHome(home, plan.uid)) {
|
|
135
|
+
return blocked(plan, `the engine home for ${plan.uid} is not under this user's sync-core folder`);
|
|
136
|
+
}
|
|
137
|
+
const configPath = path.join(plan.engineHome, ".hq", "sync-core", "config.toml");
|
|
138
|
+
let sync;
|
|
139
|
+
try {
|
|
140
|
+
sync = record(record(parseToml(fs.readFileSync(configPath, "utf8")))?.sync);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return blocked(plan, `the engine config at ${configPath} is missing or invalid`);
|
|
144
|
+
}
|
|
145
|
+
if (!sync || sync.engine !== "rust-test")
|
|
146
|
+
return blocked(plan, "the engine config must select engine rust-test");
|
|
147
|
+
if (plan.phase === "full" && (sync.mode !== "full" || sync.full_vault !== true)) {
|
|
148
|
+
return blocked(plan, "the engine config must select engine rust-test in full-vault mode");
|
|
149
|
+
}
|
|
150
|
+
if (plan.phase === "warming" && sync.mode !== "readonly") {
|
|
151
|
+
return blocked(plan, "the engine config must select readonly mode while the index warms");
|
|
152
|
+
}
|
|
153
|
+
const vaults = sync.vaults;
|
|
154
|
+
if (!Array.isArray(vaults) || vaults.length !== 1 || vaults[0] !== plan.uid) {
|
|
155
|
+
return blocked(plan, `the engine config must name exactly ${plan.uid}`);
|
|
156
|
+
}
|
|
157
|
+
if (!hqRoot || typeof sync.hq_root !== "string" || path.resolve(sync.hq_root) !== path.resolve(hqRoot)) {
|
|
158
|
+
return blocked(plan, "the engine config must use the daemon's HQ folder");
|
|
159
|
+
}
|
|
160
|
+
if (plan.kind === "company") {
|
|
161
|
+
const roots = record(sync.roots);
|
|
162
|
+
if (!plan.slug || roots?.[plan.uid] !== `companies/${plan.slug}`) {
|
|
163
|
+
return blocked(plan, `the engine config must map ${plan.uid} to companies/${plan.slug ?? "<slug>"}`);
|
|
164
|
+
}
|
|
165
|
+
if (!fs.existsSync(path.join(hqRoot, "companies", plan.slug))) {
|
|
166
|
+
return blocked(plan, `companies/${plan.slug} does not exist under the HQ folder`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const ownedPath = path.join(plan.engineHome, ".hq", "sync-core", "owned", `${plan.uid}.json`);
|
|
170
|
+
if (plan.phase === "full") {
|
|
171
|
+
try {
|
|
172
|
+
const owned = record(JSON.parse(fs.readFileSync(ownedPath, "utf8")));
|
|
173
|
+
if (owned?.vault !== plan.uid)
|
|
174
|
+
throw new Error("mismatch");
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return blocked(plan, `the engine ownership record for ${plan.uid} is missing or invalid`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (fs.existsSync(ownedPath)) {
|
|
181
|
+
return blocked(plan, `the engine ownership record for ${plan.uid} must not exist while the index warms`);
|
|
182
|
+
}
|
|
183
|
+
for (const link of ["menubar.json", "work-mesh", "cognito-tokens.json"]) {
|
|
184
|
+
try {
|
|
185
|
+
fs.readlinkSync(path.join(plan.engineHome, ".hq", link));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return blocked(plan, `the engine home for ${plan.uid} is missing its ${link} link`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return plan;
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=engine-home.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type FlagClientOptions, type FlagResolution } from "@indigoai-us/hq-flags-client";
|
|
2
|
+
export declare const SYNC_RUST_ENGINE_ROLLOUT_FLAG = "sync.rust-engine-rollout";
|
|
3
|
+
export interface RolloutFlagClient {
|
|
4
|
+
ready(): Promise<void>;
|
|
5
|
+
explain(flagKey: string, options?: {
|
|
6
|
+
fallback?: boolean;
|
|
7
|
+
}): FlagResolution;
|
|
8
|
+
close(): void;
|
|
9
|
+
}
|
|
10
|
+
export interface ReadCompanyRolloutFlagOptions {
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
createClient?: (options: FlagClientOptions) => RolloutFlagClient;
|
|
13
|
+
}
|
|
14
|
+
export declare class RolloutFlagReadError extends Error {
|
|
15
|
+
}
|
|
16
|
+
export declare function readCompanyRolloutFlag(companyUid: string, token: string, options?: ReadCompanyRolloutFlagOptions): Promise<boolean>;
|
|
17
|
+
//# sourceMappingURL=flag.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createFlagClient, readLocalOverride } from "@indigoai-us/hq-flags-client";
|
|
2
|
+
import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
|
|
3
|
+
export const SYNC_RUST_ENGINE_ROLLOUT_FLAG = "sync.rust-engine-rollout";
|
|
4
|
+
const READ_TIMEOUT_MS = 5_000;
|
|
5
|
+
export class RolloutFlagReadError extends Error {
|
|
6
|
+
}
|
|
7
|
+
export async function readCompanyRolloutFlag(companyUid, token, options = {}) {
|
|
8
|
+
const env = options.env ?? process.env;
|
|
9
|
+
const override = readLocalOverride(SYNC_RUST_ENGINE_ROLLOUT_FLAG, env);
|
|
10
|
+
if (override !== null)
|
|
11
|
+
return override;
|
|
12
|
+
let failure;
|
|
13
|
+
const onError = (error) => {
|
|
14
|
+
failure ??= error;
|
|
15
|
+
};
|
|
16
|
+
const client = (options.createClient ?? createFlagClient)({
|
|
17
|
+
endpoint: resolveFlagRegistryEndpoint(undefined, env),
|
|
18
|
+
getToken: () => token,
|
|
19
|
+
env: {},
|
|
20
|
+
refreshIntervalMs: 0,
|
|
21
|
+
requestTimeoutMs: READ_TIMEOUT_MS,
|
|
22
|
+
companyUid,
|
|
23
|
+
onError,
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
await client.ready();
|
|
27
|
+
const resolution = client.explain(SYNC_RUST_ENGINE_ROLLOUT_FLAG, { fallback: false });
|
|
28
|
+
if (failure !== undefined) {
|
|
29
|
+
throw new RolloutFlagReadError(`could not read ${SYNC_RUST_ENGINE_ROLLOUT_FLAG} for ${companyUid}: ${failure instanceof Error ? failure.message : String(failure)}`);
|
|
30
|
+
}
|
|
31
|
+
return resolution.source === "snapshot" && resolution.value === true;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof RolloutFlagReadError)
|
|
35
|
+
throw error;
|
|
36
|
+
throw new RolloutFlagReadError(`could not read ${SYNC_RUST_ENGINE_ROLLOUT_FLAG} for ${companyUid}: ${error instanceof Error ? error.message : String(error)}`);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
try {
|
|
40
|
+
client.close();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
failure ??= undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=flag.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type SyncCorePlanFigures, type SyncCoreVault } from "./engine-home.js";
|
|
2
|
+
import { type SyncCoreBinaryInstall } from "./release.js";
|
|
3
|
+
import { type SyncCoreRolloutState } from "./rollout-state.js";
|
|
4
|
+
export interface SyncCoreMembership {
|
|
5
|
+
companyUid: string;
|
|
6
|
+
companySlug?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SyncCoreReconcileDeps {
|
|
9
|
+
home: string;
|
|
10
|
+
hqRoot: string;
|
|
11
|
+
tokenFile: string;
|
|
12
|
+
companiesOnly: boolean;
|
|
13
|
+
listMemberships: () => Promise<SyncCoreMembership[]>;
|
|
14
|
+
resolvePersonUid: () => Promise<string | null>;
|
|
15
|
+
readRolloutFlag: (companyUid: string) => Promise<boolean>;
|
|
16
|
+
ensureBinary: (release: string) => Promise<SyncCoreBinaryInstall>;
|
|
17
|
+
release?: string;
|
|
18
|
+
now?: () => Date;
|
|
19
|
+
log?: (line: string) => void;
|
|
20
|
+
}
|
|
21
|
+
export interface SyncCoreReconcileResult {
|
|
22
|
+
state: SyncCoreRolloutState;
|
|
23
|
+
started: string[];
|
|
24
|
+
promoted: string[];
|
|
25
|
+
stopped: string[];
|
|
26
|
+
kept: string[];
|
|
27
|
+
warnings: string[];
|
|
28
|
+
}
|
|
29
|
+
export declare function promotionHold(vault: SyncCoreVault, plan: SyncCorePlanFigures, release: string): string | undefined;
|
|
30
|
+
export declare function reconcileSyncCore(deps: SyncCoreReconcileDeps): Promise<SyncCoreReconcileResult>;
|
|
31
|
+
//# sourceMappingURL=reconcile.d.ts.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { readCompaniesManifestMap } from "../work-context/repo-remote.js";
|
|
4
|
+
import { readVaultSyncSwitches, writeVaultSyncSwitches } from "../vault-sync-switches.js";
|
|
5
|
+
import { clearEngineStatus, engineReadOnlyPlan, removeEngineOwnership, syncCoreEngineHome, writeEngineHome, } from "./engine-home.js";
|
|
6
|
+
import { SYNC_CORE_COMPANY_FULL_RELEASE, SYNC_CORE_PROMOTION_OP_LIMIT, SYNC_CORE_RELEASE, companyFullModeAllowed, installedSyncCoreRelease, syncCoreBinaryPath, } from "./release.js";
|
|
7
|
+
import { readSyncCoreRolloutState, writeSyncCoreRolloutState, } from "./rollout-state.js";
|
|
8
|
+
function message(error) {
|
|
9
|
+
return error instanceof Error ? error.message : String(error);
|
|
10
|
+
}
|
|
11
|
+
export function promotionHold(vault, plan, release) {
|
|
12
|
+
if (vault.kind === "company" && !companyFullModeAllowed(release)) {
|
|
13
|
+
const needed = SYNC_CORE_COMPANY_FULL_RELEASE ? `engine release ${SYNC_CORE_COMPANY_FULL_RELEASE}` : "an engine release that";
|
|
14
|
+
return `company vaults stay read-only until ${needed} scopes the local tree to this account's visibility (${release} installed)`;
|
|
15
|
+
}
|
|
16
|
+
if (plan.deletes > SYNC_CORE_PROMOTION_OP_LIMIT) {
|
|
17
|
+
return `the read-only plan would delete ${plan.deletes} paths (limit ${SYNC_CORE_PROMOTION_OP_LIMIT})`;
|
|
18
|
+
}
|
|
19
|
+
if (plan.uploads > SYNC_CORE_PROMOTION_OP_LIMIT) {
|
|
20
|
+
return `the read-only plan would upload ${plan.uploads} files against ${plan.remoteRows} server rows (limit ${SYNC_CORE_PROMOTION_OP_LIMIT})`;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
function localCompanies(hqRoot, memberships) {
|
|
25
|
+
const manifest = readCompaniesManifestMap(hqRoot) ?? {};
|
|
26
|
+
const byUid = new Map();
|
|
27
|
+
for (const [slug, entry] of Object.entries(manifest)) {
|
|
28
|
+
const uid = entry?.cloud_uid;
|
|
29
|
+
if (typeof uid === "string" && /^[a-z0-9][a-z0-9_-]*$/.test(slug))
|
|
30
|
+
byUid.set(uid, slug);
|
|
31
|
+
}
|
|
32
|
+
const candidates = [];
|
|
33
|
+
for (const membership of memberships) {
|
|
34
|
+
const slug = byUid.get(membership.companyUid);
|
|
35
|
+
if (!slug)
|
|
36
|
+
continue;
|
|
37
|
+
if (!fs.existsSync(path.join(hqRoot, "companies", slug)))
|
|
38
|
+
continue;
|
|
39
|
+
candidates.push({ uid: membership.companyUid, kind: "company", slug });
|
|
40
|
+
}
|
|
41
|
+
return candidates;
|
|
42
|
+
}
|
|
43
|
+
export async function reconcileSyncCore(deps) {
|
|
44
|
+
const now = deps.now ?? (() => new Date());
|
|
45
|
+
const log = deps.log ?? (() => { });
|
|
46
|
+
const release = deps.release ?? SYNC_CORE_RELEASE;
|
|
47
|
+
const warnings = [];
|
|
48
|
+
const previous = readSyncCoreRolloutState(deps.home);
|
|
49
|
+
const previousByUid = new Map((previous?.vaults ?? []).map((vault) => [vault.uid, vault]));
|
|
50
|
+
const memberships = await deps.listMemberships();
|
|
51
|
+
const desired = [];
|
|
52
|
+
let anyCompanyOn = false;
|
|
53
|
+
for (const company of localCompanies(deps.hqRoot, memberships)) {
|
|
54
|
+
let on;
|
|
55
|
+
try {
|
|
56
|
+
on = await deps.readRolloutFlag(company.uid);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
on = previousByUid.has(company.uid);
|
|
60
|
+
warnings.push(`${message(error)}; keeping ${company.slug} ${on ? "on the new engine" : "on the current sync"}`);
|
|
61
|
+
}
|
|
62
|
+
if (on) {
|
|
63
|
+
anyCompanyOn = true;
|
|
64
|
+
desired.push(company);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!deps.companiesOnly && anyCompanyOn) {
|
|
68
|
+
let personUid = null;
|
|
69
|
+
try {
|
|
70
|
+
personUid = await deps.resolvePersonUid();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const kept = [...previousByUid.values()].find((vault) => vault.kind === "personal");
|
|
74
|
+
personUid = kept?.uid ?? null;
|
|
75
|
+
warnings.push(`${message(error)}; ${kept ? "keeping the personal vault on the new engine" : "leaving the personal vault on the current sync"}`);
|
|
76
|
+
}
|
|
77
|
+
if (personUid)
|
|
78
|
+
desired.push({ uid: personUid, kind: "personal" });
|
|
79
|
+
}
|
|
80
|
+
let binaryPath = syncCoreBinaryPath(deps.home);
|
|
81
|
+
let installedRelease = installedSyncCoreRelease(deps.home);
|
|
82
|
+
if (desired.length > 0) {
|
|
83
|
+
try {
|
|
84
|
+
const install = await deps.ensureBinary(release);
|
|
85
|
+
binaryPath = install.binaryPath;
|
|
86
|
+
installedRelease = install.release;
|
|
87
|
+
if (install.installed)
|
|
88
|
+
log(`installed hq-syncd ${install.release} at ${install.binaryPath}`);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
warnings.push(`${message(error)}; ${installedRelease ? `keeping hq-syncd ${installedRelease}` : "no vault moves until the engine download succeeds"}`);
|
|
92
|
+
if (!installedRelease)
|
|
93
|
+
desired.length = 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const at = now().toISOString();
|
|
97
|
+
const switchesBefore = readVaultSyncSwitches(deps.home);
|
|
98
|
+
const vaults = [];
|
|
99
|
+
const started = [];
|
|
100
|
+
const promoted = [];
|
|
101
|
+
const kept = [];
|
|
102
|
+
const companies = {};
|
|
103
|
+
let personalOff = false;
|
|
104
|
+
for (const vault of desired) {
|
|
105
|
+
const before = previousByUid.get(vault.uid);
|
|
106
|
+
const engineHome = syncCoreEngineHome(deps.home, vault.uid);
|
|
107
|
+
if (!before)
|
|
108
|
+
clearEngineStatus(deps.home, vault.uid);
|
|
109
|
+
const plan = engineReadOnlyPlan(engineHome, vault.uid) ?? undefined;
|
|
110
|
+
let phase = before?.phase === "full" ? "full" : "warming";
|
|
111
|
+
let hold;
|
|
112
|
+
if (phase === "warming" && plan) {
|
|
113
|
+
hold = promotionHold(vault, plan, installedRelease ?? release);
|
|
114
|
+
if (!hold)
|
|
115
|
+
phase = "full";
|
|
116
|
+
}
|
|
117
|
+
writeEngineHome({ home: deps.home, hqRoot: deps.hqRoot, vault, phase, tokenFile: deps.tokenFile });
|
|
118
|
+
const alreadyOff = vault.kind === "personal"
|
|
119
|
+
? switchesBefore.personalOff
|
|
120
|
+
: switchesBefore.offCompanies.has(vault.slug ?? "");
|
|
121
|
+
const legacyAlreadyOff = before?.phase === "full" ? before.legacyAlreadyOff === true : phase === "full" && alreadyOff;
|
|
122
|
+
const legacyOffSince = phase === "full" ? before?.legacyOffSince ?? at : undefined;
|
|
123
|
+
vaults.push({
|
|
124
|
+
uid: vault.uid,
|
|
125
|
+
kind: vault.kind,
|
|
126
|
+
...(vault.slug ? { slug: vault.slug } : {}),
|
|
127
|
+
phase,
|
|
128
|
+
engineHome,
|
|
129
|
+
...(legacyOffSince ? { legacyOffSince } : {}),
|
|
130
|
+
...(legacyAlreadyOff ? { legacyAlreadyOff: true } : {}),
|
|
131
|
+
...(plan ? { plan } : {}),
|
|
132
|
+
...(hold ? { hold } : {}),
|
|
133
|
+
});
|
|
134
|
+
if (hold && before?.hold !== hold)
|
|
135
|
+
log(`${vault.uid}: staying read-only: ${hold}`);
|
|
136
|
+
if (phase === "full") {
|
|
137
|
+
if (vault.kind === "company" && vault.slug)
|
|
138
|
+
companies[vault.slug] = false;
|
|
139
|
+
else
|
|
140
|
+
personalOff = true;
|
|
141
|
+
}
|
|
142
|
+
if (!before)
|
|
143
|
+
started.push(vault.uid);
|
|
144
|
+
else if (before.phase !== phase)
|
|
145
|
+
promoted.push(vault.uid);
|
|
146
|
+
else
|
|
147
|
+
kept.push(vault.uid);
|
|
148
|
+
}
|
|
149
|
+
const stopped = [];
|
|
150
|
+
let personalBackOn = false;
|
|
151
|
+
for (const vault of previousByUid.values()) {
|
|
152
|
+
if (desired.some((candidate) => candidate.uid === vault.uid))
|
|
153
|
+
continue;
|
|
154
|
+
stopped.push(vault.uid);
|
|
155
|
+
if (vault.phase !== "full" || vault.legacyAlreadyOff)
|
|
156
|
+
continue;
|
|
157
|
+
if (vault.kind === "company" && vault.slug)
|
|
158
|
+
companies[vault.slug] = true;
|
|
159
|
+
else
|
|
160
|
+
personalBackOn = true;
|
|
161
|
+
}
|
|
162
|
+
const state = {
|
|
163
|
+
version: 1,
|
|
164
|
+
readAt: at,
|
|
165
|
+
release: installedRelease ?? release,
|
|
166
|
+
binaryPath,
|
|
167
|
+
vaults,
|
|
168
|
+
...(warnings.length > 0 ? { error: warnings.join("; ") } : {}),
|
|
169
|
+
};
|
|
170
|
+
writeSyncCoreRolloutState(deps.home, state);
|
|
171
|
+
for (const uid of stopped)
|
|
172
|
+
removeEngineOwnership(deps.home, uid);
|
|
173
|
+
const patch = {
|
|
174
|
+
...(personalOff ? { personal: false } : personalBackOn ? { personal: true } : {}),
|
|
175
|
+
companies,
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
if (writeVaultSyncSwitches(deps.home, patch))
|
|
179
|
+
log("updated the sync switches in menubar.json");
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
warnings.push(`could not update the sync switches: ${message(error)}`);
|
|
183
|
+
state.error = warnings.join("; ");
|
|
184
|
+
writeSyncCoreRolloutState(deps.home, state);
|
|
185
|
+
}
|
|
186
|
+
for (const uid of started)
|
|
187
|
+
log(`${uid}: warming the new sync engine's index (home ${syncCoreEngineHome(deps.home, uid)})`);
|
|
188
|
+
for (const uid of promoted)
|
|
189
|
+
log(`${uid}: index warm; moving to the new sync engine`);
|
|
190
|
+
for (const uid of stopped)
|
|
191
|
+
log(`${uid}: back on the current sync`);
|
|
192
|
+
for (const warning of warnings)
|
|
193
|
+
log(`warning: ${warning}`);
|
|
194
|
+
return { state, started, promoted, stopped, kept, warnings };
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const SYNC_CORE_RELEASE = "v0.1.13";
|
|
2
|
+
export declare const SYNC_CORE_COMPANY_FULL_RELEASE: string | null;
|
|
3
|
+
export declare const SYNC_CORE_PROMOTION_OP_LIMIT = 100;
|
|
4
|
+
export declare function compareSyncCoreReleases(a: string, b: string): number;
|
|
5
|
+
export declare function companyFullModeAllowed(release: string): boolean;
|
|
6
|
+
export declare const SYNC_CORE_DOWNLOAD_BASE = "https://indigoai-public.s3.amazonaws.com/hq-sync-core";
|
|
7
|
+
export declare const SYNC_CORE_DOWNLOAD_BASE_ENV = "HQ_SYNC_CORE_DOWNLOAD_BASE";
|
|
8
|
+
export declare function syncCoreBinaryPath(home?: string): string;
|
|
9
|
+
export declare function syncCoreReleaseMarkerPath(home?: string): string;
|
|
10
|
+
export declare function syncCoreAssetName(release: string, platform: NodeJS.Platform, arch: string): string | null;
|
|
11
|
+
export declare function syncCoreDownloadBase(env?: NodeJS.ProcessEnv): string;
|
|
12
|
+
export declare function parseSha256Sums(text: string): Map<string, string>;
|
|
13
|
+
export declare function installedSyncCoreRelease(home: string): string | null;
|
|
14
|
+
export interface EnsureSyncCoreBinaryOptions {
|
|
15
|
+
home: string;
|
|
16
|
+
platform: NodeJS.Platform;
|
|
17
|
+
arch: string;
|
|
18
|
+
release?: string;
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
env?: NodeJS.ProcessEnv;
|
|
21
|
+
fetch?: typeof fetch;
|
|
22
|
+
run?: (file: string, args: string[]) => void;
|
|
23
|
+
}
|
|
24
|
+
export interface SyncCoreBinaryInstall {
|
|
25
|
+
binaryPath: string;
|
|
26
|
+
release: string;
|
|
27
|
+
installed: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare class SyncCoreReleaseError extends Error {
|
|
30
|
+
}
|
|
31
|
+
export declare function ensureSyncCoreBinary(opts: EnsureSyncCoreBinaryOptions): Promise<SyncCoreBinaryInstall>;
|
|
32
|
+
//# sourceMappingURL=release.d.ts.map
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
export const SYNC_CORE_RELEASE = "v0.1.13";
|
|
7
|
+
export const SYNC_CORE_COMPANY_FULL_RELEASE = null;
|
|
8
|
+
export const SYNC_CORE_PROMOTION_OP_LIMIT = 100;
|
|
9
|
+
export function compareSyncCoreReleases(a, b) {
|
|
10
|
+
const parse = (release) => release.replace(/^v/, "").split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
11
|
+
const [x, y] = [parse(a), parse(b)];
|
|
12
|
+
for (let i = 0; i < Math.max(x.length, y.length); i += 1) {
|
|
13
|
+
const diff = (x[i] ?? 0) - (y[i] ?? 0);
|
|
14
|
+
if (diff !== 0)
|
|
15
|
+
return diff;
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
export function companyFullModeAllowed(release) {
|
|
20
|
+
return SYNC_CORE_COMPANY_FULL_RELEASE !== null && compareSyncCoreReleases(release, SYNC_CORE_COMPANY_FULL_RELEASE) >= 0;
|
|
21
|
+
}
|
|
22
|
+
export const SYNC_CORE_DOWNLOAD_BASE = "https://indigoai-public.s3.amazonaws.com/hq-sync-core";
|
|
23
|
+
export const SYNC_CORE_DOWNLOAD_BASE_ENV = "HQ_SYNC_CORE_DOWNLOAD_BASE";
|
|
24
|
+
const DOWNLOAD_TIMEOUT_MS = 600_000;
|
|
25
|
+
export function syncCoreBinaryPath(home = os.homedir()) {
|
|
26
|
+
return path.join(home, ".hq", "bin", "hq-syncd");
|
|
27
|
+
}
|
|
28
|
+
export function syncCoreReleaseMarkerPath(home = os.homedir()) {
|
|
29
|
+
return path.join(home, ".hq", "bin", "hq-syncd.release");
|
|
30
|
+
}
|
|
31
|
+
export function syncCoreAssetName(release, platform, arch) {
|
|
32
|
+
if (platform === "darwin")
|
|
33
|
+
return `hq-sync-core-${release}-macos-universal.tar.gz`;
|
|
34
|
+
if (platform === "linux" && arch === "x64")
|
|
35
|
+
return `hq-sync-core-${release}-linux-x64.tar.gz`;
|
|
36
|
+
if (platform === "linux" && arch === "arm64")
|
|
37
|
+
return `hq-sync-core-${release}-linux-arm64.tar.gz`;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
export function syncCoreDownloadBase(env = process.env) {
|
|
41
|
+
const override = env[SYNC_CORE_DOWNLOAD_BASE_ENV]?.trim();
|
|
42
|
+
return (override || SYNC_CORE_DOWNLOAD_BASE).replace(/\/+$/, "");
|
|
43
|
+
}
|
|
44
|
+
export function parseSha256Sums(text) {
|
|
45
|
+
const sums = new Map();
|
|
46
|
+
for (const line of text.split("\n")) {
|
|
47
|
+
const match = /^([0-9a-f]{64})\s+\*?(\S+)\s*$/i.exec(line.trim());
|
|
48
|
+
if (match)
|
|
49
|
+
sums.set(match[2], match[1].toLowerCase());
|
|
50
|
+
}
|
|
51
|
+
return sums;
|
|
52
|
+
}
|
|
53
|
+
export function installedSyncCoreRelease(home) {
|
|
54
|
+
try {
|
|
55
|
+
const marker = fs.readFileSync(syncCoreReleaseMarkerPath(home), "utf8").trim();
|
|
56
|
+
const binary = syncCoreBinaryPath(home);
|
|
57
|
+
if (!marker || !fs.statSync(binary).isFile())
|
|
58
|
+
return null;
|
|
59
|
+
fs.accessSync(binary, fs.constants.X_OK);
|
|
60
|
+
return marker;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class SyncCoreReleaseError extends Error {
|
|
67
|
+
}
|
|
68
|
+
function findFile(dir, name) {
|
|
69
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
70
|
+
const full = path.join(dir, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
const nested = findFile(full, name);
|
|
73
|
+
if (nested)
|
|
74
|
+
return nested;
|
|
75
|
+
}
|
|
76
|
+
else if (entry.name === name) {
|
|
77
|
+
return full;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
async function download(fetcher, url) {
|
|
83
|
+
let response;
|
|
84
|
+
try {
|
|
85
|
+
response = await fetcher(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw new SyncCoreReleaseError(`could not download ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
}
|
|
90
|
+
if (!response.ok)
|
|
91
|
+
throw new SyncCoreReleaseError(`could not download ${url}: HTTP ${response.status}`);
|
|
92
|
+
return Buffer.from(await response.arrayBuffer());
|
|
93
|
+
}
|
|
94
|
+
export async function ensureSyncCoreBinary(opts) {
|
|
95
|
+
const release = opts.release ?? SYNC_CORE_RELEASE;
|
|
96
|
+
const binaryPath = syncCoreBinaryPath(opts.home);
|
|
97
|
+
if (installedSyncCoreRelease(opts.home) === release)
|
|
98
|
+
return { binaryPath, release, installed: false };
|
|
99
|
+
const asset = syncCoreAssetName(release, opts.platform, opts.arch);
|
|
100
|
+
if (!asset)
|
|
101
|
+
throw new SyncCoreReleaseError(`the sync engine has no ${release} build for ${opts.platform} on ${opts.arch}`);
|
|
102
|
+
const base = `${opts.baseUrl?.replace(/\/+$/, "") ?? syncCoreDownloadBase(opts.env)}/${release}`;
|
|
103
|
+
const fetcher = opts.fetch ?? fetch;
|
|
104
|
+
const run = opts.run ?? ((file, args) => {
|
|
105
|
+
execFileSync(file, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
106
|
+
});
|
|
107
|
+
const sums = parseSha256Sums((await download(fetcher, `${base}/SHA256SUMS`)).toString("utf8"));
|
|
108
|
+
const expected = sums.get(asset);
|
|
109
|
+
if (!expected)
|
|
110
|
+
throw new SyncCoreReleaseError(`SHA256SUMS for ${release} does not list ${asset}`);
|
|
111
|
+
const bytes = await download(fetcher, `${base}/${asset}`);
|
|
112
|
+
const actual = crypto.createHash("sha256").update(bytes).digest("hex");
|
|
113
|
+
if (actual !== expected)
|
|
114
|
+
throw new SyncCoreReleaseError(`${asset} did not match SHA256SUMS (${actual} != ${expected})`);
|
|
115
|
+
const binDir = path.dirname(binaryPath);
|
|
116
|
+
fs.mkdirSync(binDir, { recursive: true, mode: 0o700 });
|
|
117
|
+
const staging = fs.mkdtempSync(path.join(binDir, ".hq-syncd-staging-"));
|
|
118
|
+
try {
|
|
119
|
+
const archive = path.join(staging, asset);
|
|
120
|
+
fs.writeFileSync(archive, bytes, { mode: 0o600 });
|
|
121
|
+
const extracted = path.join(staging, "extracted");
|
|
122
|
+
fs.mkdirSync(extracted, { mode: 0o700 });
|
|
123
|
+
run("tar", ["-xzf", archive, "-C", extracted]);
|
|
124
|
+
const unpacked = findFile(extracted, "hq-syncd");
|
|
125
|
+
if (!unpacked)
|
|
126
|
+
throw new SyncCoreReleaseError(`${asset} does not contain hq-syncd`);
|
|
127
|
+
const prepared = path.join(staging, "hq-syncd");
|
|
128
|
+
fs.copyFileSync(unpacked, prepared);
|
|
129
|
+
fs.chmodSync(prepared, 0o700);
|
|
130
|
+
fs.renameSync(prepared, binaryPath);
|
|
131
|
+
const marker = syncCoreReleaseMarkerPath(opts.home);
|
|
132
|
+
fs.writeFileSync(`${marker}.tmp`, `${release}\n`, { mode: 0o600 });
|
|
133
|
+
fs.renameSync(`${marker}.tmp`, marker);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
return { binaryPath, release, installed: true };
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=release.js.map
|