dineway 0.1.34 → 0.1.36

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 (52) hide show
  1. package/README.md +30 -4
  2. package/dist/api/route-utils.d.mts +2 -2
  3. package/dist/api/schemas/index.d.mts +1 -1
  4. package/dist/{api-BaFOFZnE.mjs → api-K0U9SYx7.mjs} +1 -1
  5. package/dist/astro/index.d.mts +2 -2
  6. package/dist/astro/index.mjs +1 -1
  7. package/dist/astro/middleware/auth.d.mts +2 -2
  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 +2 -2
  39. package/dist/{bylines-CeNpt_vk.d.mts → bylines-C8EfvtqH.d.mts} +25 -25
  40. package/dist/cli/index.mjs +532 -114
  41. package/dist/db/index.mjs +1 -1
  42. package/dist/index.d.mts +2 -2
  43. package/dist/index.mjs +3 -3
  44. package/dist/media/local-runtime.d.mts +2 -2
  45. package/dist/plugins/adapt-sandbox-entry.d.mts +2 -2
  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-CP8eY2L-.d.mts → runtime-BM9sqnzO.d.mts} +2 -2
  49. package/dist/runtime.d.mts +2 -2
  50. package/dist/version-BCYrkQqz.mjs +6 -0
  51. package/package.json +1 -1
  52. package/dist/version-HcqOJZFv.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,7 @@ 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
194
  const NEWLINE_PATTERN = /\r?\n/g;
