@open-mercato/cli 0.6.5-develop.5116.1.f0af9e5080 → 0.6.5-develop.5155.1.148d10a46d

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 (56) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/bin.js +1 -0
  3. package/dist/bin.js.map +2 -2
  4. package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.meta.js +7 -0
  5. package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.meta.js.map +7 -0
  6. package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.spec.js +63 -0
  7. package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.spec.js.map +7 -0
  8. package/dist/lib/deploy/railway/env.js +98 -0
  9. package/dist/lib/deploy/railway/env.js.map +7 -0
  10. package/dist/lib/deploy/railway/graphql-client.js +99 -0
  11. package/dist/lib/deploy/railway/graphql-client.js.map +7 -0
  12. package/dist/lib/deploy/railway/index.js +1115 -0
  13. package/dist/lib/deploy/railway/index.js.map +7 -0
  14. package/dist/lib/deploy/railway/operations.js +249 -0
  15. package/dist/lib/deploy/railway/operations.js.map +7 -0
  16. package/dist/lib/deploy/railway/options.js +155 -0
  17. package/dist/lib/deploy/railway/options.js.map +7 -0
  18. package/dist/lib/deploy/railway/railway-cli.js +61 -0
  19. package/dist/lib/deploy/railway/railway-cli.js.map +7 -0
  20. package/dist/lib/deploy/railway/redaction.js +88 -0
  21. package/dist/lib/deploy/railway/redaction.js.map +7 -0
  22. package/dist/lib/deploy/railway/source.js +128 -0
  23. package/dist/lib/deploy/railway/source.js.map +7 -0
  24. package/dist/lib/deploy/railway/state.js +41 -0
  25. package/dist/lib/deploy/railway/state.js.map +7 -0
  26. package/dist/lib/deploy/railway/token.js +46 -0
  27. package/dist/lib/deploy/railway/token.js.map +7 -0
  28. package/dist/lib/deploy/railway/types.js +1 -0
  29. package/dist/lib/deploy/railway/types.js.map +7 -0
  30. package/dist/mercato.js +23 -8
  31. package/dist/mercato.js.map +2 -2
  32. package/package.json +10 -10
  33. package/src/bin.ts +1 -0
  34. package/src/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.meta.ts +3 -0
  35. package/src/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.spec.ts +71 -0
  36. package/src/lib/deploy/railway/__tests__/dry-run.test.ts +61 -0
  37. package/src/lib/deploy/railway/__tests__/env.test.ts +55 -0
  38. package/src/lib/deploy/railway/__tests__/graphql-client.test.ts +80 -0
  39. package/src/lib/deploy/railway/__tests__/operations.test.ts +21 -0
  40. package/src/lib/deploy/railway/__tests__/options.test.ts +78 -0
  41. package/src/lib/deploy/railway/__tests__/redaction.test.ts +62 -0
  42. package/src/lib/deploy/railway/__tests__/source.test.ts +130 -0
  43. package/src/lib/deploy/railway/__tests__/state.test.ts +24 -0
  44. package/src/lib/deploy/railway/__tests__/token.test.ts +39 -0
  45. package/src/lib/deploy/railway/env.ts +112 -0
  46. package/src/lib/deploy/railway/graphql-client.ts +128 -0
  47. package/src/lib/deploy/railway/index.ts +1454 -0
  48. package/src/lib/deploy/railway/operations.ts +248 -0
  49. package/src/lib/deploy/railway/options.ts +167 -0
  50. package/src/lib/deploy/railway/railway-cli.ts +65 -0
  51. package/src/lib/deploy/railway/redaction.ts +91 -0
  52. package/src/lib/deploy/railway/source.ts +143 -0
  53. package/src/lib/deploy/railway/state.ts +45 -0
  54. package/src/lib/deploy/railway/token.ts +49 -0
  55. package/src/lib/deploy/railway/types.ts +87 -0
  56. package/src/mercato.ts +33 -8
