dineway 0.1.35 → 0.1.37

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.
Files changed (51) hide show
  1. package/README.md +15 -3
  2. package/dist/api/route-utils.d.mts +1 -1
  3. package/dist/api/schemas/index.d.mts +1 -1
  4. package/dist/{api-Ow6RbraA.mjs → api-DpeH2EYq.mjs} +1 -1
  5. package/dist/astro/index.d.mts +1 -1
  6. package/dist/astro/index.mjs +1 -1
  7. package/dist/astro/middleware/auth.d.mts +1 -1
  8. package/dist/astro/middleware/seed.mjs +1 -1
  9. package/dist/astro/middleware.mjs +4 -4
  10. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +1 -1
  11. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +1 -1
  12. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +1 -1
  13. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +1 -1
  14. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +1 -1
  15. package/dist/astro/routes/api/admin/plugins/index.mjs +1 -1
  16. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +1 -1
  17. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +1 -1
  18. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +1 -1
  19. package/dist/astro/routes/api/admin/plugins/updates.mjs +1 -1
  20. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +1 -1
  21. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +1 -1
  22. package/dist/astro/routes/api/auth/dev-bypass.mjs +1 -1
  23. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +1 -1
  24. package/dist/astro/routes/api/health.mjs +1 -1
  25. package/dist/astro/routes/api/manifest.mjs +1 -1
  26. package/dist/astro/routes/api/mcp.mjs +1 -1
  27. package/dist/astro/routes/api/openapi.json.mjs +1 -1
  28. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +1 -1
  29. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +1 -1
  30. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +1 -1
  31. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +1 -1
  32. package/dist/astro/routes/api/schema/collections/index.mjs +1 -1
  33. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +1 -1
  34. package/dist/astro/routes/api/schema/orphans/index.mjs +1 -1
  35. package/dist/astro/routes/api/setup/dev-bypass.mjs +1 -1
  36. package/dist/astro/routes/api/setup/index.mjs +1 -1
  37. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  38. package/dist/astro/types.d.mts +1 -1
  39. package/dist/{bylines-CtD_p_1z.d.mts → bylines-CNe_OWKA.d.mts} +16 -16
  40. package/dist/cli/index.mjs +302 -72
  41. package/dist/db/index.mjs +1 -1
  42. package/dist/index.d.mts +1 -1
  43. package/dist/index.mjs +3 -3
  44. package/dist/media/local-runtime.d.mts +1 -1
  45. package/dist/plugins/adapt-sandbox-entry.d.mts +1 -1
  46. package/dist/{preview-5HuX6fjF.mjs → preview-BhgxNRWI.mjs} +1 -1
  47. package/dist/{runner-lqEiJbO-.mjs → runner-S3smkgdc.mjs} +15 -2
  48. package/dist/runtime.d.mts +1 -1
  49. package/dist/version-BWhBEejU.mjs +6 -0
  50. package/package.json +2 -2
  51. package/dist/version-q9Wd8cwL.mjs +0 -6
@@ -2,7 +2,7 @@
2
2
  import { t as __exportAll } from "../chunk-ClPoSABd.mjs";
3
3
  import { n as createDatabase } from "../connection-BCNICDWN.mjs";
4
4
  import { c as listTablesLike } from "../dialect-helpers-TkdbkFad.mjs";
5
- import { r as runMigrations, t as getMigrationStatus } from "../runner-lqEiJbO-.mjs";
5
+ import { i as runMigrations, n as getMigrationStatus, t as getMigrationFingerprint } from "../runner-S3smkgdc.mjs";
6
6
  import { r as isI18nEnabled } from "../config-XW5tMrH8.mjs";
7
7
  import { n as slugify } from "../slugify-BzGxlOFx.mjs";
8
8
  import { t as ContentRepository } from "../content-DvpMad_N.mjs";
