@indigoai-us/hq-cli 5.345.137 → 5.345.139
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 +12 -0
- package/dist/commands/storage-offload.d.ts +154 -42
- package/dist/commands/storage-offload.js +469 -111
- package/dist/commands/storage.d.ts +12 -1
- package/dist/commands/storage.js +97 -17
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +577 -441
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.139] — 2026-10-15
|
|
6
|
+
- `hq storage offload` and `hq storage fetch` no longer fail about 15 to 25 minutes into a run. Personal and company
|
|
7
|
+
vault credentials are now re-vended before they expire, for the whole run and in the middle of a multipart upload.
|
|
8
|
+
When S3 still refuses a request's credentials, HQ re-vends once and retries that request once. A request refused
|
|
9
|
+
again fails, and an interrupted company upload resumes from its last finished part on the next run.
|
|
10
|
+
- Vault errors now name the request, the vault, the HTTP status and the error code, for example
|
|
11
|
+
`HEAD failed in company vault 'acme': HTTP 403 Forbidden (no error body: credentials expired or access denied)`,
|
|
12
|
+
instead of a bare `UnknownError`.
|
|
13
|
+
|
|
14
|
+
## [5.345.138] — 2026-10-15
|
|
15
|
+
- `hq storage offload --current` can now free company files larger than the 50 MB sync limit. It uploads each one to its company vault, checks the vault copy against the file's sha256 and size, records it as cloud-only, writes the `.hqcloud` placeholder, and only then removes the local copy. An interrupted upload leaves the file in place and resumes on the next run. The dry run shows `upload+offload` with the byte count and the vault key. A path the `.hqinclude` allowlist excludes is refused with the line to add. `hq storage fetch` checks the vault copy's sha256 before restoring it. The upload path turns on once the bundled hq-cloud includes the new upload exports; until then these files report that HQ sync needs an update.
|
|
16
|
+
|
|
5
17
|
## [5.345.137] — 2026-10-15
|
|
6
18
|
- The bundled sync engine now uses hq-cloud 6.18.76. On Windows, the hq-core update and rescue lay down native links after the overlay, turn git symlink stubs from Windows clones into real links, and upload symlinks with the correct target. Sync merges `board.json` by entry id instead of replacing it, merges append-only `.jsonl` conflicts instead of parking them, keeps any `.hqcloud` placeholder on pull, and no longer syncs live-meeting runtime state or server sync manifests.
|
|
7
19
|
|
|
@@ -1,38 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `hq storage offload` / `hq storage fetch` — move big files
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* `hq storage offload` / `hq storage fetch` — move big files out of the HQ
|
|
3
|
+
* folder into HQ cloud storage and leave small linked placeholders behind.
|
|
4
|
+
* Shared contract: owner-approved offload design, 2026-10-09; company upload
|
|
5
|
+
* for files over the sync limit approved 2026-10-15.
|
|
5
6
|
*
|
|
6
7
|
* Two kinds of big file:
|
|
7
8
|
* - History copies (old versions inside the HQ root git history). The CLI
|
|
8
|
-
* uploads each blob, verifies size + sha256
|
|
9
|
-
* verified blobs to core `hq-storage.sh
|
|
10
|
-
* to the pointer text. Working-tree
|
|
9
|
+
* uploads each blob to the caller's PERSONAL vault, verifies size + sha256
|
|
10
|
+
* + git blob id, then hands only verified blobs to core `hq-storage.sh
|
|
11
|
+
* strip-large`, which rewrites history to the pointer text. Working-tree
|
|
12
|
+
* files are never touched.
|
|
11
13
|
* - Current files (`--current [paths...]`, idle big files in the working
|
|
12
|
-
* tree). Each file goes to the vault that owns its path
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
14
|
+
* tree). Each file goes to the vault that owns its path:
|
|
15
|
+
* - A file in a company folder belongs to that company's vault, and only
|
|
16
|
+
* an active member can offload it. When HQ sync already stored this
|
|
17
|
+
* exact version (journal hash + size, and the vault object still has
|
|
18
|
+
* the journal's ETag), the file is replaced by a placeholder pointing
|
|
19
|
+
* at that object. A file over the sync limit (SYNC_MAX_FILE_BYTES),
|
|
20
|
+
* which sync never uploads, is uploaded to the company vault at its
|
|
21
|
+
* sync key by a resumable, checksum-verified multipart upload
|
|
22
|
+
* (`upload+offload`). Once the vault copy is verified the file is
|
|
23
|
+
* recorded in the sync journal as synced and cloud-only, then the
|
|
24
|
+
* placeholder is written, then the local file is removed. A path the
|
|
25
|
+
* sync ignore rules exclude is refused and the excluding rule is named
|
|
26
|
+
* (for `.hqinclude`, with the exact line to add). Other unsynced
|
|
27
|
+
* company files are skipped with "run hq sync first".
|
|
28
|
+
* - Any other file points at its personal-vault sync object the same
|
|
29
|
+
* way, or is uploaded to the personal vault's `.hq-offload/blobs/`.
|
|
30
|
+
* Current-file offload runs only when the installed HQ sync holds every
|
|
31
|
+
* `.hqcloud` sibling (hq-cloud CLOUD_PLACEHOLDER_HOLDS_ANY_SIBLING) and
|
|
32
|
+
* skips `.hq-offload/`; otherwise it reports "update HQ sync".
|
|
23
33
|
*
|
|
24
|
-
* Fetch also reads placeholders that HQ sync
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* it can never sync back.
|
|
34
|
+
* Fetch also reads placeholders that HQ sync wrote on a teammate's machine
|
|
35
|
+
* (`vault: company`, the company vault key, `sha256: unknown`, `remote-etag`).
|
|
36
|
+
* Those download from the company vault, only for an active member, and
|
|
37
|
+
* verify by size, ETag and, when the object carries it, its `hq-sha256`
|
|
38
|
+
* metadata. A placeholder marked `orphaned: true` (a teammate deleted the
|
|
39
|
+
* file) is refused unless `--force-orphan` is given, which writes the caller's
|
|
40
|
+
* own personal-vault copy to a file outside HQ so it can never sync back.
|
|
31
41
|
*
|
|
32
|
-
* Tenant isolation: uploads only ever go to the caller's
|
|
33
|
-
* the personal store refuses a company vault bucket. A
|
|
34
|
-
*
|
|
35
|
-
*
|
|
42
|
+
* Tenant isolation: history and personal uploads only ever go to the caller's
|
|
43
|
+
* personal vault, and the personal store refuses a company vault bucket. A
|
|
44
|
+
* company vault is opened only for a company the caller is an active member
|
|
45
|
+
* of, and only for a file in that company's folder; the company uploader must
|
|
46
|
+
* name that company and refuses a personal bucket.
|
|
36
47
|
*/
|
|
37
48
|
import { GetObjectCommand, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
38
49
|
import type { ScriptRunner } from "./storage.js";
|
|
@@ -47,6 +58,13 @@ export declare const ORPHANED_MESSAGE = "This file was deleted by a teammate. It
|
|
|
47
58
|
export declare const UPDATE_SYNC = "update HQ sync";
|
|
48
59
|
/** Reason on company files HQ sync has not stored in the company vault yet. */
|
|
49
60
|
export declare const RUN_SYNC_FIRST = "run hq sync first";
|
|
61
|
+
/**
|
|
62
|
+
* HQ sync's per-file limit (hq-cloud SYNC_MAX_FILE_BYTES; sync skips files
|
|
63
|
+
* strictly larger). storage.ts passes hq-cloud's value when it exports one.
|
|
64
|
+
*/
|
|
65
|
+
export declare const SYNC_MAX_FILE_BYTES: number;
|
|
66
|
+
/** Object metadata key holding the sha256 of a company-vault offload upload. */
|
|
67
|
+
export declare const VAULT_SHA256_METADATA_KEY = "hq-sha256";
|
|
50
68
|
/** Top-level HQ-root folder that holds offloaded blobs in the personal vault. */
|
|
51
69
|
export declare const OFFLOAD_TOP_LEVEL = ".hq-offload";
|
|
52
70
|
/**
|
|
@@ -112,18 +130,64 @@ export interface BlobStore {
|
|
|
112
130
|
export interface OffloadS3Client {
|
|
113
131
|
send(cmd: PutObjectCommand | HeadObjectCommand | GetObjectCommand): Promise<unknown>;
|
|
114
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* True when S3 refused the request's credentials: an expired, invalid or
|
|
135
|
+
* denied token, or a 403 with no error body (what S3 sends for HEAD).
|
|
136
|
+
*/
|
|
137
|
+
export declare function isCredentialRejection(err: unknown): boolean;
|
|
138
|
+
/** A failed vault request, described with its HTTP status, error code and vault. */
|
|
139
|
+
export declare class VaultRequestError extends Error {
|
|
140
|
+
readonly status?: number;
|
|
141
|
+
readonly code?: string;
|
|
142
|
+
constructor(message: string, opts: {
|
|
143
|
+
status?: number;
|
|
144
|
+
code?: string;
|
|
145
|
+
cause: unknown;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Describes an AWS error from `op` against `where` so the user sees the HTTP
|
|
150
|
+
* status, the error code and the vault, never the SDK's bare "UnknownError".
|
|
151
|
+
* Errors that did not come from AWS pass through unchanged.
|
|
152
|
+
*/
|
|
153
|
+
export declare function describeVaultError(err: unknown, op: string, where: string, retried?: boolean): unknown;
|
|
154
|
+
/** Turns any per-file failure into one line for the run's error list. */
|
|
155
|
+
export declare function failureMessage(err: unknown): string;
|
|
156
|
+
/**
|
|
157
|
+
* Runs vault requests on `initial` and, when S3 refuses the credentials,
|
|
158
|
+
* re-vends once and retries the request once on the new client. Requests
|
|
159
|
+
* refused on the same client share one re-vend. A request refused again
|
|
160
|
+
* after the re-vend fails with a described error; it is never retried twice.
|
|
161
|
+
*/
|
|
162
|
+
export declare function credentialRetry<C>(initial: C, where: string, revend?: () => Promise<C>): <T>(op: string, fn: (client: C) => Promise<T>) => Promise<T>;
|
|
163
|
+
/**
|
|
164
|
+
* Wraps every method of `initial` (a multipart client) so each call gets
|
|
165
|
+
* {@link credentialRetry}: one re-vend and one retry when S3 refuses the
|
|
166
|
+
* credentials, and a described error otherwise.
|
|
167
|
+
*/
|
|
168
|
+
export declare function credentialRetryingClient<C extends object>(initial: C, where: string, revend: () => Promise<C>): C;
|
|
169
|
+
export interface S3StoreOptions<C> {
|
|
170
|
+
/** How errors name the vault, e.g. "personal vault" or "company vault 'acme'". */
|
|
171
|
+
where?: string;
|
|
172
|
+
/** Vends fresh credentials and returns a client that uses them. */
|
|
173
|
+
revend?: () => Promise<C>;
|
|
174
|
+
}
|
|
115
175
|
/**
|
|
116
176
|
* S3-backed store over the caller's personal vault bucket. Accepts only
|
|
117
177
|
* personal vault buckets (`hq-vault-prs-*`) so a mis-resolved entity can never
|
|
118
178
|
* receive offloaded bytes.
|
|
119
179
|
*/
|
|
120
|
-
export declare function s3BlobStore(bucket: string, s3: OffloadS3Client): BlobStore;
|
|
180
|
+
export declare function s3BlobStore(bucket: string, s3: OffloadS3Client, opts?: S3StoreOptions<OffloadS3Client>): BlobStore;
|
|
121
181
|
/** Read-only access to vault objects at their sync keys, pinned to an ETag. */
|
|
122
182
|
export interface VaultObjectStore {
|
|
123
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* Size and unquoted ETag of `key`, or null when it is not there. `sha256` is
|
|
185
|
+
* the object's `hq-sha256` metadata, set on company-vault offload uploads.
|
|
186
|
+
*/
|
|
124
187
|
head(key: string): Promise<{
|
|
125
188
|
size: number;
|
|
126
189
|
etag: string;
|
|
190
|
+
sha256?: string;
|
|
127
191
|
} | null>;
|
|
128
192
|
/** Download `key` to `dest` only if its ETag is `etag`; returns the served ETag. */
|
|
129
193
|
download(key: string, dest: string, etag: string): Promise<string>;
|
|
@@ -131,7 +195,7 @@ export interface VaultObjectStore {
|
|
|
131
195
|
export interface VaultObjectS3Client {
|
|
132
196
|
send(cmd: GetObjectCommand | HeadObjectCommand): Promise<unknown>;
|
|
133
197
|
}
|
|
134
|
-
export declare function s3VaultObjectStore(bucket: string, s3: VaultObjectS3Client): VaultObjectStore;
|
|
198
|
+
export declare function s3VaultObjectStore(bucket: string, s3: VaultObjectS3Client, opts?: S3StoreOptions<VaultObjectS3Client>): VaultObjectStore;
|
|
135
199
|
export interface OffloadDeps {
|
|
136
200
|
hqRoot: string;
|
|
137
201
|
now: () => Date;
|
|
@@ -169,6 +233,55 @@ export interface OffloadDeps {
|
|
|
169
233
|
syncRecord?: (target: OffloadTarget) => Promise<SyncRecord | null>;
|
|
170
234
|
/** File operations for current-file offload (tests inject to see the order). */
|
|
171
235
|
placeholderFs?: PlaceholderFs;
|
|
236
|
+
/** HQ sync's per-file limit in bytes. Defaults to SYNC_MAX_FILE_BYTES. */
|
|
237
|
+
syncMaxFileBytes?: number;
|
|
238
|
+
/**
|
|
239
|
+
* Whether HQ sync's ignore rules exclude an HQ-root-relative path, and which
|
|
240
|
+
* rule does (hq-cloud createIgnoreExplainer). Null or absent: not excluded.
|
|
241
|
+
*/
|
|
242
|
+
syncExclusion?: (rel: string) => SyncExclusion | null;
|
|
243
|
+
/**
|
|
244
|
+
* Open a multipart uploader for a company vault. Called only for a company
|
|
245
|
+
* the caller is an active member of. Absent: company files over the sync
|
|
246
|
+
* limit report "update HQ sync".
|
|
247
|
+
*/
|
|
248
|
+
openCompanyUploader?: (slug: string) => Promise<CompanyUploader>;
|
|
249
|
+
/**
|
|
250
|
+
* Record `target` in the sync journal as synced and cloud-only (hq-cloud
|
|
251
|
+
* recordOffloadedObject), so sync neither re-uploads it nor pulls it back.
|
|
252
|
+
*/
|
|
253
|
+
recordCloudOnly?: (target: OffloadTarget, object: VerifiedUpload) => Promise<void>;
|
|
254
|
+
/** Folder for resumable multipart upload state. Defaults to ~/.hq/offload-uploads. */
|
|
255
|
+
uploadStateDir?: () => string;
|
|
256
|
+
}
|
|
257
|
+
/** Why HQ sync skips a path: the ignore layer that excludes it. */
|
|
258
|
+
export interface SyncExclusion {
|
|
259
|
+
excluded: boolean;
|
|
260
|
+
/** "built-in", "defaults", ".hqignore", ".hqsyncignore" or ".hqinclude allowlist". */
|
|
261
|
+
layer: string | null;
|
|
262
|
+
/** For the .hqinclude allowlist: the line that would include the path. */
|
|
263
|
+
includeLine?: string;
|
|
264
|
+
}
|
|
265
|
+
/** A vault object whose size and sha256 were verified after upload. */
|
|
266
|
+
export interface VerifiedUpload {
|
|
267
|
+
etag: string;
|
|
268
|
+
size: number;
|
|
269
|
+
sha256: string;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Resumable, checksum-verified multipart upload into one company vault
|
|
273
|
+
* (hq-cloud uploadLargeObjectVerified). `upload` resumes from `stateFile`
|
|
274
|
+
* when it holds an upload for the same file, and resolves only after the
|
|
275
|
+
* object's size, part count and sha256 metadata are verified.
|
|
276
|
+
*/
|
|
277
|
+
export interface CompanyUploader {
|
|
278
|
+
slug: string;
|
|
279
|
+
bucket: string;
|
|
280
|
+
upload(input: {
|
|
281
|
+
key: string;
|
|
282
|
+
file: string;
|
|
283
|
+
stateFile: string;
|
|
284
|
+
}): Promise<VerifiedUpload>;
|
|
172
285
|
}
|
|
173
286
|
/** The vault that owns a working-tree path, and the object key there. */
|
|
174
287
|
export type OffloadTarget = {
|
|
@@ -273,7 +386,15 @@ export interface CurrentFilePlan {
|
|
|
273
386
|
company?: string;
|
|
274
387
|
/** HQ sync already stored this exact version in the owning vault. */
|
|
275
388
|
synced: boolean;
|
|
276
|
-
|
|
389
|
+
/**
|
|
390
|
+
* upload+offload: a company file over the sync limit, uploaded to the
|
|
391
|
+
* company vault at `vault_key`, then replaced by a placeholder.
|
|
392
|
+
*/
|
|
393
|
+
action: "point-at-vault" | "upload" | "upload+offload" | "skip";
|
|
394
|
+
/** Company vault key for upload+offload. */
|
|
395
|
+
vault_key?: string;
|
|
396
|
+
/** The HQ sync ignore layer that excludes this path. */
|
|
397
|
+
excluded_by?: string;
|
|
277
398
|
reason?: string;
|
|
278
399
|
}
|
|
279
400
|
export interface CurrentOffloaded {
|
|
@@ -281,7 +402,7 @@ export interface CurrentOffloaded {
|
|
|
281
402
|
bytes: number;
|
|
282
403
|
vault: "personal" | "company";
|
|
283
404
|
company?: string;
|
|
284
|
-
action: "point-at-vault" | "upload";
|
|
405
|
+
action: "point-at-vault" | "upload" | "upload+offload";
|
|
285
406
|
}
|
|
286
407
|
export interface OffloadOptions {
|
|
287
408
|
minBytes: number;
|
|
@@ -405,13 +526,4 @@ export declare function getOffloadStatus(deps: OffloadDeps, opts: {
|
|
|
405
526
|
export declare function gitExportBlob(hqRoot: string): (sha: string, dest: string) => Promise<void>;
|
|
406
527
|
export declare function gitListPlaceholders(hqRoot: string): () => Promise<string[]>;
|
|
407
528
|
export declare function defaultTmpDir(): string;
|
|
408
|
-
export declare function personalS3Store(input: {
|
|
409
|
-
bucket: string;
|
|
410
|
-
region: string;
|
|
411
|
-
credentials: {
|
|
412
|
-
accessKeyId: string;
|
|
413
|
-
secretAccessKey: string;
|
|
414
|
-
sessionToken: string;
|
|
415
|
-
};
|
|
416
|
-
}): BlobStore;
|
|
417
529
|
//# sourceMappingURL=storage-offload.d.ts.map
|