hazo_env 0.10.0 → 0.10.2
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/CHANGE_LOG.md +22 -0
- package/dist/cli.js +0 -0
- package/dist/lib/i18n.d.ts +2 -0
- package/dist/lib/i18n.d.ts.map +1 -0
- package/dist/lib/i18n.js +21 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -0
- package/dist/locales/en.json +1 -0
- package/package.json +17 -13
- package/dist/mask/registry.d.ts +0 -8
- package/dist/mask/registry.d.ts.map +0 -1
- package/dist/mask/registry.js +0 -29
- package/dist/mask/ruleset.d.ts +0 -15
- package/dist/mask/ruleset.d.ts.map +0 -1
- package/dist/mask/ruleset.js +0 -77
- package/dist/migrate/audit.d.ts +0 -21
- package/dist/migrate/audit.d.ts.map +0 -1
- package/dist/migrate/audit.js +0 -36
- package/dist/migrate/clear.d.ts +0 -3
- package/dist/migrate/clear.d.ts.map +0 -1
- package/dist/migrate/clear.js +0 -117
- package/dist/migrate/db-dump-restore.d.ts +0 -24
- package/dist/migrate/db-dump-restore.d.ts.map +0 -1
- package/dist/migrate/db-dump-restore.js +0 -72
- package/dist/migrate/db.d.ts +0 -17
- package/dist/migrate/db.d.ts.map +0 -1
- package/dist/migrate/db.js +0 -127
- package/dist/migrate/db.postgrest.d.ts +0 -4
- package/dist/migrate/db.postgrest.d.ts.map +0 -1
- package/dist/migrate/db.postgrest.js +0 -95
- package/dist/migrate/files.d.ts +0 -15
- package/dist/migrate/files.d.ts.map +0 -1
- package/dist/migrate/files.js +0 -131
- package/dist/migrate/progress.d.ts +0 -5
- package/dist/migrate/progress.d.ts.map +0 -1
- package/dist/migrate/progress.js +0 -24
- package/dist/migrate/run.d.ts +0 -16
- package/dist/migrate/run.d.ts.map +0 -1
- package/dist/migrate/run.js +0 -270
- package/dist/migrate/snapshot.d.ts +0 -8
- package/dist/migrate/snapshot.d.ts.map +0 -1
- package/dist/migrate/snapshot.js +0 -51
- package/dist/migrate/transport.d.ts +0 -3
- package/dist/migrate/transport.d.ts.map +0 -1
- package/dist/migrate/transport.js +0 -12
- package/dist/migrate/verify.d.ts +0 -9
- package/dist/migrate/verify.d.ts.map +0 -1
- package/dist/migrate/verify.js +0 -141
- package/dist/resolve/backup.d.ts +0 -23
- package/dist/resolve/backup.d.ts.map +0 -1
- package/dist/resolve/backup.js +0 -52
- package/dist/resolve/migrate.d.ts +0 -3
- package/dist/resolve/migrate.d.ts.map +0 -1
- package/dist/resolve/migrate.js +0 -29
package/dist/migrate/db.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
// hazo_env/src/migrate/db.ts — per-table DB copy with schema parity check
|
|
2
|
-
import { HazoError, optional_import } from 'hazo_core';
|
|
3
|
-
import { isSecretColumn } from '../lib/secret_columns.js';
|
|
4
|
-
const PAGE_SIZE = 500;
|
|
5
|
-
// The HazoConnectAdapter interface types rawQuery as (sql, RequestInit?) but the
|
|
6
|
-
// SQLite adapter extends it with { params?: unknown[] }. Cast through unknown to
|
|
7
|
-
// satisfy both the interface type and the runtime expectation.
|
|
8
|
-
function rawQuery(adapter, sql, params = []) {
|
|
9
|
-
return adapter.rawQuery(sql, { params });
|
|
10
|
-
}
|
|
11
|
-
async function getRawTables(adapter) {
|
|
12
|
-
const result = await rawQuery(adapter, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name");
|
|
13
|
-
return result.map((r) => r.name);
|
|
14
|
-
}
|
|
15
|
-
async function getMigrationNames(adapter) {
|
|
16
|
-
// Check if _migrations table exists
|
|
17
|
-
const tables = await getRawTables(adapter);
|
|
18
|
-
if (!tables.includes('_migrations'))
|
|
19
|
-
return null;
|
|
20
|
-
const rows = await rawQuery(adapter, 'SELECT name FROM _migrations ORDER BY name');
|
|
21
|
-
return rows.map((r) => r.name);
|
|
22
|
-
}
|
|
23
|
-
export async function copyDb(srcAdapter, tgtAdapter, opts = {}) {
|
|
24
|
-
// PostgREST path: dispatch to dedicated implementation
|
|
25
|
-
if (opts.type === 'postgrest') {
|
|
26
|
-
const { copyDbPostgrest } = await import('./db.postgrest.js');
|
|
27
|
-
return copyDbPostgrest(srcAdapter, tgtAdapter, opts);
|
|
28
|
-
}
|
|
29
|
-
const connect = await optional_import('hazo_connect/server');
|
|
30
|
-
if (!connect) {
|
|
31
|
-
throw new HazoError({
|
|
32
|
-
code: 'HAZO_ENV_MISSING_DEPENDENCY',
|
|
33
|
-
pkg: 'hazo_env',
|
|
34
|
-
message: 'hazo_connect is required for DB migration. Install hazo_connect to continue.',
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
// Schema parity check via _migrations (SQLite only — PostgREST apps may not use _migrations)
|
|
38
|
-
const srcMigrations = await getMigrationNames(srcAdapter);
|
|
39
|
-
const tgtMigrations = await getMigrationNames(tgtAdapter);
|
|
40
|
-
if (srcMigrations === null || tgtMigrations === null) {
|
|
41
|
-
throw new HazoError({
|
|
42
|
-
code: 'HAZO_ENV_SCHEMA_MISMATCH',
|
|
43
|
-
pkg: 'hazo_env',
|
|
44
|
-
message: `Schema parity check failed: _migrations table is missing on ${srcMigrations === null ? 'source' : 'target'}. Both databases must have matching _migrations tables.`,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
const srcSet = new Set(srcMigrations);
|
|
48
|
-
const tgtSet = new Set(tgtMigrations);
|
|
49
|
-
const missingInTgt = [...srcSet].filter((n) => !tgtSet.has(n));
|
|
50
|
-
const missingInSrc = [...tgtSet].filter((n) => !srcSet.has(n));
|
|
51
|
-
if (missingInTgt.length > 0 || missingInSrc.length > 0) {
|
|
52
|
-
throw new HazoError({
|
|
53
|
-
code: 'HAZO_ENV_SCHEMA_MISMATCH',
|
|
54
|
-
pkg: 'hazo_env',
|
|
55
|
-
message: `Schema parity check failed: migration name-sets differ.\n Missing in target: ${missingInTgt.join(', ') || 'none'}\n Missing in source: ${missingInSrc.join(', ') || 'none'}`,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
// Get tables to copy
|
|
59
|
-
const allTables = await getRawTables(srcAdapter);
|
|
60
|
-
// Exclude system/internal tables
|
|
61
|
-
const SKIP = new Set(['_migrations', 'sqlite_sequence', 'hazo_audit_events', 'hazo_audit_intent']);
|
|
62
|
-
let tablesToCopy = allTables.filter((t) => !SKIP.has(t));
|
|
63
|
-
if (opts.tables && opts.tables !== '*') {
|
|
64
|
-
tablesToCopy = tablesToCopy.filter((t) => opts.tables.includes(t));
|
|
65
|
-
}
|
|
66
|
-
let totalRows = 0;
|
|
67
|
-
let totalScrubbed = 0;
|
|
68
|
-
for (const table of tablesToCopy) {
|
|
69
|
-
opts.onProgress?.(`Copying table: ${table}`);
|
|
70
|
-
// Delete all rows in target
|
|
71
|
-
await rawQuery(tgtAdapter, `DELETE FROM "${table}"`);
|
|
72
|
-
// Get schema to find PK column and all columns
|
|
73
|
-
const schema = await rawQuery(srcAdapter, `PRAGMA table_info("${table}")`);
|
|
74
|
-
const pkCol = schema.find((c) => c.pk === 1)?.name ?? 'id';
|
|
75
|
-
const allCols = schema.map((c) => c.name);
|
|
76
|
-
// Page through source
|
|
77
|
-
const srcService = connect.createCrudService(srcAdapter, table, { autoId: false });
|
|
78
|
-
let offset = 0;
|
|
79
|
-
let pageRows = [];
|
|
80
|
-
do {
|
|
81
|
-
pageRows = await srcService.list((qb) => qb.order(pkCol, 'asc').limit(PAGE_SIZE).offset(offset));
|
|
82
|
-
if (pageRows.length === 0)
|
|
83
|
-
break;
|
|
84
|
-
for (let row of pageRows) {
|
|
85
|
-
// Drop secret columns
|
|
86
|
-
const cleaned = {};
|
|
87
|
-
let didScrub = false;
|
|
88
|
-
for (const col of allCols) {
|
|
89
|
-
if (isSecretColumn(col)) {
|
|
90
|
-
didScrub = true;
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
cleaned[col] = row[col];
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Apply scrub hook if provided (masking, Phase B)
|
|
97
|
-
if (opts.scrubHook) {
|
|
98
|
-
const hooked = opts.scrubHook(table, cleaned);
|
|
99
|
-
if (hooked !== cleaned)
|
|
100
|
-
didScrub = true;
|
|
101
|
-
row = hooked;
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
row = cleaned;
|
|
105
|
-
}
|
|
106
|
-
if (didScrub)
|
|
107
|
-
totalScrubbed++;
|
|
108
|
-
// Only insert columns that exist in schema
|
|
109
|
-
const insertRow = {};
|
|
110
|
-
for (const col of Object.keys(row)) {
|
|
111
|
-
if (allCols.includes(col))
|
|
112
|
-
insertRow[col] = row[col];
|
|
113
|
-
}
|
|
114
|
-
// Use raw INSERT to preserve PKs
|
|
115
|
-
const cols = Object.keys(insertRow);
|
|
116
|
-
if (cols.length === 0)
|
|
117
|
-
continue;
|
|
118
|
-
const placeholders = cols.map(() => '?').join(', ');
|
|
119
|
-
const values = cols.map((c) => insertRow[c]);
|
|
120
|
-
await rawQuery(tgtAdapter, `INSERT OR REPLACE INTO "${table}" (${cols.map((c) => `"${c}"`).join(', ')}) VALUES (${placeholders})`, values);
|
|
121
|
-
totalRows++;
|
|
122
|
-
}
|
|
123
|
-
offset += PAGE_SIZE;
|
|
124
|
-
} while (pageRows.length === PAGE_SIZE);
|
|
125
|
-
}
|
|
126
|
-
return { tables: tablesToCopy.length, rows: totalRows, scrubbed: totalScrubbed };
|
|
127
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { HazoConnectAdapter } from 'hazo_connect';
|
|
2
|
-
import type { DbCopyOptions, DbCopyResult } from './db.js';
|
|
3
|
-
export declare function copyDbPostgrest(srcAdapter: HazoConnectAdapter, tgtAdapter: HazoConnectAdapter, opts?: DbCopyOptions): Promise<DbCopyResult>;
|
|
4
|
-
//# sourceMappingURL=db.postgrest.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"db.postgrest.d.ts","sourceRoot":"","sources":["../../src/migrate/db.postgrest.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAM3D,wBAAsB,eAAe,CACnC,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,kBAAkB,EAC9B,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CA+GvB"}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
// hazo_env/src/migrate/db.postgrest.ts — PostgREST-based DB copy (REST-only, no raw SQL)
|
|
2
|
-
import { HazoError, optional_import } from 'hazo_core';
|
|
3
|
-
import { isSecretColumn } from '../lib/secret_columns.js';
|
|
4
|
-
const PAGE_SIZE = 500;
|
|
5
|
-
export async function copyDbPostgrest(srcAdapter, tgtAdapter, opts = {}) {
|
|
6
|
-
const connect = await optional_import('hazo_connect/server');
|
|
7
|
-
if (!connect) {
|
|
8
|
-
throw new HazoError({
|
|
9
|
-
code: 'HAZO_ENV_MISSING_DEPENDENCY',
|
|
10
|
-
pkg: 'hazo_env',
|
|
11
|
-
message: 'hazo_connect is required for DB migration. Install hazo_connect to continue.',
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
// Table list is required for PostgREST (no auto-discovery — FK ordering unsafe without explicit list)
|
|
15
|
-
if (!opts.tables || opts.tables === '*') {
|
|
16
|
-
throw new HazoError({
|
|
17
|
-
code: 'HAZO_ENV_MISSING_TABLE_LIST',
|
|
18
|
-
pkg: 'hazo_env',
|
|
19
|
-
message: 'PostgREST migration requires an explicit table list. ' +
|
|
20
|
-
'Set [migrate] tables = ... in hazo_env_config.ini or pass --tables to the CLI.',
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
const orderedTables = opts.tables;
|
|
24
|
-
const preserveSet = new Set(opts.preserve ?? []);
|
|
25
|
-
const pkOverrides = opts.pkOverrides ?? {};
|
|
26
|
-
// Tables to actually process (exclude preserve list)
|
|
27
|
-
const tablesToProcess = orderedTables.filter((t) => !preserveSet.has(t));
|
|
28
|
-
// --- Clear pass: reverse dependency order ---
|
|
29
|
-
opts.onProgress?.('Clearing target tables (reverse order for FK safety)...');
|
|
30
|
-
for (const table of [...tablesToProcess].reverse()) {
|
|
31
|
-
const pk = pkOverrides[table] ?? 'id';
|
|
32
|
-
try {
|
|
33
|
-
// PostgREST match-all DELETE: DELETE /table?pk=not.is.null
|
|
34
|
-
await tgtAdapter.rawQuery(`/${table}?${pk}=not.is.null`, { method: 'DELETE' });
|
|
35
|
-
opts.onProgress?.(`Cleared: ${table}`);
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
39
|
-
opts.onProgress?.(`Warning: could not clear ${table}: ${msg}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
let totalRows = 0;
|
|
43
|
-
let totalScrubbed = 0;
|
|
44
|
-
// --- Copy pass: forward dependency order ---
|
|
45
|
-
for (const table of tablesToProcess) {
|
|
46
|
-
const tableIdx = tablesToProcess.indexOf(table);
|
|
47
|
-
opts.onProgress?.(`Copying table ${tableIdx + 1}/${tablesToProcess.length}: ${table}`);
|
|
48
|
-
opts.onTableProgress?.(tableIdx + 1, tablesToProcess.length, table);
|
|
49
|
-
const pk = pkOverrides[table] ?? 'id';
|
|
50
|
-
const srcService = connect.createCrudService(srcAdapter, table, { autoId: false });
|
|
51
|
-
const tgtService = connect.createCrudService(tgtAdapter, table, { autoId: false });
|
|
52
|
-
let offset = 0;
|
|
53
|
-
let pageRows = [];
|
|
54
|
-
do {
|
|
55
|
-
pageRows = await srcService.list((qb) =>
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
-
qb.order(pk, 'asc').limit(PAGE_SIZE).offset(offset));
|
|
58
|
-
if (pageRows.length === 0)
|
|
59
|
-
break;
|
|
60
|
-
const batch = [];
|
|
61
|
-
for (let row of pageRows) {
|
|
62
|
-
const cleaned = {};
|
|
63
|
-
let didScrub = false;
|
|
64
|
-
for (const [col, val] of Object.entries(row)) {
|
|
65
|
-
if (isSecretColumn(col)) {
|
|
66
|
-
didScrub = true;
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
cleaned[col] = val;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
let processedRow = cleaned;
|
|
73
|
-
if (opts.scrubHook) {
|
|
74
|
-
const hooked = opts.scrubHook(table, cleaned);
|
|
75
|
-
if (hooked !== cleaned)
|
|
76
|
-
didScrub = true;
|
|
77
|
-
processedRow = hooked;
|
|
78
|
-
}
|
|
79
|
-
if (didScrub)
|
|
80
|
-
totalScrubbed++;
|
|
81
|
-
batch.push(processedRow);
|
|
82
|
-
}
|
|
83
|
-
if (batch.length > 0) {
|
|
84
|
-
await tgtService.insert(batch);
|
|
85
|
-
totalRows += batch.length;
|
|
86
|
-
opts.onProgress?.(` inserted ${batch.length} rows into ${table}`);
|
|
87
|
-
}
|
|
88
|
-
offset += PAGE_SIZE;
|
|
89
|
-
} while (pageRows.length === PAGE_SIZE);
|
|
90
|
-
}
|
|
91
|
-
// Note: PostgREST has no REST endpoint for NOTIFY. Schema reload only needed after DDL.
|
|
92
|
-
// If schema DDL changed since last reload, run: NOTIFY pgrst, 'reload schema' in Postgres.
|
|
93
|
-
opts.onProgress?.("DB copy complete. If schema DDL changed, run NOTIFY pgrst, 'reload schema' in Postgres.");
|
|
94
|
-
return { tables: tablesToProcess.length, rows: totalRows, scrubbed: totalScrubbed };
|
|
95
|
-
}
|
package/dist/migrate/files.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { HazoEnv } from '../types/index.js';
|
|
2
|
-
export interface FilesCopyOptions {
|
|
3
|
-
fromEnv: HazoEnv;
|
|
4
|
-
toEnv: HazoEnv;
|
|
5
|
-
transport?: 'local' | 'rsync';
|
|
6
|
-
fileScrubHook?: (filePath: string) => Promise<void>;
|
|
7
|
-
onProgress?: (msg: string, percent?: number) => void;
|
|
8
|
-
}
|
|
9
|
-
export interface FilesCopyResult {
|
|
10
|
-
copied: number;
|
|
11
|
-
bytes: number;
|
|
12
|
-
placeholdered: number;
|
|
13
|
-
}
|
|
14
|
-
export declare function copyFiles(opts: FilesCopyOptions): Promise<FilesCopyResult>;
|
|
15
|
-
//# sourceMappingURL=files.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/migrate/files.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAC9B,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB;AAoFD,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkDhF"}
|
package/dist/migrate/files.js
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
// hazo_env/src/migrate/files.ts — local file-tree copy with PDF scrubbing (Phase C)
|
|
2
|
-
//
|
|
3
|
-
// PDF scrubbing via hazo_pdf/server (optional peer dep). When HAZO_ENV_MASK_KEY
|
|
4
|
-
// is set and a copied file has a .pdf extension, mask_pdf is called with an
|
|
5
|
-
// empty targets list (structure-only scrub / reserved for future target lists).
|
|
6
|
-
// Failures are non-fatal warnings — the file copy still completes.
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import { spawn } from 'node:child_process';
|
|
10
|
-
import { resolveFilesRoot } from '../resolve/files.js';
|
|
11
|
-
import { resolveSshConfig } from '../resolve/ssh.js';
|
|
12
|
-
import { assertSafeSshField, setupKnownHosts } from './ssh-exec.js';
|
|
13
|
-
function resolveEnvFilesRoot(env) {
|
|
14
|
-
return resolveFilesRoot(env);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Attempt to scrub a PDF file in-place using hazo_pdf/mask_pdf.
|
|
18
|
-
* Non-fatal: any failure is silently swallowed — the copy always completes.
|
|
19
|
-
* Called only when HAZO_ENV_MASK_KEY is set.
|
|
20
|
-
*/
|
|
21
|
-
async function scrubPdfFile(filePath) {
|
|
22
|
-
try {
|
|
23
|
-
const pdfMod = await import('hazo_pdf/server').catch(() => null);
|
|
24
|
-
if (!pdfMod?.mask_pdf)
|
|
25
|
-
return; // hazo_pdf not installed or mask_pdf not exported
|
|
26
|
-
const { readFileSync, writeFileSync } = await import('node:fs');
|
|
27
|
-
const bytes = readFileSync(filePath);
|
|
28
|
-
const scrubbed = await pdfMod.mask_pdf(new Uint8Array(bytes), []);
|
|
29
|
-
writeFileSync(filePath, Buffer.from(scrubbed));
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
// Non-fatal — PDF scrubbing failure is a warning, not an abort
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
async function copyFilesRsync(opts, ssh) {
|
|
36
|
-
const tgtRoot = resolveEnvFilesRoot(opts.toEnv);
|
|
37
|
-
fs.mkdirSync(tgtRoot, { recursive: true });
|
|
38
|
-
assertSafeSshField('user', ssh.user);
|
|
39
|
-
assertSafeSshField('host', ssh.host);
|
|
40
|
-
assertSafeSshField('key', ssh.key);
|
|
41
|
-
assertSafeSshField('path', ssh.path);
|
|
42
|
-
const { knownHosts, strict } = setupKnownHosts(ssh);
|
|
43
|
-
const remote = `${ssh.user}@${ssh.host}:${ssh.path}/`;
|
|
44
|
-
const sshCmd = `ssh -i ${ssh.key} -o StrictHostKeyChecking=${strict} -o UserKnownHostsFile=${knownHosts}`;
|
|
45
|
-
const args = ['-az', '--partial', '--info=progress2', '-e', sshCmd, remote, tgtRoot + '/'];
|
|
46
|
-
return new Promise((resolve, reject) => {
|
|
47
|
-
const proc = spawn('rsync', args);
|
|
48
|
-
let totalFiles = 0;
|
|
49
|
-
let doneFiles = 0;
|
|
50
|
-
let totalBytes = 0;
|
|
51
|
-
proc.stdout.on('data', (chunk) => {
|
|
52
|
-
const text = chunk.toString();
|
|
53
|
-
// Parse --info=progress2: lines like " 117,504,614 100% 108.64MB/s 0:00:01 (xfr#123, to-chk=0/456)"
|
|
54
|
-
for (const line of text.split('\n')) {
|
|
55
|
-
// Extract to-chk=N/TOTAL
|
|
56
|
-
const chkMatch = line.match(/to-chk=(\d+)\/(\d+)/);
|
|
57
|
-
if (chkMatch) {
|
|
58
|
-
const remaining = parseInt(chkMatch[1], 10);
|
|
59
|
-
const total = parseInt(chkMatch[2], 10);
|
|
60
|
-
if (totalFiles === 0)
|
|
61
|
-
totalFiles = total;
|
|
62
|
-
doneFiles = total - remaining;
|
|
63
|
-
const pct = total > 0 ? Math.round((doneFiles / total) * 100) : 0;
|
|
64
|
-
opts.onProgress?.(`rsync: ${doneFiles}/${total} files (${pct}%)`, pct);
|
|
65
|
-
}
|
|
66
|
-
// Extract bytes: first number on lines with a %
|
|
67
|
-
const bytesMatch = line.match(/^\s+([\d,]+)\s+\d+%/);
|
|
68
|
-
if (bytesMatch) {
|
|
69
|
-
totalBytes = parseInt(bytesMatch[1].replace(/,/g, ''), 10);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
proc.stderr.on('data', (chunk) => {
|
|
74
|
-
opts.onProgress?.(`rsync: ${chunk.toString().trim()}`, undefined);
|
|
75
|
-
});
|
|
76
|
-
proc.on('error', (err) => reject(err));
|
|
77
|
-
proc.on('close', (code) => {
|
|
78
|
-
if (code !== 0) {
|
|
79
|
-
reject(new Error(`rsync exited with code ${code}`));
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
resolve({ copied: doneFiles || totalFiles, bytes: totalBytes, placeholdered: 0 });
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
export async function copyFiles(opts) {
|
|
88
|
-
const ssh = resolveSshConfig(opts.fromEnv);
|
|
89
|
-
if (opts.transport === 'rsync' || (opts.transport !== 'local' && ssh !== null)) {
|
|
90
|
-
if (!ssh)
|
|
91
|
-
throw new Error(`rsync requested but no [transport.ssh.${opts.fromEnv}] config found`);
|
|
92
|
-
return copyFilesRsync(opts, ssh);
|
|
93
|
-
}
|
|
94
|
-
const srcRoot = resolveEnvFilesRoot(opts.fromEnv);
|
|
95
|
-
const tgtRoot = resolveEnvFilesRoot(opts.toEnv);
|
|
96
|
-
if (!fs.existsSync(srcRoot)) {
|
|
97
|
-
opts.onProgress?.(`No files directory at ${srcRoot} — skipping file copy`);
|
|
98
|
-
return { copied: 0, bytes: 0, placeholdered: 0 };
|
|
99
|
-
}
|
|
100
|
-
fs.mkdirSync(tgtRoot, { recursive: true });
|
|
101
|
-
let copied = 0;
|
|
102
|
-
let bytes = 0;
|
|
103
|
-
async function copyDir(src, tgt) {
|
|
104
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
105
|
-
for (const entry of entries) {
|
|
106
|
-
const srcPath = path.join(src, entry.name);
|
|
107
|
-
const tgtPath = path.join(tgt, entry.name);
|
|
108
|
-
if (entry.isDirectory()) {
|
|
109
|
-
fs.mkdirSync(tgtPath, { recursive: true });
|
|
110
|
-
await copyDir(srcPath, tgtPath);
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
fs.copyFileSync(srcPath, tgtPath);
|
|
114
|
-
const stat = fs.statSync(srcPath);
|
|
115
|
-
bytes += stat.size;
|
|
116
|
-
copied++;
|
|
117
|
-
opts.onProgress?.(`Copied: ${entry.name}`);
|
|
118
|
-
// Phase C: PDF scrubbing when HAZO_ENV_MASK_KEY is set
|
|
119
|
-
if (entry.name.toLowerCase().endsWith('.pdf') && process.env['HAZO_ENV_MASK_KEY']) {
|
|
120
|
-
await scrubPdfFile(tgtPath);
|
|
121
|
-
}
|
|
122
|
-
// Legacy sync hook (still supported for callers that pass one)
|
|
123
|
-
if (opts.fileScrubHook) {
|
|
124
|
-
await opts.fileScrubHook(tgtPath);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
await copyDir(srcRoot, tgtRoot);
|
|
130
|
-
return { copied, bytes, placeholdered: 0 };
|
|
131
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { MigrationProgress } from '../types/index.js';
|
|
2
|
-
export declare function writeMigrationProgress(progressDir: string, jobId: string, p: MigrationProgress): void;
|
|
3
|
-
export declare function readMigrationProgress(progressDir: string, jobId: string): MigrationProgress | null;
|
|
4
|
-
export declare function clearMigrationProgress(progressDir: string, jobId: string): void;
|
|
5
|
-
//# sourceMappingURL=progress.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../../src/migrate/progress.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAGrG;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAQlG;AAED,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAG/E"}
|
package/dist/migrate/progress.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// hazo_env/src/migrate/progress.ts — Filesystem-backed migration progress store
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
export function writeMigrationProgress(progressDir, jobId, p) {
|
|
5
|
-
fs.mkdirSync(progressDir, { recursive: true });
|
|
6
|
-
fs.writeFileSync(path.join(progressDir, `${jobId}.json`), JSON.stringify(p), 'utf-8');
|
|
7
|
-
}
|
|
8
|
-
export function readMigrationProgress(progressDir, jobId) {
|
|
9
|
-
const filePath = path.join(progressDir, `${jobId}.json`);
|
|
10
|
-
try {
|
|
11
|
-
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
12
|
-
return JSON.parse(raw);
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
export function clearMigrationProgress(progressDir, jobId) {
|
|
19
|
-
const filePath = path.join(progressDir, `${jobId}.json`);
|
|
20
|
-
try {
|
|
21
|
-
fs.unlinkSync(filePath);
|
|
22
|
-
}
|
|
23
|
-
catch { /* ignore */ }
|
|
24
|
-
}
|
package/dist/migrate/run.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { MigrationRequest, MigrationResult } from '../types/index.js';
|
|
2
|
-
/**
|
|
3
|
-
* Resolve the effective table list handed to copyDb.
|
|
4
|
-
*
|
|
5
|
-
* `'*'` (and an unset request) both mean "all tables". The SQL path can
|
|
6
|
-
* auto-discover that set, but the PostgREST path cannot (no FK ordering without
|
|
7
|
-
* an explicit list) and rejects `'*'`. So when the request asks for "all", we
|
|
8
|
-
* fall back to the configured `[migrate] tables` list — the app's canonical
|
|
9
|
-
* FK-ordered definition of "all". Only when no config list exists do we pass
|
|
10
|
-
* `'*'` through (SQL auto-discovers; PostgREST then errors as designed).
|
|
11
|
-
*
|
|
12
|
-
* An explicit `string[]` from the request always wins.
|
|
13
|
-
*/
|
|
14
|
-
export declare function resolveTableList(reqTables: '*' | string[] | undefined, configTables: string[] | undefined): '*' | string[];
|
|
15
|
-
export declare function runMigration(req: MigrationRequest): Promise<MigrationResult>;
|
|
16
|
-
//# sourceMappingURL=run.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/migrate/run.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAkC,MAAM,mBAAmB,CAAC;AAU3G;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,GAAG,GAAG,MAAM,EAAE,GAAG,SAAS,EACrC,YAAY,EAAE,MAAM,EAAE,GAAG,SAAS,GACjC,GAAG,GAAG,MAAM,EAAE,CAGhB;AAkDD,wBAAsB,YAAY,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAyMlF"}
|