@@ -27,9 +27,9 @@ import { createHeaderAwareFetch, customHeadersInterceptor, isRedirectResponse, r
27
27
  import { o as convertDataForRead } from "../transport-B7kO-4ee.mjs";
28
28
  import { DinewayClient } from "../client/index.mjs";
29
29
  import { LocalStorage } from "../storage/local.mjs";
30
+ import { createHash } from "node:crypto";
30
31
  import { imageSize } from "image-size";
31
32
  import { createGzipDecoder, unpackTar } from "modern-tar";
32
- import { createHash } from "node:crypto";
33
33
  import { createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
34
34
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
35
35
  import { defineCommand, runCommand, runMain } from "citty";
@@ -190,7 +190,15 @@ function removeMarketplaceCredential(registryUrl) {
190
190
  //#region src/cli/project-env.ts
191
191
  const DEFAULT_DINEWAY_URL = "http://localhost:4321";
192
192
  const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
193
- const LINE_SPLIT_PATTERN = /\r?\n/;
193
+ const LINE_SPLIT_PATTERN$1 = /\r?\n/;
194
+ const DINEWAY_LOCAL_STATE_GITIGNORE_COMMENT = "# Dineway local state";
195
+ const DINEWAY_LOCAL_STATE_GITIGNORE_ENTRIES = [
196
+ ".dineway/deploy.json",
197
+ ".dineway/forgeway.json",
198
+ ".dineway/types.ts",
199
+ ".dineway/schema.json",
200
+ ".dineway/uploads/"
201
+ ];
194
202
  const NEWLINE_PATTERN = /\r?\n/g;
195
203
  function parseDotenvValue(value) {
196
204
  const trimmed = value.trim();
@@ -199,7 +207,7 @@ function parseDotenvValue(value) {
199
207
  }
200
208
  function parseDotenv(text) {
201
209
  const env = {};
202
- for (const line of text.split(LINE_SPLIT_PATTERN)) {
210
+ for (const line of text.split(LINE_SPLIT_PATTERN$1)) {
203
211
  const trimmed = line.trim();
204
212
  if (!trimmed || trimmed.startsWith("#")) continue;
205
213
  const normalized = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
@@ -233,7 +241,7 @@ function formatEnvValue(value) {
233
241
  async function upsertProjectEnv(cwd, values) {
234
242
  const envPath = join(cwd, ".env");
235
243
  const existing = await readFile(envPath, "utf-8").catch(() => "");
236
- const lines = existing ? existing.split(LINE_SPLIT_PATTERN) : [];
244
+ const lines = existing ? existing.split(LINE_SPLIT_PATTERN$1) : [];
237
245
  const seen = /* @__PURE__ */ new Set();
238
246
  const nextLines = lines.map((line) => {
239
247
  const trimmed = line.trim();
@@ -255,9 +263,18 @@ async function upsertProjectEnv(cwd, values) {
255
263
  async function ensureProjectGitignoreEntry(cwd, entry) {
256
264
  const gitignorePath = join(cwd, ".gitignore");
257
265
  const existing = await readFile(gitignorePath, "utf-8").catch(() => "");
258
- if (existing.split(LINE_SPLIT_PATTERN).map((line) => line.trim()).includes(entry)) return;
266
+ if (existing.split(LINE_SPLIT_PATTERN$1).map((line) => line.trim()).includes(entry)) return;
259
267
  await writeFile(gitignorePath, `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${entry}\n`, "utf-8");
260
268
  }
269
+ async function ensureProjectGitignoreDinewayLocalState(cwd) {
270
+ const gitignorePath = join(cwd, ".gitignore");
271
+ const existing = await readFile(gitignorePath, "utf-8").catch(() => "");
272
+ const lines = new Set(existing.split(LINE_SPLIT_PATTERN$1).map((line) => line.trim()));
273
+ const missingEntries = DINEWAY_LOCAL_STATE_GITIGNORE_ENTRIES.filter((entry) => !lines.has(entry));
274
+ if (missingEntries.length === 0) return;
275
+ const block = [DINEWAY_LOCAL_STATE_GITIGNORE_COMMENT, ...missingEntries].join("\n");
276
+ await writeFile(gitignorePath, `${existing}${existing.trim() ? existing.endsWith("\n") ? "\n" : "\n\n" : ""}${block}\n`, "utf-8");
277
+ }
261
278
 
262
279
  //#endregion
263
280
  //#region src/cli/client-factory.ts
@@ -1553,45 +1570,67 @@ const flyTarget = {
1553
1570
 
1554
1571
  //#endregion
1555
1572
  //#region src/cli/commands/deploy/utils/config.ts
1573
+ const DINEWAY_DIR_NAME = ".dineway";
1574
+ const DEPLOY_STATE_FILE_NAME = "deploy.json";
1575
+ function deployStatePath(cwd) {
1576
+ return join(cwd, DINEWAY_DIR_NAME, DEPLOY_STATE_FILE_NAME);
1577
+ }
1578
+ function emptyDeployState() {
1579
+ return { version: 1 };
1580
+ }
1556
1581
  async function readDeployPackageJson(cwd) {
1557
1582
  const content = await readFile(join(cwd, "package.json"), "utf-8");
1558
1583
  return JSON.parse(content);
1559
1584
  }
1585
+ async function readDeployState(cwd) {
1586
+ try {
1587
+ const content = await readFile(deployStatePath(cwd), "utf-8");
1588
+ return {
1589
+ ...emptyDeployState(),
1590
+ ...JSON.parse(content),
1591
+ version: 1
1592
+ };
1593
+ } catch (error) {
1594
+ if (error && typeof error === "object" && error.code === "ENOENT") return emptyDeployState();
1595
+ throw error;
1596
+ }
1597
+ }
1598
+ async function writeDeployState(cwd, state) {
1599
+ await mkdir(join(cwd, DINEWAY_DIR_NAME), { recursive: true });
1600
+ await writeFile(deployStatePath(cwd), JSON.stringify({
1601
+ ...state,
1602
+ version: 1
1603
+ }, null, " ") + "\n", "utf-8");
1604
+ }
1560
1605
  async function readSavedDeployTarget(cwd) {
1561
- return (await readDeployPackageJson(cwd)).dineway?.deploy?.target;
1606
+ return (await readDeployState(cwd)).target;
1562
1607
  }
1563
1608
  async function writeDeployTarget(cwd, target) {
1564
- const pkgPath = join(cwd, "package.json");
1565
- const pkg = await readDeployPackageJson(cwd);
1566
- pkg.dineway = {
1567
- ...pkg.dineway,
1568
- deploy: {
1569
- ...pkg.dineway?.deploy,
1570
- target
1571
- }
1572
- };
1573
- await writeFile(pkgPath, JSON.stringify(pkg, null, " ") + "\n", "utf-8");
1609
+ await writeDeployState(cwd, {
1610
+ ...await readDeployState(cwd),
1611
+ target
1612
+ });
1613
+ }
1614
+ async function readForgewayDeployMetadata(cwd) {
1615
+ return (await readDeployState(cwd)).targets?.forgeway ?? {};
1574
1616
  }
1575
1617
  async function writeForgewayDeployMetadata(cwd, metadata) {
1576
- const pkgPath = join(cwd, "package.json");
1577
- const pkg = await readDeployPackageJson(cwd);
1578
- pkg.dineway = {
1579
- ...pkg.dineway,
1580
- deploy: {
1581
- ...pkg.dineway?.deploy,
1618
+ const state = await readDeployState(cwd);
1619
+ await writeDeployState(cwd, {
1620
+ ...state,
1621
+ targets: {
1622
+ ...state.targets,
1582
1623
  forgeway: {
1583
- ...pkg.dineway?.deploy?.forgeway,
1624
+ ...state.targets?.forgeway,
1584
1625
  ...metadata
1585
1626
  }
1586
1627
  }
1587
- };
1588
- await writeFile(pkgPath, JSON.stringify(pkg, null, " ") + "\n", "utf-8");
1628
+ });
1589
1629
  }
1590
1630
 
1591
1631
  //#endregion
1592
1632
  //#region src/cli/commands/deploy/utils/forgeway-secrets.ts
1593
1633
  const DINEWAY_CONFIG_DIR_NAME = ".dineway";
1594
- const DINEWAY_CONFIG_GITIGNORE = "*\n!.gitignore\n";
1595
1634
  const TRAILING_SLASH_PATTERN$1 = /\/$/;
1596
1635
  function getDinewayConfigDir(cwd = process.cwd()) {
1597
1636
  return join(cwd, DINEWAY_CONFIG_DIR_NAME);
@@ -1608,19 +1647,10 @@ async function readStore(cwd = process.cwd()) {
1608
1647
  }
1609
1648
  }
1610
1649
  async function writeStore(store, cwd = process.cwd()) {
1611
- const dir = getDinewayConfigDir(cwd);
1612
- await mkdir(dir, {
1650
+ await mkdir(getDinewayConfigDir(cwd), {
1613
1651
  recursive: true,
1614
1652
  mode: 448
1615
1653
  });
1616
- try {
1617
- await writeFile(join(dir, ".gitignore"), DINEWAY_CONFIG_GITIGNORE, {
1618
- encoding: "utf-8",
1619
- flag: "wx"
1620
- });
1621
- } catch (error) {
1622
- if (!error || typeof error !== "object" || error.code !== "EEXIST") throw error;
1623
- }
1624
1654
  const file = getForgewaySecretPath(cwd);
1625
1655
  await writeFile(file, JSON.stringify(store, null, " ") + "\n", {
1626
1656
  encoding: "utf-8",
@@ -1665,6 +1695,7 @@ const DATABASE_ENV_VAR_NAMES = ["DINEWAY_DATABASE_URL", "DINEWAY_DATABASE_AUTH_T
1665
1695
  const POLL_INTERVAL_MS$1 = 5e3;
1666
1696
  const POLL_TIMEOUT_MS$1 = 3e5;
1667
1697
  const DIRECT_UPLOAD_CONCURRENCY = 8;
1698
+ const FILE_HASH_CONCURRENCY = 8;
1668
1699
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1669
1700
  const PLACE_ID_PATTERN = /^[A-Za-z0-9_-]{10,}$/;
1670
1701
  const SLUG_SEPARATOR_PATTERN = /[^a-z0-9]+/g;
@@ -1675,6 +1706,10 @@ const LOCATION_PARTS_PATTERN = /[,;|/]+|\s+-\s+/u;
1675
1706
  const JWT_PATTERN = /[A-Za-z0-9_-]{32,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g;
1676
1707
  const TOKEN_ASSIGNMENT_PATTERN = /(DINEWAY_DATABASE_AUTH_TOKEN|DATABASE_AUTH_TOKEN|AUTH_TOKEN)=\S+/gi;
1677
1708
  const TRAILING_SLASH_PATTERN = /\/$/;
1709
+ const LEADING_SLASHES_PATTERN = /^\/+/;
1710
+ const TRAILING_SLASHES_PATTERN = /\/+$/;
1711
+ const REGEXP_ESCAPE_PATTERN = /[|\\{}()[\]^$+?.]/g;
1712
+ const LINE_SPLIT_PATTERN = /\r?\n/;
1678
1713
  const BACKSLASH_PATTERN = /\\/g;
1679
1714
  const SHADOW_EMAIL_PATTERN = /^shadow_[a-f0-9]{16}@dineway\.ai$/i;
1680
1715
  const FOOD_TYPES = new Set([
@@ -1689,6 +1724,8 @@ const EXCLUDE_PATTERNS = [
1689
1724
  "node_modules",
1690
1725
  ".git",
1691
1726
  ".next",
1727
+ ".astro",
1728
+ ".plan",
1692
1729
  ".env",
1693
1730
  ".env.local",
1694
1731
  "dist",
@@ -1710,7 +1747,11 @@ const EXCLUDE_PATTERNS = [
1710
1747
  ".turbo",
1711
1748
  ".cache",
1712
1749
  "skills",
1713
- "coverage"
1750
+ "coverage",
1751
+ "uploads",
1752
+ "data.db",
1753
+ "data.db-shm",
1754
+ "data.db-wal"
1714
1755
  ];
1715
1756
  var ForgewayApiError = class extends Error {
1716
1757
  constructor(message, status) {
@@ -1746,6 +1787,39 @@ function getEnv(name) {
1746
1787
  const value = process.env[name];
1747
1788
  return value && value.trim() ? value.trim() : void 0;
1748
1789
  }
1790
+ function isVerboseDeploy(options) {
1791
+ return options?.verbose === true || getEnv("DINEWAY_DEPLOY_VERBOSE") === "1";
1792
+ }
1793
+ function formatDuration(ms) {
1794
+ if (ms < 1e3) return `${ms}ms`;
1795
+ return `${Math.round(ms / 100) / 10}s`;
1796
+ }
1797
+ function formatBytes$1(bytes) {
1798
+ if (bytes < 1024) return `${bytes} B`;
1799
+ const units = [
1800
+ "KB",
1801
+ "MB",
1802
+ "GB"
1803
+ ];
1804
+ let value = bytes / 1024;
1805
+ for (let index = 0; index < units.length; index++) {
1806
+ const unit = units[index];
1807
+ if (value < 1024 || index === units.length - 1) return `${value.toFixed(value >= 10 ? 0 : 1)} ${unit}`;
1808
+ value /= 1024;
1809
+ }
1810
+ return `${bytes} B`;
1811
+ }
1812
+ function deploymentFileSize(files) {
1813
+ return files.reduce((total, file) => total + file.size, 0);
1814
+ }
1815
+ function formatSeedSummary(seed) {
1816
+ if (!seed) return "no seed";
1817
+ return [
1818
+ `collections ${seed.collections.created}/${seed.collections.skipped}/${seed.collections.updated}`,
1819
+ `content ${seed.content.created}/${seed.content.skipped}/${seed.content.updated}`,
1820
+ `media ${seed.media.created}/${seed.media.skipped}`
1821
+ ].join(", ");
1822
+ }
1749
1823
  async function readJsonResponse(response) {
1750
1824
  return await response.json().catch(() => null);
1751
1825
  }
@@ -1841,8 +1915,8 @@ async function promptRequired(question, initialValue, deps, errorMessage) {
1841
1915
  if (!answer) throw new Error(errorMessage);
1842
1916
  return answer;
1843
1917
  }
1844
- function getSavedForgewayMetadata(pkg) {
1845
- return pkg.dineway?.deploy?.forgeway ?? {};
1918
+ async function readSavedForgewayMetadata(cwd) {
1919
+ return await readForgewayDeployMetadata(cwd).catch(() => ({}));
1846
1920
  }
1847
1921
  async function resolveForgewayAccountEmail(options, grant, cwd, deps) {
1848
1922
  const stored = await (deps.readCredentials ?? readForgewayCredentials)(cwd);
@@ -1920,8 +1994,7 @@ async function resolveFormalAccountCredentials(context, grant, email, deps) {
1920
1994
  return credentials;
1921
1995
  }
1922
1996
  async function resolveShadowProjectContext(cwd, options, deps) {
1923
- const pkg = await readDeployPackageJson(cwd).catch(() => null);
1924
- const saved = pkg ? getSavedForgewayMetadata(pkg) : {};
1997
+ const saved = await readSavedForgewayMetadata(cwd);
1925
1998
  const platformApiUrl = normalizePlatformApiUrl(getEnv("DINEWAY_API_BASE_URL") ?? getEnv("FORGEWAY_API_URL") ?? (typeof saved.platformApiUrl === "string" ? saved.platformApiUrl : void 0));
1926
1999
  const savedPlaceId = options.placeId ?? getEnv("DINEWAY_PLACE_ID") ?? (typeof saved.placeId === "string" ? saved.placeId : void 0);
1927
2000
  let grant = await (deps.readShadowGrant ?? readForgewayShadowGrant)(platformApiUrl, savedPlaceId, cwd);
@@ -2010,8 +2083,12 @@ async function createShadowUser(platformApiUrl, placeId, deps) {
2010
2083
  function isRecord(value) {
2011
2084
  return typeof value === "object" && value !== null && !Array.isArray(value);
2012
2085
  }
2086
+ function scalarText(value) {
2087
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
2088
+ return "";
2089
+ }
2013
2090
  function uniqueStrings(values) {
2014
- return [...new Set(values.map((value) => String(value || "").trim()).filter(Boolean))];
2091
+ return [...new Set(values.map((value) => scalarText(value).trim()).filter(Boolean))];
2015
2092
  }
2016
2093
  function displayText(value) {
2017
2094
  if (typeof value === "string" || typeof value === "number") return String(value);
@@ -2020,10 +2097,10 @@ function displayText(value) {
2020
2097
  return "";
2021
2098
  }
2022
2099
  function normalizeCompare(value) {
2023
- return String(value || "").toLowerCase().normalize("NFKD").replace(DIACRITICS_PATTERN, "").replace(NON_COMPARE_PATTERN, "");
2100
+ return scalarText(value).toLowerCase().normalize("NFKD").replace(DIACRITICS_PATTERN, "").replace(NON_COMPARE_PATTERN, "");
2024
2101
  }
2025
2102
  function candidatePlaceId(candidate) {
2026
- return String(candidate.placeId || candidate.id || "").trim();
2103
+ return (scalarText(candidate.placeId) || scalarText(candidate.id)).trim();
2027
2104
  }
2028
2105
  function candidateNameTexts(candidate) {
2029
2106
  return uniqueStrings([
@@ -2140,7 +2217,7 @@ function scorePlaceCandidate(candidate, context) {
2140
2217
  score += 10;
2141
2218
  reasons.push("food_type");
2142
2219
  }
2143
- const businessStatus = String(candidate.businessStatus ?? candidate.business_status ?? "").toUpperCase();
2220
+ const businessStatus = (scalarText(candidate.businessStatus) || scalarText(candidate.business_status)).toUpperCase();
2144
2221
  if (businessStatus === "OPERATIONAL") {
2145
2222
  score += 4;
2146
2223
  reasons.push("operational");
@@ -2391,8 +2468,7 @@ async function createDeploymentSite(context, input, deps) {
2391
2468
  });
2392
2469
  }
2393
2470
  async function resolveDeploymentSite(cwd, context, options, seedPath, deps, preResolvedRestaurant) {
2394
- const pkg = await readDeployPackageJson(cwd).catch(() => null);
2395
- const saved = pkg ? getSavedForgewayMetadata(pkg) : {};
2471
+ const saved = await readSavedForgewayMetadata(cwd);
2396
2472
  const explicitSite = options.site;
2397
2473
  const savedSite = typeof saved.siteId === "string" ? saved.siteId : saved.siteSlug;
2398
2474
  const siteRef = context.placeId ? explicitSite : explicitSite ?? savedSite;
@@ -2422,20 +2498,50 @@ function redactDatabaseOutput(value, secrets) {
2422
2498
  for (const secret of secrets) if (secret.length > 0) redacted = redacted.split(secret).join("[redacted]");
2423
2499
  return redacted.replace(JWT_PATTERN, "[redacted-token]").replace(TOKEN_ASSIGNMENT_PATTERN, "$1=[redacted]");
2424
2500
  }
2501
+ async function hashFileSha256(filePath) {
2502
+ const hash = createHash("sha256");
2503
+ for await (const chunk of createReadStream(filePath)) hash.update(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2504
+ return `sha256:${hash.digest("hex")}`;
2505
+ }
2506
+ async function buildDinewayInitializationFingerprint(seedPath) {
2507
+ const migration = getMigrationFingerprint();
2508
+ return {
2509
+ version: 1,
2510
+ migrationHash: migration.migrationHash,
2511
+ migrationCount: migration.migrationCount,
2512
+ seedHash: seedPath ? await hashFileSha256(seedPath) : null
2513
+ };
2514
+ }
2515
+ function initializationFingerprintsMatch(left, right) {
2516
+ return left?.version === right.version && left.migrationHash === right.migrationHash && left.migrationCount === right.migrationCount && left.seedHash === right.seedHash;
2517
+ }
2518
+ function localDatabaseInitializationMatches(metadata, siteId, databaseId, fingerprint) {
2519
+ return metadata?.status === "succeeded" && metadata.siteId === siteId && metadata.databaseId === databaseId && initializationFingerprintsMatch(metadata.fingerprint, fingerprint);
2520
+ }
2521
+ async function readLocalDatabaseInitializationMetadata(cwd) {
2522
+ return (await readSavedForgewayMetadata(cwd)).databaseInitialization;
2523
+ }
2524
+ async function writeLocalDatabaseInitializationMetadata(cwd, metadata) {
2525
+ await writeForgewayDeployMetadata(cwd, { databaseInitialization: {
2526
+ ...metadata,
2527
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2528
+ } });
2529
+ }
2425
2530
  async function runDinewayInitialization(options) {
2426
2531
  const db = createDatabase({
2427
2532
  url: options.credentials.databaseUrl,
2428
2533
  authToken: options.credentials.authToken
2429
2534
  });
2430
2535
  try {
2431
- await runMigrations(db);
2536
+ const migrations = await runMigrations(db);
2537
+ let seedResult = null;
2432
2538
  if (options.seedPath) {
2433
2539
  const seed = JSON.parse(await readFile(options.seedPath, "utf-8"));
2434
2540
  const validation = validateSeed(seed);
2435
2541
  if (!validation.valid) throw new Error(`Seed validation failed: ${validation.errors.join("; ")}`);
2436
2542
  const uploadsDir = resolve(options.cwd, "uploads");
2437
2543
  await mkdir(uploadsDir, { recursive: true });
2438
- await applySeed(db, seed, {
2544
+ seedResult = await applySeed(db, seed, {
2439
2545
  includeContent: true,
2440
2546
  onConflict: "skip",
2441
2547
  storage: new LocalStorage({
@@ -2444,6 +2550,10 @@ async function runDinewayInitialization(options) {
2444
2550
  })
2445
2551
  });
2446
2552
  }
2553
+ return {
2554
+ migrations,
2555
+ seed: seedResult
2556
+ };
2447
2557
  } catch (error) {
2448
2558
  const message = error instanceof Error ? error.message : String(error);
2449
2559
  throw new Error(redactDatabaseOutput(message, [options.credentials.authToken, options.credentials.databaseUrl]), { cause: error });
@@ -2474,25 +2584,75 @@ async function recordSiteDatabaseInitialization(context, siteId, databaseId, sta
2474
2584
  });
2475
2585
  }
2476
2586
  async function initializeSiteDatabaseBeforeDeploy(options) {
2587
+ const fingerprint = await buildDinewayInitializationFingerprint(options.seedPath);
2588
+ const bindStart = Date.now();
2477
2589
  const databaseId = (await bindSiteDatabase(options.context, options.siteId, options.database, options.deps)).binding.database.id;
2590
+ if (options.verbose) consola.info(`Database bound in ${formatDuration(Date.now() - bindStart)}`);
2591
+ const localInitialization = await readLocalDatabaseInitializationMetadata(options.cwd);
2592
+ if (!options.force && localDatabaseInitializationMatches(localInitialization, options.siteId, databaseId, fingerprint)) return {
2593
+ status: "skipped",
2594
+ databaseId,
2595
+ fingerprint
2596
+ };
2597
+ const revealStart = Date.now();
2478
2598
  const credentials = await revealDatabaseRuntimeCredentials(options.context, databaseId, options.deps);
2599
+ if (options.verbose) consola.info(`Database credentials revealed in ${formatDuration(Date.now() - revealStart)}`);
2479
2600
  try {
2480
- await (options.deps.runDinewayInitialization ?? runDinewayInitialization)({
2601
+ const result = await (options.deps.runDinewayInitialization ?? runDinewayInitialization)({
2481
2602
  cwd: options.cwd,
2482
2603
  credentials,
2483
2604
  seedPath: options.seedPath
2484
2605
  });
2485
2606
  await recordSiteDatabaseInitialization(options.context, options.siteId, credentials.databaseId || databaseId, "succeeded", void 0, options.deps);
2607
+ await writeLocalDatabaseInitializationMetadata(options.cwd, {
2608
+ siteId: options.siteId,
2609
+ databaseId: credentials.databaseId || databaseId,
2610
+ status: "succeeded",
2611
+ fingerprint
2612
+ });
2613
+ return {
2614
+ status: "initialized",
2615
+ databaseId: credentials.databaseId || databaseId,
2616
+ fingerprint,
2617
+ result
2618
+ };
2486
2619
  } catch (error) {
2487
2620
  const message = redactDatabaseOutput(error instanceof Error ? error.message : String(error), [credentials.authToken, credentials.databaseUrl]);
2488
2621
  await recordSiteDatabaseInitialization(options.context, options.siteId, credentials.databaseId || databaseId, "failed", message, options.deps).catch(() => void 0);
2622
+ await writeLocalDatabaseInitializationMetadata(options.cwd, {
2623
+ siteId: options.siteId,
2624
+ databaseId: credentials.databaseId || databaseId,
2625
+ status: "failed",
2626
+ fingerprint,
2627
+ error: message.slice(0, 2e3)
2628
+ }).catch(() => void 0);
2489
2629
  throw new Error(message, { cause: error });
2490
2630
  }
2491
2631
  }
2492
- function shouldExclude(name) {
2632
+ function normalizeIgnorePattern(pattern) {
2633
+ return pattern.trim().replace(BACKSLASH_PATTERN, "/").replace(LEADING_SLASHES_PATTERN, "").replace(TRAILING_SLASHES_PATTERN, "");
2634
+ }
2635
+ function wildcardPatternToRegExp(pattern) {
2636
+ const source = pattern.split("**").map((part) => part.split("*").map((segment) => segment.replace(REGEXP_ESCAPE_PATTERN, "\\$&")).join("[^/]*")).join(".*");
2637
+ return new RegExp(`^${source}$`);
2638
+ }
2639
+ function matchesIgnorePattern(path, rawPattern) {
2640
+ const pattern = normalizeIgnorePattern(rawPattern);
2641
+ if (!pattern || pattern.startsWith("#")) return false;
2642
+ if (pattern.includes("*")) return wildcardPatternToRegExp(pattern).test(path);
2643
+ if (!pattern.includes("/")) return path === pattern || path.startsWith(`${pattern}/`) || basename(path) === pattern;
2644
+ return path === pattern || path.startsWith(`${pattern}/`);
2645
+ }
2646
+ async function readDeploymentIgnorePatterns(sourceDir) {
2647
+ const ignorePath = join(sourceDir, ".dinewayignore");
2648
+ if (!await fileExists$8(ignorePath)) return [];
2649
+ return (await readFile(ignorePath, "utf-8")).split(LINE_SPLIT_PATTERN).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
2650
+ }
2651
+ function shouldExclude(name, customPatterns = []) {
2493
2652
  const normalized = name.replace(BACKSLASH_PATTERN, "/");
2494
2653
  for (const pattern of EXCLUDE_PATTERNS) if (normalized === pattern || normalized.startsWith(`${pattern}/`) || normalized.endsWith(`/${pattern}`) || normalized.includes(`/${pattern}/`)) return true;
2495
- return normalized.endsWith(".log");
2654
+ if (normalized.endsWith(".log")) return true;
2655
+ return customPatterns.some((pattern) => matchesIgnorePattern(normalized, pattern));
2496
2656
  }
2497
2657
  function normalizeRelativePath(sourceDir, absolutePath) {
2498
2658
  return relative(sourceDir, absolutePath).replace(BACKSLASH_PATTERN, "/");
@@ -2511,6 +2671,8 @@ async function hashFile(filePath) {
2511
2671
  };
2512
2672
  }
2513
2673
  async function collectDeploymentFiles(sourceDir) {
2674
+ const customIgnorePatterns = await readDeploymentIgnorePatterns(sourceDir);
2675
+ const candidates = [];
2514
2676
  const files = [];
2515
2677
  async function walk(currentDir) {
2516
2678
  const entries = await readdir(currentDir, { withFileTypes: true });
@@ -2518,22 +2680,29 @@ async function collectDeploymentFiles(sourceDir) {
2518
2680
  for (const entry of entries) {
2519
2681
  const absolutePath = join(currentDir, entry.name);
2520
2682
  const normalizedPath = normalizeRelativePath(sourceDir, absolutePath);
2521
- if (!normalizedPath || shouldExclude(normalizedPath)) continue;
2683
+ if (!normalizedPath || shouldExclude(normalizedPath, customIgnorePatterns)) continue;
2522
2684
  if (entry.isDirectory()) {
2523
2685
  await walk(absolutePath);
2524
2686
  continue;
2525
2687
  }
2526
2688
  if (!entry.isFile()) continue;
2527
- const { sha, size } = await hashFile(absolutePath);
2528
- files.push({
2689
+ candidates.push({
2529
2690
  absolutePath,
2530
- path: normalizedPath,
2531
- sha,
2532
- size
2691
+ path: normalizedPath
2533
2692
  });
2534
2693
  }
2535
2694
  }
2536
2695
  await walk(sourceDir);
2696
+ await runWithConcurrency(candidates, FILE_HASH_CONCURRENCY, async ({ absolutePath, path }) => {
2697
+ const { sha, size } = await hashFile(absolutePath);
2698
+ files.push({
2699
+ absolutePath,
2700
+ path,
2701
+ sha,
2702
+ size
2703
+ });
2704
+ });
2705
+ files.sort((a, b) => a.path.localeCompare(b.path));
2537
2706
  return files;
2538
2707
  }
2539
2708
  async function runWithConcurrency(items, concurrency, worker) {
@@ -2571,16 +2740,23 @@ function getDeploymentError(metadata) {
2571
2740
  if (!metadata || typeof metadata.error !== "object" || metadata.error === null) return null;
2572
2741
  return metadata.error.errorMessage ?? null;
2573
2742
  }
2574
- async function pollDeployment(context, deploymentId, deps) {
2743
+ async function pollDeployment(context, deploymentId, deps, verbose) {
2575
2744
  const pollIntervalMs = deps.pollIntervalMs ?? POLL_INTERVAL_MS$1;
2576
2745
  const pollTimeoutMs = deps.pollTimeoutMs ?? POLL_TIMEOUT_MS$1;
2577
2746
  const startTime = Date.now();
2578
2747
  let deployment = null;
2748
+ let firstPoll = true;
2749
+ let lastStatus = null;
2579
2750
  while (Date.now() - startTime < pollTimeoutMs) {
2580
- if (pollIntervalMs > 0) await new Promise((done) => setTimeout(done, pollIntervalMs));
2751
+ if (!firstPoll && pollIntervalMs > 0) await new Promise((done) => setTimeout(done, pollIntervalMs));
2752
+ firstPoll = false;
2581
2753
  await ossFetch(context, `/api/deployments/${encodeURIComponent(deploymentId)}/sync`, deps, { method: "POST" }).catch(() => void 0);
2582
2754
  deployment = await ossFetch(context, `/api/deployments/${encodeURIComponent(deploymentId)}`, deps);
2583
2755
  const status = deployment.status.toUpperCase();
2756
+ if (verbose || status !== lastStatus) {
2757
+ consola.info(`Deployment status: ${status} (${formatDuration(Date.now() - startTime)} elapsed)`);
2758
+ lastStatus = status;
2759
+ }
2584
2760
  if (status === "READY") break;
2585
2761
  if (status === "ERROR" || status === "CANCELED") throw new Error(getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`);
2586
2762
  if (pollIntervalMs === 0) break;
@@ -2594,8 +2770,13 @@ async function pollDeployment(context, deploymentId, deps) {
2594
2770
  }
2595
2771
  async function deploySiteProject(options) {
2596
2772
  options.deps.onEvent?.("scan");
2773
+ const scanStart = Date.now();
2774
+ consola.start("Scanning deploy files");
2597
2775
  const localFiles = await (options.deps.collectDeploymentFiles ?? collectDeploymentFiles)(options.sourceDir);
2598
2776
  if (localFiles.length === 0) throw new Error("No deployable files found in the source directory.");
2777
+ consola.success(`Scanned ${localFiles.length} files (${formatBytes$1(deploymentFileSize(localFiles))}) in ${formatDuration(Date.now() - scanStart)}`);
2778
+ const createStart = Date.now();
2779
+ consola.start("Creating Forgeway deployment");
2599
2780
  const createResult = await ossFetch(options.context, `/api/deployments/sites/${encodeURIComponent(options.siteId)}/deploy`, options.deps, {
2600
2781
  method: "POST",
2601
2782
  body: JSON.stringify({
@@ -2607,15 +2788,25 @@ async function deploySiteProject(options) {
2607
2788
  }))
2608
2789
  })
2609
2790
  });
2791
+ if (options.verbose) consola.info(`Deployment manifest accepted in ${formatDuration(Date.now() - createStart)}`);
2610
2792
  const localFileByPath = new Map(localFiles.map((file) => [file.path, file]));
2611
- await runWithConcurrency(createResult.files.filter((file) => !file.uploadedAt), DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
2793
+ const pendingFiles = createResult.files.filter((file) => !file.uploadedAt);
2794
+ const pendingSize = pendingFiles.reduce((total, file) => total + file.size, 0);
2795
+ if (pendingFiles.length === 0) consola.success("No file uploads needed");
2796
+ else consola.start(`Uploading ${pendingFiles.length} changed files (${formatBytes$1(pendingSize)})`);
2797
+ let uploadedCount = 0;
2798
+ await runWithConcurrency(pendingFiles, DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
2612
2799
  const localFile = localFileByPath.get(manifestFile.path);
2613
2800
  if (!localFile) throw new Error(`Forgeway returned an unknown file path: ${manifestFile.path}`);
2614
2801
  if (localFile.sha !== manifestFile.sha || localFile.size !== manifestFile.size) throw new Error(`Forgeway file metadata mismatch for: ${manifestFile.path}`);
2615
2802
  await uploadDirectDeploymentFile(options.context, createResult.id, manifestFile, localFile, options.deps);
2803
+ uploadedCount += 1;
2804
+ if (options.verbose || uploadedCount === pendingFiles.length || uploadedCount % 10 === 0) consola.info(`Uploaded ${uploadedCount}/${pendingFiles.length} files`);
2616
2805
  });
2806
+ consola.start("Starting Forgeway deployment");
2617
2807
  await startDirectDeployment(options.context, createResult.id, options.startBody, options.deps);
2618
- const result = await pollDeployment(options.context, createResult.id, options.deps);
2808
+ consola.start("Waiting for Forgeway deployment");
2809
+ const result = await pollDeployment(options.context, createResult.id, options.deps, options.verbose);
2619
2810
  return {
2620
2811
  deploymentId: createResult.id,
2621
2812
  ...result,
@@ -2632,7 +2823,12 @@ async function persistForgewayCliEnv(cwd, params) {
2632
2823
  }
2633
2824
  function formatDinewayAdminBootstrapMessage(bootstrap) {
2634
2825
  if (!bootstrap) return "";
2635
- if (bootstrap.status === "already_issued") return `Dineway admin bootstrap: ${bootstrap.message}`;
2826
+ if (bootstrap.status === "already_issued") return [
2827
+ "Dineway admin access is ready. Local CLI defaults were saved to .env.",
2828
+ `Dineway admin email: ${bootstrap.adminEmail}`,
2829
+ `Dineway admin bootstrap: ${bootstrap.message}`,
2830
+ "CLI example: npx dineway whoami"
2831
+ ].join("\n");
2636
2832
  return [
2637
2833
  "Dineway admin access is ready. Local CLI defaults were saved to .env.",
2638
2834
  `Dineway admin email: ${bootstrap.adminEmail}`,
@@ -2645,7 +2841,10 @@ async function deployForgeway(cwd, options, deps = {}) {
2645
2841
  const sourceDir = resolve(cwd);
2646
2842
  if (!(await stat(sourceDir).catch(() => null))?.isDirectory()) throw new Error(`"${sourceDir}" is not a valid directory.`);
2647
2843
  if (EXCLUDE_PATTERNS.includes(basename(sourceDir))) throw new Error(`"${basename(sourceDir)}" is an excluded directory and cannot be deployed.`);
2648
- const seedPath = await resolveSeedPath$1(cwd, options.seed);
2844
+ await ensureProjectGitignoreDinewayLocalState(cwd);
2845
+ const verbose = isVerboseDeploy(options);
2846
+ const seedPath = options.skipSeed ? null : await resolveSeedPath$1(cwd, options.seed);
2847
+ if (options.skipSeed) consola.info("Skipping seed initialization by request");
2649
2848
  const { context, restaurant } = await resolveProjectContext(cwd, options, deps);
2650
2849
  await ensureRestaurantClaim(context, restaurant, deps);
2651
2850
  const { site } = await resolveDeploymentSite(cwd, context, options, seedPath, deps, restaurant);
@@ -2654,15 +2853,22 @@ async function deployForgeway(cwd, options, deps = {}) {
2654
2853
  consola.start("Initializing Forgeway managed database");
2655
2854
  try {
2656
2855
  deps.onEvent?.("initialize");
2657
- await initializeSiteDatabaseBeforeDeploy({
2856
+ const initialization = await initializeSiteDatabaseBeforeDeploy({
2658
2857
  context,
2659
2858
  siteId: site.id,
2660
2859
  database,
2661
2860
  cwd,
2662
2861
  seedPath,
2862
+ force: options.forceDbInit === true,
2863
+ verbose,
2663
2864
  deps
2664
2865
  });
2665
- consola.success("Database initialized");
2866
+ if (initialization.status === "skipped") consola.success("Database already initialized for current schema and seed (local metadata)");
2867
+ else {
2868
+ const applied = initialization.result?.migrations.applied.length ?? 0;
2869
+ const seedSummary = formatSeedSummary(initialization.result?.seed);
2870
+ consola.success(`Database initialized: ${applied} migrations applied, ${seedSummary}`);
2871
+ }
2666
2872
  } catch (error) {
2667
2873
  consola.error("Database initialization failed");
2668
2874
  throw error;
@@ -2674,12 +2880,13 @@ async function deployForgeway(cwd, options, deps = {}) {
2674
2880
  siteId: site.id,
2675
2881
  sourceDir,
2676
2882
  startBody: {},
2677
- deps
2883
+ deps,
2884
+ verbose
2678
2885
  });
2679
2886
  const url = deployment.liveUrl ?? `https://${site.domain}`;
2680
2887
  await (deps.writeProjectEnv ?? persistForgewayCliEnv)(cwd, {
2681
2888
  siteUrl: url,
2682
- token: deployment.dinewayAdminBootstrap?.status === "issued" ? deployment.dinewayAdminBootstrap.apiToken : void 0
2889
+ token: deployment.dinewayAdminBootstrap?.apiToken
2683
2890
  });
2684
2891
  const bootstrapMessage = formatDinewayAdminBootstrapMessage(deployment.dinewayAdminBootstrap);
2685
2892
  const statusMessage = deployment.isReady ? `Deploy complete: ${url}` : `Deploy started for ${site.domain}. Check Forgeway for build status.`;
@@ -3246,6 +3453,21 @@ const deployCommand = defineCommand({
3246
3453
  type: "string",
3247
3454
  description: "Seed file path for Forgeway Dineway initialization",
3248
3455
  required: false
3456
+ },
3457
+ "skip-seed": {
3458
+ type: "boolean",
3459
+ description: "Skip applying seed data during Forgeway database initialization",
3460
+ default: false
3461
+ },
3462
+ "force-db-init": {
3463
+ type: "boolean",
3464
+ description: "Force Forgeway database migrations and seed even when initialization fingerprints match",
3465
+ default: false
3466
+ },
3467
+ verbose: {
3468
+ type: "boolean",
3469
+ description: "Show detailed Forgeway deploy progress and timing logs",
3470
+ default: false
3249
3471
  }
3250
3472
  },
3251
3473
  async run({ args }) {
@@ -3262,7 +3484,10 @@ const deployCommand = defineCommand({
3262
3484
  restaurantName: args["restaurant-name"],
3263
3485
  city: args.city,
3264
3486
  database: args.database,
3265
- seed: args.seed
3487
+ seed: args.seed,
3488
+ skipSeed: args["skip-seed"],
3489
+ forceDbInit: args["force-db-init"],
3490
+ verbose: args.verbose
3266
3491
  };
3267
3492
  consola.start(`Preparing ${target.label} deploy`);
3268
3493
  await ensureTargetCli(target, cwd, {
@@ -3290,8 +3515,13 @@ const deployCommand = defineCommand({
3290
3515
  const build = options.dryRun ? null : await runBuild(cwd, options.env);
3291
3516
  consola.info(options.dryRun ? "[dry-run] Would run build" : `Built with ${build?.display}`);
3292
3517
  }
3293
- if (!options.dryRun) await writeDeployTarget(cwd, target.name);
3294
- else consola.info(`[dry-run] Would save dineway.deploy.target=${target.name}`);
3518
+ if (!options.dryRun) {
3519
+ await ensureProjectGitignoreDinewayLocalState(cwd);
3520
+ await writeDeployTarget(cwd, target.name);
3521
+ } else {
3522
+ consola.info("[dry-run] Would ensure Dineway local state entries in .gitignore");
3523
+ consola.info(`[dry-run] Would save .dineway/deploy.json target=${target.name}`);
3524
+ }
3295
3525
  const result = await target.deploy(cwd, options);
3296
3526
  if (result.url) consola.success(result.url);
3297
3527
  consola.success(result.message);