@moku-labs/worker 0.9.2 → 0.10.0
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/LICENSE +21 -0
- package/README.md +142 -279
- package/dist/{cli--EPl98vG.mjs → cli-D6i-Kugx.mjs} +427 -256
- package/dist/{cli-imQGo0tc.cjs → cli-Dnb-P_pp.cjs} +427 -256
- package/dist/cli.cjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +2 -1
|
@@ -1512,6 +1512,287 @@ const verifyAuth = async (ctx) => {
|
|
|
1512
1512
|
};
|
|
1513
1513
|
};
|
|
1514
1514
|
//#endregion
|
|
1515
|
+
//#region src/plugins/deploy/infra/render.ts
|
|
1516
|
+
/**
|
|
1517
|
+
* Derive a human-readable name from a resource descriptor: the Cloudflare resource `name` for the
|
|
1518
|
+
* provisioned kinds (kv/r2/d1/queue), or the exported `className` for a Durable Object (which has no
|
|
1519
|
+
* provisioned name). Used in both the provision events and the branded panels so the two agree.
|
|
1520
|
+
*
|
|
1521
|
+
* @param resource - The resource descriptor.
|
|
1522
|
+
* @returns A short name identifying the resource.
|
|
1523
|
+
* @example
|
|
1524
|
+
* ```ts
|
|
1525
|
+
* resourceName({ kind: "kv", name: "tracker-cache", binding: "CACHE" }); // "tracker-cache"
|
|
1526
|
+
* ```
|
|
1527
|
+
*/
|
|
1528
|
+
const resourceName = (resource) => resource.kind === "do" ? resource.className : resource.name;
|
|
1529
|
+
/**
|
|
1530
|
+
* Format a `kind name` cell, padding the kind so the names line up in a column.
|
|
1531
|
+
*
|
|
1532
|
+
* @param kind - The resource kind (kv / r2 / d1 / queue / do).
|
|
1533
|
+
* @param name - The resource name.
|
|
1534
|
+
* @returns The aligned `kind name` cell.
|
|
1535
|
+
* @example
|
|
1536
|
+
* ```ts
|
|
1537
|
+
* cell("kv", "CACHE"); // "kv CACHE"
|
|
1538
|
+
* ```
|
|
1539
|
+
*/
|
|
1540
|
+
const cell = (kind, name) => `${kind.padEnd(6)}${name}`;
|
|
1541
|
+
/**
|
|
1542
|
+
* Row tag for a Durable Object — it ships with the Worker (`wrangler deploy` creates the namespace),
|
|
1543
|
+
* so it is NEVER labelled `(exists)` (the planner never queried the account for it). Shared by the
|
|
1544
|
+
* plan and provision-result panels so the two always read the same.
|
|
1545
|
+
*/
|
|
1546
|
+
const SHIPS_WITH_WORKER = "(ships with worker)";
|
|
1547
|
+
/**
|
|
1548
|
+
* ANSI SGR matcher — built from `String.fromCharCode(27)` (the ESC byte) so no control character
|
|
1549
|
+
* appears in a regex literal (which both linters reject).
|
|
1550
|
+
*/
|
|
1551
|
+
const ANSI_SGR = new RegExp(String.raw`${String.fromCodePoint(27)}\[[0-9;]*m`, "gu");
|
|
1552
|
+
/**
|
|
1553
|
+
* Strip ANSI SGR escape sequences so a captured (colorized) error renders as plain, readable text.
|
|
1554
|
+
*
|
|
1555
|
+
* @param text - The (possibly colorized) text.
|
|
1556
|
+
* @returns The text with ANSI color codes removed.
|
|
1557
|
+
* @example
|
|
1558
|
+
* ```ts
|
|
1559
|
+
* stripAnsi(`${String.fromCharCode(27)}[31mX${String.fromCharCode(27)}[0m`); // "X"
|
|
1560
|
+
* ```
|
|
1561
|
+
*/
|
|
1562
|
+
const stripAnsi = (text) => text.replaceAll(ANSI_SGR, "");
|
|
1563
|
+
/**
|
|
1564
|
+
* Clean a captured (colorized, multi-line, wrapper-wrapped) provision error down to its meaningful
|
|
1565
|
+
* text: strip ANSI, drop the wrapper lines (the branded prefix, wrangler's log-file pointer), strip
|
|
1566
|
+
* each `✘ [ERROR]` marker, and join what's left. Returns the FULL message (the caller word-wraps it)
|
|
1567
|
+
* so the user reads the actual reason — never a truncated `…`.
|
|
1568
|
+
*
|
|
1569
|
+
* @param message - The captured error message.
|
|
1570
|
+
* @returns The full, plain failure reason.
|
|
1571
|
+
* @example
|
|
1572
|
+
* ```ts
|
|
1573
|
+
* cleanError("[moku-worker] wrangler exited…\n ✘ [ERROR] The bucket name is invalid.");
|
|
1574
|
+
* // "The bucket name is invalid."
|
|
1575
|
+
* ```
|
|
1576
|
+
*/
|
|
1577
|
+
const cleanError = (message) => {
|
|
1578
|
+
const cleaned = stripAnsi(message).split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => !/^\[moku-worker\]/u.test(line)).filter((line) => !/logs were written to/iu.test(line)).map((line) => line.replace(/^✘\s*/u, "").replace(/^\[error\]\s*/iu, "")).join(" ");
|
|
1579
|
+
return cleaned.length > 0 ? cleaned : stripAnsi(message).trim();
|
|
1580
|
+
};
|
|
1581
|
+
/**
|
|
1582
|
+
* Word-wrap text to `width` columns (never splitting inside a word), so a long failure reason reads
|
|
1583
|
+
* as a tidy indented block instead of forcing the box wide or scrolling off the edge.
|
|
1584
|
+
*
|
|
1585
|
+
* @param text - The text to wrap.
|
|
1586
|
+
* @param width - The maximum column width per line.
|
|
1587
|
+
* @returns The wrapped lines.
|
|
1588
|
+
* @example
|
|
1589
|
+
* ```ts
|
|
1590
|
+
* wrapText("a long sentence to wrap", 10); // ["a long", "sentence", "to wrap"]
|
|
1591
|
+
* ```
|
|
1592
|
+
*/
|
|
1593
|
+
const wrapText = (text, width) => {
|
|
1594
|
+
const lines = [];
|
|
1595
|
+
let line = "";
|
|
1596
|
+
for (const word of text.split(/\s+/u).filter(Boolean)) if (line.length === 0) line = word;
|
|
1597
|
+
else if (line.length + 1 + word.length <= width) line += ` ${word}`;
|
|
1598
|
+
else {
|
|
1599
|
+
lines.push(line);
|
|
1600
|
+
line = word;
|
|
1601
|
+
}
|
|
1602
|
+
if (line.length > 0) lines.push(line);
|
|
1603
|
+
return lines;
|
|
1604
|
+
};
|
|
1605
|
+
/**
|
|
1606
|
+
* Render the infra preflight plan as a branded panel: a dim summary line (counts + account) then one
|
|
1607
|
+
* row per declared resource — a pink `+` for those to create, a dim `~ (exists)` for those already
|
|
1608
|
+
* present, and a dim `~ (ships with worker)` for Durable Objects (created by `wrangler deploy`, never
|
|
1609
|
+
* pre-provisioned). When nothing needs creating it still renders, so the user sees the full picture.
|
|
1610
|
+
*
|
|
1611
|
+
* @param ui - The branded console to render through.
|
|
1612
|
+
* @param plan - The infra plan (existing vs missing vs ships-with-Worker) from checkInfra()/planInfra().
|
|
1613
|
+
* @example
|
|
1614
|
+
* ```ts
|
|
1615
|
+
* renderPlan(ui, await planInfra(ctx, manifest));
|
|
1616
|
+
* ```
|
|
1617
|
+
*/
|
|
1618
|
+
const renderPlan = (ui, plan) => {
|
|
1619
|
+
const { palette } = ui;
|
|
1620
|
+
const counts = [`${String(plan.missing.length)} to create`, `${String(plan.exists.length)} exist`];
|
|
1621
|
+
if (plan.ships.length > 0) counts.push(`${String(plan.ships.length)} with worker`);
|
|
1622
|
+
const summary = palette.dim(`${counts.join(" · ")} · ${plan.account}`);
|
|
1623
|
+
const createRows = plan.missing.map((resource) => `${palette.pink("+")} ${cell(resource.kind, resourceName(resource))}`);
|
|
1624
|
+
const existsRows = plan.exists.map((ref) => `${palette.dim("~")} ${cell(ref.resource.kind, resourceName(ref.resource))} ${palette.dim("(exists)")}`);
|
|
1625
|
+
const shipsRows = plan.ships.map((resource) => `${palette.dim("~")} ${cell(resource.kind, resourceName(resource))} ${palette.dim(SHIPS_WITH_WORKER)}`);
|
|
1626
|
+
ui.heading("Infra plan");
|
|
1627
|
+
ui.box([
|
|
1628
|
+
summary,
|
|
1629
|
+
"",
|
|
1630
|
+
...createRows,
|
|
1631
|
+
...existsRows,
|
|
1632
|
+
...shipsRows
|
|
1633
|
+
]);
|
|
1634
|
+
};
|
|
1635
|
+
/**
|
|
1636
|
+
* Render the provision result as a branded panel — a green `✓` per created resource, a dim `~` per
|
|
1637
|
+
* skipped, a dim `~ (ships with worker)` per Durable Object, a red `✗` per failure, then a summary
|
|
1638
|
+
* line (failed count red when non-zero) — followed, when anything failed, by a detail block printing
|
|
1639
|
+
* each failure's FULL reason (ANSI-stripped and word-wrapped) so it is actually readable instead of
|
|
1640
|
+
* truncated inside the box.
|
|
1641
|
+
*
|
|
1642
|
+
* @param ui - The branded console to render through.
|
|
1643
|
+
* @param result - The provision result from provisionInfra()/the deploy pipeline.
|
|
1644
|
+
* @example
|
|
1645
|
+
* ```ts
|
|
1646
|
+
* renderProvisionResult(ui, await provisionInfra(plan));
|
|
1647
|
+
* ```
|
|
1648
|
+
*/
|
|
1649
|
+
const renderProvisionResult = (ui, result) => {
|
|
1650
|
+
const { palette } = ui;
|
|
1651
|
+
const createdRows = result.created.map((ref) => `${palette.green("✓")} ${cell(ref.resource.kind, resourceName(ref.resource))}`);
|
|
1652
|
+
const skippedRows = result.skipped.map((ref) => `${palette.dim("~")} ${cell(ref.resource.kind, resourceName(ref.resource))} ${palette.dim("(exists)")}`);
|
|
1653
|
+
const bundledRows = result.bundled.map((resource) => `${palette.dim("~")} ${cell(resource.kind, resourceName(resource))} ${palette.dim(SHIPS_WITH_WORKER)}`);
|
|
1654
|
+
const failedRows = result.failed.map((failure) => `${palette.red("✗")} ${cell(failure.resource.kind, resourceName(failure.resource))}`);
|
|
1655
|
+
const failedCount = result.failed.length > 0 ? palette.red(`${String(result.failed.length)} failed`) : "0 failed";
|
|
1656
|
+
const counts = [`${String(result.created.length)} created`, `${String(result.skipped.length)} exist`];
|
|
1657
|
+
if (result.bundled.length > 0) counts.push(`${String(result.bundled.length)} with worker`);
|
|
1658
|
+
const summary = `${counts.join(" · ")} · ${failedCount}`;
|
|
1659
|
+
ui.heading("Provisioned");
|
|
1660
|
+
ui.box([
|
|
1661
|
+
...createdRows,
|
|
1662
|
+
...skippedRows,
|
|
1663
|
+
...bundledRows,
|
|
1664
|
+
...failedRows,
|
|
1665
|
+
"",
|
|
1666
|
+
summary
|
|
1667
|
+
]);
|
|
1668
|
+
if (result.failed.length > 0) {
|
|
1669
|
+
ui.line();
|
|
1670
|
+
for (const failure of result.failed) {
|
|
1671
|
+
ui.line(` ${palette.red("✗")} ${cell(failure.resource.kind, resourceName(failure.resource))}`);
|
|
1672
|
+
for (const wrapped of wrapText(cleanError(failure.error), ui.width - 4)) ui.line(palette.dim(` ${wrapped}`));
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
/**
|
|
1677
|
+
* Format an elapsed duration compactly: sub-second as `820ms`, otherwise one-decimal seconds (`4.2s`),
|
|
1678
|
+
* and minutes once it crosses 60s (`1m04s`) so a long deploy stays readable.
|
|
1679
|
+
*
|
|
1680
|
+
* @param ms - The elapsed milliseconds.
|
|
1681
|
+
* @returns The compact duration string.
|
|
1682
|
+
* @example
|
|
1683
|
+
* ```ts
|
|
1684
|
+
* formatDuration(4234); // "4.2s"
|
|
1685
|
+
* ```
|
|
1686
|
+
*/
|
|
1687
|
+
const formatDuration = (ms) => {
|
|
1688
|
+
if (ms < 1e3) return `${String(ms)}ms`;
|
|
1689
|
+
const seconds = ms / 1e3;
|
|
1690
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
1691
|
+
const whole = Math.floor(seconds);
|
|
1692
|
+
return `${String(Math.floor(whole / 60))}m${String(whole % 60).padStart(2, "0")}s`;
|
|
1693
|
+
};
|
|
1694
|
+
/**
|
|
1695
|
+
* Render the terminal deploy summary as a branded panel — the headline the user actually wants. The
|
|
1696
|
+
* live URL leads on its own line (pink, so it is the first thing the eye lands on), then a dim
|
|
1697
|
+
* key/value block: the target stage, the resource tally (with a red `failed` count when non-zero),
|
|
1698
|
+
* and the wall-clock time the whole deploy took. Replaces the prior single `deployed → url` line.
|
|
1699
|
+
*
|
|
1700
|
+
* @param ui - The branded console to render through.
|
|
1701
|
+
* @param summary - The deploy summary fields.
|
|
1702
|
+
* @param summary.url - The live deployed URL (the panel headline).
|
|
1703
|
+
* @param summary.stage - The target stage the worker deployed to.
|
|
1704
|
+
* @param summary.created - How many resources were created this run.
|
|
1705
|
+
* @param summary.exists - How many resources already existed (skipped).
|
|
1706
|
+
* @param summary.bundled - How many Durable Objects shipped with the Worker.
|
|
1707
|
+
* @param summary.failed - How many resources failed to provision.
|
|
1708
|
+
* @param summary.elapsedMs - The wall-clock deploy duration in milliseconds.
|
|
1709
|
+
* @example
|
|
1710
|
+
* ```ts
|
|
1711
|
+
* renderDeploySummary(ui, { url, stage: "production", created: 0, exists: 5, bundled: 1, failed: 0, elapsedMs: 4234 });
|
|
1712
|
+
* ```
|
|
1713
|
+
*/
|
|
1714
|
+
const renderDeploySummary = (ui, summary) => {
|
|
1715
|
+
const { palette } = ui;
|
|
1716
|
+
const parts = [`${String(summary.exists)} exist`, `${String(summary.created)} created`];
|
|
1717
|
+
if (summary.bundled > 0) parts.push(`${String(summary.bundled)} with worker`);
|
|
1718
|
+
const tally = parts.join(" · ");
|
|
1719
|
+
const failedLabel = palette.red(`${String(summary.failed)} failed`);
|
|
1720
|
+
const resources = summary.failed > 0 ? `${tally} · ${failedLabel}` : tally;
|
|
1721
|
+
ui.heading("Deployed");
|
|
1722
|
+
ui.box([
|
|
1723
|
+
palette.pink(summary.url),
|
|
1724
|
+
"",
|
|
1725
|
+
`${palette.dim("stage".padEnd(10))}${summary.stage}`,
|
|
1726
|
+
`${palette.dim("resources".padEnd(10))}${resources}`,
|
|
1727
|
+
`${palette.dim("took".padEnd(10))}${formatDuration(summary.elapsedMs)}`
|
|
1728
|
+
]);
|
|
1729
|
+
};
|
|
1730
|
+
/**
|
|
1731
|
+
* Render the D1 migration outcome as a branded panel — the readable replacement for wrangler's raw
|
|
1732
|
+
* `d1 migrations apply` TUI. One row per database: the `d1 <binding>` cell plus a pink `N applied`
|
|
1733
|
+
* count (or a dim `up to date` when nothing was pending), with each applied migration filename listed
|
|
1734
|
+
* beneath as a green `✓`. A dim scope footer (`remote` / `local`) names which database was touched.
|
|
1735
|
+
* The caller renders this only when at least one database actually ran migrations.
|
|
1736
|
+
*
|
|
1737
|
+
* @param ui - The branded console to render through.
|
|
1738
|
+
* @param outcomes - The per-database migration outcomes (one per d1 instance that declares migrations).
|
|
1739
|
+
* @param scope - Which database the migrations ran against: `remote` (Cloudflare) or `local` (dev).
|
|
1740
|
+
* @example
|
|
1741
|
+
* ```ts
|
|
1742
|
+
* renderMigrateSummary(ui, [{ binding: "DB", applied: ["0003_x.sql"], upToDate: false }], "remote");
|
|
1743
|
+
* ```
|
|
1744
|
+
*/
|
|
1745
|
+
const renderMigrateSummary = (ui, outcomes, scope) => {
|
|
1746
|
+
const { palette } = ui;
|
|
1747
|
+
const rows = [];
|
|
1748
|
+
for (const outcome of outcomes) {
|
|
1749
|
+
const count = outcome.applied.length;
|
|
1750
|
+
const appliedLabel = palette.pink(count > 0 ? `${String(count)} applied` : "applied");
|
|
1751
|
+
const status = outcome.upToDate ? palette.dim("up to date") : appliedLabel;
|
|
1752
|
+
rows.push(`${cell("d1", outcome.binding)} ${status}`);
|
|
1753
|
+
for (const name of outcome.applied) rows.push(` ${palette.green("✓")} ${palette.dim(name)}`);
|
|
1754
|
+
}
|
|
1755
|
+
ui.heading("Migrated");
|
|
1756
|
+
ui.box([
|
|
1757
|
+
...rows,
|
|
1758
|
+
"",
|
|
1759
|
+
palette.dim(scope)
|
|
1760
|
+
]);
|
|
1761
|
+
};
|
|
1762
|
+
/**
|
|
1763
|
+
* Render the seed outcome as a branded panel — the readable replacement for wrangler's raw
|
|
1764
|
+
* `d1 execute` / `kv key delete` TUI. Leads with the loaded `file → binding` (pink file), an optional
|
|
1765
|
+
* dim stats line (rows written / statements, only the parts wrangler reported), then — when the seed
|
|
1766
|
+
* cleared cached KV keys — a `KV reset` block listing each `~ binding key` so the user sees exactly
|
|
1767
|
+
* what was dropped. A dim scope footer (`remote` / `local`) names which database was seeded.
|
|
1768
|
+
*
|
|
1769
|
+
* @param ui - The branded console to render through.
|
|
1770
|
+
* @param outcome - The seed outcome (file, target binding, best-effort counts, the KV keys reset).
|
|
1771
|
+
* @param scope - Which database the seed ran against: `remote` (Cloudflare) or `local` (dev).
|
|
1772
|
+
* @example
|
|
1773
|
+
* ```ts
|
|
1774
|
+
* renderSeedSummary(ui, { file: "db/seed.sql", binding: "DB", rowsWritten: 18, resetKv: [] }, "remote");
|
|
1775
|
+
* ```
|
|
1776
|
+
*/
|
|
1777
|
+
const renderSeedSummary = (ui, outcome, scope) => {
|
|
1778
|
+
const { palette } = ui;
|
|
1779
|
+
const lines = [`${palette.pink(outcome.file)} ${palette.dim("→")} ${outcome.binding}`];
|
|
1780
|
+
const stats = [];
|
|
1781
|
+
if (outcome.rowsWritten !== void 0) stats.push(`${String(outcome.rowsWritten)} rows written`);
|
|
1782
|
+
if (outcome.statements !== void 0) stats.push(`${String(outcome.statements)} statements`);
|
|
1783
|
+
if (stats.length > 0) lines.push(palette.dim(stats.join(" · ")));
|
|
1784
|
+
if (outcome.resetKv.length > 0) {
|
|
1785
|
+
lines.push("", palette.dim("KV reset"));
|
|
1786
|
+
for (const entry of outcome.resetKv) lines.push(`${palette.dim("~")} ${entry.binding} ${palette.dim(entry.key)}`);
|
|
1787
|
+
}
|
|
1788
|
+
ui.heading("Seeded");
|
|
1789
|
+
ui.box([
|
|
1790
|
+
...lines,
|
|
1791
|
+
"",
|
|
1792
|
+
palette.dim(scope)
|
|
1793
|
+
]);
|
|
1794
|
+
};
|
|
1795
|
+
//#endregion
|
|
1515
1796
|
//#region src/plugins/deploy/runner.ts
|
|
1516
1797
|
/**
|
|
1517
1798
|
* @file deploy plugin — wrangler subprocess wrapper (node:child_process).
|
|
@@ -1619,6 +1900,61 @@ const runWranglerInherit = (args) => {
|
|
|
1619
1900
|
* Node-only; never imported by the runtime Worker bundle.
|
|
1620
1901
|
*/
|
|
1621
1902
|
/**
|
|
1903
|
+
* Parse the best-effort row/statement counts from wrangler's `d1 execute` output so the branded seed
|
|
1904
|
+
* panel can report them — degrading gracefully (each field simply omitted) when wrangler's format
|
|
1905
|
+
* differs or the runner streamed instead of captured. Wrangler prints lines like "🚣 18 commands
|
|
1906
|
+
* executed" and a rows-written total; both are matched loosely (case-insensitive).
|
|
1907
|
+
*
|
|
1908
|
+
* @param output - The captured stdout from `wrangler d1 execute` (empty when the runner streamed).
|
|
1909
|
+
* @returns The parsed counts — each field present only when found.
|
|
1910
|
+
* @example
|
|
1911
|
+
* ```ts
|
|
1912
|
+
* parseSeedStats("🚣 18 commands executed (30 rows written)"); // { statements: 18, rowsWritten: 30 }
|
|
1913
|
+
* ```
|
|
1914
|
+
*/
|
|
1915
|
+
const parseSeedStats = (output) => {
|
|
1916
|
+
const rows = /(\d{1,12}) rows? written/iu.exec(output);
|
|
1917
|
+
const commands = /(\d{1,12}) commands? executed/iu.exec(output) ?? /executed (\d{1,12}) commands?/iu.exec(output);
|
|
1918
|
+
const result = {};
|
|
1919
|
+
if (commands?.[1] !== void 0) result.statements = Number(commands[1]);
|
|
1920
|
+
if (rows?.[1] !== void 0) result.rowsWritten = Number(rows[1]);
|
|
1921
|
+
return result;
|
|
1922
|
+
};
|
|
1923
|
+
/**
|
|
1924
|
+
* Parse which migrations wrangler applied from its captured `d1 migrations apply` output, so the
|
|
1925
|
+
* branded migrate panel can name them instead of dumping wrangler's raw migration TUI. `upToDate` is
|
|
1926
|
+
* true when wrangler reported nothing pending ("No migrations to apply"); otherwise every
|
|
1927
|
+
* `NNNN_name.sql` filename token in the output is collected in order (de-duplicated). Degrades
|
|
1928
|
+
* safely — an unrecognized format yields no names, and the panel falls back to a generic "applied".
|
|
1929
|
+
* Lives here (not in api.ts) so both the deploy path and the dev path parse it without a cycle.
|
|
1930
|
+
*
|
|
1931
|
+
* @param output - The captured stdout from `wrangler d1 migrations apply`.
|
|
1932
|
+
* @returns The applied migration filenames and whether the database was already up to date.
|
|
1933
|
+
* @example
|
|
1934
|
+
* ```ts
|
|
1935
|
+
* parseMigrationsApplied("Applied 0003_x.sql\n0004_y.sql"); // { applied: ["0003_x.sql", "0004_y.sql"], upToDate: false }
|
|
1936
|
+
* ```
|
|
1937
|
+
*/
|
|
1938
|
+
const parseMigrationsApplied = (output) => {
|
|
1939
|
+
if (/no migrations to apply/iu.test(output)) return {
|
|
1940
|
+
applied: [],
|
|
1941
|
+
upToDate: true
|
|
1942
|
+
};
|
|
1943
|
+
const applied = [];
|
|
1944
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1945
|
+
for (const match of output.matchAll(/\b\d{3,}_[A-Za-z0-9_-]+\.sql\b/gu)) {
|
|
1946
|
+
const name = match[0];
|
|
1947
|
+
if (!seen.has(name)) {
|
|
1948
|
+
seen.add(name);
|
|
1949
|
+
applied.push(name);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
return {
|
|
1953
|
+
applied,
|
|
1954
|
+
upToDate: false
|
|
1955
|
+
};
|
|
1956
|
+
};
|
|
1957
|
+
/**
|
|
1622
1958
|
* Resolve the single configured d1 database (or the one bound to `binding` when several exist) from
|
|
1623
1959
|
* the d1 plugin's manifest. The shared resolver behind `seed()`, the post-deploy seed, and the dev
|
|
1624
1960
|
* seed; throws a branded error when the choice is ambiguous (none/several, no binding) or unknown.
|
|
@@ -1647,27 +1983,30 @@ const resolveD1 = (ctx, binding) => {
|
|
|
1647
1983
|
* injectable dev path.
|
|
1648
1984
|
*
|
|
1649
1985
|
* @param ctx - The deploy plugin context.
|
|
1650
|
-
* @param run - The wrangler runner to execute each command through
|
|
1986
|
+
* @param run - The wrangler runner to execute each command through (a CAPTURING runner lets the
|
|
1987
|
+
* returned outcome report row/statement counts; a streaming one still works, just without them).
|
|
1651
1988
|
* @param seed - The resolved seed config (SQL file, optional binding, KV keys to reset).
|
|
1652
1989
|
* @param scope - The wrangler scope: `--remote` (deploy) or `--local` (dev).
|
|
1653
|
-
* @returns
|
|
1990
|
+
* @returns The seed outcome (file, target binding, best-effort counts, and the KV keys that were reset).
|
|
1654
1991
|
* @throws {Error} When no d1 database is configured, or the seed's binding cannot be resolved.
|
|
1655
1992
|
* @example
|
|
1656
1993
|
* ```ts
|
|
1657
|
-
* await runConfiguredSeed(ctx,
|
|
1994
|
+
* const outcome = await runConfiguredSeed(ctx, runWrangler, ctx.config.seed, "--remote");
|
|
1658
1995
|
* ```
|
|
1659
1996
|
*/
|
|
1660
1997
|
const runConfiguredSeed = async (ctx, run, seed, scope) => {
|
|
1661
1998
|
if (!ctx.has("d1")) throw new Error("[moku-worker] seed: no d1 database is configured.");
|
|
1662
|
-
|
|
1999
|
+
const database = resolveD1(ctx, seed.binding);
|
|
2000
|
+
const executed = await run([
|
|
1663
2001
|
"d1",
|
|
1664
2002
|
"execute",
|
|
1665
|
-
|
|
2003
|
+
database.binding,
|
|
1666
2004
|
scope,
|
|
1667
2005
|
"--file",
|
|
1668
2006
|
seed.file
|
|
1669
2007
|
]);
|
|
1670
|
-
|
|
2008
|
+
const resetKv = seed.resetKv ?? [];
|
|
2009
|
+
for (const entry of resetKv) await run([
|
|
1671
2010
|
"kv",
|
|
1672
2011
|
"key",
|
|
1673
2012
|
"delete",
|
|
@@ -1676,6 +2015,12 @@ const runConfiguredSeed = async (ctx, run, seed, scope) => {
|
|
|
1676
2015
|
entry.binding,
|
|
1677
2016
|
scope
|
|
1678
2017
|
]);
|
|
2018
|
+
return {
|
|
2019
|
+
file: seed.file,
|
|
2020
|
+
binding: database.binding,
|
|
2021
|
+
resetKv,
|
|
2022
|
+
...parseSeedStats(typeof executed === "string" ? executed : "")
|
|
2023
|
+
};
|
|
1679
2024
|
};
|
|
1680
2025
|
//#endregion
|
|
1681
2026
|
//#region src/plugins/deploy/dev/build.ts
|
|
@@ -2021,7 +2366,8 @@ const seedLocal = async (ctx, deps) => {
|
|
|
2021
2366
|
phase: "seed",
|
|
2022
2367
|
detail: config.file
|
|
2023
2368
|
});
|
|
2024
|
-
await runConfiguredSeed(ctx, deps.runWrangler, config, "--local");
|
|
2369
|
+
const outcome = await runConfiguredSeed(ctx, deps.runWrangler, config, "--local");
|
|
2370
|
+
renderSeedSummary(createBrandConsole(), outcome, "local");
|
|
2025
2371
|
};
|
|
2026
2372
|
/**
|
|
2027
2373
|
* Run a long-lived dev session: cold build → (local d1 migrate) → (local seed) → spawn `wrangler
|
|
@@ -2057,13 +2403,21 @@ const runDev = async (ctx, opts, deps) => {
|
|
|
2057
2403
|
phase: "migrate",
|
|
2058
2404
|
detail: "d1 (local)"
|
|
2059
2405
|
});
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2406
|
+
const outcomes = [];
|
|
2407
|
+
for (const binding of migrationBindings) {
|
|
2408
|
+
const output = await deps.runWrangler([
|
|
2409
|
+
"d1",
|
|
2410
|
+
"migrations",
|
|
2411
|
+
"apply",
|
|
2412
|
+
binding,
|
|
2413
|
+
"--local"
|
|
2414
|
+
]);
|
|
2415
|
+
outcomes.push({
|
|
2416
|
+
binding,
|
|
2417
|
+
...parseMigrationsApplied(output)
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
renderMigrateSummary(createBrandConsole(), outcomes, "local");
|
|
2067
2421
|
}
|
|
2068
2422
|
if (seed) await seedLocal(ctx, deps);
|
|
2069
2423
|
ctx.emit("dev:phase", {
|
|
@@ -2176,222 +2530,6 @@ const planInfra = async (ctx, manifest) => {
|
|
|
2176
2530
|
};
|
|
2177
2531
|
};
|
|
2178
2532
|
//#endregion
|
|
2179
|
-
//#region src/plugins/deploy/infra/render.ts
|
|
2180
|
-
/**
|
|
2181
|
-
* Derive a human-readable name from a resource descriptor: the Cloudflare resource `name` for the
|
|
2182
|
-
* provisioned kinds (kv/r2/d1/queue), or the exported `className` for a Durable Object (which has no
|
|
2183
|
-
* provisioned name). Used in both the provision events and the branded panels so the two agree.
|
|
2184
|
-
*
|
|
2185
|
-
* @param resource - The resource descriptor.
|
|
2186
|
-
* @returns A short name identifying the resource.
|
|
2187
|
-
* @example
|
|
2188
|
-
* ```ts
|
|
2189
|
-
* resourceName({ kind: "kv", name: "tracker-cache", binding: "CACHE" }); // "tracker-cache"
|
|
2190
|
-
* ```
|
|
2191
|
-
*/
|
|
2192
|
-
const resourceName = (resource) => resource.kind === "do" ? resource.className : resource.name;
|
|
2193
|
-
/**
|
|
2194
|
-
* Format a `kind name` cell, padding the kind so the names line up in a column.
|
|
2195
|
-
*
|
|
2196
|
-
* @param kind - The resource kind (kv / r2 / d1 / queue / do).
|
|
2197
|
-
* @param name - The resource name.
|
|
2198
|
-
* @returns The aligned `kind name` cell.
|
|
2199
|
-
* @example
|
|
2200
|
-
* ```ts
|
|
2201
|
-
* cell("kv", "CACHE"); // "kv CACHE"
|
|
2202
|
-
* ```
|
|
2203
|
-
*/
|
|
2204
|
-
const cell = (kind, name) => `${kind.padEnd(6)}${name}`;
|
|
2205
|
-
/**
|
|
2206
|
-
* Row tag for a Durable Object — it ships with the Worker (`wrangler deploy` creates the namespace),
|
|
2207
|
-
* so it is NEVER labelled `(exists)` (the planner never queried the account for it). Shared by the
|
|
2208
|
-
* plan and provision-result panels so the two always read the same.
|
|
2209
|
-
*/
|
|
2210
|
-
const SHIPS_WITH_WORKER = "(ships with worker)";
|
|
2211
|
-
/**
|
|
2212
|
-
* ANSI SGR matcher — built from `String.fromCharCode(27)` (the ESC byte) so no control character
|
|
2213
|
-
* appears in a regex literal (which both linters reject).
|
|
2214
|
-
*/
|
|
2215
|
-
const ANSI_SGR = new RegExp(String.raw`${String.fromCodePoint(27)}\[[0-9;]*m`, "gu");
|
|
2216
|
-
/**
|
|
2217
|
-
* Strip ANSI SGR escape sequences so a captured (colorized) error renders as plain, readable text.
|
|
2218
|
-
*
|
|
2219
|
-
* @param text - The (possibly colorized) text.
|
|
2220
|
-
* @returns The text with ANSI color codes removed.
|
|
2221
|
-
* @example
|
|
2222
|
-
* ```ts
|
|
2223
|
-
* stripAnsi(`${String.fromCharCode(27)}[31mX${String.fromCharCode(27)}[0m`); // "X"
|
|
2224
|
-
* ```
|
|
2225
|
-
*/
|
|
2226
|
-
const stripAnsi = (text) => text.replaceAll(ANSI_SGR, "");
|
|
2227
|
-
/**
|
|
2228
|
-
* Clean a captured (colorized, multi-line, wrapper-wrapped) provision error down to its meaningful
|
|
2229
|
-
* text: strip ANSI, drop the wrapper lines (the branded prefix, wrangler's log-file pointer), strip
|
|
2230
|
-
* each `✘ [ERROR]` marker, and join what's left. Returns the FULL message (the caller word-wraps it)
|
|
2231
|
-
* so the user reads the actual reason — never a truncated `…`.
|
|
2232
|
-
*
|
|
2233
|
-
* @param message - The captured error message.
|
|
2234
|
-
* @returns The full, plain failure reason.
|
|
2235
|
-
* @example
|
|
2236
|
-
* ```ts
|
|
2237
|
-
* cleanError("[moku-worker] wrangler exited…\n ✘ [ERROR] The bucket name is invalid.");
|
|
2238
|
-
* // "The bucket name is invalid."
|
|
2239
|
-
* ```
|
|
2240
|
-
*/
|
|
2241
|
-
const cleanError = (message) => {
|
|
2242
|
-
const cleaned = stripAnsi(message).split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => !/^\[moku-worker\]/u.test(line)).filter((line) => !/logs were written to/iu.test(line)).map((line) => line.replace(/^✘\s*/u, "").replace(/^\[error\]\s*/iu, "")).join(" ");
|
|
2243
|
-
return cleaned.length > 0 ? cleaned : stripAnsi(message).trim();
|
|
2244
|
-
};
|
|
2245
|
-
/**
|
|
2246
|
-
* Word-wrap text to `width` columns (never splitting inside a word), so a long failure reason reads
|
|
2247
|
-
* as a tidy indented block instead of forcing the box wide or scrolling off the edge.
|
|
2248
|
-
*
|
|
2249
|
-
* @param text - The text to wrap.
|
|
2250
|
-
* @param width - The maximum column width per line.
|
|
2251
|
-
* @returns The wrapped lines.
|
|
2252
|
-
* @example
|
|
2253
|
-
* ```ts
|
|
2254
|
-
* wrapText("a long sentence to wrap", 10); // ["a long", "sentence", "to wrap"]
|
|
2255
|
-
* ```
|
|
2256
|
-
*/
|
|
2257
|
-
const wrapText = (text, width) => {
|
|
2258
|
-
const lines = [];
|
|
2259
|
-
let line = "";
|
|
2260
|
-
for (const word of text.split(/\s+/u).filter(Boolean)) if (line.length === 0) line = word;
|
|
2261
|
-
else if (line.length + 1 + word.length <= width) line += ` ${word}`;
|
|
2262
|
-
else {
|
|
2263
|
-
lines.push(line);
|
|
2264
|
-
line = word;
|
|
2265
|
-
}
|
|
2266
|
-
if (line.length > 0) lines.push(line);
|
|
2267
|
-
return lines;
|
|
2268
|
-
};
|
|
2269
|
-
/**
|
|
2270
|
-
* Render the infra preflight plan as a branded panel: a dim summary line (counts + account) then one
|
|
2271
|
-
* row per declared resource — a pink `+` for those to create, a dim `~ (exists)` for those already
|
|
2272
|
-
* present, and a dim `~ (ships with worker)` for Durable Objects (created by `wrangler deploy`, never
|
|
2273
|
-
* pre-provisioned). When nothing needs creating it still renders, so the user sees the full picture.
|
|
2274
|
-
*
|
|
2275
|
-
* @param ui - The branded console to render through.
|
|
2276
|
-
* @param plan - The infra plan (existing vs missing vs ships-with-Worker) from checkInfra()/planInfra().
|
|
2277
|
-
* @example
|
|
2278
|
-
* ```ts
|
|
2279
|
-
* renderPlan(ui, await planInfra(ctx, manifest));
|
|
2280
|
-
* ```
|
|
2281
|
-
*/
|
|
2282
|
-
const renderPlan = (ui, plan) => {
|
|
2283
|
-
const { palette } = ui;
|
|
2284
|
-
const counts = [`${String(plan.missing.length)} to create`, `${String(plan.exists.length)} exist`];
|
|
2285
|
-
if (plan.ships.length > 0) counts.push(`${String(plan.ships.length)} with worker`);
|
|
2286
|
-
const summary = palette.dim(`${counts.join(" · ")} · ${plan.account}`);
|
|
2287
|
-
const createRows = plan.missing.map((resource) => `${palette.pink("+")} ${cell(resource.kind, resourceName(resource))}`);
|
|
2288
|
-
const existsRows = plan.exists.map((ref) => `${palette.dim("~")} ${cell(ref.resource.kind, resourceName(ref.resource))} ${palette.dim("(exists)")}`);
|
|
2289
|
-
const shipsRows = plan.ships.map((resource) => `${palette.dim("~")} ${cell(resource.kind, resourceName(resource))} ${palette.dim(SHIPS_WITH_WORKER)}`);
|
|
2290
|
-
ui.heading("Infra plan");
|
|
2291
|
-
ui.box([
|
|
2292
|
-
summary,
|
|
2293
|
-
"",
|
|
2294
|
-
...createRows,
|
|
2295
|
-
...existsRows,
|
|
2296
|
-
...shipsRows
|
|
2297
|
-
]);
|
|
2298
|
-
};
|
|
2299
|
-
/**
|
|
2300
|
-
* Render the provision result as a branded panel — a green `✓` per created resource, a dim `~` per
|
|
2301
|
-
* skipped, a dim `~ (ships with worker)` per Durable Object, a red `✗` per failure, then a summary
|
|
2302
|
-
* line (failed count red when non-zero) — followed, when anything failed, by a detail block printing
|
|
2303
|
-
* each failure's FULL reason (ANSI-stripped and word-wrapped) so it is actually readable instead of
|
|
2304
|
-
* truncated inside the box.
|
|
2305
|
-
*
|
|
2306
|
-
* @param ui - The branded console to render through.
|
|
2307
|
-
* @param result - The provision result from provisionInfra()/the deploy pipeline.
|
|
2308
|
-
* @example
|
|
2309
|
-
* ```ts
|
|
2310
|
-
* renderProvisionResult(ui, await provisionInfra(plan));
|
|
2311
|
-
* ```
|
|
2312
|
-
*/
|
|
2313
|
-
const renderProvisionResult = (ui, result) => {
|
|
2314
|
-
const { palette } = ui;
|
|
2315
|
-
const createdRows = result.created.map((ref) => `${palette.green("✓")} ${cell(ref.resource.kind, resourceName(ref.resource))}`);
|
|
2316
|
-
const skippedRows = result.skipped.map((ref) => `${palette.dim("~")} ${cell(ref.resource.kind, resourceName(ref.resource))} ${palette.dim("(exists)")}`);
|
|
2317
|
-
const bundledRows = result.bundled.map((resource) => `${palette.dim("~")} ${cell(resource.kind, resourceName(resource))} ${palette.dim(SHIPS_WITH_WORKER)}`);
|
|
2318
|
-
const failedRows = result.failed.map((failure) => `${palette.red("✗")} ${cell(failure.resource.kind, resourceName(failure.resource))}`);
|
|
2319
|
-
const failedCount = result.failed.length > 0 ? palette.red(`${String(result.failed.length)} failed`) : "0 failed";
|
|
2320
|
-
const counts = [`${String(result.created.length)} created`, `${String(result.skipped.length)} exist`];
|
|
2321
|
-
if (result.bundled.length > 0) counts.push(`${String(result.bundled.length)} with worker`);
|
|
2322
|
-
const summary = `${counts.join(" · ")} · ${failedCount}`;
|
|
2323
|
-
ui.heading("Provisioned");
|
|
2324
|
-
ui.box([
|
|
2325
|
-
...createdRows,
|
|
2326
|
-
...skippedRows,
|
|
2327
|
-
...bundledRows,
|
|
2328
|
-
...failedRows,
|
|
2329
|
-
"",
|
|
2330
|
-
summary
|
|
2331
|
-
]);
|
|
2332
|
-
if (result.failed.length > 0) {
|
|
2333
|
-
ui.line();
|
|
2334
|
-
for (const failure of result.failed) {
|
|
2335
|
-
ui.line(` ${palette.red("✗")} ${cell(failure.resource.kind, resourceName(failure.resource))}`);
|
|
2336
|
-
for (const wrapped of wrapText(cleanError(failure.error), ui.width - 4)) ui.line(palette.dim(` ${wrapped}`));
|
|
2337
|
-
}
|
|
2338
|
-
}
|
|
2339
|
-
};
|
|
2340
|
-
/**
|
|
2341
|
-
* Format an elapsed duration compactly: sub-second as `820ms`, otherwise one-decimal seconds (`4.2s`),
|
|
2342
|
-
* and minutes once it crosses 60s (`1m04s`) so a long deploy stays readable.
|
|
2343
|
-
*
|
|
2344
|
-
* @param ms - The elapsed milliseconds.
|
|
2345
|
-
* @returns The compact duration string.
|
|
2346
|
-
* @example
|
|
2347
|
-
* ```ts
|
|
2348
|
-
* formatDuration(4234); // "4.2s"
|
|
2349
|
-
* ```
|
|
2350
|
-
*/
|
|
2351
|
-
const formatDuration = (ms) => {
|
|
2352
|
-
if (ms < 1e3) return `${String(ms)}ms`;
|
|
2353
|
-
const seconds = ms / 1e3;
|
|
2354
|
-
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
2355
|
-
const whole = Math.floor(seconds);
|
|
2356
|
-
return `${String(Math.floor(whole / 60))}m${String(whole % 60).padStart(2, "0")}s`;
|
|
2357
|
-
};
|
|
2358
|
-
/**
|
|
2359
|
-
* Render the terminal deploy summary as a branded panel — the headline the user actually wants. The
|
|
2360
|
-
* live URL leads on its own line (pink, so it is the first thing the eye lands on), then a dim
|
|
2361
|
-
* key/value block: the target stage, the resource tally (with a red `failed` count when non-zero),
|
|
2362
|
-
* and the wall-clock time the whole deploy took. Replaces the prior single `deployed → url` line.
|
|
2363
|
-
*
|
|
2364
|
-
* @param ui - The branded console to render through.
|
|
2365
|
-
* @param summary - The deploy summary fields.
|
|
2366
|
-
* @param summary.url - The live deployed URL (the panel headline).
|
|
2367
|
-
* @param summary.stage - The target stage the worker deployed to.
|
|
2368
|
-
* @param summary.created - How many resources were created this run.
|
|
2369
|
-
* @param summary.exists - How many resources already existed (skipped).
|
|
2370
|
-
* @param summary.bundled - How many Durable Objects shipped with the Worker.
|
|
2371
|
-
* @param summary.failed - How many resources failed to provision.
|
|
2372
|
-
* @param summary.elapsedMs - The wall-clock deploy duration in milliseconds.
|
|
2373
|
-
* @example
|
|
2374
|
-
* ```ts
|
|
2375
|
-
* renderDeploySummary(ui, { url, stage: "production", created: 0, exists: 5, bundled: 1, failed: 0, elapsedMs: 4234 });
|
|
2376
|
-
* ```
|
|
2377
|
-
*/
|
|
2378
|
-
const renderDeploySummary = (ui, summary) => {
|
|
2379
|
-
const { palette } = ui;
|
|
2380
|
-
const parts = [`${String(summary.exists)} exist`, `${String(summary.created)} created`];
|
|
2381
|
-
if (summary.bundled > 0) parts.push(`${String(summary.bundled)} with worker`);
|
|
2382
|
-
const tally = parts.join(" · ");
|
|
2383
|
-
const failedLabel = palette.red(`${String(summary.failed)} failed`);
|
|
2384
|
-
const resources = summary.failed > 0 ? `${tally} · ${failedLabel}` : tally;
|
|
2385
|
-
ui.heading("Deployed");
|
|
2386
|
-
ui.box([
|
|
2387
|
-
palette.pink(summary.url),
|
|
2388
|
-
"",
|
|
2389
|
-
`${palette.dim("stage".padEnd(10))}${summary.stage}`,
|
|
2390
|
-
`${palette.dim("resources".padEnd(10))}${resources}`,
|
|
2391
|
-
`${palette.dim("took".padEnd(10))}${formatDuration(summary.elapsedMs)}`
|
|
2392
|
-
]);
|
|
2393
|
-
};
|
|
2394
|
-
//#endregion
|
|
2395
2533
|
//#region src/plugins/deploy/naming.ts
|
|
2396
2534
|
/**
|
|
2397
2535
|
* @file deploy plugin — stage-aware resource naming.
|
|
@@ -3352,25 +3490,34 @@ const guidedDeployStep = async (ctx, manifest, stage, deps) => {
|
|
|
3352
3490
|
* migrations dir — the generic, deploy-owned analogue of `wrangler d1 migrations apply <binding>
|
|
3353
3491
|
* --remote`. The wrangler config was written earlier in the pipeline, so each binding resolves. The
|
|
3354
3492
|
* caller runs this only AFTER a successful deploy, so a deploy that never happened never migrates a
|
|
3355
|
-
* remote DB.
|
|
3356
|
-
* the
|
|
3493
|
+
* remote DB. CAPTURES wrangler's output (the raw TUI is hidden; {@link parseMigrationsApplied} turns
|
|
3494
|
+
* it into the branded panel's facts); throws on the first non-zero exit (the caller folds it into the
|
|
3495
|
+
* report, where the captured error is still surfaced).
|
|
3357
3496
|
*
|
|
3358
3497
|
* @param ctx - The deploy plugin context.
|
|
3359
|
-
* @returns
|
|
3498
|
+
* @returns The per-database migration outcomes (one per d1 instance that declares migrations).
|
|
3360
3499
|
* @example
|
|
3361
3500
|
* ```ts
|
|
3362
|
-
* await applyRemoteMigrations(ctx);
|
|
3501
|
+
* const outcomes = await applyRemoteMigrations(ctx);
|
|
3363
3502
|
* ```
|
|
3364
3503
|
*/
|
|
3365
3504
|
const applyRemoteMigrations = async (ctx) => {
|
|
3366
|
-
if (!ctx.has("d1")) return;
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3505
|
+
if (!ctx.has("d1")) return [];
|
|
3506
|
+
const outcomes = [];
|
|
3507
|
+
for (const database of ctx.require(d1Plugin).deployManifest()) if (database.migrations !== void 0) {
|
|
3508
|
+
const output = await runWrangler([
|
|
3509
|
+
"d1",
|
|
3510
|
+
"migrations",
|
|
3511
|
+
"apply",
|
|
3512
|
+
database.binding,
|
|
3513
|
+
"--remote"
|
|
3514
|
+
]);
|
|
3515
|
+
outcomes.push({
|
|
3516
|
+
binding: database.binding,
|
|
3517
|
+
...parseMigrationsApplied(output)
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
3520
|
+
return outcomes;
|
|
3374
3521
|
};
|
|
3375
3522
|
/**
|
|
3376
3523
|
* Render a post-deploy step's failure as a branded line and capture its message into `errors` —
|
|
@@ -3414,9 +3561,13 @@ const runPostDeploy = async (ctx, want) => {
|
|
|
3414
3561
|
const errors = [];
|
|
3415
3562
|
let migration = "skipped";
|
|
3416
3563
|
if (want.migration) try {
|
|
3417
|
-
|
|
3564
|
+
ctx.emit("deploy:phase", {
|
|
3565
|
+
phase: "migrate",
|
|
3566
|
+
detail: "remote D1"
|
|
3567
|
+
});
|
|
3568
|
+
const outcomes = await applyRemoteMigrations(ctx);
|
|
3418
3569
|
migration = "applied";
|
|
3419
|
-
|
|
3570
|
+
if (outcomes.length > 0) renderMigrateSummary(ui, outcomes, "remote");
|
|
3420
3571
|
} catch (error) {
|
|
3421
3572
|
migration = "failed";
|
|
3422
3573
|
captureFailure(ui, errors, error);
|
|
@@ -3431,9 +3582,13 @@ const runPostDeploy = async (ctx, want) => {
|
|
|
3431
3582
|
seed = "failed";
|
|
3432
3583
|
captureFailure(ui, errors, /* @__PURE__ */ new Error("[moku-worker] deploy({ seed: true }) but no seed is configured — set pluginConfigs.deploy.seed."));
|
|
3433
3584
|
} else try {
|
|
3434
|
-
|
|
3585
|
+
ctx.emit("deploy:phase", {
|
|
3586
|
+
phase: "seed",
|
|
3587
|
+
detail: config.file
|
|
3588
|
+
});
|
|
3589
|
+
const outcome = await runConfiguredSeed(ctx, runWrangler, config, "--remote");
|
|
3435
3590
|
seed = "applied";
|
|
3436
|
-
ui
|
|
3591
|
+
renderSeedSummary(ui, outcome, "remote");
|
|
3437
3592
|
} catch (error) {
|
|
3438
3593
|
seed = "failed";
|
|
3439
3594
|
captureFailure(ui, errors, error);
|
|
@@ -3571,14 +3726,16 @@ const createDeployApi = (ctx) => ({
|
|
|
3571
3726
|
* Execute a SQL file against a configured D1 database via `wrangler d1 execute` — for seeding dev
|
|
3572
3727
|
* data. Local by default (applies that database's migrations first so the file's tables exist);
|
|
3573
3728
|
* `opts.remote` seeds Cloudflare (schema is applied by `deploy`). Generates the wrangler config up
|
|
3574
|
-
* front so the binding resolves even on a first run.
|
|
3729
|
+
* front so the binding resolves even on a first run. CAPTURES wrangler's output and renders a
|
|
3730
|
+
* branded "Migrated" / "Seeded" summary (the raw migration/execute TUI is hidden) so the command
|
|
3731
|
+
* reads the same as the rest of the deploy UX; a failure still surfaces the real wrangler error.
|
|
3575
3732
|
*
|
|
3576
3733
|
* @param sqlFile - Path to the SQL file to execute (e.g. "db/seed.sql").
|
|
3577
3734
|
* @param opts - Optional options.
|
|
3578
3735
|
* @param opts.stage - Stage for the generated config's resource names (defaults to the app stage).
|
|
3579
3736
|
* @param opts.binding - The d1 binding to target when more than one is configured (e.g. "DB").
|
|
3580
3737
|
* @param opts.remote - Seed the remote (Cloudflare) D1 instead of the local one.
|
|
3581
|
-
* @returns Resolves once wrangler finishes executing the file.
|
|
3738
|
+
* @returns Resolves once wrangler finishes executing the file and the summary is rendered.
|
|
3582
3739
|
* @example
|
|
3583
3740
|
* ```ts
|
|
3584
3741
|
* await api.seed("db/seed.sql"); // local default d1 (migrate, then execute)
|
|
@@ -3591,14 +3748,22 @@ const createDeployApi = (ctx) => ({
|
|
|
3591
3748
|
await writeWranglerConfig(ctx.config.configFile, assembleManifest(ctx, stage), {}, wranglerExtra(ctx.config));
|
|
3592
3749
|
const target = resolveD1(ctx, opts?.binding);
|
|
3593
3750
|
const scope = opts?.remote === true ? "--remote" : "--local";
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3751
|
+
const where = opts?.remote === true ? "remote" : "local";
|
|
3752
|
+
const ui = createBrandConsole();
|
|
3753
|
+
if (scope === "--local" && target.migrations !== void 0) {
|
|
3754
|
+
const migrated = await runWrangler([
|
|
3755
|
+
"d1",
|
|
3756
|
+
"migrations",
|
|
3757
|
+
"apply",
|
|
3758
|
+
target.binding,
|
|
3759
|
+
"--local"
|
|
3760
|
+
]);
|
|
3761
|
+
renderMigrateSummary(ui, [{
|
|
3762
|
+
binding: target.binding,
|
|
3763
|
+
...parseMigrationsApplied(migrated)
|
|
3764
|
+
}], where);
|
|
3765
|
+
}
|
|
3766
|
+
const executed = await runWrangler([
|
|
3602
3767
|
"d1",
|
|
3603
3768
|
"execute",
|
|
3604
3769
|
target.binding,
|
|
@@ -3606,6 +3771,12 @@ const createDeployApi = (ctx) => ({
|
|
|
3606
3771
|
"--file",
|
|
3607
3772
|
sqlFile
|
|
3608
3773
|
]);
|
|
3774
|
+
renderSeedSummary(ui, {
|
|
3775
|
+
file: sqlFile,
|
|
3776
|
+
binding: target.binding,
|
|
3777
|
+
resetKv: [],
|
|
3778
|
+
...parseSeedStats(executed)
|
|
3779
|
+
}, where);
|
|
3609
3780
|
},
|
|
3610
3781
|
/**
|
|
3611
3782
|
* Scaffold a starting wrangler config (and CI files when ci is set).
|