195
195
  function parseDotenvValue(value) {
196
196
  const trimmed = value.trim();
@@ -199,7 +199,7 @@ function parseDotenvValue(value) {
199
199
  }
200
200
  function parseDotenv(text) {
201
201
  const env = {};
202
- for (const line of text.split(LINE_SPLIT_PATTERN)) {
202
+ for (const line of text.split(LINE_SPLIT_PATTERN$1)) {
203
203
  const trimmed = line.trim();
204
204
  if (!trimmed || trimmed.startsWith("#")) continue;
205
205
  const normalized = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
@@ -233,7 +233,7 @@ function formatEnvValue(value) {
233
233
  async function upsertProjectEnv(cwd, values) {
234
234
  const envPath = join(cwd, ".env");
235
235
  const existing = await readFile(envPath, "utf-8").catch(() => "");
236
- const lines = existing ? existing.split(LINE_SPLIT_PATTERN) : [];
236
+ const lines = existing ? existing.split(LINE_SPLIT_PATTERN$1) : [];
237
237
  const seen = /* @__PURE__ */ new Set();
238
238
  const nextLines = lines.map((line) => {
239
239
  const trimmed = line.trim();
@@ -255,7 +255,7 @@ async function upsertProjectEnv(cwd, values) {
255
255
  async function ensureProjectGitignoreEntry(cwd, entry) {
256
256
  const gitignorePath = join(cwd, ".gitignore");
257
257
  const existing = await readFile(gitignorePath, "utf-8").catch(() => "");
258
- if (existing.split(LINE_SPLIT_PATTERN).map((line) => line.trim()).includes(entry)) return;
258
+ if (existing.split(LINE_SPLIT_PATTERN$1).map((line) => line.trim()).includes(entry)) return;
259
259
  await writeFile(gitignorePath, `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${entry}\n`, "utf-8");
260
260
  }
261
261
 
@@ -1637,7 +1637,7 @@ async function writeForgewayCredentials(credentials, cwd) {
1637
1637
  await writeStore(store, cwd);
1638
1638
  }
1639
1639
  function shadowGrantKey(platformApiUrl, placeId) {
1640
- return `${platformApiUrl.replace(TRAILING_SLASH_PATTERN$1, "")}#${placeId}`;
1640
+ return `${platformApiUrl.replace(TRAILING_SLASH_PATTERN$1, "")}#${placeId ?? "__unbound__"}`;
1641
1641
  }
1642
1642
  async function readForgewayShadowGrant(platformApiUrl, placeId, cwd) {
1643
1643
  const store = await readStore(cwd);
@@ -1665,23 +1665,37 @@ const DATABASE_ENV_VAR_NAMES = ["DINEWAY_DATABASE_URL", "DINEWAY_DATABASE_AUTH_T
1665
1665
  const POLL_INTERVAL_MS$1 = 5e3;
1666
1666
  const POLL_TIMEOUT_MS$1 = 3e5;
1667
1667
  const DIRECT_UPLOAD_CONCURRENCY = 8;
1668
- const DEFAULT_BOOTSTRAP_PLACE_ID = "ChIJs5ydyTiuEmsR0fRSlU0C7k0";
1668
+ const FILE_HASH_CONCURRENCY = 8;
1669
1669
  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;
1670
1670
  const PLACE_ID_PATTERN = /^[A-Za-z0-9_-]{10,}$/;
1671
1671
  const SLUG_SEPARATOR_PATTERN = /[^a-z0-9]+/g;
1672
1672
  const LEADING_TRAILING_HYPHEN_PATTERN = /^-+|-+$/g;
1673
+ const DIACRITICS_PATTERN = /[\u0300-\u036f]/g;
1674
+ const NON_COMPARE_PATTERN = /[^\p{Letter}\p{Number}]+/gu;
1675
+ const LOCATION_PARTS_PATTERN = /[,;|/]+|\s+-\s+/u;
1673
1676
  const JWT_PATTERN = /[A-Za-z0-9_-]{32,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g;
1674
1677
  const TOKEN_ASSIGNMENT_PATTERN = /(DINEWAY_DATABASE_AUTH_TOKEN|DATABASE_AUTH_TOKEN|AUTH_TOKEN)=\S+/gi;
1675
1678
  const TRAILING_SLASH_PATTERN = /\/$/;
1679
+ const LEADING_SLASHES_PATTERN = /^\/+/;
1680
+ const TRAILING_SLASHES_PATTERN = /\/+$/;
1681
+ const REGEXP_ESCAPE_PATTERN = /[|\\{}()[\]^$+?.]/g;
1682
+ const LINE_SPLIT_PATTERN = /\r?\n/;
1676
1683
  const BACKSLASH_PATTERN = /\\/g;
1677
- const DIACRITICS_PATTERN = /[\u0300-\u036f]/g;
1678
- const NON_COMPARE_PATTERN = /[^\p{Letter}\p{Number}]+/gu;
1679
- const LOCATION_PARTS_PATTERN = /[,;|/]+|\s+-\s+/u;
1680
1684
  const SHADOW_EMAIL_PATTERN = /^shadow_[a-f0-9]{16}@dineway\.ai$/i;
1685
+ const FOOD_TYPES = new Set([
1686
+ "restaurant",
1687
+ "cafe",
1688
+ "food",
1689
+ "meal_takeaway",
1690
+ "meal_delivery",
1691
+ "bakery"
1692
+ ]);
1681
1693
  const EXCLUDE_PATTERNS = [
1682
1694
  "node_modules",
1683
1695
  ".git",
1684
1696
  ".next",
1697
+ ".astro",
1698
+ ".plan",
1685
1699
  ".env",
1686
1700
  ".env.local",
1687
1701
  "dist",
@@ -1703,7 +1717,11 @@ const EXCLUDE_PATTERNS = [
1703
1717
  ".turbo",
1704
1718
  ".cache",
1705
1719
  "skills",
1706
- "coverage"
1720
+ "coverage",
1721
+ "uploads",
1722
+ "data.db",
1723
+ "data.db-shm",
1724
+ "data.db-wal"
1707
1725
  ];
1708
1726
  var ForgewayApiError = class extends Error {
1709
1727
  constructor(message, status) {
@@ -1739,6 +1757,39 @@ function getEnv(name) {
1739
1757
  const value = process.env[name];
1740
1758
  return value && value.trim() ? value.trim() : void 0;
1741
1759
  }
1760
+ function isVerboseDeploy(options) {
1761
+ return options?.verbose === true || getEnv("DINEWAY_DEPLOY_VERBOSE") === "1";
1762
+ }
1763
+ function formatDuration(ms) {
1764
+ if (ms < 1e3) return `${ms}ms`;
1765
+ return `${Math.round(ms / 100) / 10}s`;
1766
+ }
1767
+ function formatBytes$1(bytes) {
1768
+ if (bytes < 1024) return `${bytes} B`;
1769
+ const units = [
1770
+ "KB",
1771
+ "MB",
1772
+ "GB"
1773
+ ];
1774
+ let value = bytes / 1024;
1775
+ for (let index = 0; index < units.length; index++) {
1776
+ const unit = units[index];
1777
+ if (value < 1024 || index === units.length - 1) return `${value.toFixed(value >= 10 ? 0 : 1)} ${unit}`;
1778
+ value /= 1024;
1779
+ }
1780
+ return `${bytes} B`;
1781
+ }
1782
+ function deploymentFileSize(files) {
1783
+ return files.reduce((total, file) => total + file.size, 0);
1784
+ }
1785
+ function formatSeedSummary(seed) {
1786
+ if (!seed) return "no seed";
1787
+ return [
1788
+ `collections ${seed.collections.created}/${seed.collections.skipped}/${seed.collections.updated}`,
1789
+ `content ${seed.content.created}/${seed.content.skipped}/${seed.content.updated}`,
1790
+ `media ${seed.media.created}/${seed.media.skipped}`
1791
+ ].join(", ");
1792
+ }
1742
1793
  async function readJsonResponse(response) {
1743
1794
  return await response.json().catch(() => null);
1744
1795
  }
@@ -1792,7 +1843,7 @@ async function refreshFormalAccessToken(context, deps) {
1792
1843
  return payload.accessToken;
1793
1844
  }
1794
1845
  async function refreshShadowAccessToken(context, deps) {
1795
- if (!context.refreshToken || !context.placeId) throw new Error("Dineway shadow deploy grant is missing its refresh token.");
1846
+ if (!context.refreshToken) throw new Error("Dineway shadow deploy grant is missing its refresh token.");
1796
1847
  const response = await forgewayRequest(`${context.platformApiUrl}/api/auth/refresh?client_type=server`, {
1797
1848
  method: "POST",
1798
1849
  headers: { "Content-Type": "application/json" },
@@ -1849,7 +1900,6 @@ function isStoredFormalCredentialForGrant(stored, grant, email) {
1849
1900
  if (normalizeEmail(stored.user.email) !== email) return false;
1850
1901
  if (stored.user.emailVerified !== true) return false;
1851
1902
  if (isShadowEmail(stored.user.email)) return false;
1852
- if (grant.user?.id && stored.user.id !== grant.user.id) return false;
1853
1903
  return true;
1854
1904
  }
1855
1905
  function assertVerifiedFormalAccountUser(user, expectedEmail) {
@@ -1917,7 +1967,7 @@ async function resolveShadowProjectContext(cwd, options, deps) {
1917
1967
  const pkg = await readDeployPackageJson(cwd).catch(() => null);
1918
1968
  const saved = pkg ? getSavedForgewayMetadata(pkg) : {};
1919
1969
  const platformApiUrl = normalizePlatformApiUrl(getEnv("DINEWAY_API_BASE_URL") ?? getEnv("FORGEWAY_API_URL") ?? (typeof saved.platformApiUrl === "string" ? saved.platformApiUrl : void 0));
1920
- const savedPlaceId = getEnv("DINEWAY_PLACE_ID") ?? (typeof saved.placeId === "string" ? saved.placeId : void 0);
1970
+ const savedPlaceId = options.placeId ?? getEnv("DINEWAY_PLACE_ID") ?? (typeof saved.placeId === "string" ? saved.placeId : void 0);
1921
1971
  let grant = await (deps.readShadowGrant ?? readForgewayShadowGrant)(platformApiUrl, savedPlaceId, cwd);
1922
1972
  let restaurant;
1923
1973
  if (!grant) {
@@ -1936,6 +1986,28 @@ async function resolveShadowProjectContext(cwd, options, deps) {
1936
1986
  name: options.restaurantName ?? grant.restaurantName,
1937
1987
  city: options.city ?? grant.city
1938
1988
  };
1989
+ if (!grant.placeId) {
1990
+ const shadowRestaurant = restaurant?.name && restaurant.city ? {
1991
+ name: restaurant.name,
1992
+ city: restaurant.city
1993
+ } : await resolveShadowRestaurantInfo(options, deps);
1994
+ restaurant = shadowRestaurant;
1995
+ grant = savedPlaceId ? await bindShadowGrantPlace({
1996
+ platformApiUrl,
1997
+ grant,
1998
+ placeId: savedPlaceId,
1999
+ restaurant: shadowRestaurant,
2000
+ cwd,
2001
+ deps
2002
+ }) : await resolvePlaceIdWithShadowSearch({
2003
+ platformApiUrl,
2004
+ grant,
2005
+ restaurant: shadowRestaurant,
2006
+ cwd,
2007
+ deps
2008
+ });
2009
+ }
2010
+ if (!grant.placeId) throw new Error("Could not resolve a Dineway place id for Forgeway deploy.");
1939
2011
  const shadowContext = {
1940
2012
  projectDir: cwd,
1941
2013
  platformApiUrl,
@@ -1965,107 +2037,280 @@ async function resolveShadowProjectContext(cwd, options, deps) {
1965
2037
  async function resolveProjectContext(cwd, options, deps) {
1966
2038
  return await resolveShadowProjectContext(cwd, options, deps);
1967
2039
  }
2040
+ async function createShadowUser(platformApiUrl, placeId, deps) {
2041
+ const response = await forgewayRequest(`${platformApiUrl}/api/auth/users/shadow`, {
2042
+ method: "POST",
2043
+ headers: { "Content-Type": "application/json" },
2044
+ body: JSON.stringify({
2045
+ ...placeId ? { placeId } : {},
2046
+ language: "en"
2047
+ })
2048
+ }, deps);
2049
+ if (!response.ok) throw new Error(await parseErrorResponse(response));
2050
+ const payload = await response.json();
2051
+ if (!payload.accessToken || !payload.refreshToken) throw new Error("Dineway shadow user response was missing tokens.");
2052
+ return payload;
2053
+ }
1968
2054
  function isRecord(value) {
1969
- return typeof value === "object" && value !== null;
2055
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2056
+ }
2057
+ function uniqueStrings(values) {
2058
+ return [...new Set(values.map((value) => String(value || "").trim()).filter(Boolean))];
1970
2059
  }
1971
- function getText(value) {
2060
+ function displayText(value) {
1972
2061
  if (typeof value === "string" || typeof value === "number") return String(value);
1973
- if (Array.isArray(value)) return value.map(getText).filter(Boolean).join(" ");
1974
- if (isRecord(value)) return getText(value.text) || getText(value.name) || getText(value.displayName) || getText(value.longText) || getText(value.shortText) || getText(value.value);
2062
+ if (Array.isArray(value)) return value.map(displayText).filter(Boolean).join(" ");
2063
+ if (isRecord(value)) return displayText(value.text) || displayText(value.name) || displayText(value.displayName) || displayText(value.longText) || displayText(value.shortText) || displayText(value.long_name) || displayText(value.short_name) || displayText(value.value);
1975
2064
  return "";
1976
2065
  }
1977
- function compareText(value) {
1978
- return value.toLowerCase().normalize("NFKD").replace(DIACRITICS_PATTERN, "").replace(NON_COMPARE_PATTERN, "");
2066
+ function normalizeCompare(value) {
2067
+ return String(value || "").toLowerCase().normalize("NFKD").replace(DIACRITICS_PATTERN, "").replace(NON_COMPARE_PATTERN, "");
1979
2068
  }
1980
2069
  function candidatePlaceId(candidate) {
1981
2070
  return String(candidate.placeId || candidate.id || "").trim();
1982
2071
  }
1983
- function candidateNames(candidate) {
1984
- return [
1985
- getText(candidate.displayName),
1986
- getText(candidate.name),
1987
- getText(candidate.businessName),
1988
- getText(candidate.title)
1989
- ].filter(Boolean);
2072
+ function candidateNameTexts(candidate) {
2073
+ return uniqueStrings([
2074
+ displayText(candidate.displayName),
2075
+ displayText(candidate.name),
2076
+ displayText(candidate.businessName),
2077
+ displayText(candidate.title)
2078
+ ]);
2079
+ }
2080
+ function candidateAddressTexts(candidate) {
2081
+ const addressComponents = Array.isArray(candidate.addressComponents) ? candidate.addressComponents.flatMap((component) => isRecord(component) ? [
2082
+ displayText(component.longText),
2083
+ displayText(component.shortText),
2084
+ displayText(component.long_name),
2085
+ displayText(component.short_name),
2086
+ displayText(component.name)
2087
+ ] : []) : [];
2088
+ return uniqueStrings([
2089
+ displayText(candidate.formattedAddress),
2090
+ displayText(candidate.shortFormattedAddress),
2091
+ displayText(candidate.address),
2092
+ displayText(candidate.vicinity),
2093
+ displayText(candidate.plusCode?.compoundCode),
2094
+ displayText(candidate.plus_code?.compound_code),
2095
+ ...addressComponents
2096
+ ]);
2097
+ }
2098
+ function candidateTypes(candidate) {
2099
+ return uniqueStrings([
2100
+ ...Array.isArray(candidate.types) ? candidate.types : [],
2101
+ candidate.primaryType,
2102
+ candidate.primary_type
2103
+ ]).map((type) => type.toLowerCase());
2104
+ }
2105
+ function locationParts(value) {
2106
+ return uniqueStrings(value.split(LOCATION_PARTS_PATTERN).map((part) => normalizeCompare(part)).filter(Boolean));
2107
+ }
2108
+ function scoreNameMatch(queryName, candidateNames) {
2109
+ let bestScore = 0;
2110
+ let bestReason = "";
2111
+ for (const name of candidateNames) {
2112
+ const currentName = normalizeCompare(name);
2113
+ if (!queryName || !currentName) continue;
2114
+ let score = 0;
2115
+ let reason = "";
2116
+ if (currentName === queryName) {
2117
+ score = 120;
2118
+ reason = "exact_name";
2119
+ } else if (currentName.includes(queryName)) {
2120
+ score = 80;
2121
+ reason = "candidate_contains_name";
2122
+ } else if (queryName.includes(currentName) && currentName.length >= Math.min(4, queryName.length)) {
2123
+ score = 45;
2124
+ reason = "query_contains_candidate_name";
2125
+ }
2126
+ if (score > bestScore) {
2127
+ bestScore = score;
2128
+ bestReason = reason;
2129
+ }
2130
+ }
2131
+ return {
2132
+ score: bestScore,
2133
+ reason: bestReason
2134
+ };
1990
2135
  }
1991
- function candidateLocationText(candidate) {
1992
- return [
1993
- getText(candidate.formattedAddress),
1994
- getText(candidate.shortFormattedAddress),
1995
- getText(candidate.address),
1996
- getText(candidate.vicinity),
1997
- getText(candidate.plusCode),
1998
- getText(candidate.addressComponents)
1999
- ].filter(Boolean).join(" ");
2000
- }
2001
- function selectPlaceCandidate(candidates, restaurantName, city) {
2002
- const targetName = compareText(restaurantName);
2003
- const cityParts = city.split(LOCATION_PARTS_PATTERN).map((part) => compareText(part)).filter(Boolean);
2136
+ function scoreLocationMatch(city, candidate) {
2137
+ const queryLocation = normalizeCompare(city);
2138
+ const parts = locationParts(city);
2139
+ const candidateLocation = normalizeCompare(candidateAddressTexts(candidate).join(" "));
2140
+ let score = 0;
2141
+ const reasons = [];
2142
+ if (!candidateLocation) return {
2143
+ score,
2144
+ reasons
2145
+ };
2146
+ if (queryLocation && candidateLocation.includes(queryLocation)) return {
2147
+ score: 55,
2148
+ reasons: ["location_full_match"]
2149
+ };
2150
+ const [primaryPart, ...secondaryParts] = parts;
2151
+ if (primaryPart && candidateLocation.includes(primaryPart)) {
2152
+ score += 38;
2153
+ reasons.push("location_primary_match");
2154
+ }
2155
+ for (const part of secondaryParts) if (part.length >= 3 && candidateLocation.includes(part)) {
2156
+ score += 10;
2157
+ reasons.push("location_secondary_match");
2158
+ }
2159
+ return {
2160
+ score,
2161
+ reasons
2162
+ };
2163
+ }
2164
+ function scorePlaceCandidate(candidate, context) {
2165
+ const queryName = normalizeCompare(context.name);
2166
+ const currentPlaceId = candidatePlaceId(candidate);
2167
+ let score = 0;
2168
+ const reasons = [];
2169
+ if (context.providedPlaceId && currentPlaceId === context.providedPlaceId) {
2170
+ score += 1e4;
2171
+ reasons.push("provided_place_id");
2172
+ }
2173
+ const nameMatch = scoreNameMatch(queryName, candidateNameTexts(candidate));
2174
+ if (nameMatch.score) {
2175
+ score += nameMatch.score;
2176
+ reasons.push(nameMatch.reason);
2177
+ }
2178
+ const locationMatch = scoreLocationMatch(context.city, candidate);
2179
+ if (locationMatch.score) {
2180
+ score += locationMatch.score;
2181
+ reasons.push(...locationMatch.reasons);
2182
+ }
2183
+ if (candidateTypes(candidate).some((type) => FOOD_TYPES.has(type))) {
2184
+ score += 10;
2185
+ reasons.push("food_type");
2186
+ }
2187
+ const businessStatus = String(candidate.businessStatus ?? candidate.business_status ?? "").toUpperCase();
2188
+ if (businessStatus === "OPERATIONAL") {
2189
+ score += 4;
2190
+ reasons.push("operational");
2191
+ } else if (businessStatus === "CLOSED_PERMANENTLY") {
2192
+ score -= 60;
2193
+ reasons.push("closed_permanently");
2194
+ }
2195
+ const rating = Number(candidate.rating);
2196
+ if (Number.isFinite(rating)) score += Math.min(Math.max(rating, 0), 5);
2197
+ const reviewCount = Number(candidate.userRatingCount ?? candidate.user_ratings_total);
2198
+ if (Number.isFinite(reviewCount)) score += Math.min(Math.max(reviewCount, 0), 500) / 100;
2199
+ return {
2200
+ score,
2201
+ reason: reasons.join("+") || "fallback"
2202
+ };
2203
+ }
2204
+ function selectPlaceCandidate(candidates, context) {
2004
2205
  let best = null;
2206
+ let bestScore = Number.NEGATIVE_INFINITY;
2207
+ let bestReason = "fallback";
2005
2208
  for (const candidate of candidates) {
2006
- if (!candidatePlaceId(candidate)) continue;
2007
- const names = candidateNames(candidate).map(compareText);
2008
- const location = compareText(candidateLocationText(candidate));
2009
- let score = 0;
2010
- if (names.some((name) => name === targetName)) score += 80;
2011
- else if (names.some((name) => name.includes(targetName) || targetName.includes(name))) score += 55;
2012
- score += cityParts.filter((part) => location.includes(part)).length * 20;
2013
- const rating = typeof candidate.rating === "number" ? candidate.rating : Number(candidate.rating ?? 0);
2014
- if (rating > 0) score += Math.min(rating, 5);
2015
- const reviewCount = typeof candidate.userRatingCount === "number" ? candidate.userRatingCount : Number(candidate.userRatingCount ?? 0);
2016
- if (reviewCount > 0) score += Math.min(Math.log10(reviewCount + 1), 4);
2017
- if (!best || score > best.score) best = {
2018
- candidate,
2019
- score
2020
- };
2021
- }
2022
- if (!best) throw new Error(`Could not find a deployable place for "${restaurantName}" in "${city}".`);
2023
- return best.candidate;
2209
+ const { score, reason } = scorePlaceCandidate(candidate, context);
2210
+ if (score > bestScore) {
2211
+ best = candidate;
2212
+ bestScore = score;
2213
+ bestReason = reason;
2214
+ }
2215
+ }
2216
+ return best ? {
2217
+ candidate: best,
2218
+ score: bestScore,
2219
+ reason: bestReason
2220
+ } : null;
2221
+ }
2222
+ function extractPlaceCandidates(payload) {
2223
+ const root = isRecord(payload) ? payload : {};
2224
+ const nestedData = isRecord(root.data) ? root.data : {};
2225
+ const places = root.places ?? root.results ?? nestedData.places ?? nestedData.results;
2226
+ if (!Array.isArray(places)) throw new Error("Dineway place search response did not include places/results.");
2227
+ return places.filter(isRecord);
2228
+ }
2229
+ function shadowContextFromGrant(cwd, platformApiUrl, grant, restaurant) {
2230
+ return {
2231
+ projectDir: cwd,
2232
+ platformApiUrl,
2233
+ authKind: "shadow-grant",
2234
+ accessToken: grant.accessToken,
2235
+ refreshToken: grant.refreshToken,
2236
+ user: grant.user,
2237
+ ossHost: platformApiUrl,
2238
+ placeId: grant.placeId,
2239
+ restaurantName: restaurant?.name ?? grant.restaurantName,
2240
+ city: restaurant?.city ?? grant.city
2241
+ };
2024
2242
  }
2025
- function extractPlaces(payload) {
2026
- return (isRecord(payload) ? Array.isArray(payload.places) ? payload.places : Array.isArray(payload.results) ? payload.results : isRecord(payload.data) && Array.isArray(payload.data.places) ? payload.data.places : isRecord(payload.data) && Array.isArray(payload.data.results) ? payload.data.results : [] : []).filter(isRecord);
2243
+ function grantFromShadowContext(context, grant, overrides = {}) {
2244
+ return {
2245
+ ...grant,
2246
+ platformApiUrl: context.platformApiUrl,
2247
+ accessToken: context.accessToken ?? grant.accessToken,
2248
+ refreshToken: context.refreshToken ?? grant.refreshToken,
2249
+ placeId: context.placeId ?? grant.placeId,
2250
+ restaurantName: context.restaurantName ?? grant.restaurantName,
2251
+ city: context.city ?? grant.city,
2252
+ user: context.user ?? grant.user,
2253
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2254
+ ...overrides
2255
+ };
2027
2256
  }
2028
- async function createShadowUser(platformApiUrl, placeId, deps) {
2029
- const response = await forgewayRequest(`${platformApiUrl}/api/auth/users/shadow`, {
2257
+ async function searchPlacesWithShadowGrant(options) {
2258
+ const context = shadowContextFromGrant(options.cwd, options.platformApiUrl, options.grant, options.restaurant);
2259
+ const payload = await shadowUpgradeFetch(context, "/api/places/search", options.deps, {
2260
+ method: "POST",
2261
+ body: JSON.stringify({ textQuery: `${options.restaurant.name} in ${options.restaurant.city}` })
2262
+ });
2263
+ return {
2264
+ grant: grantFromShadowContext(context, options.grant),
2265
+ candidates: extractPlaceCandidates(payload)
2266
+ };
2267
+ }
2268
+ async function bindShadowGrantPlace(options) {
2269
+ const context = shadowContextFromGrant(options.cwd, options.platformApiUrl, options.grant, options.restaurant);
2270
+ const payload = await shadowUpgradeFetch(context, "/api/auth/users/shadow/place", options.deps, {
2030
2271
  method: "POST",
2031
- headers: { "Content-Type": "application/json" },
2032
2272
  body: JSON.stringify({
2033
- placeId,
2273
+ placeId: options.placeId,
2034
2274
  language: "en"
2035
2275
  })
2036
- }, deps);
2037
- if (!response.ok) throw new Error(await parseErrorResponse(response));
2038
- const payload = await response.json();
2039
- if (!payload.accessToken || !payload.refreshToken) throw new Error("Dineway shadow user response was missing tokens.");
2040
- return payload;
2276
+ });
2277
+ const grant = grantFromShadowContext(context, options.grant, {
2278
+ placeId: options.placeId,
2279
+ accessToken: payload.accessToken ?? context.accessToken ?? options.grant.accessToken,
2280
+ refreshToken: payload.refreshToken ?? context.refreshToken ?? options.grant.refreshToken,
2281
+ restaurantName: options.restaurant.name,
2282
+ city: options.restaurant.city,
2283
+ user: payload.user ? normalizeForgewayUser(payload.user) : context.user ?? options.grant.user
2284
+ });
2285
+ await (options.deps.writeShadowGrant ?? writeForgewayShadowGrant)(grant, options.cwd);
2286
+ return grant;
2041
2287
  }
2042
- async function searchPlaces(platformApiUrl, accessToken, restaurantName, city, deps) {
2043
- const response = await forgewayRequest(`${platformApiUrl}/api/places/search`, {
2044
- method: "POST",
2045
- headers: {
2046
- "Content-Type": "application/json",
2047
- Authorization: `Bearer ${accessToken}`
2048
- },
2049
- body: JSON.stringify({ textQuery: `${restaurantName} in ${city}` })
2050
- }, deps);
2051
- if (!response.ok) throw new Error(await parseErrorResponse(response));
2052
- return extractPlaces(await response.json());
2288
+ async function resolvePlaceIdWithShadowSearch(options) {
2289
+ const { grant, candidates } = await searchPlacesWithShadowGrant(options);
2290
+ if (candidates.length === 0) throw new Error(`Dineway place search returned no candidates for "${options.restaurant.name} in ${options.restaurant.city}".`);
2291
+ const selected = selectPlaceCandidate(candidates, {
2292
+ name: options.restaurant.name,
2293
+ city: options.restaurant.city
2294
+ });
2295
+ const finalPlaceId = selected ? candidatePlaceId(selected.candidate) : "";
2296
+ if (!PLACE_ID_PATTERN.test(finalPlaceId)) throw new Error("Could not resolve a valid Dineway place id for Forgeway deploy.");
2297
+ return await bindShadowGrantPlace({
2298
+ ...options,
2299
+ grant,
2300
+ placeId: finalPlaceId
2301
+ });
2053
2302
  }
2054
2303
  async function createShadowGrant(options) {
2055
- const initialPlaceId = options.placeId || DEFAULT_BOOTSTRAP_PLACE_ID;
2056
- const initialAuth = await createShadowUser(options.platformApiUrl, initialPlaceId, options.deps);
2057
- let finalPlaceId = options.placeId;
2058
- if (!finalPlaceId) finalPlaceId = candidatePlaceId(selectPlaceCandidate(await searchPlaces(options.platformApiUrl, initialAuth.accessToken, options.restaurantName, options.city, options.deps), options.restaurantName, options.city));
2059
- if (!PLACE_ID_PATTERN.test(finalPlaceId)) throw new Error("Could not resolve a valid Dineway place id for Forgeway deploy.");
2060
- const finalAuth = finalPlaceId === initialPlaceId ? initialAuth : await createShadowUser(options.platformApiUrl, finalPlaceId, options.deps);
2304
+ if (options.placeId && !PLACE_ID_PATTERN.test(options.placeId)) throw new Error("Could not resolve a valid Dineway place id for Forgeway deploy.");
2305
+ const auth = await createShadowUser(options.platformApiUrl, options.placeId, options.deps);
2061
2306
  const grant = {
2062
2307
  platformApiUrl: options.platformApiUrl,
2063
- placeId: finalPlaceId,
2064
- accessToken: finalAuth.accessToken,
2065
- refreshToken: finalAuth.refreshToken,
2308
+ ...options.placeId ? { placeId: options.placeId } : {},
2309
+ accessToken: auth.accessToken,
2310
+ refreshToken: auth.refreshToken,
2066
2311
  restaurantName: options.restaurantName,
2067
2312
  city: options.city,
2068
- user: finalAuth.user ? normalizeForgewayUser(finalAuth.user) : void 0,
2313
+ user: auth.user ? normalizeForgewayUser(auth.user) : void 0,
2069
2314
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2070
2315
  };
2071
2316
  await (options.deps.writeShadowGrant ?? writeForgewayShadowGrant)(grant, options.cwd);
@@ -2158,6 +2403,25 @@ async function getDeploymentSite(context, site, deps) {
2158
2403
  throw error;
2159
2404
  }
2160
2405
  }
2406
+ async function getDeploymentSiteByPlaceId(context, placeId, deps) {
2407
+ try {
2408
+ return normalizeSiteDetail(await ossFetch(context, `/api/deployments/sites/by-place/${encodeURIComponent(placeId)}`, deps));
2409
+ } catch (error) {
2410
+ if (error instanceof ForgewayApiError && error.status === 404) return null;
2411
+ throw error;
2412
+ }
2413
+ }
2414
+ async function ensureRestaurantClaim(context, restaurant, deps) {
2415
+ if (!context.placeId) throw new Error("Forgeway restaurant deploy is missing a place ID for claim ownership.");
2416
+ await ossFetch(context, "/api/restaurant-claims/ensure", deps, {
2417
+ method: "POST",
2418
+ body: JSON.stringify({
2419
+ placeId: context.placeId,
2420
+ ...restaurant?.name ? { placeName: restaurant.name } : {},
2421
+ ...restaurant?.city ? { city: restaurant.city } : {}
2422
+ })
2423
+ });
2424
+ }
2161
2425
  async function createDeploymentSite(context, input, deps) {
2162
2426
  return await ossFetch(context, "/api/deployments/sites", deps, {
2163
2427
  method: "POST",
@@ -2175,10 +2439,12 @@ async function resolveDeploymentSite(cwd, context, options, seedPath, deps, preR
2175
2439
  const saved = pkg ? getSavedForgewayMetadata(pkg) : {};
2176
2440
  const explicitSite = options.site;
2177
2441
  const savedSite = typeof saved.siteId === "string" ? saved.siteId : saved.siteSlug;
2178
- const siteRef = explicitSite ?? savedSite;
2179
- const restaurant = preResolvedRestaurant?.name && preResolvedRestaurant.city ? preResolvedRestaurant : await resolveRestaurantInfo(options, seedPath, deps, !siteRef);
2442
+ const siteRef = context.placeId ? explicitSite : explicitSite ?? savedSite;
2443
+ const existingByPlace = context.placeId ? await getDeploymentSiteByPlaceId(context, context.placeId, deps) : null;
2444
+ const restaurant = preResolvedRestaurant?.name && preResolvedRestaurant.city ? preResolvedRestaurant : await resolveRestaurantInfo(options, seedPath, deps, !existingByPlace && !siteRef);
2180
2445
  const targetSlug = siteRef ?? deriveForgewaySiteSlug(restaurant.name || "", restaurant.city || "");
2181
- let site = await getDeploymentSite(context, targetSlug, deps);
2446
+ let site = existingByPlace;
2447
+ if (!site && siteRef) site = await getDeploymentSite(context, targetSlug, deps);
2182
2448
  if (!site) site = await createDeploymentSite(context, {
2183
2449
  slug: targetSlug,
2184
2450
  restaurant,
@@ -2200,20 +2466,52 @@ function redactDatabaseOutput(value, secrets) {
2200
2466
  for (const secret of secrets) if (secret.length > 0) redacted = redacted.split(secret).join("[redacted]");
2201
2467
  return redacted.replace(JWT_PATTERN, "[redacted-token]").replace(TOKEN_ASSIGNMENT_PATTERN, "$1=[redacted]");
2202
2468
  }
2469
+ async function hashFileSha256(filePath) {
2470
+ const hash = createHash("sha256");
2471
+ for await (const chunk of createReadStream(filePath)) hash.update(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2472
+ return `sha256:${hash.digest("hex")}`;
2473
+ }
2474
+ async function buildDinewayInitializationFingerprint(seedPath) {
2475
+ const migration = getMigrationFingerprint();
2476
+ return {
2477
+ version: 1,
2478
+ migrationHash: migration.migrationHash,
2479
+ migrationCount: migration.migrationCount,
2480
+ seedHash: seedPath ? await hashFileSha256(seedPath) : null
2481
+ };
2482
+ }
2483
+ function initializationFingerprintsMatch(left, right) {
2484
+ return left?.version === right.version && left.migrationHash === right.migrationHash && left.migrationCount === right.migrationCount && left.seedHash === right.seedHash;
2485
+ }
2486
+ function localDatabaseInitializationMatches(metadata, siteId, databaseId, fingerprint) {
2487
+ return metadata?.status === "succeeded" && metadata.siteId === siteId && metadata.databaseId === databaseId && initializationFingerprintsMatch(metadata.fingerprint, fingerprint);
2488
+ }
2489
+ async function readLocalDatabaseInitializationMetadata(cwd) {
2490
+ const pkg = await readDeployPackageJson(cwd).catch(() => null);
2491
+ if (!pkg) return void 0;
2492
+ return getSavedForgewayMetadata(pkg).databaseInitialization;
2493
+ }
2494
+ async function writeLocalDatabaseInitializationMetadata(cwd, metadata) {
2495
+ await writeForgewayDeployMetadata(cwd, { databaseInitialization: {
2496
+ ...metadata,
2497
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2498
+ } });
2499
+ }
2203
2500
  async function runDinewayInitialization(options) {
2204
2501
  const db = createDatabase({
2205
2502
  url: options.credentials.databaseUrl,
2206
2503
  authToken: options.credentials.authToken
2207
2504
  });
2208
2505
  try {
2209
- await runMigrations(db);
2506
+ const migrations = await runMigrations(db);
2507
+ let seedResult = null;
2210
2508
  if (options.seedPath) {
2211
2509
  const seed = JSON.parse(await readFile(options.seedPath, "utf-8"));
2212
2510
  const validation = validateSeed(seed);
2213
2511
  if (!validation.valid) throw new Error(`Seed validation failed: ${validation.errors.join("; ")}`);
2214
2512
  const uploadsDir = resolve(options.cwd, "uploads");
2215
2513
  await mkdir(uploadsDir, { recursive: true });
2216
- await applySeed(db, seed, {
2514
+ seedResult = await applySeed(db, seed, {
2217
2515
  includeContent: true,
2218
2516
  onConflict: "skip",
2219
2517
  storage: new LocalStorage({
@@ -2222,6 +2520,10 @@ async function runDinewayInitialization(options) {
2222
2520
  })
2223
2521
  });
2224
2522
  }
2523
+ return {
2524
+ migrations,
2525
+ seed: seedResult
2526
+ };
2225
2527
  } catch (error) {
2226
2528
  const message = error instanceof Error ? error.message : String(error);
2227
2529
  throw new Error(redactDatabaseOutput(message, [options.credentials.authToken, options.credentials.databaseUrl]), { cause: error });
@@ -2252,25 +2554,75 @@ async function recordSiteDatabaseInitialization(context, siteId, databaseId, sta
2252
2554
  });
2253
2555
  }
2254
2556
  async function initializeSiteDatabaseBeforeDeploy(options) {
2557
+ const fingerprint = await buildDinewayInitializationFingerprint(options.seedPath);
2558
+ const bindStart = Date.now();
2255
2559
  const databaseId = (await bindSiteDatabase(options.context, options.siteId, options.database, options.deps)).binding.database.id;
2560
+ if (options.verbose) consola.info(`Database bound in ${formatDuration(Date.now() - bindStart)}`);
2561
+ const localInitialization = await readLocalDatabaseInitializationMetadata(options.cwd);
2562
+ if (!options.force && localDatabaseInitializationMatches(localInitialization, options.siteId, databaseId, fingerprint)) return {
2563
+ status: "skipped",
2564
+ databaseId,
2565
+ fingerprint
2566
+ };
2567
+ const revealStart = Date.now();
2256
2568
  const credentials = await revealDatabaseRuntimeCredentials(options.context, databaseId, options.deps);
2569
+ if (options.verbose) consola.info(`Database credentials revealed in ${formatDuration(Date.now() - revealStart)}`);
2257
2570
  try {
2258
- await (options.deps.runDinewayInitialization ?? runDinewayInitialization)({
2571
+ const result = await (options.deps.runDinewayInitialization ?? runDinewayInitialization)({
2259
2572
  cwd: options.cwd,
2260
2573
  credentials,
2261
2574
  seedPath: options.seedPath
2262
2575
  });
2263
2576
  await recordSiteDatabaseInitialization(options.context, options.siteId, credentials.databaseId || databaseId, "succeeded", void 0, options.deps);
2577
+ await writeLocalDatabaseInitializationMetadata(options.cwd, {
2578
+ siteId: options.siteId,
2579
+ databaseId: credentials.databaseId || databaseId,
2580
+ status: "succeeded",
2581
+ fingerprint
2582
+ });
2583
+ return {
2584
+ status: "initialized",
2585
+ databaseId: credentials.databaseId || databaseId,
2586
+ fingerprint,
2587
+ result
2588
+ };
2264
2589
  } catch (error) {
2265
2590
  const message = redactDatabaseOutput(error instanceof Error ? error.message : String(error), [credentials.authToken, credentials.databaseUrl]);
2266
2591
  await recordSiteDatabaseInitialization(options.context, options.siteId, credentials.databaseId || databaseId, "failed", message, options.deps).catch(() => void 0);
2592
+ await writeLocalDatabaseInitializationMetadata(options.cwd, {
2593
+ siteId: options.siteId,
2594
+ databaseId: credentials.databaseId || databaseId,
2595
+ status: "failed",
2596
+ fingerprint,
2597
+ error: message.slice(0, 2e3)
2598
+ }).catch(() => void 0);
2267
2599
  throw new Error(message, { cause: error });
2268
2600
  }
2269
2601
  }
2270
- function shouldExclude(name) {
2602
+ function normalizeIgnorePattern(pattern) {
2603
+ return pattern.trim().replace(BACKSLASH_PATTERN, "/").replace(LEADING_SLASHES_PATTERN, "").replace(TRAILING_SLASHES_PATTERN, "");
2604
+ }
2605
+ function wildcardPatternToRegExp(pattern) {
2606
+ const source = pattern.split("**").map((part) => part.split("*").map((segment) => segment.replace(REGEXP_ESCAPE_PATTERN, "\\$&")).join("[^/]*")).join(".*");
2607
+ return new RegExp(`^${source}$`);
2608
+ }
2609
+ function matchesIgnorePattern(path, rawPattern) {
2610
+ const pattern = normalizeIgnorePattern(rawPattern);
2611
+ if (!pattern || pattern.startsWith("#")) return false;
2612
+ if (pattern.includes("*")) return wildcardPatternToRegExp(pattern).test(path);
2613
+ if (!pattern.includes("/")) return path === pattern || path.startsWith(`${pattern}/`) || basename(path) === pattern;
2614
+ return path === pattern || path.startsWith(`${pattern}/`);
2615
+ }
2616
+ async function readDeploymentIgnorePatterns(sourceDir) {
2617
+ const ignorePath = join(sourceDir, ".dinewayignore");
2618
+ if (!await fileExists$8(ignorePath)) return [];
2619
+ return (await readFile(ignorePath, "utf-8")).split(LINE_SPLIT_PATTERN).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
2620
+ }
2621
+ function shouldExclude(name, customPatterns = []) {
2271
2622
  const normalized = name.replace(BACKSLASH_PATTERN, "/");
2272
2623
  for (const pattern of EXCLUDE_PATTERNS) if (normalized === pattern || normalized.startsWith(`${pattern}/`) || normalized.endsWith(`/${pattern}`) || normalized.includes(`/${pattern}/`)) return true;
2273
- return normalized.endsWith(".log");
2624
+ if (normalized.endsWith(".log")) return true;
2625
+ return customPatterns.some((pattern) => matchesIgnorePattern(normalized, pattern));
2274
2626
  }
2275
2627
  function normalizeRelativePath(sourceDir, absolutePath) {
2276
2628
  return relative(sourceDir, absolutePath).replace(BACKSLASH_PATTERN, "/");
@@ -2289,6 +2641,8 @@ async function hashFile(filePath) {
2289
2641
  };
2290
2642
  }
2291
2643
  async function collectDeploymentFiles(sourceDir) {
2644
+ const customIgnorePatterns = await readDeploymentIgnorePatterns(sourceDir);
2645
+ const candidates = [];
2292
2646
  const files = [];
2293
2647
  async function walk(currentDir) {
2294
2648
  const entries = await readdir(currentDir, { withFileTypes: true });
@@ -2296,22 +2650,29 @@ async function collectDeploymentFiles(sourceDir) {
2296
2650
  for (const entry of entries) {
2297
2651
  const absolutePath = join(currentDir, entry.name);
2298
2652
  const normalizedPath = normalizeRelativePath(sourceDir, absolutePath);
2299
- if (!normalizedPath || shouldExclude(normalizedPath)) continue;
2653
+ if (!normalizedPath || shouldExclude(normalizedPath, customIgnorePatterns)) continue;
2300
2654
  if (entry.isDirectory()) {
2301
2655
  await walk(absolutePath);
2302
2656
  continue;
2303
2657
  }
2304
2658
  if (!entry.isFile()) continue;
2305
- const { sha, size } = await hashFile(absolutePath);
2306
- files.push({
2659
+ candidates.push({
2307
2660
  absolutePath,
2308
- path: normalizedPath,
2309
- sha,
2310
- size
2661
+ path: normalizedPath
2311
2662
  });
2312
2663
  }
2313
2664
  }
2314
2665
  await walk(sourceDir);
2666
+ await runWithConcurrency(candidates, FILE_HASH_CONCURRENCY, async ({ absolutePath, path }) => {
2667
+ const { sha, size } = await hashFile(absolutePath);
2668
+ files.push({
2669
+ absolutePath,
2670
+ path,
2671
+ sha,
2672
+ size
2673
+ });
2674
+ });
2675
+ files.sort((a, b) => a.path.localeCompare(b.path));
2315
2676
  return files;
2316
2677
  }
2317
2678
  async function runWithConcurrency(items, concurrency, worker) {
@@ -2349,16 +2710,23 @@ function getDeploymentError(metadata) {
2349
2710
  if (!metadata || typeof metadata.error !== "object" || metadata.error === null) return null;
2350
2711
  return metadata.error.errorMessage ?? null;
2351
2712
  }
2352
- async function pollDeployment(context, deploymentId, deps) {
2713
+ async function pollDeployment(context, deploymentId, deps, verbose) {
2353
2714
  const pollIntervalMs = deps.pollIntervalMs ?? POLL_INTERVAL_MS$1;
2354
2715
  const pollTimeoutMs = deps.pollTimeoutMs ?? POLL_TIMEOUT_MS$1;
2355
2716
  const startTime = Date.now();
2356
2717
  let deployment = null;
2718
+ let firstPoll = true;
2719
+ let lastStatus = null;
2357
2720
  while (Date.now() - startTime < pollTimeoutMs) {
2358
- if (pollIntervalMs > 0) await new Promise((done) => setTimeout(done, pollIntervalMs));
2721
+ if (!firstPoll && pollIntervalMs > 0) await new Promise((done) => setTimeout(done, pollIntervalMs));
2722
+ firstPoll = false;
2359
2723
  await ossFetch(context, `/api/deployments/${encodeURIComponent(deploymentId)}/sync`, deps, { method: "POST" }).catch(() => void 0);
2360
2724
  deployment = await ossFetch(context, `/api/deployments/${encodeURIComponent(deploymentId)}`, deps);
2361
2725
  const status = deployment.status.toUpperCase();
2726
+ if (verbose || status !== lastStatus) {
2727
+ consola.info(`Deployment status: ${status} (${formatDuration(Date.now() - startTime)} elapsed)`);
2728
+ lastStatus = status;
2729
+ }
2362
2730
  if (status === "READY") break;
2363
2731
  if (status === "ERROR" || status === "CANCELED") throw new Error(getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`);
2364
2732
  if (pollIntervalMs === 0) break;
@@ -2372,8 +2740,13 @@ async function pollDeployment(context, deploymentId, deps) {
2372
2740
  }
2373
2741
  async function deploySiteProject(options) {
2374
2742
  options.deps.onEvent?.("scan");
2743
+ const scanStart = Date.now();
2744
+ consola.start("Scanning deploy files");
2375
2745
  const localFiles = await (options.deps.collectDeploymentFiles ?? collectDeploymentFiles)(options.sourceDir);
2376
2746
  if (localFiles.length === 0) throw new Error("No deployable files found in the source directory.");
2747
+ consola.success(`Scanned ${localFiles.length} files (${formatBytes$1(deploymentFileSize(localFiles))}) in ${formatDuration(Date.now() - scanStart)}`);
2748
+ const createStart = Date.now();
2749
+ consola.start("Creating Forgeway deployment");
2377
2750
  const createResult = await ossFetch(options.context, `/api/deployments/sites/${encodeURIComponent(options.siteId)}/deploy`, options.deps, {
2378
2751
  method: "POST",
2379
2752
  body: JSON.stringify({
@@ -2385,15 +2758,25 @@ async function deploySiteProject(options) {
2385
2758
  }))
2386
2759
  })
2387
2760
  });
2761
+ if (options.verbose) consola.info(`Deployment manifest accepted in ${formatDuration(Date.now() - createStart)}`);
2388
2762
  const localFileByPath = new Map(localFiles.map((file) => [file.path, file]));
2389
- await runWithConcurrency(createResult.files.filter((file) => !file.uploadedAt), DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
2763
+ const pendingFiles = createResult.files.filter((file) => !file.uploadedAt);
2764
+ const pendingSize = pendingFiles.reduce((total, file) => total + file.size, 0);
2765
+ if (pendingFiles.length === 0) consola.success("No file uploads needed");
2766
+ else consola.start(`Uploading ${pendingFiles.length} changed files (${formatBytes$1(pendingSize)})`);
2767
+ let uploadedCount = 0;
2768
+ await runWithConcurrency(pendingFiles, DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
2390
2769
  const localFile = localFileByPath.get(manifestFile.path);
2391
2770
  if (!localFile) throw new Error(`Forgeway returned an unknown file path: ${manifestFile.path}`);
2392
2771
  if (localFile.sha !== manifestFile.sha || localFile.size !== manifestFile.size) throw new Error(`Forgeway file metadata mismatch for: ${manifestFile.path}`);
2393
2772
  await uploadDirectDeploymentFile(options.context, createResult.id, manifestFile, localFile, options.deps);
2773
+ uploadedCount += 1;
2774
+ if (options.verbose || uploadedCount === pendingFiles.length || uploadedCount % 10 === 0) consola.info(`Uploaded ${uploadedCount}/${pendingFiles.length} files`);
2394
2775
  });
2776
+ consola.start("Starting Forgeway deployment");
2395
2777
  await startDirectDeployment(options.context, createResult.id, options.startBody, options.deps);
2396
- const result = await pollDeployment(options.context, createResult.id, options.deps);
2778
+ consola.start("Waiting for Forgeway deployment");
2779
+ const result = await pollDeployment(options.context, createResult.id, options.deps, options.verbose);
2397
2780
  return {
2398
2781
  deploymentId: createResult.id,
2399
2782
  ...result,
@@ -2423,23 +2806,33 @@ async function deployForgeway(cwd, options, deps = {}) {
2423
2806
  const sourceDir = resolve(cwd);
2424
2807
  if (!(await stat(sourceDir).catch(() => null))?.isDirectory()) throw new Error(`"${sourceDir}" is not a valid directory.`);
2425
2808
  if (EXCLUDE_PATTERNS.includes(basename(sourceDir))) throw new Error(`"${basename(sourceDir)}" is an excluded directory and cannot be deployed.`);
2426
- const seedPath = await resolveSeedPath$1(cwd, options.seed);
2809
+ const verbose = isVerboseDeploy(options);
2810
+ const seedPath = options.skipSeed ? null : await resolveSeedPath$1(cwd, options.seed);
2811
+ if (options.skipSeed) consola.info("Skipping seed initialization by request");
2427
2812
  const { context, restaurant } = await resolveProjectContext(cwd, options, deps);
2813
+ await ensureRestaurantClaim(context, restaurant, deps);
2428
2814
  const { site } = await resolveDeploymentSite(cwd, context, options, seedPath, deps, restaurant);
2429
2815
  const database = parseDatabaseMode(options.database);
2430
2816
  if (database !== "none") {
2431
2817
  consola.start("Initializing Forgeway managed database");
2432
2818
  try {
2433
2819
  deps.onEvent?.("initialize");
2434
- await initializeSiteDatabaseBeforeDeploy({
2820
+ const initialization = await initializeSiteDatabaseBeforeDeploy({
2435
2821
  context,
2436
2822
  siteId: site.id,
2437
2823
  database,
2438
2824
  cwd,
2439
2825
  seedPath,
2826
+ force: options.forceDbInit === true,
2827
+ verbose,
2440
2828
  deps
2441
2829
  });
2442
- consola.success("Database initialized");
2830
+ if (initialization.status === "skipped") consola.success("Database already initialized for current schema and seed (local metadata)");
2831
+ else {
2832
+ const applied = initialization.result?.migrations.applied.length ?? 0;
2833
+ const seedSummary = formatSeedSummary(initialization.result?.seed);
2834
+ consola.success(`Database initialized: ${applied} migrations applied, ${seedSummary}`);
2835
+ }
2443
2836
  } catch (error) {
2444
2837
  consola.error("Database initialization failed");
2445
2838
  throw error;
@@ -2451,7 +2844,8 @@ async function deployForgeway(cwd, options, deps = {}) {
2451
2844
  siteId: site.id,
2452
2845
  sourceDir,
2453
2846
  startBody: {},
2454
- deps
2847
+ deps,
2848
+ verbose
2455
2849
  });
2456
2850
  const url = deployment.liveUrl ?? `https://${site.domain}`;
2457
2851
  await (deps.writeProjectEnv ?? persistForgewayCliEnv)(cwd, {
@@ -2994,6 +3388,11 @@ const deployCommand = defineCommand({
2994
3388
  description: "Forgeway deployment site ID or slug",
2995
3389
  required: false
2996
3390
  },
3391
+ "place-id": {
3392
+ type: "string",
3393
+ description: "Restaurant Google place ID for Forgeway claim and site recovery",
3394
+ required: false
3395
+ },
2997
3396
  email: {
2998
3397
  type: "string",
2999
3398
  description: "Dineway account email for deploy authorization",
@@ -3018,6 +3417,21 @@ const deployCommand = defineCommand({
3018
3417
  type: "string",
3019
3418
  description: "Seed file path for Forgeway Dineway initialization",
3020
3419
  required: false
3420
+ },
3421
+ "skip-seed": {
3422
+ type: "boolean",
3423
+ description: "Skip applying seed data during Forgeway database initialization",
3424
+ default: false
3425
+ },
3426
+ "force-db-init": {
3427
+ type: "boolean",
3428
+ description: "Force Forgeway database migrations and seed even when initialization fingerprints match",
3429
+ default: false
3430
+ },
3431
+ verbose: {
3432
+ type: "boolean",
3433
+ description: "Show detailed Forgeway deploy progress and timing logs",
3434
+ default: false
3021
3435
  }
3022
3436
  },
3023
3437
  async run({ args }) {
@@ -3029,11 +3443,15 @@ const deployCommand = defineCommand({
3029
3443
  yes: args.yes,
3030
3444
  dryRun: args["dry-run"],
3031
3445
  site: args.site,
3446
+ placeId: args["place-id"],
3032
3447
  email: args.email,
3033
3448
  restaurantName: args["restaurant-name"],
3034
3449
  city: args.city,
3035
3450
  database: args.database,
3036
- seed: args.seed
3451
+ seed: args.seed,
3452
+ skipSeed: args["skip-seed"],
3453
+ forceDbInit: args["force-db-init"],
3454
+ verbose: args.verbose
3037
3455
  };
3038
3456
  consola.start(`Preparing ${target.label} deploy`);
3039
3457
  await ensureTargetCli(target, cwd, {