@indigoai-us/hq-cli 5.345.80 → 5.345.82

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,249 @@
1
+ /**
2
+ * `hq storage` — how much space HQ backups use, and pruning of old ones.
3
+ *
4
+ * Two stores:
5
+ * - Local autosave history (the HQ root git repo). Measured and pruned by the
6
+ * hq-core script `core/scripts/hq-storage.sh`; older HQ installs lack it and
7
+ * report `{ available: false, reason: "update HQ" }`.
8
+ * - Cloud file history: S3 object versioning on company vault buckets. Read
9
+ * with ListObjectVersions and pruned with batched DeleteObjects, using
10
+ * membership-scoped credentials from `/sts/vend`.
11
+ *
12
+ * Tenant isolation: cloud work only ever targets companies in the caller's own
13
+ * active memberships (`/membership/me`). `--company` narrows that set; a slug
14
+ * outside it is rejected before any credentials are vended. Deleting cloud
15
+ * history is for owners and admins only: prune checks the membership role
16
+ * before vending, and `status` marks each company with `can_delete`.
17
+ *
18
+ * `offload` and `fetch` (big files to the personal vault) live in
19
+ * `storage-offload.ts`; this file registers them and adds the offload section
20
+ * to `status`.
21
+ */
22
+ import { Command } from "commander";
23
+ import { type ReclaimReport } from "./storage-core.js";
24
+ import { ListObjectVersionsCommand, DeleteObjectsCommand, type ListObjectVersionsCommandOutput, type DeleteObjectsCommandOutput } from "@aws-sdk/client-s3";
25
+ import { type OffloadResult, type OffloadStatus } from "./storage-offload.js";
26
+ export type TrancheId = "7d" | "30d" | "90d" | "365d" | "older";
27
+ export declare const TRANCHES: readonly {
28
+ id: TrancheId;
29
+ label: string;
30
+ maxAgeDays: number;
31
+ }[];
32
+ export declare function trancheFor(lastModified: Date, now: Date): TrancheId;
33
+ export type LocalReport = Record<string, unknown> & {
34
+ available?: boolean;
35
+ reason?: string;
36
+ };
37
+ export interface CloudTranche {
38
+ id: TrancheId;
39
+ label: string;
40
+ count: number;
41
+ bytes: number;
42
+ }
43
+ export interface CloudCompanyStatus {
44
+ company: string;
45
+ available: boolean;
46
+ /** Whether the caller may delete this company's old versions (owner/admin). */
47
+ can_delete: boolean;
48
+ current_bytes: number;
49
+ noncurrent_bytes: number;
50
+ noncurrent_count: number;
51
+ delete_markers: number;
52
+ tranches: CloudTranche[];
53
+ error?: string;
54
+ /** When these figures were listed (ISO). Older than now when served from the cache. */
55
+ listed_at?: string;
56
+ }
57
+ export interface StorageStatus {
58
+ local: LocalReport;
59
+ cloud: CloudCompanyStatus[];
60
+ /** Set when cloud history could not be read at all, e.g. not signed in. */
61
+ cloud_error?: string;
62
+ offload?: OffloadStatus;
63
+ generated_at: string;
64
+ }
65
+ export interface CloudPruneResult {
66
+ company: string;
67
+ deleted_count: number;
68
+ deleted_bytes: number;
69
+ errors: string[];
70
+ /** Set when the company was not touched, e.g. the caller is not an owner/admin. */
71
+ skipped_reason?: string;
72
+ /** True when S3 refused the delete for this company (role not granted). */
73
+ access_denied?: boolean;
74
+ }
75
+ export interface StoragePruneResult {
76
+ local: LocalReport | null;
77
+ cloud: CloudPruneResult[];
78
+ dry_run: boolean;
79
+ /** Expired backups dropped before a real local prune; absent otherwise. */
80
+ reclaim?: ReclaimReport;
81
+ }
82
+ /** Subset of S3Client this command uses. Injectable for tests. */
83
+ export interface StorageS3Client {
84
+ send(cmd: ListObjectVersionsCommand): Promise<ListObjectVersionsCommandOutput>;
85
+ send(cmd: DeleteObjectsCommand): Promise<DeleteObjectsCommandOutput>;
86
+ }
87
+ export interface CompanyRef {
88
+ uid: string;
89
+ slug: string;
90
+ /** Membership role from `/membership/me`: owner, admin, member or guest. */
91
+ role?: string;
92
+ }
93
+ /** Cloud history delete is owners and admins only. A missing role is not enough. */
94
+ export declare function canDeleteCloudHistory(company: CompanyRef): boolean;
95
+ export declare const DELETE_ROLE_REASON = "Only company owners and admins can delete cloud file history.";
96
+ export interface CompanyStore {
97
+ bucket: string;
98
+ s3: StorageS3Client;
99
+ }
100
+ export type ScriptRunner = (scriptPath: string, args: string[]) => Promise<{
101
+ code: number;
102
+ stdout: string;
103
+ stderr: string;
104
+ }>;
105
+ export interface StorageDeps {
106
+ hqRoot: string;
107
+ now: () => Date;
108
+ /** The caller's active company memberships. */
109
+ listCompanies: () => Promise<CompanyRef[]>;
110
+ /** Vend credentials and resolve the bucket for one member company. */
111
+ openCompany: (company: CompanyRef) => Promise<CompanyStore>;
112
+ runScript: ScriptRunner;
113
+ confirm: (message: string) => Promise<boolean>;
114
+ log: (line: string) => void;
115
+ /** Offload section for `status`. Absent in deps that only prune. */
116
+ getOffload?: () => Promise<OffloadStatus>;
117
+ /** Cache of cloud status per company (see cloudStatusCache). Absent = always list. */
118
+ cloudCache?: CloudStatusCache;
119
+ }
120
+ /**
121
+ * Listing every version of a large vault takes minutes (about 2 for 2.4M
122
+ * versions, most of it parsing S3's XML), so `status` keeps each company's
123
+ * cloud figures for CLOUD_CACHE_TTL_MS. Only complete, successful listings are
124
+ * kept; a real cloud prune drops the company's entry; dry runs and real prunes
125
+ * always list live.
126
+ */
127
+ export interface CloudStatusCache {
128
+ read(company: CompanyRef): CloudCompanyStatus | undefined;
129
+ write(company: CompanyRef, status: CloudCompanyStatus): void;
130
+ drop(company: CompanyRef): void;
131
+ }
132
+ export declare const CLOUD_CACHE_TTL_MS: number;
133
+ /** File cache under <hq>/workspace/.storage-cache/cloud-<company uid>.json. */
134
+ export declare function cloudStatusCache(hqRoot: string, now: () => Date, ttlMs?: number): CloudStatusCache;
135
+ export declare function localScriptPath(hqRoot: string): string;
136
+ /**
137
+ * Core `drop-backups --expired` right before a real local prune; undefined
138
+ * when HQ lacks it. Status, dry runs and fetch never call it.
139
+ */
140
+ export declare function dropExpiredLocal(deps: StorageDeps): Promise<ReclaimReport | undefined>;
141
+ export declare function getLocalReport(deps: StorageDeps): Promise<LocalReport>;
142
+ export declare function selectCompanies(memberships: CompanyRef[], requested: string | undefined): CompanyRef[];
143
+ /**
144
+ * Active memberships from a `/membership/me` body, one per company. A company
145
+ * listed more than once keeps its highest-privilege role.
146
+ */
147
+ export declare function companiesFromMemberships(body: unknown): CompanyRef[];
148
+ export interface VersionEntry {
149
+ key: string;
150
+ versionId: string;
151
+ isLatest: boolean;
152
+ lastModified: Date;
153
+ size: number;
154
+ isDeleteMarker: boolean;
155
+ /**
156
+ * When this version stopped being current: the LastModified of the next
157
+ * newer version or delete marker of the same key. Unset for current entries.
158
+ */
159
+ noncurrentSince?: Date;
160
+ }
161
+ /**
162
+ * Fill `noncurrentSince` on every noncurrent entry. A noncurrent version's
163
+ * age is how long it has been a backup, not when it was written: a file
164
+ * written two years ago and overwritten yesterday became an undo copy
165
+ * yesterday. Within a key the current entry comes first, then newest to
166
+ * oldest. A noncurrent entry with no newer entry in the listing falls back
167
+ * to its own LastModified.
168
+ */
169
+ export declare function annotateNoncurrentSince(entries: VersionEntry[]): VersionEntry[];
170
+ /** Folder levels split into parallel listings (`a/`, then `a/b/`). */
171
+ export declare const LIST_SPLIT_DEPTH = 2;
172
+ /** Listings in flight at once per bucket. */
173
+ export declare const LIST_CONCURRENCY = 32;
174
+ /**
175
+ * A printable-ASCII key strictly between `lo` and `hi` in S3's (UTF-8 byte)
176
+ * order, or undefined when none is found. Used to split one long listing into
177
+ * two that run at the same time.
178
+ */
179
+ export declare function keyBetween(lo: string, hi: string): string | undefined;
180
+ /** Plain message when S3 refuses a version delete for one company. */
181
+ export declare function deleteDeniedMessage(slug: string): string;
182
+ export declare function describeCloudError(err: unknown, action: "list" | "delete", slug?: string): string;
183
+ export declare function getCloudStatus(company: CompanyRef, deps: StorageDeps): Promise<CloudCompanyStatus>;
184
+ /**
185
+ * Versions eligible for prune: noncurrent content versions and noncurrent
186
+ * delete markers that stopped being current before the cutoff (see
187
+ * annotateNoncurrentSince). The current version of a key (content or delete
188
+ * marker) is never selected.
189
+ */
190
+ export declare function selectPrunable(versions: VersionEntry[], cutoff: Date): VersionEntry[];
191
+ export declare function pruneCloud(company: CompanyRef, cutoff: Date, dryRun: boolean, deps: StorageDeps): Promise<CloudPruneResult>;
192
+ export declare function runStorageStatus(opts: {
193
+ company?: string;
194
+ noCache?: boolean;
195
+ }, deps: StorageDeps): Promise<StorageStatus>;
196
+ export declare function parseCutoff(value: string, flag: string): Date;
197
+ export declare function runStoragePrune(opts: {
198
+ localBefore?: string;
199
+ cloudBefore?: string;
200
+ company?: string;
201
+ dryRun: boolean;
202
+ yes: boolean;
203
+ }, deps: StorageDeps): Promise<StoragePruneResult | undefined>;
204
+ export declare function formatBytes(bytes: number): string;
205
+ export declare function formatStorageStatus(status: StorageStatus): string;
206
+ export declare function formatOffloadStatus(o: OffloadStatus): string[];
207
+ export declare function formatOffload(r: OffloadResult): string;
208
+ /** One line on when backed-up space comes back, or nothing. */
209
+ export declare function reclaimLine(r: Record<string, unknown>): string | undefined;
210
+ /** Readable name for a ref that keeps old history: "branch wip", "tag v1". */
211
+ export declare function holderName(ref: string): string;
212
+ /** Plain refusal text when branches or tags keep the history prune would remove. */
213
+ export declare function formatHoldingRefusal(local: LocalReport): string[];
214
+ /** Exit code for a prune result: 4 for nothing to free, 5 for a precondition, else 0/1. */
215
+ export declare function pruneExitCode(result: StoragePruneResult): number;
216
+ /** Plain lines for a local prune that was not refused (dry or real). */
217
+ export declare function formatLocalPrune(local: LocalReport, dryRun: boolean): string[];
218
+ export declare function formatStoragePrune(result: StoragePruneResult): string;
219
+ export declare const defaultRunScript: ScriptRunner;
220
+ export declare const NEEDS_YES = "No terminal to confirm in. Re-run with --yes to go ahead, or --dry-run to preview.";
221
+ /**
222
+ * Ask y/N on a terminal. Without a TTY the question could never be answered,
223
+ * so it throws and the command exits non-zero instead of hanging or exiting 0.
224
+ */
225
+ export declare function confirmOnTty(message: string, input?: NodeJS.ReadableStream & {
226
+ isTTY?: boolean;
227
+ }, output?: NodeJS.WritableStream): Promise<boolean>;
228
+ /** STS credentials from one vend, with the time they stop working. */
229
+ export interface VendedCredentials {
230
+ accessKeyId: string;
231
+ secretAccessKey: string;
232
+ sessionToken: string;
233
+ expiresAt?: string;
234
+ }
235
+ /**
236
+ * AWS credential provider that vends fresh credentials on first use and again
237
+ * whenever the previous ones are within `marginMs` of expiring. Each vend calls
238
+ * `vend` anew, which signs in again (the CLI refreshes an expiring Cognito
239
+ * session there), so a cloud listing or delete that starts after a long local
240
+ * scan, or that itself outlasts one STS session, never sends an expired token.
241
+ */
242
+ export declare function refreshingVendedCredentials(vend: () => Promise<VendedCredentials>, now?: () => number, marginMs?: number): () => Promise<{
243
+ accessKeyId: string;
244
+ secretAccessKey: string;
245
+ sessionToken: string;
246
+ expiration?: Date;
247
+ }>;
248
+ export declare function registerStorageCommand(program: Command): Command;
249
+ //# sourceMappingURL=storage.d.ts.map