@@ -0,0 +1,61 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { redactText } from "./redaction.js";
3
+ function runRailway(args, cwd, token, secrets) {
4
+ const result = spawnSync("railway", args, {
5
+ cwd,
6
+ encoding: "utf8",
7
+ env: { ...process.env, RAILWAY_API_TOKEN: token },
8
+ stdio: ["ignore", "pipe", "pipe"]
9
+ });
10
+ const output = `${result.stdout ?? ""}
11
+ ${result.stderr ?? ""}`.trim();
12
+ if (result.status !== 0) {
13
+ throw new Error(redactText(output || `railway ${args[0] ?? ""} failed`, secrets));
14
+ }
15
+ return output;
16
+ }
17
+ function uploadLocalSource(input) {
18
+ runRailway(
19
+ [
20
+ "link",
21
+ "--project",
22
+ input.projectId,
23
+ "--environment",
24
+ input.environmentId,
25
+ "--service",
26
+ input.serviceId
27
+ ],
28
+ input.cwd,
29
+ input.token,
30
+ [input.token]
31
+ );
32
+ const output = runRailway(
33
+ [
34
+ "up",
35
+ "--environment",
36
+ input.environmentId,
37
+ "--service",
38
+ input.serviceName,
39
+ "--detach",
40
+ "--json"
41
+ ],
42
+ input.cwd,
43
+ input.token,
44
+ [input.token]
45
+ );
46
+ try {
47
+ const parsed = JSON.parse(output);
48
+ if (parsed && typeof parsed === "object") {
49
+ const record = parsed;
50
+ const deploymentId = record.deploymentId ?? record.deployment_id ?? record.id;
51
+ return typeof deploymentId === "string" ? deploymentId : null;
52
+ }
53
+ } catch {
54
+ return null;
55
+ }
56
+ return null;
57
+ }
58
+ export {
59
+ uploadLocalSource
60
+ };
61
+ //# sourceMappingURL=railway-cli.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/lib/deploy/railway/railway-cli.ts"],
4
+ "sourcesContent": ["import { spawnSync } from 'node:child_process'\nimport { redactText } from './redaction'\n\nfunction runRailway(args: string[], cwd: string, token: string, secrets: string[]): string {\n const result = spawnSync('railway', args, {\n cwd,\n encoding: 'utf8',\n env: { ...process.env, RAILWAY_API_TOKEN: token },\n stdio: ['ignore', 'pipe', 'pipe'],\n })\n const output = `${result.stdout ?? ''}\\n${result.stderr ?? ''}`.trim()\n if (result.status !== 0) {\n throw new Error(redactText(output || `railway ${args[0] ?? ''} failed`, secrets))\n }\n return output\n}\n\nexport function uploadLocalSource(input: {\n cwd: string\n projectId: string\n environmentId: string\n serviceId: string\n serviceName: string\n token: string\n}): string | null {\n runRailway(\n [\n 'link',\n '--project',\n input.projectId,\n '--environment',\n input.environmentId,\n '--service',\n input.serviceId,\n ],\n input.cwd,\n input.token,\n [input.token],\n )\n const output = runRailway(\n [\n 'up',\n '--environment',\n input.environmentId,\n '--service',\n input.serviceName,\n '--detach',\n '--json',\n ],\n input.cwd,\n input.token,\n [input.token],\n )\n try {\n const parsed: unknown = JSON.parse(output)\n if (parsed && typeof parsed === 'object') {\n const record = parsed as Record<string, unknown>\n const deploymentId = record.deploymentId ?? record.deployment_id ?? record.id\n return typeof deploymentId === 'string' ? deploymentId : null\n }\n } catch {\n return null\n }\n return null\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAE3B,SAAS,WAAW,MAAgB,KAAa,OAAe,SAA2B;AACzF,QAAM,SAAS,UAAU,WAAW,MAAM;AAAA,IACxC;AAAA,IACA,UAAU;AAAA,IACV,KAAK,EAAE,GAAG,QAAQ,KAAK,mBAAmB,MAAM;AAAA,IAChD,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,EAClC,CAAC;AACD,QAAM,SAAS,GAAG,OAAO,UAAU,EAAE;AAAA,EAAK,OAAO,UAAU,EAAE,GAAG,KAAK;AACrE,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,MAAM,WAAW,UAAU,WAAW,KAAK,CAAC,KAAK,EAAE,WAAW,OAAO,CAAC;AAAA,EAClF;AACA,SAAO;AACT;AAEO,SAAS,kBAAkB,OAOhB;AAChB;AAAA,IACE;AAAA,MACE;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA,MAAM;AAAA,IACR;AAAA,IACA,MAAM;AAAA,IACN,MAAM;AAAA,IACN,CAAC,MAAM,KAAK;AAAA,EACd;AACA,QAAM,SAAS;AAAA,IACb;AAAA,MACE;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA;AAAA,IACF;AAAA,IACA,MAAM;AAAA,IACN,MAAM;AAAA,IACN,CAAC,MAAM,KAAK;AAAA,EACd;AACA,MAAI;AACF,UAAM,SAAkB,KAAK,MAAM,MAAM;AACzC,QAAI,UAAU,OAAO,WAAW,UAAU;AACxC,YAAM,SAAS;AACf,YAAM,eAAe,OAAO,gBAAgB,OAAO,iBAAiB,OAAO;AAC3E,aAAO,OAAO,iBAAiB,WAAW,eAAe;AAAA,IAC3D;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,88 @@
1
+ import { createHash } from "node:crypto";
2
+ const SENSITIVE_KEY_PATTERN = /(secret|token|password|private|credential|encryption|fallback|api[_-]?key|database_url|redis_url)/i;
3
+ const CREDENTIAL_PATTERNS = [
4
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
5
+ /\bsk-[A-Za-z0-9_-]{12,}\b/,
6
+ /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/,
7
+ /\bgh[pousr]_[A-Za-z0-9]{20,}\b/,
8
+ /\bAKIA[0-9A-Z]{16}\b/
9
+ ];
10
+ function fingerprintSecret(value) {
11
+ return `sha256:${createHash("sha256").update(value).digest("hex").slice(0, 8)}`;
12
+ }
13
+ function isSensitiveKey(key) {
14
+ return SENSITIVE_KEY_PATTERN.test(key);
15
+ }
16
+ function detectCredentialValue(value, railwayToken) {
17
+ if (railwayToken && value.includes(railwayToken)) return true;
18
+ return CREDENTIAL_PATTERNS.some((pattern) => pattern.test(value));
19
+ }
20
+ function assertSafeVariables(variables, options = {}) {
21
+ const allowedKeys = new Set(options.allowedKeys ?? []);
22
+ for (const [key, value] of Object.entries(variables)) {
23
+ if (!allowedKeys.has(key) && detectCredentialValue(value, options.railwayToken)) {
24
+ throw new Error(
25
+ `Refusing to upload credential-like value from ${key}. Pass --allow-secret-passthrough ${key} to allow this exact key.`
26
+ );
27
+ }
28
+ }
29
+ }
30
+ function formatVariableValue(key, value, railwayToken) {
31
+ if (isSensitiveKey(key) || detectCredentialValue(value, railwayToken)) {
32
+ return `<redacted> (${fingerprintSecret(value)})`;
33
+ }
34
+ return value;
35
+ }
36
+ function redactText(value, secrets = []) {
37
+ let redacted = value;
38
+ for (const secret of secrets.filter(Boolean).sort((left, right) => right.length - left.length)) {
39
+ redacted = redacted.split(secret).join("****");
40
+ }
41
+ for (const pattern of CREDENTIAL_PATTERNS) {
42
+ redacted = redacted.replace(new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`), "****");
43
+ }
44
+ return redacted.replace(/Bearer\s+[A-Za-z0-9._~+/-]+/gi, "Bearer ****");
45
+ }
46
+ function redactStructuredValue(value, secrets = []) {
47
+ if (typeof value === "string") return redactText(value, secrets);
48
+ if (Array.isArray(value)) {
49
+ return value.map((item) => redactStructuredValue(item, secrets));
50
+ }
51
+ if (!value || typeof value !== "object") return value;
52
+ return Object.fromEntries(
53
+ Object.entries(value).map(([key, nestedValue]) => [
54
+ key,
55
+ isSensitiveKey(key) ? "<redacted>" : redactStructuredValue(nestedValue, secrets)
56
+ ])
57
+ );
58
+ }
59
+ function collectSensitiveStructuredValues(value) {
60
+ const collected = /* @__PURE__ */ new Set();
61
+ function visit(current) {
62
+ if (Array.isArray(current)) {
63
+ current.forEach(visit);
64
+ return;
65
+ }
66
+ if (!current || typeof current !== "object") return;
67
+ for (const [key, nestedValue] of Object.entries(current)) {
68
+ if (isSensitiveKey(key) && typeof nestedValue === "string" && nestedValue.length > 0) {
69
+ collected.add(nestedValue);
70
+ } else {
71
+ visit(nestedValue);
72
+ }
73
+ }
74
+ }
75
+ visit(value);
76
+ return Array.from(collected);
77
+ }
78
+ export {
79
+ assertSafeVariables,
80
+ collectSensitiveStructuredValues,
81
+ detectCredentialValue,
82
+ fingerprintSecret,
83
+ formatVariableValue,
84
+ isSensitiveKey,
85
+ redactStructuredValue,
86
+ redactText
87
+ };
88
+ //# sourceMappingURL=redaction.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/lib/deploy/railway/redaction.ts"],
4
+ "sourcesContent": ["import { createHash } from 'node:crypto'\n\nconst SENSITIVE_KEY_PATTERN = /(secret|token|password|private|credential|encryption|fallback|api[_-]?key|database_url|redis_url)/i\nconst CREDENTIAL_PATTERNS = [\n /-----BEGIN [A-Z ]*PRIVATE KEY-----/,\n /\\bsk-[A-Za-z0-9_-]{12,}\\b/,\n /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/,\n /\\bgh[pousr]_[A-Za-z0-9]{20,}\\b/,\n /\\bAKIA[0-9A-Z]{16}\\b/,\n]\n\nexport function fingerprintSecret(value: string): string {\n return `sha256:${createHash('sha256').update(value).digest('hex').slice(0, 8)}`\n}\n\nexport function isSensitiveKey(key: string): boolean {\n return SENSITIVE_KEY_PATTERN.test(key)\n}\n\nexport function detectCredentialValue(value: string, railwayToken?: string): boolean {\n if (railwayToken && value.includes(railwayToken)) return true\n return CREDENTIAL_PATTERNS.some((pattern) => pattern.test(value))\n}\n\nexport function assertSafeVariables(\n variables: Record<string, string>,\n options: { railwayToken?: string; allowedKeys?: string[] } = {},\n): void {\n const allowedKeys = new Set(options.allowedKeys ?? [])\n for (const [key, value] of Object.entries(variables)) {\n if (!allowedKeys.has(key) && detectCredentialValue(value, options.railwayToken)) {\n throw new Error(\n `Refusing to upload credential-like value from ${key}. Pass --allow-secret-passthrough ${key} to allow this exact key.`,\n )\n }\n }\n}\n\nexport function formatVariableValue(key: string, value: string, railwayToken?: string): string {\n if (isSensitiveKey(key) || detectCredentialValue(value, railwayToken)) {\n return `<redacted> (${fingerprintSecret(value)})`\n }\n return value\n}\n\nexport function redactText(value: string, secrets: string[] = []): string {\n let redacted = value\n for (const secret of secrets.filter(Boolean).sort((left, right) => right.length - left.length)) {\n redacted = redacted.split(secret).join('****')\n }\n for (const pattern of CREDENTIAL_PATTERNS) {\n redacted = redacted.replace(new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`), '****')\n }\n return redacted.replace(/Bearer\\s+[A-Za-z0-9._~+/-]+/gi, 'Bearer ****')\n}\n\nexport function redactStructuredValue(value: unknown, secrets: string[] = []): unknown {\n if (typeof value === 'string') return redactText(value, secrets)\n if (Array.isArray(value)) {\n return value.map((item) => redactStructuredValue(item, secrets))\n }\n if (!value || typeof value !== 'object') return value\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, nestedValue]) => [\n key,\n isSensitiveKey(key) ? '<redacted>' : redactStructuredValue(nestedValue, secrets),\n ]),\n )\n}\n\nexport function collectSensitiveStructuredValues(value: unknown): string[] {\n const collected = new Set<string>()\n\n function visit(current: unknown): void {\n if (Array.isArray(current)) {\n current.forEach(visit)\n return\n }\n if (!current || typeof current !== 'object') return\n for (const [key, nestedValue] of Object.entries(current as Record<string, unknown>)) {\n if (isSensitiveKey(key) && typeof nestedValue === 'string' && nestedValue.length > 0) {\n collected.add(nestedValue)\n } else {\n visit(nestedValue)\n }\n }\n }\n\n visit(value)\n return Array.from(collected)\n}\n"],
5
+ "mappings": "AAAA,SAAS,kBAAkB;AAE3B,MAAM,wBAAwB;AAC9B,MAAM,sBAAsB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,kBAAkB,OAAuB;AACvD,SAAO,UAAU,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC;AAC/E;AAEO,SAAS,eAAe,KAAsB;AACnD,SAAO,sBAAsB,KAAK,GAAG;AACvC;AAEO,SAAS,sBAAsB,OAAe,cAAgC;AACnF,MAAI,gBAAgB,MAAM,SAAS,YAAY,EAAG,QAAO;AACzD,SAAO,oBAAoB,KAAK,CAAC,YAAY,QAAQ,KAAK,KAAK,CAAC;AAClE;AAEO,SAAS,oBACd,WACA,UAA6D,CAAC,GACxD;AACN,QAAM,cAAc,IAAI,IAAI,QAAQ,eAAe,CAAC,CAAC;AACrD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,SAAS,GAAG;AACpD,QAAI,CAAC,YAAY,IAAI,GAAG,KAAK,sBAAsB,OAAO,QAAQ,YAAY,GAAG;AAC/E,YAAM,IAAI;AAAA,QACR,iDAAiD,GAAG,qCAAqC,GAAG;AAAA,MAC9F;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,oBAAoB,KAAa,OAAe,cAA+B;AAC7F,MAAI,eAAe,GAAG,KAAK,sBAAsB,OAAO,YAAY,GAAG;AACrE,WAAO,eAAe,kBAAkB,KAAK,CAAC;AAAA,EAChD;AACA,SAAO;AACT;AAEO,SAAS,WAAW,OAAe,UAAoB,CAAC,GAAW;AACxE,MAAI,WAAW;AACf,aAAW,UAAU,QAAQ,OAAO,OAAO,EAAE,KAAK,CAAC,MAAM,UAAU,MAAM,SAAS,KAAK,MAAM,GAAG;AAC9F,eAAW,SAAS,MAAM,MAAM,EAAE,KAAK,MAAM;AAAA,EAC/C;AACA,aAAW,WAAW,qBAAqB;AACzC,eAAW,SAAS,QAAQ,IAAI,OAAO,QAAQ,QAAQ,QAAQ,MAAM,SAAS,GAAG,IAAI,QAAQ,QAAQ,GAAG,QAAQ,KAAK,GAAG,GAAG,MAAM;AAAA,EACnI;AACA,SAAO,SAAS,QAAQ,iCAAiC,aAAa;AACxE;AAEO,SAAS,sBAAsB,OAAgB,UAAoB,CAAC,GAAY;AACrF,MAAI,OAAO,UAAU,SAAU,QAAO,WAAW,OAAO,OAAO;AAC/D,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,CAAC,SAAS,sBAAsB,MAAM,OAAO,CAAC;AAAA,EACjE;AACA,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,SAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,KAAgC,EAAE,IAAI,CAAC,CAAC,KAAK,WAAW,MAAM;AAAA,MAC3E;AAAA,MACA,eAAe,GAAG,IAAI,eAAe,sBAAsB,aAAa,OAAO;AAAA,IACjF,CAAC;AAAA,EACH;AACF;AAEO,SAAS,iCAAiC,OAA0B;AACzE,QAAM,YAAY,oBAAI,IAAY;AAElC,WAAS,MAAM,SAAwB;AACrC,QAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,cAAQ,QAAQ,KAAK;AACrB;AAAA,IACF;AACA,QAAI,CAAC,WAAW,OAAO,YAAY,SAAU;AAC7C,eAAW,CAAC,KAAK,WAAW,KAAK,OAAO,QAAQ,OAAkC,GAAG;AACnF,UAAI,eAAe,GAAG,KAAK,OAAO,gBAAgB,YAAY,YAAY,SAAS,GAAG;AACpF,kBAAU,IAAI,WAAW;AAAA,MAC3B,OAAO;AACL,cAAM,WAAW;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK;AACX,SAAO,MAAM,KAAK,SAAS;AAC7B;",
6
+ "names": []
7
+ }
@@ -0,0 +1,128 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ const defaultRunner = (command, args, cwd) => execFileSync(command, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
5
+ function tryRun(runner, command, args, cwd) {
6
+ try {
7
+ return runner(command, args, cwd);
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+ function normalizeGitRepository(remoteUrl) {
13
+ const trimmed = remoteUrl.trim().replace(/\.git$/, "");
14
+ const sshMatch = trimmed.match(/^git@github\.com:([^/]+\/[^/]+)$/);
15
+ if (sshMatch) return sshMatch[1] ?? null;
16
+ try {
17
+ const parsed = new URL(trimmed);
18
+ if (parsed.hostname !== "github.com") return null;
19
+ return parsed.pathname.replace(/^\/+/, "") || null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ function inspectGitSource(cwd, runner = defaultRunner) {
25
+ const branch = tryRun(runner, "git", ["branch", "--show-current"], cwd);
26
+ const remoteUrl = tryRun(runner, "git", ["remote", "get-url", "origin"], cwd);
27
+ if (!branch || !remoteUrl) return null;
28
+ const repo = normalizeGitRepository(remoteUrl);
29
+ if (!repo) return null;
30
+ const commitSha = tryRun(runner, "git", ["rev-parse", "HEAD"], cwd) ?? void 0;
31
+ return {
32
+ mode: "git",
33
+ reason: `GitHub remote origin and branch ${branch} detected`,
34
+ repo,
35
+ branch,
36
+ commitSha
37
+ };
38
+ }
39
+ function assertGitDeployReady(cwd, runner = defaultRunner) {
40
+ const status = tryRun(runner, "git", ["status", "--porcelain"], cwd);
41
+ if (status === null) throw new Error("Git is required for --source git.");
42
+ if (status.length > 0) throw new Error("Git-backed deploy requires a clean working tree.");
43
+ const upstream = tryRun(runner, "git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], cwd);
44
+ if (!upstream) throw new Error("The current branch has no upstream. Push it before a Git-backed deploy.");
45
+ const ahead = tryRun(runner, "git", ["rev-list", "--count", "@{u}..HEAD"], cwd);
46
+ if (ahead === null || Number.parseInt(ahead, 10) > 0) {
47
+ throw new Error("The current branch has local commits that are not pushed to its upstream.");
48
+ }
49
+ const behind = tryRun(runner, "git", ["rev-list", "--count", "HEAD..@{u}"], cwd);
50
+ if (behind === null || Number.parseInt(behind, 10) > 0) {
51
+ throw new Error("The current branch is behind its upstream. Pull the latest commits before a Git-backed deploy.");
52
+ }
53
+ }
54
+ function isRailwayCliAvailable(cwd, runner = defaultRunner) {
55
+ return tryRun(runner, "railway", ["--version"], cwd) !== null;
56
+ }
57
+ function resolveRailwaySource(mode, cwd, runner = defaultRunner) {
58
+ const gitSource = inspectGitSource(cwd, runner);
59
+ if (mode === "git") {
60
+ if (!gitSource) {
61
+ throw new Error("--source git requires a GitHub origin remote and a current branch.");
62
+ }
63
+ assertGitDeployReady(cwd, runner);
64
+ return gitSource;
65
+ }
66
+ if (mode === "local") {
67
+ if (!isRailwayCliAvailable(cwd, runner)) {
68
+ throw new Error("--source local requires the Railway CLI. Install it from https://docs.railway.com/reference/cli-api.");
69
+ }
70
+ return { mode: "local", reason: "Local upload explicitly requested" };
71
+ }
72
+ if (gitSource) {
73
+ try {
74
+ assertGitDeployReady(cwd, runner);
75
+ return gitSource;
76
+ } catch (error) {
77
+ if (!isRailwayCliAvailable(cwd, runner)) throw error;
78
+ const reason = error instanceof Error ? error.message : "Git source is not deployable";
79
+ return {
80
+ mode: "local",
81
+ reason: `${reason} Falling back to railway up.`
82
+ };
83
+ }
84
+ }
85
+ if (!isRailwayCliAvailable(cwd, runner)) {
86
+ throw new Error("No usable GitHub remote found and the Railway CLI is unavailable for local upload.");
87
+ }
88
+ return { mode: "local", reason: "No usable GitHub remote found; falling back to railway up" };
89
+ }
90
+ const REQUIRED_IGNORE_ENTRIES = [
91
+ { label: ".env", matches: (line) => line === ".env" || line === ".env*" },
92
+ { label: ".env.*", matches: (line) => line === ".env.*" || line === ".env*" },
93
+ { label: "*.pem", matches: (line) => line === "*.pem" },
94
+ { label: "*.key", matches: (line) => line === "*.key" },
95
+ { label: "id_rsa", matches: (line) => line === "id_*" || line === "id_rsa" },
96
+ { label: "id_ed25519", matches: (line) => line === "id_*" || line === "id_ed25519" },
97
+ { label: ".git", matches: (line) => line === ".git" },
98
+ { label: ".railway", matches: (line) => line === ".railway" },
99
+ { label: "node_modules", matches: (line) => line === "node_modules" },
100
+ { label: ".yarn/cache", matches: (line) => line === ".yarn/cache" },
101
+ { label: ".next", matches: (line) => line === ".next" },
102
+ { label: ".turbo", matches: (line) => line === ".turbo" },
103
+ { label: "*.db", matches: (line) => line === "*.db" },
104
+ { label: "*.sqlite", matches: (line) => line === "*.sqlite" || line === "*.sqlite*" },
105
+ { label: "*.sqlite3", matches: (line) => line === "*.sqlite3" || line === "*.sqlite*" },
106
+ { label: ".mercato/railway.json", matches: (line) => line === ".mercato/railway.json" || line === ".mercato/railway.json*" },
107
+ { label: ".mercato/railway.json.local", matches: (line) => line === ".mercato/railway.json.local" || line === ".mercato/railway.json*" }
108
+ ];
109
+ function assertLocalUploadSafe(cwd) {
110
+ const ignorePath = resolve(cwd, ".railwayignore");
111
+ if (!existsSync(ignorePath)) {
112
+ throw new Error("Local Railway upload requires a .railwayignore file.");
113
+ }
114
+ const entries = readFileSync(ignorePath, "utf8").split(/\r?\n/).map((line) => line.trim().replace(/^\/+|\/+$/g, "")).filter((line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("!"));
115
+ const missing = REQUIRED_IGNORE_ENTRIES.filter((requirement) => !entries.some(requirement.matches)).map((requirement) => requirement.label);
116
+ if (missing.length > 0) {
117
+ throw new Error(`.railwayignore is missing required safety entries: ${missing.join(", ")}`);
118
+ }
119
+ }
120
+ export {
121
+ assertGitDeployReady,
122
+ assertLocalUploadSafe,
123
+ inspectGitSource,
124
+ isRailwayCliAvailable,
125
+ normalizeGitRepository,
126
+ resolveRailwaySource
127
+ };
128
+ //# sourceMappingURL=source.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/lib/deploy/railway/source.ts"],
4
+ "sourcesContent": ["import { execFileSync } from 'node:child_process'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { resolve } from 'node:path'\nimport type { RailwaySource, RailwaySourceMode } from './types'\n\ntype CommandRunner = (command: string, args: string[], cwd: string) => string\n\nconst defaultRunner: CommandRunner = (command, args, cwd) =>\n execFileSync(command, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim()\n\nfunction tryRun(runner: CommandRunner, command: string, args: string[], cwd: string): string | null {\n try {\n return runner(command, args, cwd)\n } catch {\n return null\n }\n}\n\nexport function normalizeGitRepository(remoteUrl: string): string | null {\n const trimmed = remoteUrl.trim().replace(/\\.git$/, '')\n const sshMatch = trimmed.match(/^git@github\\.com:([^/]+\\/[^/]+)$/)\n if (sshMatch) return sshMatch[1] ?? null\n try {\n const parsed = new URL(trimmed)\n if (parsed.hostname !== 'github.com') return null\n return parsed.pathname.replace(/^\\/+/, '') || null\n } catch {\n return null\n }\n}\n\nexport function inspectGitSource(cwd: string, runner: CommandRunner = defaultRunner): RailwaySource | null {\n const branch = tryRun(runner, 'git', ['branch', '--show-current'], cwd)\n const remoteUrl = tryRun(runner, 'git', ['remote', 'get-url', 'origin'], cwd)\n if (!branch || !remoteUrl) return null\n const repo = normalizeGitRepository(remoteUrl)\n if (!repo) return null\n const commitSha = tryRun(runner, 'git', ['rev-parse', 'HEAD'], cwd) ?? undefined\n return {\n mode: 'git',\n reason: `GitHub remote origin and branch ${branch} detected`,\n repo,\n branch,\n commitSha,\n }\n}\n\nexport function assertGitDeployReady(cwd: string, runner: CommandRunner = defaultRunner): void {\n const status = tryRun(runner, 'git', ['status', '--porcelain'], cwd)\n if (status === null) throw new Error('Git is required for --source git.')\n if (status.length > 0) throw new Error('Git-backed deploy requires a clean working tree.')\n const upstream = tryRun(runner, 'git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], cwd)\n if (!upstream) throw new Error('The current branch has no upstream. Push it before a Git-backed deploy.')\n const ahead = tryRun(runner, 'git', ['rev-list', '--count', '@{u}..HEAD'], cwd)\n if (ahead === null || Number.parseInt(ahead, 10) > 0) {\n throw new Error('The current branch has local commits that are not pushed to its upstream.')\n }\n const behind = tryRun(runner, 'git', ['rev-list', '--count', 'HEAD..@{u}'], cwd)\n if (behind === null || Number.parseInt(behind, 10) > 0) {\n throw new Error('The current branch is behind its upstream. Pull the latest commits before a Git-backed deploy.')\n }\n}\n\nexport function isRailwayCliAvailable(cwd: string, runner: CommandRunner = defaultRunner): boolean {\n return tryRun(runner, 'railway', ['--version'], cwd) !== null\n}\n\nexport function resolveRailwaySource(\n mode: RailwaySourceMode,\n cwd: string,\n runner: CommandRunner = defaultRunner,\n): RailwaySource {\n const gitSource = inspectGitSource(cwd, runner)\n if (mode === 'git') {\n if (!gitSource) {\n throw new Error('--source git requires a GitHub origin remote and a current branch.')\n }\n assertGitDeployReady(cwd, runner)\n return gitSource\n }\n if (mode === 'local') {\n if (!isRailwayCliAvailable(cwd, runner)) {\n throw new Error('--source local requires the Railway CLI. Install it from https://docs.railway.com/reference/cli-api.')\n }\n return { mode: 'local', reason: 'Local upload explicitly requested' }\n }\n if (gitSource) {\n try {\n assertGitDeployReady(cwd, runner)\n return gitSource\n } catch (error) {\n if (!isRailwayCliAvailable(cwd, runner)) throw error\n const reason = error instanceof Error ? error.message : 'Git source is not deployable'\n return {\n mode: 'local',\n reason: `${reason} Falling back to railway up.`,\n }\n }\n }\n if (!isRailwayCliAvailable(cwd, runner)) {\n throw new Error('No usable GitHub remote found and the Railway CLI is unavailable for local upload.')\n }\n return { mode: 'local', reason: 'No usable GitHub remote found; falling back to railway up' }\n}\n\nconst REQUIRED_IGNORE_ENTRIES = [\n { label: '.env', matches: (line: string) => line === '.env' || line === '.env*' },\n { label: '.env.*', matches: (line: string) => line === '.env.*' || line === '.env*' },\n { label: '*.pem', matches: (line: string) => line === '*.pem' },\n { label: '*.key', matches: (line: string) => line === '*.key' },\n { label: 'id_rsa', matches: (line: string) => line === 'id_*' || line === 'id_rsa' },\n { label: 'id_ed25519', matches: (line: string) => line === 'id_*' || line === 'id_ed25519' },\n { label: '.git', matches: (line: string) => line === '.git' },\n { label: '.railway', matches: (line: string) => line === '.railway' },\n { label: 'node_modules', matches: (line: string) => line === 'node_modules' },\n { label: '.yarn/cache', matches: (line: string) => line === '.yarn/cache' },\n { label: '.next', matches: (line: string) => line === '.next' },\n { label: '.turbo', matches: (line: string) => line === '.turbo' },\n { label: '*.db', matches: (line: string) => line === '*.db' },\n { label: '*.sqlite', matches: (line: string) => line === '*.sqlite' || line === '*.sqlite*' },\n { label: '*.sqlite3', matches: (line: string) => line === '*.sqlite3' || line === '*.sqlite*' },\n { label: '.mercato/railway.json', matches: (line: string) =>\n line === '.mercato/railway.json' || line === '.mercato/railway.json*' },\n { label: '.mercato/railway.json.local', matches: (line: string) =>\n line === '.mercato/railway.json.local' || line === '.mercato/railway.json*' },\n]\n\nexport function assertLocalUploadSafe(cwd: string): void {\n const ignorePath = resolve(cwd, '.railwayignore')\n if (!existsSync(ignorePath)) {\n throw new Error('Local Railway upload requires a .railwayignore file.')\n }\n const entries = readFileSync(ignorePath, 'utf8')\n .split(/\\r?\\n/)\n .map((line) => line.trim().replace(/^\\/+|\\/+$/g, ''))\n .filter((line) => line.length > 0 && !line.startsWith('#') && !line.startsWith('!'))\n const missing = REQUIRED_IGNORE_ENTRIES\n .filter((requirement) => !entries.some(requirement.matches))\n .map((requirement) => requirement.label)\n if (missing.length > 0) {\n throw new Error(`.railwayignore is missing required safety entries: ${missing.join(', ')}`)\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,41 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ function railwayStatePath(cwd, track) {
4
+ return resolve(cwd, ".mercato", track ? "railway.json" : "railway.json.local");
5
+ }
6
+ function isRailwayState(value) {
7
+ if (!value || typeof value !== "object") return false;
8
+ const record = value;
9
+ return record.schemaVersion === 1 && record.provider === "railway" && typeof record.projectName === "string" && typeof record.environments === "object" && record.environments !== null;
10
+ }
11
+ function loadRailwayState(path) {
12
+ if (!existsSync(path)) return null;
13
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
14
+ if (!isRailwayState(parsed)) {
15
+ throw new Error(`Unsupported or invalid Railway state file: ${path}`);
16
+ }
17
+ return parsed;
18
+ }
19
+ function createRailwayState(projectName, cliVersion) {
20
+ return {
21
+ schemaVersion: 1,
22
+ provider: "railway",
23
+ projectName,
24
+ environments: {},
25
+ writtenBy: { cliVersion }
26
+ };
27
+ }
28
+ function saveRailwayState(path, state) {
29
+ mkdirSync(dirname(path), { recursive: true });
30
+ const temporaryPath = `${path}.tmp`;
31
+ writeFileSync(temporaryPath, `${JSON.stringify(state, null, 2)}
32
+ `, { mode: 384 });
33
+ renameSync(temporaryPath, path);
34
+ }
35
+ export {
36
+ createRailwayState,
37
+ loadRailwayState,
38
+ railwayStatePath,
39
+ saveRailwayState
40
+ };
41
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/lib/deploy/railway/state.ts"],
4
+ "sourcesContent": ["import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport type { RailwayState } from './types'\n\nexport function railwayStatePath(cwd: string, track: boolean): string {\n return resolve(cwd, '.mercato', track ? 'railway.json' : 'railway.json.local')\n}\n\nfunction isRailwayState(value: unknown): value is RailwayState {\n if (!value || typeof value !== 'object') return false\n const record = value as Record<string, unknown>\n return (\n record.schemaVersion === 1 &&\n record.provider === 'railway' &&\n typeof record.projectName === 'string' &&\n typeof record.environments === 'object' &&\n record.environments !== null\n )\n}\n\nexport function loadRailwayState(path: string): RailwayState | null {\n if (!existsSync(path)) return null\n const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))\n if (!isRailwayState(parsed)) {\n throw new Error(`Unsupported or invalid Railway state file: ${path}`)\n }\n return parsed\n}\n\nexport function createRailwayState(projectName: string, cliVersion: string): RailwayState {\n return {\n schemaVersion: 1,\n provider: 'railway',\n projectName,\n environments: {},\n writtenBy: { cliVersion },\n }\n}\n\nexport function saveRailwayState(path: string, state: RailwayState): void {\n mkdirSync(dirname(path), { recursive: true })\n const temporaryPath = `${path}.tmp`\n writeFileSync(temporaryPath, `${JSON.stringify(state, null, 2)}\\n`, { mode: 0o600 })\n renameSync(temporaryPath, path)\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY,WAAW,cAAc,YAAY,qBAAqB;AAC/E,SAAS,SAAS,eAAe;AAG1B,SAAS,iBAAiB,KAAa,OAAwB;AACpE,SAAO,QAAQ,KAAK,YAAY,QAAQ,iBAAiB,oBAAoB;AAC/E;AAEA,SAAS,eAAe,OAAuC;AAC7D,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,SAAS;AACf,SACE,OAAO,kBAAkB,KACzB,OAAO,aAAa,aACpB,OAAO,OAAO,gBAAgB,YAC9B,OAAO,OAAO,iBAAiB,YAC/B,OAAO,iBAAiB;AAE5B;AAEO,SAAS,iBAAiB,MAAmC;AAClE,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,QAAM,SAAkB,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;AAC7D,MAAI,CAAC,eAAe,MAAM,GAAG;AAC3B,UAAM,IAAI,MAAM,8CAA8C,IAAI,EAAE;AAAA,EACtE;AACA,SAAO;AACT;AAEO,SAAS,mBAAmB,aAAqB,YAAkC;AACxF,SAAO;AAAA,IACL,eAAe;AAAA,IACf,UAAU;AAAA,IACV;AAAA,IACA,cAAc,CAAC;AAAA,IACf,WAAW,EAAE,WAAW;AAAA,EAC1B;AACF;AAEO,SAAS,iBAAiB,MAAc,OAA2B;AACxE,YAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC5C,QAAM,gBAAgB,GAAG,IAAI;AAC7B,gBAAc,eAAe,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA,GAAM,EAAE,MAAM,IAAM,CAAC;AACnF,aAAW,eAAe,IAAI;AAChC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,46 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ function railwayTokenConfigPath(environment = process.env, platform = process.platform) {
5
+ if (platform === "win32") {
6
+ return join(environment.APPDATA || join(homedir(), "AppData", "Roaming"), "open-mercato", "railway.json");
7
+ }
8
+ return join(environment.XDG_CONFIG_HOME || join(homedir(), ".config"), "open-mercato", "railway.json");
9
+ }
10
+ function readCachedRailwayToken(path, platform = process.platform) {
11
+ if (!existsSync(path)) return null;
12
+ if (platform !== "win32") {
13
+ const mode = statSync(path).mode & 511;
14
+ if ((mode & 63) !== 0) {
15
+ throw new Error(`Refusing to read ${path}: permissions must be 0600.`);
16
+ }
17
+ }
18
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
19
+ if (!parsed || typeof parsed !== "object") return null;
20
+ const token = parsed.token;
21
+ return typeof token === "string" && token.trim() ? token.trim() : null;
22
+ }
23
+ function writeCachedRailwayToken(path, token) {
24
+ mkdirSync(dirname(path), { recursive: true });
25
+ writeFileSync(path, `${JSON.stringify({ token }, null, 2)}
26
+ `, { mode: 384 });
27
+ if (process.platform !== "win32") chmodSync(path, 384);
28
+ }
29
+ function resolveRailwayToken(input) {
30
+ const environment = input.environment ?? process.env;
31
+ if (input.flagToken?.trim()) return { token: input.flagToken.trim(), source: "flag" };
32
+ if (environment.RAILWAY_API_TOKEN?.trim()) {
33
+ return { token: environment.RAILWAY_API_TOKEN.trim(), source: "environment" };
34
+ }
35
+ const configPath = input.configPath ?? railwayTokenConfigPath(environment);
36
+ const cached = readCachedRailwayToken(configPath);
37
+ if (cached) return { token: cached, source: "cache" };
38
+ return { token: null, source: null };
39
+ }
40
+ export {
41
+ railwayTokenConfigPath,
42
+ readCachedRailwayToken,
43
+ resolveRailwayToken,
44
+ writeCachedRailwayToken
45
+ };
46
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/lib/deploy/railway/token.ts"],
4
+ "sourcesContent": ["import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\n\nexport function railwayTokenConfigPath(\n environment: NodeJS.ProcessEnv = process.env,\n platform: NodeJS.Platform = process.platform,\n): string {\n if (platform === 'win32') {\n return join(environment.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'open-mercato', 'railway.json')\n }\n return join(environment.XDG_CONFIG_HOME || join(homedir(), '.config'), 'open-mercato', 'railway.json')\n}\n\nexport function readCachedRailwayToken(path: string, platform: NodeJS.Platform = process.platform): string | null {\n if (!existsSync(path)) return null\n if (platform !== 'win32') {\n const mode = statSync(path).mode & 0o777\n if ((mode & 0o077) !== 0) {\n throw new Error(`Refusing to read ${path}: permissions must be 0600.`)\n }\n }\n const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))\n if (!parsed || typeof parsed !== 'object') return null\n const token = (parsed as Record<string, unknown>).token\n return typeof token === 'string' && token.trim() ? token.trim() : null\n}\n\nexport function writeCachedRailwayToken(path: string, token: string): void {\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, `${JSON.stringify({ token }, null, 2)}\\n`, { mode: 0o600 })\n if (process.platform !== 'win32') chmodSync(path, 0o600)\n}\n\nexport function resolveRailwayToken(input: {\n flagToken?: string\n environment?: NodeJS.ProcessEnv\n configPath?: string\n}): { token: string | null; source: 'flag' | 'environment' | 'cache' | null } {\n const environment = input.environment ?? process.env\n if (input.flagToken?.trim()) return { token: input.flagToken.trim(), source: 'flag' }\n if (environment.RAILWAY_API_TOKEN?.trim()) {\n return { token: environment.RAILWAY_API_TOKEN.trim(), source: 'environment' }\n }\n const configPath = input.configPath ?? railwayTokenConfigPath(environment)\n const cached = readCachedRailwayToken(configPath)\n if (cached) return { token: cached, source: 'cache' }\n return { token: null, source: null }\n}\n"],
5
+ "mappings": "AAAA,SAAS,WAAW,YAAY,WAAW,cAAc,UAAU,qBAAqB;AACxF,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAEvB,SAAS,uBACd,cAAiC,QAAQ,KACzC,WAA4B,QAAQ,UAC5B;AACR,MAAI,aAAa,SAAS;AACxB,WAAO,KAAK,YAAY,WAAW,KAAK,QAAQ,GAAG,WAAW,SAAS,GAAG,gBAAgB,cAAc;AAAA,EAC1G;AACA,SAAO,KAAK,YAAY,mBAAmB,KAAK,QAAQ,GAAG,SAAS,GAAG,gBAAgB,cAAc;AACvG;AAEO,SAAS,uBAAuB,MAAc,WAA4B,QAAQ,UAAyB;AAChH,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,MAAI,aAAa,SAAS;AACxB,UAAM,OAAO,SAAS,IAAI,EAAE,OAAO;AACnC,SAAK,OAAO,QAAW,GAAG;AACxB,YAAM,IAAI,MAAM,oBAAoB,IAAI,6BAA6B;AAAA,IACvE;AAAA,EACF;AACA,QAAM,SAAkB,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;AAC7D,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,QAAM,QAAS,OAAmC;AAClD,SAAO,OAAO,UAAU,YAAY,MAAM,KAAK,IAAI,MAAM,KAAK,IAAI;AACpE;AAEO,SAAS,wBAAwB,MAAc,OAAqB;AACzE,YAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC5C,gBAAc,MAAM,GAAG,KAAK,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;AAAA,GAAM,EAAE,MAAM,IAAM,CAAC;AAC9E,MAAI,QAAQ,aAAa,QAAS,WAAU,MAAM,GAAK;AACzD;AAEO,SAAS,oBAAoB,OAI0C;AAC5E,QAAM,cAAc,MAAM,eAAe,QAAQ;AACjD,MAAI,MAAM,WAAW,KAAK,EAAG,QAAO,EAAE,OAAO,MAAM,UAAU,KAAK,GAAG,QAAQ,OAAO;AACpF,MAAI,YAAY,mBAAmB,KAAK,GAAG;AACzC,WAAO,EAAE,OAAO,YAAY,kBAAkB,KAAK,GAAG,QAAQ,cAAc;AAAA,EAC9E;AACA,QAAM,aAAa,MAAM,cAAc,uBAAuB,WAAW;AACzE,QAAM,SAAS,uBAAuB,UAAU;AAChD,MAAI,OAAQ,QAAO,EAAE,OAAO,QAAQ,QAAQ,QAAQ;AACpD,SAAO,EAAE,OAAO,MAAM,QAAQ,KAAK;AACrC;",
6
+ "names": []
7
+ }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
package/dist/mercato.js CHANGED
@@ -60,7 +60,7 @@ const TURBOPACK_CORRUPTION_PATTERNS = [
60
60
  "Unable to open static sorted file",
61
61
  "TurbopackInternalError"
62
62
  ];
63
- const BUILTIN_CLI_MODULE_IDS = /* @__PURE__ */ new Set(["queue", "generate", "db", "server", "test"]);
63
+ const BUILTIN_CLI_MODULE_IDS = /* @__PURE__ */ new Set(["queue", "generate", "deploy", "db", "server", "test"]);
64
64
  function collectNestedErrors(error, seen = /* @__PURE__ */ new Set()) {
65
65
  if (!error || seen.has(error)) {
66
66
  return [];
@@ -192,16 +192,16 @@ function isTurbopackCacheCorruption(output) {
192
192
  function removeTurbopackDevCache(appDir) {
193
193
  fs.rmSync(path.join(appDir, ".mercato", "next", "dev"), { recursive: true, force: true });
194
194
  }
195
- async function ensureEnvLoaded() {
195
+ async function ensureEnvLoaded(options = {}) {
196
196
  if (envLoaded) return;
197
197
  envLoaded = true;
198
- const quietDotenv = process.env.DOTENV_CONFIG_QUIET === "1" || process.env.DOTENV_CONFIG_QUIET === "true";
198
+ const quietDotenv = options.quiet === true || process.env.DOTENV_CONFIG_QUIET === "1" || process.env.DOTENV_CONFIG_QUIET === "true";
199
199
  try {
200
200
  const { createResolver } = await import("./lib/resolver.js");
201
201
  const resolver = createResolver();
202
202
  const appDir = resolver.getAppDir();
203
203
  const envPath = path.join(appDir, ".env");
204
- if (!fs.existsSync(envPath) && process.env.NODE_ENV !== "production") {
204
+ if (options.createIfMissing !== false && !fs.existsSync(envPath) && process.env.NODE_ENV !== "production") {
205
205
  const examplePath = path.join(appDir, ".env.example");
206
206
  if (fs.existsSync(examplePath)) {
207
207
  fs.copyFileSync(examplePath, envPath);
@@ -568,9 +568,9 @@ async function buildAllModules() {
568
568
  return all;
569
569
  }
570
570
  async function run(argv = process.argv) {
571
- await ensureEnvLoaded();
572
571
  const [, , ...parts] = argv;
573
572
  const [first, second, ...remaining] = parts;
573
+ await ensureEnvLoaded({ createIfMissing: first !== "deploy", quiet: first === "deploy" });
574
574
  if (first === "init") {
575
575
  const { execSync } = await import("child_process");
576
576
  console.log("\u{1F680} Initializing Open Mercato app...\n");
@@ -1184,6 +1184,18 @@ async function run(argv = process.argv) {
1184
1184
  }
1185
1185
  }
1186
1186
  const all = modules.slice();
1187
+ all.push({
1188
+ id: "deploy",
1189
+ cli: [
1190
+ {
1191
+ command: "railway",
1192
+ run: async (args) => {
1193
+ const { runRailwayDeploy } = await import("./lib/deploy/railway/index.js");
1194
+ await runRailwayDeploy(args);
1195
+ }
1196
+ }
1197
+ ]
1198
+ });
1187
1199
  all.push({
1188
1200
  id: "queue",
1189
1201
  cli: [
@@ -1976,11 +1988,14 @@ async function run(argv = process.argv) {
1976
1988
  }
1977
1989
  console.log("");
1978
1990
  const started = Date.now();
1979
- console.log(`\u{1F680} Running ${modName}:${cmdName} ${rest.join(" ")}`);
1991
+ const loggedArgs = modName === "deploy" && cmdName === "railway" ? (await import("./lib/deploy/railway/options.js")).redactRailwayCliArgs(rest) : rest;
1992
+ console.log(`\u{1F680} Running ${modName}:${cmdName} ${loggedArgs.join(" ")}`);
1980
1993
  try {
1981
1994
  await cmd.run(rest);
1982
- const ms = Date.now() - started;
1983
- console.log(`\u23F1\uFE0F Done in ${ms}ms`);
1995
+ if (modName !== "deploy" || cmdName !== "railway") {
1996
+ const ms = Date.now() - started;
1997
+ console.log(`\u23F1\uFE0F Done in ${ms}ms`);
1998
+ }
1984
1999
  return 0;
1985
2000
  } catch (e) {
1986
2001
  console.error(`\u{1F4A5} Failed: ${formatCliFailureMessage(modName, cmdName, e)}`);