@hyperfixation/cli 0.1.5 → 0.1.7
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/cli.d.ts +2 -2
- package/dist/cli.js +70 -13
- package/dist/cloud-steps/coolify.d.ts +6 -3
- package/dist/cloud-steps/coolify.js +10 -3
- package/dist/cloud-steps/deploy.d.ts +38 -1
- package/dist/cloud-steps/deploy.js +95 -31
- package/dist/cloud-steps/template.d.ts +2 -7
- package/dist/cloud-steps/template.js +12 -12
- package/dist/deploy-app.d.ts +32 -0
- package/dist/deploy-app.js +81 -0
- package/dist/doctor.d.ts +22 -1
- package/dist/doctor.js +151 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/new.d.ts +24 -0
- package/dist/new.js +38 -2
- package/dist/providers/coolify.d.ts +24 -0
- package/dist/providers/coolify.js +21 -0
- package/dist/restore-check.d.ts +33 -4
- package/dist/restore-check.js +71 -16
- package/dist/roles.d.ts +1 -0
- package/dist/roles.js +1 -1
- package/dist/state.d.ts +8 -0
- package/dist/state.js +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +13 -0
- package/package.json +6 -6
package/dist/restore-check.js
CHANGED
|
@@ -10,8 +10,40 @@ import { openAppState } from "./state.js";
|
|
|
10
10
|
export const SCRATCH_SUFFIX = "_restore_check";
|
|
11
11
|
/** Postgres truncates an identifier past this, which would collide with the live database. */
|
|
12
12
|
const MAX_IDENTIFIER_BYTES = 63;
|
|
13
|
-
/** Older than this and the dump gets a
|
|
14
|
-
export const STALE_DUMP_HOURS =
|
|
13
|
+
/** Older than this and the dump gets a WARN line, which — as in `hf doctor` — exits 1. */
|
|
14
|
+
export const STALE_DUMP_HOURS = 24;
|
|
15
|
+
/**
|
|
16
|
+
* Tables a live row is only ever added to, so a live count above the dump's is the app working,
|
|
17
|
+
* not lost data.
|
|
18
|
+
*
|
|
19
|
+
* X1 found this the hard way: against a 1.3-hour-old dump of a running app, 9 of 24 tables
|
|
20
|
+
* "mismatched" purely from churn since the dump, and the same check against a fresh dump matched
|
|
21
|
+
* all 24. A table earns a place here only when no code path deletes from it and none updates it
|
|
22
|
+
* in a way that lowers its count — checked against `packages/db/src/schema` and every statement
|
|
23
|
+
* in `core`, `workflows`, `auth` and `admin`. Anything else, including a table whose rows merely
|
|
24
|
+
* look permanent, stays exact: a false `ok` here hides exactly the data loss this command exists
|
|
25
|
+
* to catch.
|
|
26
|
+
*/
|
|
27
|
+
export const APPEND_ONLY_TABLES = [
|
|
28
|
+
// Insert-only ledgers: nothing but `INSERT` touches either.
|
|
29
|
+
"hf_audit",
|
|
30
|
+
"hf_activity",
|
|
31
|
+
// Ledger rows are inserted `started` and then `UPDATE`d to a terminal status — including
|
|
32
|
+
// `reconcile()`'s sweep to `abandoned`/`uncertain`, which is still an update.
|
|
33
|
+
"hf_llm_call",
|
|
34
|
+
"hf_action_log",
|
|
35
|
+
// Inserted pending and decided by `UPDATE`; `reconcile()` step (5) expires a stale one the
|
|
36
|
+
// same way. The delete guard exists precisely so an approval outlives the record it names.
|
|
37
|
+
"hf_approval",
|
|
38
|
+
// Inserted, or upserted on `(run_id, key, spec_name)` by a replayed step; never deleted.
|
|
39
|
+
"hf_score",
|
|
40
|
+
// `INSERT` at the start of a source run, `UPDATE` at its end.
|
|
41
|
+
"hf_source_run",
|
|
42
|
+
// `INSERT` at `runs.start`; every later write is an `UPDATE` of status, attempt or the
|
|
43
|
+
// fencing token. Runs are never purged — there is no retention sweep.
|
|
44
|
+
"hf_run",
|
|
45
|
+
];
|
|
46
|
+
const APPEND_ONLY = new Set(APPEND_ONLY_TABLES);
|
|
15
47
|
export class RestoreCheckError extends Error {
|
|
16
48
|
constructor(message) {
|
|
17
49
|
super(message);
|
|
@@ -72,22 +104,27 @@ export async function restoreCheck(options) {
|
|
|
72
104
|
`TO ${quoteIdent(names.migratorRole)}`);
|
|
73
105
|
await scratch.query(`GRANT CREATE, USAGE ON SCHEMA public TO ${quoteIdent(names.migratorRole)}`);
|
|
74
106
|
await runRestore(options, scratchDatabase, names.migratorRole, dump.path);
|
|
75
|
-
rows = compare(await countTables(db, names.databaseName), await countTables(scratch));
|
|
107
|
+
rows = compare(await countTables(db, names.databaseName), await countTables(scratch), options.strict ?? false);
|
|
76
108
|
}
|
|
77
109
|
finally {
|
|
78
110
|
await scratch.close();
|
|
79
111
|
}
|
|
80
|
-
const
|
|
81
|
-
|
|
112
|
+
const dumpStale = dumpAgeHours > STALE_DUMP_HOURS;
|
|
113
|
+
const matched = rows.every((row) => row.verdict === "ok");
|
|
114
|
+
// A stale dump keeps the exit code but not the timestamp: the restore itself was proved,
|
|
115
|
+
// and it is `hf doctor` that decides how long a proof stays good.
|
|
116
|
+
if (matched)
|
|
82
117
|
await options.state.patch({ lastRestoreCheckAt: now.toISOString() });
|
|
83
118
|
return {
|
|
84
119
|
databaseName: names.databaseName,
|
|
85
120
|
scratchDatabase,
|
|
86
121
|
dump,
|
|
87
122
|
dumpAgeHours,
|
|
88
|
-
dumpStale
|
|
123
|
+
dumpStale,
|
|
124
|
+
strict: options.strict ?? false,
|
|
89
125
|
rows,
|
|
90
|
-
|
|
126
|
+
matched,
|
|
127
|
+
ok: matched && !dumpStale,
|
|
91
128
|
};
|
|
92
129
|
}
|
|
93
130
|
finally {
|
|
@@ -203,23 +240,35 @@ async function countTables(db, database) {
|
|
|
203
240
|
* A table on one side only is its own verdict rather than a crash or a zero: an app migration
|
|
204
241
|
* between the backup and the check is the ordinary reason for it, and reading it as a count of
|
|
205
242
|
* zero would make an added table look like lost data.
|
|
243
|
+
*
|
|
244
|
+
* An `APPEND_ONLY_TABLES` table the live side is *ahead* on is `ok` with a `drift`, unless
|
|
245
|
+
* `strict`. A restored count above the live one is still a `mismatch` there: the dump cannot
|
|
246
|
+
* hold rows an append-only live table has since lost unless something did lose them.
|
|
206
247
|
*/
|
|
207
|
-
function compare(live, restored) {
|
|
248
|
+
function compare(live, restored, strict) {
|
|
208
249
|
const tables = [...new Set([...live.keys(), ...restored.keys()])].sort();
|
|
209
250
|
return tables.map((table) => {
|
|
210
251
|
const liveCount = live.get(table);
|
|
211
252
|
const restoredCount = restored.get(table);
|
|
253
|
+
const drifted = !strict &&
|
|
254
|
+
APPEND_ONLY.has(table) &&
|
|
255
|
+
liveCount !== undefined &&
|
|
256
|
+
restoredCount !== undefined &&
|
|
257
|
+
restoredCount < liveCount;
|
|
212
258
|
const verdict = liveCount === undefined
|
|
213
259
|
? "restored only"
|
|
214
260
|
: restoredCount === undefined
|
|
215
261
|
? "live only"
|
|
216
|
-
: liveCount === restoredCount
|
|
262
|
+
: liveCount === restoredCount || drifted
|
|
217
263
|
? "ok"
|
|
218
264
|
: "mismatch";
|
|
219
265
|
return {
|
|
220
266
|
table,
|
|
221
267
|
...(liveCount === undefined ? {} : { live: liveCount }),
|
|
222
268
|
...(restoredCount === undefined ? {} : { restored: restoredCount }),
|
|
269
|
+
...(drifted && liveCount !== undefined && restoredCount !== undefined
|
|
270
|
+
? { drift: liveCount - restoredCount }
|
|
271
|
+
: {}),
|
|
223
272
|
verdict,
|
|
224
273
|
};
|
|
225
274
|
});
|
|
@@ -227,24 +276,29 @@ function compare(live, restored) {
|
|
|
227
276
|
/** The table `hf restore-check` prints, and the two lines around it. */
|
|
228
277
|
export function formatRestoreCheck(result) {
|
|
229
278
|
const age = `${result.dumpAgeHours.toFixed(1)} h old`;
|
|
230
|
-
const lines = [
|
|
279
|
+
const lines = [
|
|
280
|
+
`${result.databaseName}: ${result.dump.path}, ${age}${result.strict ? ", strict" : ""}`,
|
|
281
|
+
];
|
|
231
282
|
if (result.dumpStale) {
|
|
232
|
-
lines.push(`
|
|
283
|
+
lines.push(`WARN: the dump is ${age} — over ${String(STALE_DUMP_HOURS)} h; ` +
|
|
284
|
+
"this compares against stale data");
|
|
233
285
|
}
|
|
234
286
|
const header = ["table", "live", "restored", "verdict"];
|
|
235
287
|
const cells = result.rows.map((row) => [
|
|
236
288
|
row.table,
|
|
237
289
|
row.live === undefined ? "—" : String(row.live),
|
|
238
290
|
row.restored === undefined ? "—" : String(row.restored),
|
|
239
|
-
row.verdict
|
|
291
|
+
row.drift === undefined ? row.verdict : `${row.verdict} (drift +${String(row.drift)})`,
|
|
240
292
|
]);
|
|
241
293
|
const widths = header.map((name, column) => Math.max(name.length, ...cells.map((row) => row[column]?.length ?? 0)));
|
|
242
294
|
const line = (row) => row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
|
|
243
295
|
lines.push(line(header), ...cells.map(line));
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
296
|
+
const drifted = result.rows.filter((row) => row.drift !== undefined).length;
|
|
297
|
+
const failed = result.rows.filter((row) => row.verdict !== "ok").length;
|
|
298
|
+
const drift = drifted === 0 ? "" : `, ${String(drifted)} with drift since the dump`;
|
|
299
|
+
lines.push(result.matched
|
|
300
|
+
? `${String(result.rows.length)} table(s) matched${drift}`
|
|
301
|
+
: `${String(failed)} of ${String(result.rows.length)} table(s) did not match${drift}`);
|
|
248
302
|
return lines;
|
|
249
303
|
}
|
|
250
304
|
/**
|
|
@@ -273,6 +327,7 @@ export async function restoreCheckApp(options) {
|
|
|
273
327
|
database: db,
|
|
274
328
|
container: await restoreContainer(runner, db, containers),
|
|
275
329
|
adminUser: admin.user,
|
|
330
|
+
strict: options.strict,
|
|
276
331
|
});
|
|
277
332
|
}
|
|
278
333
|
finally {
|
package/dist/roles.d.ts
CHANGED
package/dist/roles.js
CHANGED
|
@@ -46,6 +46,6 @@ export function credentialsOf(connectionString) {
|
|
|
46
46
|
const url = new URL(connectionString);
|
|
47
47
|
return { user: decodeURIComponent(url.username), password: decodeURIComponent(url.password) };
|
|
48
48
|
}
|
|
49
|
-
function quoteLiteral(value) {
|
|
49
|
+
export function quoteLiteral(value) {
|
|
50
50
|
return `'${value.replace(/'/g, "''")}'`;
|
|
51
51
|
}
|
package/dist/state.d.ts
CHANGED
|
@@ -59,6 +59,14 @@ export interface AppState {
|
|
|
59
59
|
* earlier `hf new --local` looks exactly like a lost run.
|
|
60
60
|
*/
|
|
61
61
|
templateStartedAt?: string;
|
|
62
|
+
/**
|
|
63
|
+
* ISO 8601, written just before the `template` step fetches into the scratch directory beside
|
|
64
|
+
* the app's path.
|
|
65
|
+
*
|
|
66
|
+
* The same proof one step earlier: a scratch directory the state does not vouch for is refused
|
|
67
|
+
* rather than deleted, and a fetch that died half way is this run's own to clear and redo.
|
|
68
|
+
*/
|
|
69
|
+
templateFetchStartedAt?: string;
|
|
62
70
|
/** `owner/name` of the app's GitHub repository. */
|
|
63
71
|
repo?: string;
|
|
64
72
|
coolify?: CoolifyState;
|
package/dist/state.js
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This package's own version, read from its `package.json` at runtime.
|
|
3
|
+
*
|
|
4
|
+
* Not a constant the build stamps in: the nine packages are one fixed version group, and a
|
|
5
|
+
* release rewrites `package.json` alone — a baked-in string would be a second copy that is wrong
|
|
6
|
+
* from the next release onwards. `../package.json` resolves the same from `src/` and from `dist/`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function cliVersion(): Promise<string>;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
/**
|
|
4
|
+
* This package's own version, read from its `package.json` at runtime.
|
|
5
|
+
*
|
|
6
|
+
* Not a constant the build stamps in: the nine packages are one fixed version group, and a
|
|
7
|
+
* release rewrites `package.json` alone — a baked-in string would be a second copy that is wrong
|
|
8
|
+
* from the next release onwards. `../package.json` resolves the same from `src/` and from `dist/`.
|
|
9
|
+
*/
|
|
10
|
+
export async function cliVersion() {
|
|
11
|
+
const manifest = JSON.parse(await readFile(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
|
|
12
|
+
return typeof manifest.version === "string" ? manifest.version : "unknown";
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperfixation/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "The hf binary and its Turborepo generator templates",
|
|
6
6
|
"repository": {
|
|
@@ -29,15 +29,15 @@
|
|
|
29
29
|
"!dist/test-support/**"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@hyperfixation/auth": "0.1.
|
|
33
|
-
"@hyperfixation/core": "0.1.
|
|
34
|
-
"@hyperfixation/db": "0.1.
|
|
32
|
+
"@hyperfixation/auth": "0.1.7",
|
|
33
|
+
"@hyperfixation/core": "0.1.7",
|
|
34
|
+
"@hyperfixation/db": "0.1.7",
|
|
35
35
|
"giget": "3.3.1",
|
|
36
36
|
"pg": "^8.23.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@hyperfixation/eslint-config": "0.1.
|
|
40
|
-
"@hyperfixation/testing": "0.1.
|
|
39
|
+
"@hyperfixation/eslint-config": "0.1.7",
|
|
40
|
+
"@hyperfixation/testing": "0.1.7",
|
|
41
41
|
"@microsoft/api-extractor": "^7.59.1",
|
|
42
42
|
"@types/pg": "^8.23.1",
|
|
43
43
|
"eslint": "^10.10.0",
|