@devdogsuga/devtools 0.1.51-next.547 → 0.1.51-next.548
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-CJtevyVy.js → cli-DV6tvG1E.js} +147 -58
- package/dist/launch.js +1 -1
- package/package.json +3 -3
|
@@ -7236,9 +7236,18 @@ var DevDataError = class extends Error {
|
|
|
7236
7236
|
this.name = "DevDataError";
|
|
7237
7237
|
}
|
|
7238
7238
|
};
|
|
7239
|
-
/**
|
|
7240
|
-
|
|
7241
|
-
|
|
7239
|
+
/**
|
|
7240
|
+
* Whether `err` is the product's own SQL being wrong: a Postgres error whose
|
|
7241
|
+
* SQLSTATE class is a cardinality violation (21), data exception (22),
|
|
7242
|
+
* integrity constraint violation (23) or syntax error or access rule
|
|
7243
|
+
* violation (42). Everything else is not the file's fault and reports as a
|
|
7244
|
+
* normal failure: a refused or dropped connection (class 08 and
|
|
7245
|
+
* `ECONNREFUSED`), an operator shutdown (57, `admin_shutdown`), a server out
|
|
7246
|
+
* of resources (53), a transaction that was rolled back (40), and so on.
|
|
7247
|
+
*/
|
|
7248
|
+
function isSqlFileError(err) {
|
|
7249
|
+
const code = err?.code;
|
|
7250
|
+
return typeof code === "string" && /^(21|22|23|42)[0-9A-Z]{3}$/.test(code);
|
|
7242
7251
|
}
|
|
7243
7252
|
/**
|
|
7244
7253
|
* The line of `text` a Postgres error points at, from the error's 1-based
|
|
@@ -7294,6 +7303,11 @@ function postgresSession(dbUrl) {
|
|
|
7294
7303
|
const present = new Set(rows.map((r) => r.nspname));
|
|
7295
7304
|
return schemas.filter((name) => !present.has(name));
|
|
7296
7305
|
},
|
|
7306
|
+
async exec(text) {
|
|
7307
|
+
await sql.begin(async (tx) => {
|
|
7308
|
+
await tx.unsafe(text);
|
|
7309
|
+
});
|
|
7310
|
+
},
|
|
7297
7311
|
async runAs(memberId, text) {
|
|
7298
7312
|
await sql.begin(async (tx) => {
|
|
7299
7313
|
await tx`select set_config('devtools.member_id', ${memberId}, true)`;
|
|
@@ -7335,7 +7349,7 @@ async function seed(session, file, text, options, log) {
|
|
|
7335
7349
|
try {
|
|
7336
7350
|
await session.runAs(member.id, text);
|
|
7337
7351
|
} catch (err) {
|
|
7338
|
-
if (!
|
|
7352
|
+
if (!isSqlFileError(err)) throw err;
|
|
7339
7353
|
const line = errorLine(err, text);
|
|
7340
7354
|
throw new DevDataError(`${file}${line === null ? "" : `:${line}`}: ${err.message}. This is a problem in the product's dev-data SQL, not in devtools; nothing was seeded (the file runs in one transaction).`);
|
|
7341
7355
|
}
|
|
@@ -7572,6 +7586,15 @@ function typesAssetName(product, hash) {
|
|
|
7572
7586
|
* Without that, the diff would start from an empty database and write the whole
|
|
7573
7587
|
* schema as one migration, which `db push` rejects as already applied.
|
|
7574
7588
|
*
|
|
7589
|
+
* The record only says what *should* be in the database. Before the diff,
|
|
7590
|
+
* sync therefore compares the database itself with the record (`supabase db
|
|
7591
|
+
* diff --from <database> --to migrations`, restricted to the product's
|
|
7592
|
+
* schemas) and re-applies whatever is missing: a dropped table, a dropped
|
|
7593
|
+
* schema, a database reset behind the record's back. The migration ledger is
|
|
7594
|
+
* never trusted to decide anything; it is rewritten to list the record when it
|
|
7595
|
+
* is missing or short. "The database already matches" is therefore a
|
|
7596
|
+
* statement about the live database.
|
|
7597
|
+
*
|
|
7575
7598
|
* It never resets the database. A reset wipes `auth.custom_oauth_providers`,
|
|
7576
7599
|
* which is where "Sign in with DevDogs" is registered (Q83); `--rebuild` is
|
|
7577
7600
|
* the explicit escape hatch for a database that has drifted from its record,
|
|
@@ -7623,6 +7646,7 @@ async function dbSync(product, cli, target, run, options) {
|
|
|
7623
7646
|
"--db-url",
|
|
7624
7647
|
"<DB_URL>"
|
|
7625
7648
|
])} (this machine has no record of what the project has had applied)`);
|
|
7649
|
+
if (options.open) log(`Would run: ${cliLine(cli, liveDiffArgs(product, target).slice(cli.prefix.length))} (and restore anything the database lost)`);
|
|
7626
7650
|
log(`Would run: ${cliLine(cli, generateArgs.slice(cli.prefix.length))}`);
|
|
7627
7651
|
log(target.kind === "local" ? `Would run: ${cliLine(cli, [
|
|
7628
7652
|
"migration",
|
|
@@ -7646,7 +7670,7 @@ async function dbSync(product, cli, target, run, options) {
|
|
|
7646
7670
|
const restored = migrationFiles(migrationsDir).length;
|
|
7647
7671
|
if (fetched.code === 0 && restored > 0) log(`Restored ${restored} applied migration${restored === 1 ? "" : "s"} from the project's history.`);
|
|
7648
7672
|
}
|
|
7649
|
-
await
|
|
7673
|
+
const restored = await restoreDatabase(product, cli, target, run, options, log);
|
|
7650
7674
|
const before = new Set(migrationFiles(migrationsDir));
|
|
7651
7675
|
let generated = await run(cli.command, generateArgs, { cwd: product.root });
|
|
7652
7676
|
if (generated.code !== 0 && isContainerFailure(generated)) {
|
|
@@ -7656,11 +7680,11 @@ async function dbSync(product, cli, target, run, options) {
|
|
|
7656
7680
|
if (generated.code !== 0) throw new SyncError(isContainerFailure(generated) ? "Could not diff the schema: Docker is needed for the shadow database. Start Docker and try again." : "Could not diff the schema against what this database has had applied.", generated.stderr || generated.stdout);
|
|
7657
7681
|
const created = migrationFiles(migrationsDir).filter((f) => !before.has(f));
|
|
7658
7682
|
if (created.length === 0) {
|
|
7659
|
-
log("The database already matches supabase/schemas.");
|
|
7683
|
+
if (!restored.applied) log("The database already matches supabase/schemas.");
|
|
7660
7684
|
return {
|
|
7661
7685
|
migration: null,
|
|
7662
|
-
applied:
|
|
7663
|
-
destructive:
|
|
7686
|
+
applied: restored.applied,
|
|
7687
|
+
destructive: restored.destructive
|
|
7664
7688
|
};
|
|
7665
7689
|
}
|
|
7666
7690
|
const migration = created[created.length - 1];
|
|
@@ -7694,71 +7718,136 @@ async function dbSync(product, cli, target, run, options) {
|
|
|
7694
7718
|
return {
|
|
7695
7719
|
migration,
|
|
7696
7720
|
applied: true,
|
|
7697
|
-
destructive
|
|
7721
|
+
destructive: [...restored.destructive, ...destructive]
|
|
7698
7722
|
};
|
|
7699
7723
|
}
|
|
7700
7724
|
/** `20261011120000` from `20261011120000_devtools_sync.sql`. */
|
|
7701
7725
|
function versionOf(file) {
|
|
7702
7726
|
return file.split("_")[0] ?? file;
|
|
7703
7727
|
}
|
|
7728
|
+
function liveDiffArgs(product, target) {
|
|
7729
|
+
return [
|
|
7730
|
+
"db",
|
|
7731
|
+
"diff",
|
|
7732
|
+
"--from",
|
|
7733
|
+
target.kind === "local" ? "local" : target.dbUrl,
|
|
7734
|
+
"--to",
|
|
7735
|
+
"migrations",
|
|
7736
|
+
"--use-pg-delta",
|
|
7737
|
+
"--schema",
|
|
7738
|
+
product.config.supabase.schemas.join(","),
|
|
7739
|
+
"--output-format",
|
|
7740
|
+
"json"
|
|
7741
|
+
];
|
|
7742
|
+
}
|
|
7704
7743
|
/**
|
|
7705
|
-
*
|
|
7706
|
-
*
|
|
7707
|
-
*
|
|
7708
|
-
*
|
|
7709
|
-
|
|
7744
|
+
* The SQL that turns the database into what the record describes, from the
|
|
7745
|
+
* CLI's pg-delta diff (database to migrations baseline). Empty when they
|
|
7746
|
+
* agree. Only the product's own schemas are compared, so the platform's
|
|
7747
|
+
* (auth, storage, ...) never show up as drift.
|
|
7748
|
+
*/
|
|
7749
|
+
async function liveDiff(product, cli, target, run, log) {
|
|
7750
|
+
const args = [...cli.prefix, ...liveDiffArgs(product, target)];
|
|
7751
|
+
let result = await run(cli.command, args, { cwd: product.root });
|
|
7752
|
+
if (result.code !== 0 && isContainerFailure(result)) {
|
|
7753
|
+
log("The shadow database did not start; trying once more.");
|
|
7754
|
+
result = await run(cli.command, args, { cwd: product.root });
|
|
7755
|
+
}
|
|
7756
|
+
if (result.code !== 0) throw new SyncError(isContainerFailure(result) ? "Could not compare the database with its record: Docker is needed for the shadow database. Start Docker and try again." : "Could not compare the database with what devtools recorded as applied.", result.stderr || result.stdout);
|
|
7757
|
+
for (const line of result.stdout.split("\n").reverse()) {
|
|
7758
|
+
const text = line.trim();
|
|
7759
|
+
if (!text.startsWith("{")) continue;
|
|
7760
|
+
try {
|
|
7761
|
+
const parsed = JSON.parse(text);
|
|
7762
|
+
if (typeof parsed.diff === "string") return parsed.diff;
|
|
7763
|
+
} catch {}
|
|
7764
|
+
}
|
|
7765
|
+
throw new SyncError("Could not read the result of comparing the database with its record.", result.stdout || result.stderr);
|
|
7766
|
+
}
|
|
7767
|
+
/**
|
|
7768
|
+
* Makes the database agree with the local record, from the database itself.
|
|
7769
|
+
* The declarative diff only compares `supabase/schemas` with
|
|
7770
|
+
* `supabase/migrations`, so a database that lost objects behind the record's
|
|
7771
|
+
* back (a table dropped by hand, a hosted project reset from the dashboard, a
|
|
7772
|
+
* checkout switched from the local stack to a hosted project) would read as
|
|
7773
|
+
* "already matches". So, with a record to compare against:
|
|
7710
7774
|
*
|
|
7711
|
-
*
|
|
7712
|
-
*
|
|
7713
|
-
*
|
|
7714
|
-
*
|
|
7715
|
-
*
|
|
7775
|
+
* 1. the database is diffed against the record, per configured schema, and
|
|
7776
|
+
* whatever it lacks is created again (data in what remains is untouched);
|
|
7777
|
+
* statements that drop something need the same approval as any change;
|
|
7778
|
+
* 2. the migration ledger is made to list every migration in the record
|
|
7779
|
+
* (marking them applied, which they now are), so the next
|
|
7780
|
+
* `migration up` applies only what is new.
|
|
7716
7781
|
*
|
|
7717
|
-
*
|
|
7718
|
-
*
|
|
7719
|
-
*
|
|
7782
|
+
* A missing or unreadable ledger decides nothing by itself: it is rewritten,
|
|
7783
|
+
* never read as "start over". A database that cannot be reached is left to
|
|
7784
|
+
* the apply step to report.
|
|
7720
7785
|
*/
|
|
7721
|
-
async function
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7786
|
+
async function restoreDatabase(product, cli, target, run, options, log) {
|
|
7787
|
+
const nothing = {
|
|
7788
|
+
applied: false,
|
|
7789
|
+
destructive: []
|
|
7790
|
+
};
|
|
7791
|
+
const files = migrationFiles(product.paths.migrationsDir);
|
|
7792
|
+
if (!options.open || files.length === 0) return nothing;
|
|
7727
7793
|
let session;
|
|
7794
|
+
let missing;
|
|
7795
|
+
let ledger;
|
|
7728
7796
|
try {
|
|
7729
7797
|
session = options.open(target.dbUrl);
|
|
7730
|
-
ledger = await session.appliedMigrations();
|
|
7731
7798
|
missing = await session.missingSchemas(product.config.supabase.schemas);
|
|
7732
7799
|
} catch (err) {
|
|
7733
|
-
log(`Could not check what the database has
|
|
7734
|
-
return;
|
|
7735
|
-
} finally {
|
|
7800
|
+
log(`Could not check what the database has (${err.message}); trusting the local record.`);
|
|
7736
7801
|
await session?.close().catch(() => void 0);
|
|
7802
|
+
return nothing;
|
|
7737
7803
|
}
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
"migration",
|
|
7745
|
-
"repair",
|
|
7746
|
-
"--status",
|
|
7747
|
-
"reverted",
|
|
7748
|
-
"--db-url",
|
|
7749
|
-
target.dbUrl,
|
|
7750
|
-
...stale
|
|
7751
|
-
], { cwd: product.root });
|
|
7752
|
-
if (reverted.code !== 0) throw new SyncError("The database lost its schema but its migration history still lists migrations as applied, and clearing that history failed.", reverted.stderr || reverted.stdout);
|
|
7804
|
+
try {
|
|
7805
|
+
try {
|
|
7806
|
+
ledger = await session.appliedMigrations();
|
|
7807
|
+
} catch (err) {
|
|
7808
|
+
ledger = void 0;
|
|
7809
|
+
log(`Could not read the migration history (${err.message}); deciding from the schemas alone.`);
|
|
7753
7810
|
}
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7811
|
+
const sql = (await liveDiff(product, cli, target, run, log)).trim();
|
|
7812
|
+
let applied = false;
|
|
7813
|
+
let destructive = [];
|
|
7814
|
+
if (sql) {
|
|
7815
|
+
destructive = destructiveStatements(sql);
|
|
7816
|
+
if (destructive.length > 0 && !options.yes) {
|
|
7817
|
+
if (!(options.confirmDestructive ? await options.confirmDestructive(sql, destructive) : false)) throw new SyncError(`The database differs from supabase/schemas and putting it right drops data in your development database (${destructive.length} statement${destructive.length === 1 ? "" : "s"}). Re-run with --yes to allow it.`, destructive.join(";\n"));
|
|
7818
|
+
}
|
|
7819
|
+
log(missing.length > 0 ? `The database is missing ${missing.length === 1 ? "schema" : "schemas"} ${missing.join(", ")}; creating ${missing.length === 1 ? "it" : "them"} again from supabase/schemas.` : "The database has lost part of supabase/schemas (a dropped table, view or function, perhaps); restoring it. Existing data is kept.");
|
|
7820
|
+
try {
|
|
7821
|
+
await session.exec(sql);
|
|
7822
|
+
} catch (err) {
|
|
7823
|
+
throw new SyncError("Could not restore what the database lost. `devtools db sync --rebuild` starts over (it wipes local data).", err.message);
|
|
7824
|
+
}
|
|
7825
|
+
applied = true;
|
|
7826
|
+
}
|
|
7827
|
+
if (ledger !== void 0) {
|
|
7828
|
+
const known = new Set(ledger ?? []);
|
|
7829
|
+
const unlisted = files.map(versionOf).filter((version) => !known.has(version));
|
|
7830
|
+
if (unlisted.length > 0) {
|
|
7831
|
+
if (ledger === null) log("The database has no migration history table; recreating it from the local record.");
|
|
7832
|
+
const repaired = await run(cli.command, [
|
|
7833
|
+
...cli.prefix,
|
|
7834
|
+
"migration",
|
|
7835
|
+
"repair",
|
|
7836
|
+
"--status",
|
|
7837
|
+
"applied",
|
|
7838
|
+
"--db-url",
|
|
7839
|
+
target.dbUrl,
|
|
7840
|
+
...unlisted
|
|
7841
|
+
], { cwd: product.root });
|
|
7842
|
+
if (repaired.code !== 0) throw new SyncError("Could not record the applied migrations in the database's history.", repaired.stderr || repaired.stdout);
|
|
7843
|
+
}
|
|
7844
|
+
}
|
|
7845
|
+
return {
|
|
7846
|
+
applied,
|
|
7847
|
+
destructive
|
|
7848
|
+
};
|
|
7849
|
+
} finally {
|
|
7850
|
+
await session.close().catch(() => void 0);
|
|
7762
7851
|
}
|
|
7763
7852
|
}
|
|
7764
7853
|
function isContainerFailure(result) {
|
|
@@ -8049,7 +8138,7 @@ async function regenerateAfterSync(product, cli, target, result, deps) {
|
|
|
8049
8138
|
log.info(`Would regenerate ${product.config.types.out}.`);
|
|
8050
8139
|
return;
|
|
8051
8140
|
}
|
|
8052
|
-
if (!result.
|
|
8141
|
+
if (!result.applied && typesAreCurrent(product)) return;
|
|
8053
8142
|
try {
|
|
8054
8143
|
await generateTypes(product, cli, target, deps.run, (line) => log.info(line));
|
|
8055
8144
|
log.success(`Regenerated ${product.config.types.out}.`);
|
|
@@ -8095,7 +8184,7 @@ function createProductHandlers(deps = defaultDeps) {
|
|
|
8095
8184
|
switch (sub) {
|
|
8096
8185
|
case "sync": {
|
|
8097
8186
|
const { result, cli, target } = await runSync(product, deps, flags);
|
|
8098
|
-
if (result.
|
|
8187
|
+
if (result.applied) log.success("Your database now matches supabase/schemas.");
|
|
8099
8188
|
await regenerateAfterSync(product, cli, target, result, deps);
|
|
8100
8189
|
return DONE;
|
|
8101
8190
|
}
|
|
@@ -8328,7 +8417,7 @@ async function runSetup(product, deps, rest) {
|
|
|
8328
8417
|
log: (line) => log.info(line),
|
|
8329
8418
|
open: deps.openSql
|
|
8330
8419
|
});
|
|
8331
|
-
log.success(result.
|
|
8420
|
+
log.success(result.applied ? "Applied supabase/schemas to your database." : "Your database already matched supabase/schemas.");
|
|
8332
8421
|
if (target.kind === "local") await waitForRest(target, deps, product);
|
|
8333
8422
|
if (rest.includes("--download-types")) try {
|
|
8334
8423
|
await downloadTypes(product, {
|
package/dist/launch.js
CHANGED
|
@@ -183,7 +183,7 @@ async function startStack() {
|
|
|
183
183
|
* post-tier-resolution path.
|
|
184
184
|
*/
|
|
185
185
|
async function dispatch(argv) {
|
|
186
|
-
const { main } = await import("./cli-
|
|
186
|
+
const { main } = await import("./cli-DV6tvG1E.js");
|
|
187
187
|
try {
|
|
188
188
|
await main(argv);
|
|
189
189
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devdogsuga/devtools",
|
|
3
|
-
"version": "0.1.51-next.
|
|
3
|
+
"version": "0.1.51-next.548",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Contributor CLI for DevDogs repositories (the platform monorepo, DogDays, DogPack): the real tools with the session's tier filled in, `setup`, `db sync`, `db types`, `dev-data`, a menu over them, the checks CI runs, and the `with-env` launcher. Ships built JS; run via `pnpm devtools` in a product repo, `pnpm dlx @devdogsuga/devtools` (or the `devtools` bin once installed) from inside a clone.",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"tsdown": "^0.23.0",
|
|
48
48
|
"vitest": "^4.1.11",
|
|
49
49
|
"@devdogsuga/cli-core": "0.0.0",
|
|
50
|
-
"@devdogsuga/
|
|
51
|
-
"@devdogsuga/
|
|
50
|
+
"@devdogsuga/config": "0.1.4-next.526",
|
|
51
|
+
"@devdogsuga/sdk": "0.1.0"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "tsdown && node scripts/write-build-info.mjs && node ../../scripts/check-bundle-imports.mjs",
|