@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
|
@@ -1535,6 +1535,287 @@ const verifyAuth = async (ctx) => {
|
|
|
1535
1535
|
};
|
|
1536
1536
|
};
|
|
1537
1537
|
//#endregion
|
|
1538
|
+
//#region src/plugins/deploy/infra/render.ts
|
|
1539
|
+
/**
|
|
1540
|
+
* Derive a human-readable name from a resource descriptor: the Cloudflare resource `name` for the
|
|
1541
|
+
* provisioned kinds (kv/r2/d1/queue), or the exported `className` for a Durable Object (which has no
|
|
1542
|
+
* provisioned name). Used in both the provision events and the branded panels so the two agree.
|
|
1543
|
+
*
|
|
1544
|
+
* @param resource - The resource descriptor.
|
|
1545
|
+
* @returns A short name identifying the resource.
|
|
1546
|
+
* @example
|
|
1547
|
+
* ```ts
|
|
1548
|
+
* resourceName({ kind: "kv", name: "tracker-cache", binding: "CACHE" }); // "tracker-cache"
|
|
1549
|
+
* ```
|
|
1550
|
+
*/
|
|
1551
|
+
const resourceName = (resource) => resource.kind === "do" ? resource.className : resource.name;
|
|
1552
|
+
/**
|
|
1553
|
+
* Format a `kind name` cell, padding the kind so the names line up in a column.
|
|
1554
|
+
*
|
|
1555
|
+
* @param kind - The resource kind (kv / r2 / d1 / queue / do).
|
|
1556
|
+
* @param name - The resource name.
|
|
1557
|
+
* @returns The aligned `kind name` cell.
|
|
1558
|
+
* @example
|
|
1559
|
+
* ```ts
|
|
1560
|
+
* cell("kv", "CACHE"); // "kv CACHE"
|
|
1561
|
+
* ```
|
|
1562
|
+
*/
|
|
1563
|
+
const cell = (kind, name) => `${kind.padEnd(6)}${name}`;
|
|
1564
|
+
/**
|
|
1565
|
+
* Row tag for a Durable Object — it ships with the Worker (`wrangler deploy` creates the namespace),
|
|
1566
|
+
* so it is NEVER labelled `(exists)` (the planner never queried the account for it). Shared by the
|
|
1567
|
+
* plan and provision-result panels so the two always read the same.
|
|
1568
|
+
*/
|
|
1569
|
+
const SHIPS_WITH_WORKER = "(ships with worker)";
|
|
1570
|
+
/**
|
|
1571
|
+
* ANSI SGR matcher — built from `String.fromCharCode(27)` (the ESC byte) so no control character
|
|
1572
|
+
* appears in a regex literal (which both linters reject).
|
|
1573
|
+
*/
|
|
1574
|
+
const ANSI_SGR = new RegExp(String.raw`${String.fromCodePoint(27)}\[[0-9;]*m`, "gu");
|
|
1575
|
+
/**
|
|
1576
|
+
* Strip ANSI SGR escape sequences so a captured (colorized) error renders as plain, readable text.
|
|
1577
|
+
*
|
|
1578
|
+
* @param text - The (possibly colorized) text.
|
|
1579
|
+
* @returns The text with ANSI color codes removed.
|
|
1580
|
+
* @example
|
|
1581
|
+
* ```ts
|
|
1582
|
+
* stripAnsi(`${String.fromCharCode(27)}[31mX${String.fromCharCode(27)}[0m`); // "X"
|
|
1583
|
+
* ```
|
|
1584
|
+
*/
|
|
1585
|
+
const stripAnsi = (text) => text.replaceAll(ANSI_SGR, "");
|
|
1586
|
+
/**
|
|
1587
|
+
* Clean a captured (colorized, multi-line, wrapper-wrapped) provision error down to its meaningful
|
|
1588
|
+
* text: strip ANSI, drop the wrapper lines (the branded prefix, wrangler's log-file pointer), strip
|
|
1589
|
+
* each `✘ [ERROR]` marker, and join what's left. Returns the FULL message (the caller word-wraps it)
|
|
1590
|
+
* so the user reads the actual reason — never a truncated `…`.
|
|
1591
|
+
*
|
|
1592
|
+
* @param message - The captured error message.
|
|
1593
|
+
* @returns The full, plain failure reason.
|
|
1594
|
+
* @example
|
|
1595
|
+
* ```ts
|
|
1596
|
+
* cleanError("[moku-worker] wrangler exited…\n ✘ [ERROR] The bucket name is invalid.");
|
|
1597
|
+
* // "The bucket name is invalid."
|
|
1598
|
+
* ```
|
|
1599
|
+
*/
|
|
1600
|
+
const cleanError = (message) => {
|
|
1601
|
+
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(" ");
|
|
1602
|
+
return cleaned.length > 0 ? cleaned : stripAnsi(message).trim();
|
|
1603
|
+
};
|
|
1604
|
+
/**
|
|
1605
|
+
* Word-wrap text to `width` columns (never splitting inside a word), so a long failure reason reads
|
|
1606
|
+
* as a tidy indented block instead of forcing the box wide or scrolling off the edge.
|
|
1607
|
+
*
|
|
1608
|
+
* @param text - The text to wrap.
|
|
1609
|
+
* @param width - The maximum column width per line.
|
|
1610
|
+
* @returns The wrapped lines.
|
|
1611
|
+
* @example
|
|
1612
|
+
* ```ts
|
|
1613
|
+
* wrapText("a long sentence to wrap", 10); // ["a long", "sentence", "to wrap"]
|
|
1614
|
+
* ```
|
|
1615
|
+
*/
|
|
1616
|
+
const wrapText = (text, width) => {
|
|
1617
|
+
const lines = [];
|
|
1618
|
+
let line = "";
|
|
1619
|
+
for (const word of text.split(/\s+/u).filter(Boolean)) if (line.length === 0) line = word;
|
|
1620
|
+
else if (line.length + 1 + word.length <= width) line += ` ${word}`;
|
|
1621
|
+
else {
|
|
1622
|
+
lines.push(line);
|
|
1623
|
+
line = word;
|
|
1624
|
+
}
|
|
1625
|
+
if (line.length > 0) lines.push(line);
|
|
1626
|
+
return lines;
|
|
1627
|
+
};
|
|
1628
|
+
/**
|
|
1629
|
+
* Render the infra preflight plan as a branded panel: a dim summary line (counts + account) then one
|
|
1630
|
+
* row per declared resource — a pink `+` for those to create, a dim `~ (exists)` for those already
|
|
1631
|
+
* present, and a dim `~ (ships with worker)` for Durable Objects (created by `wrangler deploy`, never
|
|
1632
|
+
* pre-provisioned). When nothing needs creating it still renders, so the user sees the full picture.
|
|
1633
|
+
*
|
|
1634
|
+
* @param ui - The branded console to render through.
|
|
1635
|
+
* @param plan - The infra plan (existing vs missing vs ships-with-Worker) from checkInfra()/planInfra().
|
|
1636
|
+
* @example
|
|
1637
|
+
* ```ts
|
|
1638
|
+
* renderPlan(ui, await planInfra(ctx, manifest));
|
|
1639
|
+
* ```
|
|
1640
|
+
*/
|
|
1641
|
+
const renderPlan = (ui, plan) => {
|
|
1642
|
+
const { palette } = ui;
|
|
1643
|
+
const counts = [`${String(plan.missing.length)} to create`, `${String(plan.exists.length)} exist`];
|
|
1644
|
+
if (plan.ships.length > 0) counts.push(`${String(plan.ships.length)} with worker`);
|
|
1645
|
+
const summary = palette.dim(`${counts.join(" · ")} · ${plan.account}`);
|
|
1646
|
+
const createRows = plan.missing.map((resource) => `${palette.pink("+")} ${cell(resource.kind, resourceName(resource))}`);
|
|
1647
|
+
const existsRows = plan.exists.map((ref) => `${palette.dim("~")} ${cell(ref.resource.kind, resourceName(ref.resource))} ${palette.dim("(exists)")}`);
|
|
1648
|
+
const shipsRows = plan.ships.map((resource) => `${palette.dim("~")} ${cell(resource.kind, resourceName(resource))} ${palette.dim(SHIPS_WITH_WORKER)}`);
|
|
1649
|
+
ui.heading("Infra plan");
|
|
1650
|
+
ui.box([
|
|
1651
|
+
summary,
|
|
1652
|
+
"",
|
|
1653
|
+
...createRows,
|
|
1654
|
+
...existsRows,
|
|
1655
|
+
...shipsRows
|
|
1656
|
+
]);
|
|
1657
|
+
};
|
|
1658
|
+
/**
|
|
1659
|
+
* Render the provision result as a branded panel — a green `✓` per created resource, a dim `~` per
|
|
1660
|
+
* skipped, a dim `~ (ships with worker)` per Durable Object, a red `✗` per failure, then a summary
|
|
1661
|
+
* line (failed count red when non-zero) — followed, when anything failed, by a detail block printing
|
|
1662
|
+
* each failure's FULL reason (ANSI-stripped and word-wrapped) so it is actually readable instead of
|
|
1663
|
+
* truncated inside the box.
|
|
1664
|
+
*
|
|
1665
|
+
* @param ui - The branded console to render through.
|
|
1666
|
+
* @param result - The provision result from provisionInfra()/the deploy pipeline.
|
|
1667
|
+
* @example
|
|
1668
|
+
* ```ts
|
|
1669
|
+
* renderProvisionResult(ui, await provisionInfra(plan));
|
|
1670
|
+
* ```
|
|
1671
|
+
*/
|
|
1672
|
+
const renderProvisionResult = (ui, result) => {
|
|
1673
|
+
const { palette } = ui;
|
|
1674
|
+
const createdRows = result.created.map((ref) => `${palette.green("✓")} ${cell(ref.resource.kind, resourceName(ref.resource))}`);
|
|
1675
|
+
const skippedRows = result.skipped.map((ref) => `${palette.dim("~")} ${cell(ref.resource.kind, resourceName(ref.resource))} ${palette.dim("(exists)")}`);
|
|
1676
|
+
const bundledRows = result.bundled.map((resource) => `${palette.dim("~")} ${cell(resource.kind, resourceName(resource))} ${palette.dim(SHIPS_WITH_WORKER)}`);
|
|
1677
|
+
const failedRows = result.failed.map((failure) => `${palette.red("✗")} ${cell(failure.resource.kind, resourceName(failure.resource))}`);
|
|
1678
|
+
const failedCount = result.failed.length > 0 ? palette.red(`${String(result.failed.length)} failed`) : "0 failed";
|
|
1679
|
+
const counts = [`${String(result.created.length)} created`, `${String(result.skipped.length)} exist`];
|
|
1680
|
+
if (result.bundled.length > 0) counts.push(`${String(result.bundled.length)} with worker`);
|
|
1681
|
+
const summary = `${counts.join(" · ")} · ${failedCount}`;
|
|
1682
|
+
ui.heading("Provisioned");
|
|
1683
|
+
ui.box([
|
|
1684
|
+
...createdRows,
|
|
1685
|
+
...skippedRows,
|
|
1686
|
+
...bundledRows,
|
|
1687
|
+
...failedRows,
|
|
1688
|
+
"",
|
|
1689
|
+
summary
|
|
1690
|
+
]);
|
|
1691
|
+
if (result.failed.length > 0) {
|
|
1692
|
+
ui.line();
|
|
1693
|
+
for (const failure of result.failed) {
|
|
1694
|
+
ui.line(` ${palette.red("✗")} ${cell(failure.resource.kind, resourceName(failure.resource))}`);
|
|
1695
|
+
for (const wrapped of wrapText(cleanError(failure.error), ui.width - 4)) ui.line(palette.dim(` ${wrapped}`));
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
/**
|
|
1700
|
+
* Format an elapsed duration compactly: sub-second as `820ms`, otherwise one-decimal seconds (`4.2s`),
|
|
1701
|
+
* and minutes once it crosses 60s (`1m04s`) so a long deploy stays readable.
|
|
1702
|
+
*
|
|
1703
|
+
* @param ms - The elapsed milliseconds.
|
|
1704
|
+
* @returns The compact duration string.
|
|
1705
|
+
* @example
|
|
1706
|
+
* ```ts
|
|
1707
|
+
* formatDuration(4234); // "4.2s"
|
|
1708
|
+
* ```
|
|
1709
|
+
*/
|
|
1710
|
+
const formatDuration = (ms) => {
|
|
1711
|
+
if (ms < 1e3) return `${String(ms)}ms`;
|
|
1712
|
+
const seconds = ms / 1e3;
|
|
1713
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
1714
|
+
const whole = Math.floor(seconds);
|
|
1715
|
+
return `${String(Math.floor(whole / 60))}m${String(whole % 60).padStart(2, "0")}s`;
|
|
1716
|
+
};
|
|
1717
|
+
/**
|
|
1718
|
+
* Render the terminal deploy summary as a branded panel — the headline the user actually wants. The
|
|
1719
|
+
* live URL leads on its own line (pink, so it is the first thing the eye lands on), then a dim
|
|
1720
|
+
* key/value block: the target stage, the resource tally (with a red `failed` count when non-zero),
|
|
1721
|
+
* and the wall-clock time the whole deploy took. Replaces the prior single `deployed → url` line.
|
|
1722
|
+
*
|
|
1723
|
+
* @param ui - The branded console to render through.
|
|
1724
|
+
* @param summary - The deploy summary fields.
|
|
1725
|
+
* @param summary.url - The live deployed URL (the panel headline).
|
|
1726
|
+
* @param summary.stage - The target stage the worker deployed to.
|
|
1727
|
+
* @param summary.created - How many resources were created this run.
|
|
1728
|
+
* @param summary.exists - How many resources already existed (skipped).
|
|
1729
|
+
* @param summary.bundled - How many Durable Objects shipped with the Worker.
|
|
1730
|
+
* @param summary.failed - How many resources failed to provision.
|
|
1731
|
+
* @param summary.elapsedMs - The wall-clock deploy duration in milliseconds.
|
|
1732
|
+
* @example
|
|
1733
|
+
* ```ts
|
|
1734
|
+
* renderDeploySummary(ui, { url, stage: "production", created: 0, exists: 5, bundled: 1, failed: 0, elapsedMs: 4234 });
|
|
1735
|
+
* ```
|
|
1736
|
+
*/
|
|
1737
|
+
const renderDeploySummary = (ui, summary) => {
|
|
1738
|
+
const { palette } = ui;
|
|
1739
|
+
const parts = [`${String(summary.exists)} exist`, `${String(summary.created)} created`];
|
|
1740
|
+
if (summary.bundled > 0) parts.push(`${String(summary.bundled)} with worker`);
|
|
1741
|
+
const tally = parts.join(" · ");
|
|
1742
|
+
const failedLabel = palette.red(`${String(summary.failed)} failed`);
|
|
1743
|
+
const resources = summary.failed > 0 ? `${tally} · ${failedLabel}` : tally;
|
|
1744
|
+
ui.heading("Deployed");
|
|
1745
|
+
ui.box([
|
|
1746
|
+
palette.pink(summary.url),
|
|
1747
|
+
"",
|
|
1748
|
+
`${palette.dim("stage".padEnd(10))}${summary.stage}`,
|
|
1749
|
+
`${palette.dim("resources".padEnd(10))}${resources}`,
|
|
1750
|
+
`${palette.dim("took".padEnd(10))}${formatDuration(summary.elapsedMs)}`
|
|
1751
|
+
]);
|
|
1752
|
+
};
|
|
1753
|
+
/**
|
|
1754
|
+
* Render the D1 migration outcome as a branded panel — the readable replacement for wrangler's raw
|
|
1755
|
+
* `d1 migrations apply` TUI. One row per database: the `d1 <binding>` cell plus a pink `N applied`
|
|
1756
|
+
* count (or a dim `up to date` when nothing was pending), with each applied migration filename listed
|
|
1757
|
+
* beneath as a green `✓`. A dim scope footer (`remote` / `local`) names which database was touched.
|
|
1758
|
+
* The caller renders this only when at least one database actually ran migrations.
|
|
1759
|
+
*
|
|
1760
|
+
* @param ui - The branded console to render through.
|
|
1761
|
+
* @param outcomes - The per-database migration outcomes (one per d1 instance that declares migrations).
|
|
1762
|
+
* @param scope - Which database the migrations ran against: `remote` (Cloudflare) or `local` (dev).
|
|
1763
|
+
* @example
|
|
1764
|
+
* ```ts
|
|
1765
|
+
* renderMigrateSummary(ui, [{ binding: "DB", applied: ["0003_x.sql"], upToDate: false }], "remote");
|
|
1766
|
+
* ```
|
|
1767
|
+
*/
|
|
1768
|
+
const renderMigrateSummary = (ui, outcomes, scope) => {
|
|
1769
|
+
const { palette } = ui;
|
|
1770
|
+
const rows = [];
|
|
1771
|
+
for (const outcome of outcomes) {
|
|
1772
|
+
const count = outcome.applied.length;
|
|
1773
|
+
const appliedLabel = palette.pink(count > 0 ? `${String(count)} applied` : "applied");
|
|
1774
|
+
const status = outcome.upToDate ? palette.dim("up to date") : appliedLabel;
|
|
1775
|
+
rows.push(`${cell("d1", outcome.binding)} ${status}`);
|
|
1776
|
+
for (const name of outcome.applied) rows.push(` ${palette.green("✓")} ${palette.dim(name)}`);
|
|
1777
|
+
}
|
|
1778
|
+
ui.heading("Migrated");
|
|
1779
|
+
ui.box([
|
|
1780
|
+
...rows,
|
|
1781
|
+
"",
|
|
1782
|
+
palette.dim(scope)
|
|
1783
|
+
]);
|
|
1784
|
+
};
|
|
1785
|
+
/**
|
|
1786
|
+
* Render the seed outcome as a branded panel — the readable replacement for wrangler's raw
|
|
1787
|
+
* `d1 execute` / `kv key delete` TUI. Leads with the loaded `file → binding` (pink file), an optional
|
|
1788
|
+
* dim stats line (rows written / statements, only the parts wrangler reported), then — when the seed
|
|
1789
|
+
* cleared cached KV keys — a `KV reset` block listing each `~ binding key` so the user sees exactly
|
|
1790
|
+
* what was dropped. A dim scope footer (`remote` / `local`) names which database was seeded.
|
|
1791
|
+
*
|
|
1792
|
+
* @param ui - The branded console to render through.
|
|
1793
|
+
* @param outcome - The seed outcome (file, target binding, best-effort counts, the KV keys reset).
|
|
1794
|
+
* @param scope - Which database the seed ran against: `remote` (Cloudflare) or `local` (dev).
|
|
1795
|
+
* @example
|
|
1796
|
+
* ```ts
|
|
1797
|
+
* renderSeedSummary(ui, { file: "db/seed.sql", binding: "DB", rowsWritten: 18, resetKv: [] }, "remote");
|
|
1798
|
+
* ```
|
|
1799
|
+
*/
|
|
1800
|
+
const renderSeedSummary = (ui, outcome, scope) => {
|
|
1801
|
+
const { palette } = ui;
|
|
1802
|
+
const lines = [`${palette.pink(outcome.file)} ${palette.dim("→")} ${outcome.binding}`];
|
|
1803
|
+
const stats = [];
|
|
1804
|
+
if (outcome.rowsWritten !== void 0) stats.push(`${String(outcome.rowsWritten)} rows written`);
|
|
1805
|
+
if (outcome.statements !== void 0) stats.push(`${String(outcome.statements)} statements`);
|
|
1806
|
+
if (stats.length > 0) lines.push(palette.dim(stats.join(" · ")));
|
|
1807
|
+
if (outcome.resetKv.length > 0) {
|
|
1808
|
+
lines.push("", palette.dim("KV reset"));
|
|
1809
|
+
for (const entry of outcome.resetKv) lines.push(`${palette.dim("~")} ${entry.binding} ${palette.dim(entry.key)}`);
|
|
1810
|
+
}
|
|
1811
|
+
ui.heading("Seeded");
|
|
1812
|
+
ui.box([
|
|
1813
|
+
...lines,
|
|
1814
|
+
"",
|
|
1815
|
+
palette.dim(scope)
|
|
1816
|
+
]);
|
|
1817
|
+
};
|
|
1818
|
+
//#endregion
|
|
1538
1819
|
//#region src/plugins/deploy/runner.ts
|
|
1539
1820
|
/**
|
|
1540
1821
|
* @file deploy plugin — wrangler subprocess wrapper (node:child_process).
|
|
@@ -1642,6 +1923,61 @@ const runWranglerInherit = (args) => {
|
|
|
1642
1923
|
* Node-only; never imported by the runtime Worker bundle.
|
|
1643
1924
|
*/
|
|
1644
1925
|
/**
|
|
1926
|
+
* Parse the best-effort row/statement counts from wrangler's `d1 execute` output so the branded seed
|
|
1927
|
+
* panel can report them — degrading gracefully (each field simply omitted) when wrangler's format
|
|
1928
|
+
* differs or the runner streamed instead of captured. Wrangler prints lines like "🚣 18 commands
|
|
1929
|
+
* executed" and a rows-written total; both are matched loosely (case-insensitive).
|
|
1930
|
+
*
|
|
1931
|
+
* @param output - The captured stdout from `wrangler d1 execute` (empty when the runner streamed).
|
|
1932
|
+
* @returns The parsed counts — each field present only when found.
|
|
1933
|
+
* @example
|
|
1934
|
+
* ```ts
|
|
1935
|
+
* parseSeedStats("🚣 18 commands executed (30 rows written)"); // { statements: 18, rowsWritten: 30 }
|
|
1936
|
+
* ```
|
|
1937
|
+
*/
|
|
1938
|
+
const parseSeedStats = (output) => {
|
|
1939
|
+
const rows = /(\d{1,12}) rows? written/iu.exec(output);
|
|
1940
|
+
const commands = /(\d{1,12}) commands? executed/iu.exec(output) ?? /executed (\d{1,12}) commands?/iu.exec(output);
|
|
1941
|
+
const result = {};
|
|
1942
|
+
if (commands?.[1] !== void 0) result.statements = Number(commands[1]);
|
|
1943
|
+
if (rows?.[1] !== void 0) result.rowsWritten = Number(rows[1]);
|
|
1944
|
+
return result;
|
|
1945
|
+
};
|
|
1946
|
+
/**
|
|
1947
|
+
* Parse which migrations wrangler applied from its captured `d1 migrations apply` output, so the
|
|
1948
|
+
* branded migrate panel can name them instead of dumping wrangler's raw migration TUI. `upToDate` is
|
|
1949
|
+
* true when wrangler reported nothing pending ("No migrations to apply"); otherwise every
|
|
1950
|
+
* `NNNN_name.sql` filename token in the output is collected in order (de-duplicated). Degrades
|
|
1951
|
+
* safely — an unrecognized format yields no names, and the panel falls back to a generic "applied".
|
|
1952
|
+
* Lives here (not in api.ts) so both the deploy path and the dev path parse it without a cycle.
|
|
1953
|
+
*
|
|
1954
|
+
* @param output - The captured stdout from `wrangler d1 migrations apply`.
|
|
1955
|
+
* @returns The applied migration filenames and whether the database was already up to date.
|
|
1956
|
+
* @example
|
|
1957
|
+
* ```ts
|
|
1958
|
+
* parseMigrationsApplied("Applied 0003_x.sql\n0004_y.sql"); // { applied: ["0003_x.sql", "0004_y.sql"], upToDate: false }
|
|
1959
|
+
* ```
|
|
1960
|
+
*/
|
|
1961
|
+
const parseMigrationsApplied = (output) => {
|
|
1962
|
+
if (/no migrations to apply/iu.test(output)) return {
|
|
1963
|
+
applied: [],
|
|
1964
|
+
upToDate: true
|
|
1965
|
+
};
|
|
1966
|
+
const applied = [];
|
|
1967
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1968
|
+
for (const match of output.matchAll(/\b\d{3,}_[A-Za-z0-9_-]+\.sql\b/gu)) {
|
|
1969
|
+
const name = match[0];
|
|
1970
|
+
if (!seen.has(name)) {
|
|
1971
|
+
seen.add(name);
|
|
1972
|
+
applied.push(name);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return {
|
|
1976
|
+
applied,
|
|
1977
|
+
upToDate: false
|
|
1978
|
+
};
|
|
1979
|
+
};
|
|
1980
|
+
/**
|
|
1645
1981
|
* Resolve the single configured d1 database (or the one bound to `binding` when several exist) from
|
|
1646
1982
|
* the d1 plugin's manifest. The shared resolver behind `seed()`, the post-deploy seed, and the dev
|
|
1647
1983
|
* seed; throws a branded error when the choice is ambiguous (none/several, no binding) or unknown.
|
|
@@ -1670,27 +2006,30 @@ const resolveD1 = (ctx, binding) => {
|
|
|
1670
2006
|
* injectable dev path.
|
|
1671
2007
|
*
|
|
1672
2008
|
* @param ctx - The deploy plugin context.
|
|
1673
|
-
* @param run - The wrangler runner to execute each command through
|
|
2009
|
+
* @param run - The wrangler runner to execute each command through (a CAPTURING runner lets the
|
|
2010
|
+
* returned outcome report row/statement counts; a streaming one still works, just without them).
|
|
1674
2011
|
* @param seed - The resolved seed config (SQL file, optional binding, KV keys to reset).
|
|
1675
2012
|
* @param scope - The wrangler scope: `--remote` (deploy) or `--local` (dev).
|
|
1676
|
-
* @returns
|
|
2013
|
+
* @returns The seed outcome (file, target binding, best-effort counts, and the KV keys that were reset).
|
|
1677
2014
|
* @throws {Error} When no d1 database is configured, or the seed's binding cannot be resolved.
|
|
1678
2015
|
* @example
|
|
1679
2016
|
* ```ts
|
|
1680
|
-
* await runConfiguredSeed(ctx,
|
|
2017
|
+
* const outcome = await runConfiguredSeed(ctx, runWrangler, ctx.config.seed, "--remote");
|
|
1681
2018
|
* ```
|
|
1682
2019
|
*/
|
|
1683
2020
|
const runConfiguredSeed = async (ctx, run, seed, scope) => {
|
|
1684
2021
|
if (!ctx.has("d1")) throw new Error("[moku-worker] seed: no d1 database is configured.");
|
|
1685
|
-
|
|
2022
|
+
const database = resolveD1(ctx, seed.binding);
|
|
2023
|
+
const executed = await run([
|
|
1686
2024
|
"d1",
|
|
1687
2025
|
"execute",
|
|
1688
|
-
|
|
2026
|
+
database.binding,
|
|
1689
2027
|
scope,
|
|
1690
2028
|
"--file",
|
|
1691
2029
|
seed.file
|
|
1692
2030
|
]);
|
|
1693
|
-
|
|
2031
|
+
const resetKv = seed.resetKv ?? [];
|
|
2032
|
+
for (const entry of resetKv) await run([
|
|
1694
2033
|
"kv",
|
|
1695
2034
|
"key",
|
|
1696
2035
|
"delete",
|
|
@@ -1699,6 +2038,12 @@ const runConfiguredSeed = async (ctx, run, seed, scope) => {
|
|
|
1699
2038
|
entry.binding,
|
|
1700
2039
|
scope
|
|
1701
2040
|
]);
|
|
2041
|
+
return {
|
|
2042
|
+
file: seed.file,
|
|
2043
|
+
binding: database.binding,
|
|
2044
|
+
resetKv,
|
|
2045
|
+
...parseSeedStats(typeof executed === "string" ? executed : "")
|
|
2046
|
+
};
|
|
1702
2047
|
};
|
|
1703
2048
|
//#endregion
|
|
1704
2049
|
//#region src/plugins/deploy/dev/build.ts
|
|
@@ -2044,7 +2389,8 @@ const seedLocal = async (ctx, deps) => {
|
|
|
2044
2389
|
phase: "seed",
|
|
2045
2390
|
detail: config.file
|
|
2046
2391
|
});
|
|
2047
|
-
await runConfiguredSeed(ctx, deps.runWrangler, config, "--local");
|
|
2392
|
+
const outcome = await runConfiguredSeed(ctx, deps.runWrangler, config, "--local");
|
|
2393
|
+
renderSeedSummary((0, _moku_labs_common_cli.createBrandConsole)(), outcome, "local");
|
|
2048
2394
|
};
|
|
2049
2395
|
/**
|
|
2050
2396
|
* Run a long-lived dev session: cold build → (local d1 migrate) → (local seed) → spawn `wrangler
|
|
@@ -2080,13 +2426,21 @@ const runDev = async (ctx, opts, deps) => {
|
|
|
2080
2426
|
phase: "migrate",
|
|
2081
2427
|
detail: "d1 (local)"
|
|
2082
2428
|
});
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2429
|
+
const outcomes = [];
|
|
2430
|
+
for (const binding of migrationBindings) {
|
|
2431
|
+
const output = await deps.runWrangler([
|
|
2432
|
+
"d1",
|
|
2433
|
+
"migrations",
|
|
2434
|
+
"apply",
|
|
2435
|
+
binding,
|
|
2436
|
+
"--local"
|
|
2437
|
+
]);
|
|
2438
|
+
outcomes.push({
|
|
2439
|
+
binding,
|
|
2440
|
+
...parseMigrationsApplied(output)
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
renderMigrateSummary((0, _moku_labs_common_cli.createBrandConsole)(), outcomes, "local");
|
|
2090
2444
|
}
|
|
2091
2445
|
if (seed) await seedLocal(ctx, deps);
|
|
2092
2446
|
ctx.emit("dev:phase", {
|
|
@@ -2199,222 +2553,6 @@ const planInfra = async (ctx, manifest) => {
|
|
|
2199
2553
|
};
|
|
2200
2554
|
};
|
|
2201
2555
|
//#endregion
|
|
2202
|
-
//#region src/plugins/deploy/infra/render.ts
|
|
2203
|
-
/**
|
|
2204
|
-
* Derive a human-readable name from a resource descriptor: the Cloudflare resource `name` for the
|
|
2205
|
-
* provisioned kinds (kv/r2/d1/queue), or the exported `className` for a Durable Object (which has no
|
|
2206
|
-
* provisioned name). Used in both the provision events and the branded panels so the two agree.
|
|
2207
|
-
*
|
|
2208
|
-
* @param resource - The resource descriptor.
|
|
2209
|
-
* @returns A short name identifying the resource.
|
|
2210
|
-
* @example
|
|
2211
|
-
* ```ts
|
|
2212
|
-
* resourceName({ kind: "kv", name: "tracker-cache", binding: "CACHE" }); // "tracker-cache"
|
|
2213
|
-
* ```
|
|
2214
|
-
*/
|
|
2215
|
-
const resourceName = (resource) => resource.kind === "do" ? resource.className : resource.name;
|
|
2216
|
-
/**
|
|
2217
|
-
* Format a `kind name` cell, padding the kind so the names line up in a column.
|
|
2218
|
-
*
|
|
2219
|
-
* @param kind - The resource kind (kv / r2 / d1 / queue / do).
|
|
2220
|
-
* @param name - The resource name.
|
|
2221
|
-
* @returns The aligned `kind name` cell.
|
|
2222
|
-
* @example
|
|
2223
|
-
* ```ts
|
|
2224
|
-
* cell("kv", "CACHE"); // "kv CACHE"
|
|
2225
|
-
* ```
|
|
2226
|
-
*/
|
|
2227
|
-
const cell = (kind, name) => `${kind.padEnd(6)}${name}`;
|
|
2228
|
-
/**
|
|
2229
|
-
* Row tag for a Durable Object — it ships with the Worker (`wrangler deploy` creates the namespace),
|
|
2230
|
-
* so it is NEVER labelled `(exists)` (the planner never queried the account for it). Shared by the
|
|
2231
|
-
* plan and provision-result panels so the two always read the same.
|
|
2232
|
-
*/
|
|
2233
|
-
const SHIPS_WITH_WORKER = "(ships with worker)";
|
|
2234
|
-
/**
|
|
2235
|
-
* ANSI SGR matcher — built from `String.fromCharCode(27)` (the ESC byte) so no control character
|
|
2236
|
-
* appears in a regex literal (which both linters reject).
|
|
2237
|
-
*/
|
|
2238
|
-
const ANSI_SGR = new RegExp(String.raw`${String.fromCodePoint(27)}\[[0-9;]*m`, "gu");
|
|
2239
|
-
/**
|
|
2240
|
-
* Strip ANSI SGR escape sequences so a captured (colorized) error renders as plain, readable text.
|
|
2241
|
-
*
|
|
2242
|
-
* @param text - The (possibly colorized) text.
|
|
2243
|
-
* @returns The text with ANSI color codes removed.
|
|
2244
|
-
* @example
|
|
2245
|
-
* ```ts
|
|
2246
|
-
* stripAnsi(`${String.fromCharCode(27)}[31mX${String.fromCharCode(27)}[0m`); // "X"
|
|
2247
|
-
* ```
|
|
2248
|
-
*/
|
|
2249
|
-
const stripAnsi = (text) => text.replaceAll(ANSI_SGR, "");
|
|
2250
|
-
/**
|
|
2251
|
-
* Clean a captured (colorized, multi-line, wrapper-wrapped) provision error down to its meaningful
|
|
2252
|
-
* text: strip ANSI, drop the wrapper lines (the branded prefix, wrangler's log-file pointer), strip
|
|
2253
|
-
* each `✘ [ERROR]` marker, and join what's left. Returns the FULL message (the caller word-wraps it)
|
|
2254
|
-
* so the user reads the actual reason — never a truncated `…`.
|
|
2255
|
-
*
|
|
2256
|
-
* @param message - The captured error message.
|
|
2257
|
-
* @returns The full, plain failure reason.
|
|
2258
|
-
* @example
|
|
2259
|
-
* ```ts
|
|
2260
|
-
* cleanError("[moku-worker] wrangler exited…\n ✘ [ERROR] The bucket name is invalid.");
|
|
2261
|
-
* // "The bucket name is invalid."
|
|
2262
|
-
* ```
|
|
2263
|
-
*/
|
|
2264
|
-
const cleanError = (message) => {
|
|
2265
|
-
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(" ");
|
|
2266
|
-
return cleaned.length > 0 ? cleaned : stripAnsi(message).trim();
|
|
2267
|
-
};
|
|
2268
|
-
/**
|
|
2269
|
-
* Word-wrap text to `width` columns (never splitting inside a word), so a long failure reason reads
|
|
2270
|
-
* as a tidy indented block instead of forcing the box wide or scrolling off the edge.
|
|
2271
|
-
*
|
|
2272
|
-
* @param text - The text to wrap.
|
|
2273
|
-
* @param width - The maximum column width per line.
|
|
2274
|
-
* @returns The wrapped lines.
|
|
2275
|
-
* @example
|
|
2276
|
-
* ```ts
|
|
2277
|
-
* wrapText("a long sentence to wrap", 10); // ["a long", "sentence", "to wrap"]
|
|
2278
|
-
* ```
|
|
2279
|
-
*/
|
|
2280
|
-
const wrapText = (text, width) => {
|
|
2281
|
-
const lines = [];
|
|
2282
|
-
let line = "";
|
|
2283
|
-
for (const word of text.split(/\s+/u).filter(Boolean)) if (line.length === 0) line = word;
|
|
2284
|
-
else if (line.length + 1 + word.length <= width) line += ` ${word}`;
|
|
2285
|
-
else {
|
|
2286
|
-
lines.push(line);
|
|
2287
|
-
line = word;
|
|
2288
|
-
}
|
|
2289
|
-
if (line.length > 0) lines.push(line);
|
|
2290
|
-
return lines;
|
|
2291
|
-
};
|
|
2292
|
-
/**
|
|
2293
|
-
* Render the infra preflight plan as a branded panel: a dim summary line (counts + account) then one
|
|
2294
|
-
* row per declared resource — a pink `+` for those to create, a dim `~ (exists)` for those already
|
|
2295
|
-
* present, and a dim `~ (ships with worker)` for Durable Objects (created by `wrangler deploy`, never
|
|
2296
|
-
* pre-provisioned). When nothing needs creating it still renders, so the user sees the full picture.
|
|
2297
|
-
*
|
|
2298
|
-
* @param ui - The branded console to render through.
|
|
2299
|
-
* @param plan - The infra plan (existing vs missing vs ships-with-Worker) from checkInfra()/planInfra().
|
|
2300
|
-
* @example
|
|
2301
|
-
* ```ts
|
|
2302
|
-
* renderPlan(ui, await planInfra(ctx, manifest));
|
|
2303
|
-
* ```
|
|
2304
|
-
*/
|
|
2305
|
-
const renderPlan = (ui, plan) => {
|
|
2306
|
-
const { palette } = ui;
|
|
2307
|
-
const counts = [`${String(plan.missing.length)} to create`, `${String(plan.exists.length)} exist`];
|
|
2308
|
-
if (plan.ships.length > 0) counts.push(`${String(plan.ships.length)} with worker`);
|
|
2309
|
-
const summary = palette.dim(`${counts.join(" · ")} · ${plan.account}`);
|
|
2310
|
-
const createRows = plan.missing.map((resource) => `${palette.pink("+")} ${cell(resource.kind, resourceName(resource))}`);
|
|
2311
|
-
const existsRows = plan.exists.map((ref) => `${palette.dim("~")} ${cell(ref.resource.kind, resourceName(ref.resource))} ${palette.dim("(exists)")}`);
|
|
2312
|
-
const shipsRows = plan.ships.map((resource) => `${palette.dim("~")} ${cell(resource.kind, resourceName(resource))} ${palette.dim(SHIPS_WITH_WORKER)}`);
|
|
2313
|
-
ui.heading("Infra plan");
|
|
2314
|
-
ui.box([
|
|
2315
|
-
summary,
|
|
2316
|
-
"",
|
|
2317
|
-
...createRows,
|
|
2318
|
-
...existsRows,
|
|
2319
|
-
...shipsRows
|
|
2320
|
-
]);
|
|
2321
|
-
};
|
|
2322
|
-
/**
|
|
2323
|
-
* Render the provision result as a branded panel — a green `✓` per created resource, a dim `~` per
|
|
2324
|
-
* skipped, a dim `~ (ships with worker)` per Durable Object, a red `✗` per failure, then a summary
|
|
2325
|
-
* line (failed count red when non-zero) — followed, when anything failed, by a detail block printing
|
|
2326
|
-
* each failure's FULL reason (ANSI-stripped and word-wrapped) so it is actually readable instead of
|
|
2327
|
-
* truncated inside the box.
|
|
2328
|
-
*
|
|
2329
|
-
* @param ui - The branded console to render through.
|
|
2330
|
-
* @param result - The provision result from provisionInfra()/the deploy pipeline.
|
|
2331
|
-
* @example
|
|
2332
|
-
* ```ts
|
|
2333
|
-
* renderProvisionResult(ui, await provisionInfra(plan));
|
|
2334
|
-
* ```
|
|
2335
|
-
*/
|
|
2336
|
-
const renderProvisionResult = (ui, result) => {
|
|
2337
|
-
const { palette } = ui;
|
|
2338
|
-
const createdRows = result.created.map((ref) => `${palette.green("✓")} ${cell(ref.resource.kind, resourceName(ref.resource))}`);
|
|
2339
|
-
const skippedRows = result.skipped.map((ref) => `${palette.dim("~")} ${cell(ref.resource.kind, resourceName(ref.resource))} ${palette.dim("(exists)")}`);
|
|
2340
|
-
const bundledRows = result.bundled.map((resource) => `${palette.dim("~")} ${cell(resource.kind, resourceName(resource))} ${palette.dim(SHIPS_WITH_WORKER)}`);
|
|
2341
|
-
const failedRows = result.failed.map((failure) => `${palette.red("✗")} ${cell(failure.resource.kind, resourceName(failure.resource))}`);
|
|
2342
|
-
const failedCount = result.failed.length > 0 ? palette.red(`${String(result.failed.length)} failed`) : "0 failed";
|
|
2343
|
-
const counts = [`${String(result.created.length)} created`, `${String(result.skipped.length)} exist`];
|
|
2344
|
-
if (result.bundled.length > 0) counts.push(`${String(result.bundled.length)} with worker`);
|
|
2345
|
-
const summary = `${counts.join(" · ")} · ${failedCount}`;
|
|
2346
|
-
ui.heading("Provisioned");
|
|
2347
|
-
ui.box([
|
|
2348
|
-
...createdRows,
|
|
2349
|
-
...skippedRows,
|
|
2350
|
-
...bundledRows,
|
|
2351
|
-
...failedRows,
|
|
2352
|
-
"",
|
|
2353
|
-
summary
|
|
2354
|
-
]);
|
|
2355
|
-
if (result.failed.length > 0) {
|
|
2356
|
-
ui.line();
|
|
2357
|
-
for (const failure of result.failed) {
|
|
2358
|
-
ui.line(` ${palette.red("✗")} ${cell(failure.resource.kind, resourceName(failure.resource))}`);
|
|
2359
|
-
for (const wrapped of wrapText(cleanError(failure.error), ui.width - 4)) ui.line(palette.dim(` ${wrapped}`));
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
};
|
|
2363
|
-
/**
|
|
2364
|
-
* Format an elapsed duration compactly: sub-second as `820ms`, otherwise one-decimal seconds (`4.2s`),
|
|
2365
|
-
* and minutes once it crosses 60s (`1m04s`) so a long deploy stays readable.
|
|
2366
|
-
*
|
|
2367
|
-
* @param ms - The elapsed milliseconds.
|
|
2368
|
-
* @returns The compact duration string.
|
|
2369
|
-
* @example
|
|
2370
|
-
* ```ts
|
|
2371
|
-
* formatDuration(4234); // "4.2s"
|
|
2372
|
-
* ```
|
|
2373
|
-
*/
|
|
2374
|
-
const formatDuration = (ms) => {
|
|
2375
|
-
if (ms < 1e3) return `${String(ms)}ms`;
|
|
2376
|
-
const seconds = ms / 1e3;
|
|
2377
|
-
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
2378
|
-
const whole = Math.floor(seconds);
|
|
2379
|
-
return `${String(Math.floor(whole / 60))}m${String(whole % 60).padStart(2, "0")}s`;
|
|
2380
|
-
};
|
|
2381
|
-
/**
|
|
2382
|
-
* Render the terminal deploy summary as a branded panel — the headline the user actually wants. The
|
|
2383
|
-
* live URL leads on its own line (pink, so it is the first thing the eye lands on), then a dim
|
|
2384
|
-
* key/value block: the target stage, the resource tally (with a red `failed` count when non-zero),
|
|
2385
|
-
* and the wall-clock time the whole deploy took. Replaces the prior single `deployed → url` line.
|
|
2386
|
-
*
|
|
2387
|
-
* @param ui - The branded console to render through.
|
|
2388
|
-
* @param summary - The deploy summary fields.
|
|
2389
|
-
* @param summary.url - The live deployed URL (the panel headline).
|
|
2390
|
-
* @param summary.stage - The target stage the worker deployed to.
|
|
2391
|
-
* @param summary.created - How many resources were created this run.
|
|
2392
|
-
* @param summary.exists - How many resources already existed (skipped).
|
|
2393
|
-
* @param summary.bundled - How many Durable Objects shipped with the Worker.
|
|
2394
|
-
* @param summary.failed - How many resources failed to provision.
|
|
2395
|
-
* @param summary.elapsedMs - The wall-clock deploy duration in milliseconds.
|
|
2396
|
-
* @example
|
|
2397
|
-
* ```ts
|
|
2398
|
-
* renderDeploySummary(ui, { url, stage: "production", created: 0, exists: 5, bundled: 1, failed: 0, elapsedMs: 4234 });
|
|
2399
|
-
* ```
|
|
2400
|
-
*/
|
|
2401
|
-
const renderDeploySummary = (ui, summary) => {
|
|
2402
|
-
const { palette } = ui;
|
|
2403
|
-
const parts = [`${String(summary.exists)} exist`, `${String(summary.created)} created`];
|
|
2404
|
-
if (summary.bundled > 0) parts.push(`${String(summary.bundled)} with worker`);
|
|
2405
|
-
const tally = parts.join(" · ");
|
|
2406
|
-
const failedLabel = palette.red(`${String(summary.failed)} failed`);
|
|
2407
|
-
const resources = summary.failed > 0 ? `${tally} · ${failedLabel}` : tally;
|
|
2408
|
-
ui.heading("Deployed");
|
|
2409
|
-
ui.box([
|
|
2410
|
-
palette.pink(summary.url),
|
|
2411
|
-
"",
|
|
2412
|
-
`${palette.dim("stage".padEnd(10))}${summary.stage}`,
|
|
2413
|
-
`${palette.dim("resources".padEnd(10))}${resources}`,
|
|
2414
|
-
`${palette.dim("took".padEnd(10))}${formatDuration(summary.elapsedMs)}`
|
|
2415
|
-
]);
|
|
2416
|
-
};
|
|
2417
|
-
//#endregion
|
|
2418
2556
|
//#region src/plugins/deploy/naming.ts
|
|
2419
2557
|
/**
|
|
2420
2558
|
* @file deploy plugin — stage-aware resource naming.
|
|
@@ -3375,25 +3513,34 @@ const guidedDeployStep = async (ctx, manifest, stage, deps) => {
|
|
|
3375
3513
|
* migrations dir — the generic, deploy-owned analogue of `wrangler d1 migrations apply <binding>
|
|
3376
3514
|
* --remote`. The wrangler config was written earlier in the pipeline, so each binding resolves. The
|
|
3377
3515
|
* caller runs this only AFTER a successful deploy, so a deploy that never happened never migrates a
|
|
3378
|
-
* remote DB.
|
|
3379
|
-
* the
|
|
3516
|
+
* remote DB. CAPTURES wrangler's output (the raw TUI is hidden; {@link parseMigrationsApplied} turns
|
|
3517
|
+
* it into the branded panel's facts); throws on the first non-zero exit (the caller folds it into the
|
|
3518
|
+
* report, where the captured error is still surfaced).
|
|
3380
3519
|
*
|
|
3381
3520
|
* @param ctx - The deploy plugin context.
|
|
3382
|
-
* @returns
|
|
3521
|
+
* @returns The per-database migration outcomes (one per d1 instance that declares migrations).
|
|
3383
3522
|
* @example
|
|
3384
3523
|
* ```ts
|
|
3385
|
-
* await applyRemoteMigrations(ctx);
|
|
3524
|
+
* const outcomes = await applyRemoteMigrations(ctx);
|
|
3386
3525
|
* ```
|
|
3387
3526
|
*/
|
|
3388
3527
|
const applyRemoteMigrations = async (ctx) => {
|
|
3389
|
-
if (!ctx.has("d1")) return;
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3528
|
+
if (!ctx.has("d1")) return [];
|
|
3529
|
+
const outcomes = [];
|
|
3530
|
+
for (const database of ctx.require(d1Plugin).deployManifest()) if (database.migrations !== void 0) {
|
|
3531
|
+
const output = await runWrangler([
|
|
3532
|
+
"d1",
|
|
3533
|
+
"migrations",
|
|
3534
|
+
"apply",
|
|
3535
|
+
database.binding,
|
|
3536
|
+
"--remote"
|
|
3537
|
+
]);
|
|
3538
|
+
outcomes.push({
|
|
3539
|
+
binding: database.binding,
|
|
3540
|
+
...parseMigrationsApplied(output)
|
|
3541
|
+
});
|
|
3542
|
+
}
|
|
3543
|
+
return outcomes;
|
|
3397
3544
|
};
|
|
3398
3545
|
/**
|
|
3399
3546
|
* Render a post-deploy step's failure as a branded line and capture its message into `errors` —
|
|
@@ -3437,9 +3584,13 @@ const runPostDeploy = async (ctx, want) => {
|
|
|
3437
3584
|
const errors = [];
|
|
3438
3585
|
let migration = "skipped";
|
|
3439
3586
|
if (want.migration) try {
|
|
3440
|
-
|
|
3587
|
+
ctx.emit("deploy:phase", {
|
|
3588
|
+
phase: "migrate",
|
|
3589
|
+
detail: "remote D1"
|
|
3590
|
+
});
|
|
3591
|
+
const outcomes = await applyRemoteMigrations(ctx);
|
|
3441
3592
|
migration = "applied";
|
|
3442
|
-
|
|
3593
|
+
if (outcomes.length > 0) renderMigrateSummary(ui, outcomes, "remote");
|
|
3443
3594
|
} catch (error) {
|
|
3444
3595
|
migration = "failed";
|
|
3445
3596
|
captureFailure(ui, errors, error);
|
|
@@ -3454,9 +3605,13 @@ const runPostDeploy = async (ctx, want) => {
|
|
|
3454
3605
|
seed = "failed";
|
|
3455
3606
|
captureFailure(ui, errors, /* @__PURE__ */ new Error("[moku-worker] deploy({ seed: true }) but no seed is configured — set pluginConfigs.deploy.seed."));
|
|
3456
3607
|
} else try {
|
|
3457
|
-
|
|
3608
|
+
ctx.emit("deploy:phase", {
|
|
3609
|
+
phase: "seed",
|
|
3610
|
+
detail: config.file
|
|
3611
|
+
});
|
|
3612
|
+
const outcome = await runConfiguredSeed(ctx, runWrangler, config, "--remote");
|
|
3458
3613
|
seed = "applied";
|
|
3459
|
-
ui
|
|
3614
|
+
renderSeedSummary(ui, outcome, "remote");
|
|
3460
3615
|
} catch (error) {
|
|
3461
3616
|
seed = "failed";
|
|
3462
3617
|
captureFailure(ui, errors, error);
|
|
@@ -3594,14 +3749,16 @@ const createDeployApi = (ctx) => ({
|
|
|
3594
3749
|
* Execute a SQL file against a configured D1 database via `wrangler d1 execute` — for seeding dev
|
|
3595
3750
|
* data. Local by default (applies that database's migrations first so the file's tables exist);
|
|
3596
3751
|
* `opts.remote` seeds Cloudflare (schema is applied by `deploy`). Generates the wrangler config up
|
|
3597
|
-
* front so the binding resolves even on a first run.
|
|
3752
|
+
* front so the binding resolves even on a first run. CAPTURES wrangler's output and renders a
|
|
3753
|
+
* branded "Migrated" / "Seeded" summary (the raw migration/execute TUI is hidden) so the command
|
|
3754
|
+
* reads the same as the rest of the deploy UX; a failure still surfaces the real wrangler error.
|
|
3598
3755
|
*
|
|
3599
3756
|
* @param sqlFile - Path to the SQL file to execute (e.g. "db/seed.sql").
|
|
3600
3757
|
* @param opts - Optional options.
|
|
3601
3758
|
* @param opts.stage - Stage for the generated config's resource names (defaults to the app stage).
|
|
3602
3759
|
* @param opts.binding - The d1 binding to target when more than one is configured (e.g. "DB").
|
|
3603
3760
|
* @param opts.remote - Seed the remote (Cloudflare) D1 instead of the local one.
|
|
3604
|
-
* @returns Resolves once wrangler finishes executing the file.
|
|
3761
|
+
* @returns Resolves once wrangler finishes executing the file and the summary is rendered.
|
|
3605
3762
|
* @example
|
|
3606
3763
|
* ```ts
|
|
3607
3764
|
* await api.seed("db/seed.sql"); // local default d1 (migrate, then execute)
|
|
@@ -3614,14 +3771,22 @@ const createDeployApi = (ctx) => ({
|
|
|
3614
3771
|
await writeWranglerConfig(ctx.config.configFile, assembleManifest(ctx, stage), {}, wranglerExtra(ctx.config));
|
|
3615
3772
|
const target = resolveD1(ctx, opts?.binding);
|
|
3616
3773
|
const scope = opts?.remote === true ? "--remote" : "--local";
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3774
|
+
const where = opts?.remote === true ? "remote" : "local";
|
|
3775
|
+
const ui = (0, _moku_labs_common_cli.createBrandConsole)();
|
|
3776
|
+
if (scope === "--local" && target.migrations !== void 0) {
|
|
3777
|
+
const migrated = await runWrangler([
|
|
3778
|
+
"d1",
|
|
3779
|
+
"migrations",
|
|
3780
|
+
"apply",
|
|
3781
|
+
target.binding,
|
|
3782
|
+
"--local"
|
|
3783
|
+
]);
|
|
3784
|
+
renderMigrateSummary(ui, [{
|
|
3785
|
+
binding: target.binding,
|
|
3786
|
+
...parseMigrationsApplied(migrated)
|
|
3787
|
+
}], where);
|
|
3788
|
+
}
|
|
3789
|
+
const executed = await runWrangler([
|
|
3625
3790
|
"d1",
|
|
3626
3791
|
"execute",
|
|
3627
3792
|
target.binding,
|
|
@@ -3629,6 +3794,12 @@ const createDeployApi = (ctx) => ({
|
|
|
3629
3794
|
"--file",
|
|
3630
3795
|
sqlFile
|
|
3631
3796
|
]);
|
|
3797
|
+
renderSeedSummary(ui, {
|
|
3798
|
+
file: sqlFile,
|
|
3799
|
+
binding: target.binding,
|
|
3800
|
+
resetKv: [],
|
|
3801
|
+
...parseSeedStats(executed)
|
|
3802
|
+
}, where);
|
|
3632
3803
|
},
|
|
3633
3804
|
/**
|
|
3634
3805
|
* Scaffold a starting wrangler config (and CI files when ci is set).
|