@open-mercato/cli 0.6.4-develop.4382.1.6b4f656b77 → 0.6.4

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 (109) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/agentic/guides/core.customers.md +1 -1
  3. package/dist/agentic/shared/AGENTS.md.template +56 -33
  4. package/dist/agentic/shared/ai/skills/{auto-continue-pr → om-auto-continue-pr}/SKILL.md +19 -19
  5. package/dist/agentic/shared/ai/skills/{auto-create-pr → om-auto-continue-pr}/STANDALONE.md +1 -1
  6. package/dist/agentic/shared/ai/skills/{auto-continue-pr-loop → om-auto-continue-pr-loop}/SKILL.md +32 -32
  7. package/dist/agentic/shared/ai/skills/{auto-create-pr → om-auto-create-pr}/SKILL.md +24 -24
  8. package/dist/agentic/shared/ai/skills/{auto-fix-github → om-auto-create-pr}/STANDALONE.md +1 -1
  9. package/dist/agentic/shared/ai/skills/{auto-create-pr-loop → om-auto-create-pr-loop}/SKILL.md +40 -40
  10. package/dist/agentic/shared/ai/skills/{auto-fix-github → om-auto-fix-github}/SKILL.md +3 -3
  11. package/dist/agentic/shared/ai/skills/{auto-continue-pr → om-auto-fix-github}/STANDALONE.md +1 -1
  12. package/dist/agentic/shared/ai/skills/{auto-review-pr → om-auto-review-pr}/SKILL.md +5 -5
  13. package/dist/agentic/shared/ai/skills/{auto-review-pr → om-auto-review-pr}/STANDALONE.md +1 -1
  14. package/dist/agentic/shared/ai/skills/{auto-upgrade-0.4.10-to-0.5.0 → om-auto-upgrade-0.4.10-to-0.5.0}/SKILL.md +1 -1
  15. package/dist/agentic/shared/ai/skills/{backend-ui-design → om-backend-ui-design}/SKILL.md +2 -2
  16. package/dist/agentic/shared/ai/skills/{code-review → om-code-review}/SKILL.md +1 -1
  17. package/dist/agentic/shared/ai/skills/{code-review → om-code-review}/references/review-checklist.md +3 -0
  18. package/dist/agentic/shared/ai/skills/{data-model-design → om-data-model-design}/SKILL.md +4 -2
  19. package/dist/agentic/shared/ai/skills/{eject-and-customize → om-eject-and-customize}/SKILL.md +2 -2
  20. package/dist/agentic/shared/ai/skills/{implement-spec → om-implement-spec}/SKILL.md +6 -6
  21. package/dist/agentic/shared/ai/skills/{integration-builder → om-integration-builder}/SKILL.md +1 -1
  22. package/dist/agentic/shared/ai/skills/{integration-tests → om-integration-tests}/SKILL.md +1 -1
  23. package/dist/agentic/shared/ai/skills/{module-scaffold → om-module-scaffold}/SKILL.md +5 -3
  24. package/dist/agentic/shared/ai/skills/{module-scaffold → om-module-scaffold}/references/naming-conventions.md +7 -8
  25. package/dist/agentic/shared/ai/skills/om-prepare-issue/SKILL.md +202 -0
  26. package/dist/agentic/shared/ai/skills/{spec-writing → om-spec-writing}/SKILL.md +6 -6
  27. package/dist/agentic/shared/ai/skills/{spec-writing → om-spec-writing}/references/spec-checklist.md +3 -3
  28. package/dist/agentic/shared/ai/skills/{system-extension → om-system-extension}/SKILL.md +1 -1
  29. package/dist/agentic/shared/ai/skills/{trim-unused-modules → om-trim-unused-modules}/SKILL.md +1 -1
  30. package/dist/agentic/shared/ai/skills/{troubleshooter → om-troubleshooter}/SKILL.md +1 -1
  31. package/dist/bin.js +1 -0
  32. package/dist/bin.js.map +2 -2
  33. package/dist/lib/__integration__/TC-INT-008.spec.js +1 -1
  34. package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
  35. package/dist/lib/agentic-setup.js +52 -51
  36. package/dist/lib/agentic-setup.js.map +2 -2
  37. package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.meta.js +7 -0
  38. package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.meta.js.map +7 -0
  39. package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.spec.js +63 -0
  40. package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.spec.js.map +7 -0
  41. package/dist/lib/deploy/railway/env.js +98 -0
  42. package/dist/lib/deploy/railway/env.js.map +7 -0
  43. package/dist/lib/deploy/railway/graphql-client.js +99 -0
  44. package/dist/lib/deploy/railway/graphql-client.js.map +7 -0
  45. package/dist/lib/deploy/railway/index.js +1115 -0
  46. package/dist/lib/deploy/railway/index.js.map +7 -0
  47. package/dist/lib/deploy/railway/operations.js +249 -0
  48. package/dist/lib/deploy/railway/operations.js.map +7 -0
  49. package/dist/lib/deploy/railway/options.js +155 -0
  50. package/dist/lib/deploy/railway/options.js.map +7 -0
  51. package/dist/lib/deploy/railway/railway-cli.js +61 -0
  52. package/dist/lib/deploy/railway/railway-cli.js.map +7 -0
  53. package/dist/lib/deploy/railway/redaction.js +88 -0
  54. package/dist/lib/deploy/railway/redaction.js.map +7 -0
  55. package/dist/lib/deploy/railway/source.js +128 -0
  56. package/dist/lib/deploy/railway/source.js.map +7 -0
  57. package/dist/lib/deploy/railway/state.js +41 -0
  58. package/dist/lib/deploy/railway/state.js.map +7 -0
  59. package/dist/lib/deploy/railway/token.js +46 -0
  60. package/dist/lib/deploy/railway/token.js.map +7 -0
  61. package/dist/lib/deploy/railway/types.js +1 -0
  62. package/dist/lib/deploy/railway/types.js.map +7 -0
  63. package/dist/lib/generators/openapi.js +4 -0
  64. package/dist/lib/generators/openapi.js.map +2 -2
  65. package/dist/lib/testing/integration.js +57 -26
  66. package/dist/lib/testing/integration.js.map +2 -2
  67. package/dist/lib/testing/runtime-utils.js +16 -8
  68. package/dist/lib/testing/runtime-utils.js.map +2 -2
  69. package/dist/mercato.js +23 -8
  70. package/dist/mercato.js.map +2 -2
  71. package/package.json +11 -12
  72. package/src/bin.ts +1 -0
  73. package/src/lib/__integration__/TC-INT-008.spec.ts +1 -1
  74. package/src/lib/agentic-setup.ts +52 -51
  75. package/src/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.meta.ts +3 -0
  76. package/src/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.spec.ts +71 -0
  77. package/src/lib/deploy/railway/__tests__/dry-run.test.ts +61 -0
  78. package/src/lib/deploy/railway/__tests__/env.test.ts +55 -0
  79. package/src/lib/deploy/railway/__tests__/graphql-client.test.ts +80 -0
  80. package/src/lib/deploy/railway/__tests__/operations.test.ts +21 -0
  81. package/src/lib/deploy/railway/__tests__/options.test.ts +78 -0
  82. package/src/lib/deploy/railway/__tests__/redaction.test.ts +62 -0
  83. package/src/lib/deploy/railway/__tests__/source.test.ts +130 -0
  84. package/src/lib/deploy/railway/__tests__/state.test.ts +24 -0
  85. package/src/lib/deploy/railway/__tests__/token.test.ts +39 -0
  86. package/src/lib/deploy/railway/env.ts +112 -0
  87. package/src/lib/deploy/railway/graphql-client.ts +128 -0
  88. package/src/lib/deploy/railway/index.ts +1454 -0
  89. package/src/lib/deploy/railway/operations.ts +248 -0
  90. package/src/lib/deploy/railway/options.ts +167 -0
  91. package/src/lib/deploy/railway/railway-cli.ts +65 -0
  92. package/src/lib/deploy/railway/redaction.ts +91 -0
  93. package/src/lib/deploy/railway/source.ts +143 -0
  94. package/src/lib/deploy/railway/state.ts +45 -0
  95. package/src/lib/deploy/railway/token.ts +49 -0
  96. package/src/lib/deploy/railway/types.ts +87 -0
  97. package/src/lib/generators/openapi.ts +3 -0
  98. package/src/lib/testing/__tests__/integration.test.ts +79 -0
  99. package/src/lib/testing/__tests__/runtime-utils.test.ts +45 -1
  100. package/src/lib/testing/integration.ts +74 -28
  101. package/src/lib/testing/runtime-utils.ts +17 -8
  102. package/src/mercato.ts +33 -8
  103. /package/dist/agentic/shared/ai/skills/{backend-ui-design → om-backend-ui-design}/references/ui-components.md +0 -0
  104. /package/dist/agentic/shared/ai/skills/{data-model-design → om-data-model-design}/references/mikro-orm-cheatsheet.md +0 -0
  105. /package/dist/agentic/shared/ai/skills/{integration-builder → om-integration-builder}/references/adapter-contracts.md +0 -0
  106. /package/dist/agentic/shared/ai/skills/{module-scaffold → om-module-scaffold}/references/navigation-patterns.md +0 -0
  107. /package/dist/agentic/shared/ai/skills/{spec-writing → om-spec-writing}/references/spec-template.md +0 -0
  108. /package/dist/agentic/shared/ai/skills/{system-extension → om-system-extension}/references/extension-contracts.md +0 -0
  109. /package/dist/agentic/shared/ai/skills/{troubleshooter → om-troubleshooter}/references/diagnostic-commands.md +0 -0
@@ -0,0 +1,248 @@
1
+ import type { RailwayGraphqlOperation } from './types'
2
+
3
+ export const railwayOperations = {
4
+ me: {
5
+ name: 'RailwayMe',
6
+ query: `query RailwayMe {
7
+ me { id name email workspaces { id name } }
8
+ }`,
9
+ },
10
+ project: {
11
+ name: 'RailwayProject',
12
+ query: `query RailwayProject($id: String!) {
13
+ project(id: $id) { id name workspaceId }
14
+ }`,
15
+ },
16
+ projects: {
17
+ name: 'RailwayProjects',
18
+ query: `query RailwayProjects($workspaceId: String!) {
19
+ projects(workspaceId: $workspaceId, first: 100, includeDeleted: false) {
20
+ edges { node { id name } }
21
+ }
22
+ }`,
23
+ },
24
+ projectCreate: {
25
+ name: 'RailwayProjectCreate',
26
+ mutation: true,
27
+ query: `mutation RailwayProjectCreate($input: ProjectCreateInput!) {
28
+ projectCreate(input: $input) { id name }
29
+ }`,
30
+ },
31
+ projectUpdate: {
32
+ name: 'RailwayProjectUpdate',
33
+ mutation: true,
34
+ query: `mutation RailwayProjectUpdate($id: String!, $input: ProjectUpdateInput!) {
35
+ projectUpdate(id: $id, input: $input) { id name }
36
+ }`,
37
+ },
38
+ projectDelete: {
39
+ name: 'RailwayProjectDelete',
40
+ mutation: true,
41
+ query: `mutation RailwayProjectDelete($id: String!) {
42
+ projectDelete(id: $id)
43
+ }`,
44
+ },
45
+ environments: {
46
+ name: 'RailwayEnvironments',
47
+ query: `query RailwayEnvironments($projectId: String!) {
48
+ environments(projectId: $projectId, first: 100) {
49
+ edges { node { id name } }
50
+ }
51
+ }`,
52
+ },
53
+ environment: {
54
+ name: 'RailwayEnvironment',
55
+ query: `query RailwayEnvironment($id: String!, $projectId: String!) {
56
+ environment(id: $id, projectId: $projectId) { id name }
57
+ }`,
58
+ },
59
+ environmentCreate: {
60
+ name: 'RailwayEnvironmentCreate',
61
+ mutation: true,
62
+ query: `mutation RailwayEnvironmentCreate($input: EnvironmentCreateInput!) {
63
+ environmentCreate(input: $input) { id name }
64
+ }`,
65
+ },
66
+ regions: {
67
+ name: 'RailwayRegions',
68
+ query: `query RailwayRegions($projectId: String!) {
69
+ regions(projectId: $projectId) { id name region location }
70
+ }`,
71
+ },
72
+ template: {
73
+ name: 'RailwayTemplate',
74
+ query: `query RailwayTemplate($code: String!) {
75
+ template(code: $code) { id code serializedConfig }
76
+ }`,
77
+ },
78
+ templateDeploy: {
79
+ name: 'RailwayTemplateDeploy',
80
+ mutation: true,
81
+ query: `mutation RailwayTemplateDeploy($input: TemplateDeployV2Input!) {
82
+ templateDeployV2(input: $input) { workflowId }
83
+ }`,
84
+ },
85
+ services: {
86
+ name: 'RailwayServices',
87
+ query: `query RailwayServices($projectId: String!) {
88
+ project(id: $projectId) { services { edges { node { id name } } } }
89
+ }`,
90
+ },
91
+ volumes: {
92
+ name: 'RailwayVolumes',
93
+ query: `query RailwayVolumes($projectId: String!) {
94
+ project(id: $projectId) {
95
+ volumes {
96
+ edges {
97
+ node {
98
+ id
99
+ volumeInstances {
100
+ edges { node { environmentId serviceId mountPath } }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }`,
107
+ },
108
+ service: {
109
+ name: 'RailwayService',
110
+ query: `query RailwayService($id: String!) {
111
+ service(id: $id) { id name projectId }
112
+ }`,
113
+ },
114
+ serviceCreate: {
115
+ name: 'RailwayServiceCreate',
116
+ mutation: true,
117
+ query: `mutation RailwayServiceCreate($input: ServiceCreateInput!) {
118
+ serviceCreate(input: $input) { id name }
119
+ }`,
120
+ },
121
+ serviceDelete: {
122
+ name: 'RailwayServiceDelete',
123
+ mutation: true,
124
+ query: `mutation RailwayServiceDelete($id: String!, $environmentId: String!) {
125
+ serviceDelete(id: $id, environmentId: $environmentId)
126
+ }`,
127
+ },
128
+ serviceConnect: {
129
+ name: 'RailwayServiceConnect',
130
+ mutation: true,
131
+ query: `mutation RailwayServiceConnect($id: String!, $input: ServiceConnectInput!) {
132
+ serviceConnect(id: $id, input: $input) { id }
133
+ }`,
134
+ },
135
+ serviceDisconnect: {
136
+ name: 'RailwayServiceDisconnect',
137
+ mutation: true,
138
+ query: `mutation RailwayServiceDisconnect($id: String!) {
139
+ serviceDisconnect(id: $id) { id }
140
+ }`,
141
+ },
142
+ serviceInstanceUpdate: {
143
+ name: 'RailwayServiceInstanceUpdate',
144
+ mutation: true,
145
+ query: `mutation RailwayServiceInstanceUpdate(
146
+ $environmentId: String!,
147
+ $serviceId: String!,
148
+ $input: ServiceInstanceUpdateInput!
149
+ ) {
150
+ serviceInstanceUpdate(environmentId: $environmentId, serviceId: $serviceId, input: $input)
151
+ }`,
152
+ },
153
+ variables: {
154
+ name: 'RailwayVariables',
155
+ query: `query RailwayVariables($projectId: String!, $environmentId: String!, $serviceId: String!) {
156
+ variables(projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId)
157
+ }`,
158
+ },
159
+ variableUpsert: {
160
+ name: 'RailwayVariableCollectionUpsert',
161
+ mutation: true,
162
+ query: `mutation RailwayVariableCollectionUpsert($input: VariableCollectionUpsertInput!) {
163
+ variableCollectionUpsert(input: $input)
164
+ }`,
165
+ },
166
+ deploy: {
167
+ name: 'RailwayServiceDeploy',
168
+ mutation: true,
169
+ query: `mutation RailwayServiceDeploy($environmentId: String!, $serviceId: String!, $commitSha: String) {
170
+ serviceInstanceDeployV2(environmentId: $environmentId, serviceId: $serviceId, commitSha: $commitSha)
171
+ }`,
172
+ },
173
+ deployments: {
174
+ name: 'RailwayDeployments',
175
+ query: `query RailwayDeployments($projectId: String!, $environmentId: String!, $serviceId: String!) {
176
+ deployments(
177
+ input: { projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId },
178
+ first: 10
179
+ ) {
180
+ edges { node { id status staticUrl url serviceId environmentId createdAt } }
181
+ }
182
+ }`,
183
+ },
184
+ deployment: {
185
+ name: 'RailwayDeployment',
186
+ query: `query RailwayDeployment($id: String!) {
187
+ deployment(id: $id) { id status staticUrl url serviceId environmentId }
188
+ }`,
189
+ },
190
+ buildLogs: {
191
+ name: 'RailwayBuildLogs',
192
+ query: `query RailwayBuildLogs($deploymentId: String!, $limit: Int!) {
193
+ buildLogs(deploymentId: $deploymentId, limit: $limit) { timestamp message severity }
194
+ }`,
195
+ },
196
+ deploymentLogs: {
197
+ name: 'RailwayDeploymentLogs',
198
+ query: `query RailwayDeploymentLogs($deploymentId: String!, $limit: Int!) {
199
+ deploymentLogs(deploymentId: $deploymentId, limit: $limit) { timestamp message severity }
200
+ }`,
201
+ },
202
+ domains: {
203
+ name: 'RailwayDomains',
204
+ query: `query RailwayDomains($projectId: String!, $environmentId: String!, $serviceId: String!) {
205
+ domains(projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId) {
206
+ customDomains { id domain status { verified dnsRecords { hostlabel requiredValue recordType } } }
207
+ serviceDomains { id domain }
208
+ }
209
+ }`,
210
+ },
211
+ customDomain: {
212
+ name: 'RailwayCustomDomain',
213
+ query: `query RailwayCustomDomain($id: String!, $projectId: String!) {
214
+ customDomain(id: $id, projectId: $projectId) {
215
+ id
216
+ domain
217
+ status { verified dnsRecords { hostlabel requiredValue recordType } }
218
+ }
219
+ }`,
220
+ },
221
+ serviceDomainCreate: {
222
+ name: 'RailwayServiceDomainCreate',
223
+ mutation: true,
224
+ query: `mutation RailwayServiceDomainCreate($input: ServiceDomainCreateInput!) {
225
+ serviceDomainCreate(input: $input) { id domain }
226
+ }`,
227
+ },
228
+ customDomainCreate: {
229
+ name: 'RailwayCustomDomainCreate',
230
+ mutation: true,
231
+ query: `mutation RailwayCustomDomainCreate($input: CustomDomainCreateInput!) {
232
+ customDomainCreate(input: $input) { id domain status { dnsRecords { hostlabel requiredValue recordType } } }
233
+ }`,
234
+ },
235
+ volumeCreate: {
236
+ name: 'RailwayVolumeCreate',
237
+ mutation: true,
238
+ query: `mutation RailwayVolumeCreate($input: VolumeCreateInput!) {
239
+ volumeCreate(input: $input) { id name }
240
+ }`,
241
+ },
242
+ } satisfies Record<string, RailwayGraphqlOperation>
243
+
244
+ export function railwaySchemaFingerprintSource(): string {
245
+ return Object.values(railwayOperations)
246
+ .map((operation) => `${operation.name}\n${operation.query.trim()}`)
247
+ .join('\n\n')
248
+ }
@@ -0,0 +1,167 @@
1
+ import type { RailwayDeployOptions, RailwaySourceMode } from './types'
2
+
3
+ const VALUE_FLAGS = new Set([
4
+ '--project',
5
+ '--env',
6
+ '--service',
7
+ '--source',
8
+ '--region',
9
+ '--env-file',
10
+ '--domain',
11
+ '--volume',
12
+ '--token',
13
+ '--timeout',
14
+ '--allow-secret-passthrough',
15
+ ])
16
+
17
+ const BOOLEAN_FLAGS = new Set([
18
+ '--worker',
19
+ '--no-worker',
20
+ '--no-wait-domain',
21
+ '--non-interactive',
22
+ '--dry-run',
23
+ '--cleanup',
24
+ '--yes',
25
+ '--write-env',
26
+ '--no-track',
27
+ '--force-rename',
28
+ '--verbose',
29
+ '--help',
30
+ '-h',
31
+ ])
32
+
33
+ function readFlagValue(args: string[], index: number): { flag: string; value: string; consumed: number } {
34
+ const argument = args[index] ?? ''
35
+ const equalsIndex = argument.indexOf('=')
36
+ if (equalsIndex > 0) {
37
+ const flag = argument.slice(0, equalsIndex)
38
+ const value = argument.slice(equalsIndex + 1)
39
+ if (!value) throw new Error(`Missing value for ${flag}.`)
40
+ return { flag, value, consumed: 1 }
41
+ }
42
+
43
+ const value = args[index + 1]
44
+ if (!value || value.startsWith('-')) {
45
+ throw new Error(`Missing value for ${argument}.`)
46
+ }
47
+ return { flag: argument, value, consumed: 2 }
48
+ }
49
+
50
+ function parseSourceMode(value: string): RailwaySourceMode {
51
+ if (value === 'auto' || value === 'git' || value === 'local') return value
52
+ throw new Error(`Invalid --source value "${value}". Expected auto, git, or local.`)
53
+ }
54
+
55
+ function parseTimeout(value: string): number {
56
+ const timeout = Number.parseInt(value, 10)
57
+ if (!Number.isFinite(timeout) || timeout <= 0) {
58
+ throw new Error('--timeout must be a positive number of seconds.')
59
+ }
60
+ return timeout
61
+ }
62
+
63
+ export function parseRailwayDeployOptions(
64
+ args: string[],
65
+ environment: NodeJS.ProcessEnv = process.env,
66
+ ): RailwayDeployOptions {
67
+ const options: RailwayDeployOptions = {
68
+ environment: 'production',
69
+ service: 'mercato-app',
70
+ worker: true,
71
+ source: 'auto',
72
+ waitDomain: true,
73
+ nonInteractive: environment.CI === 'true',
74
+ dryRun: false,
75
+ cleanup: false,
76
+ yes: false,
77
+ writeEnv: false,
78
+ track: true,
79
+ forceRename: false,
80
+ timeoutSeconds: 900,
81
+ allowedSecretKeys: [],
82
+ verbose: false,
83
+ help: false,
84
+ }
85
+
86
+ for (let index = 0; index < args.length;) {
87
+ const argument = args[index] ?? ''
88
+ const normalizedFlag = argument.includes('=') ? argument.slice(0, argument.indexOf('=')) : argument
89
+
90
+ if (VALUE_FLAGS.has(normalizedFlag)) {
91
+ const { flag, value, consumed } = readFlagValue(args, index)
92
+ if (flag === '--project') options.project = value
93
+ else if (flag === '--env') options.environment = value
94
+ else if (flag === '--service') options.service = value
95
+ else if (flag === '--source') options.source = parseSourceMode(value)
96
+ else if (flag === '--region') options.region = value
97
+ else if (flag === '--env-file') options.envFile = value
98
+ else if (flag === '--domain') options.domain = value
99
+ else if (flag === '--volume') options.volume = value
100
+ else if (flag === '--token') options.token = value
101
+ else if (flag === '--timeout') options.timeoutSeconds = parseTimeout(value)
102
+ else if (flag === '--allow-secret-passthrough') options.allowedSecretKeys.push(value)
103
+ index += consumed
104
+ continue
105
+ }
106
+
107
+ if (!BOOLEAN_FLAGS.has(argument)) {
108
+ throw new Error(`Unknown Railway deploy option "${argument}".`)
109
+ }
110
+
111
+ if (argument === '--worker') options.worker = true
112
+ else if (argument === '--no-worker') options.worker = false
113
+ else if (argument === '--no-wait-domain') options.waitDomain = false
114
+ else if (argument === '--non-interactive') options.nonInteractive = true
115
+ else if (argument === '--dry-run') options.dryRun = true
116
+ else if (argument === '--cleanup') options.cleanup = true
117
+ else if (argument === '--yes') options.yes = true
118
+ else if (argument === '--write-env') options.writeEnv = true
119
+ else if (argument === '--no-track') options.track = false
120
+ else if (argument === '--force-rename') options.forceRename = true
121
+ else if (argument === '--verbose') options.verbose = true
122
+ else if (argument === '--help' || argument === '-h') options.help = true
123
+ index += 1
124
+ }
125
+
126
+ if (options.cleanup && !options.dryRun && options.nonInteractive && !options.yes) {
127
+ throw new Error('--cleanup --non-interactive requires --yes.')
128
+ }
129
+ if (options.environment.trim().length === 0) throw new Error('--env cannot be empty.')
130
+ if (options.service.trim().length === 0) throw new Error('--service cannot be empty.')
131
+ return options
132
+ }
133
+
134
+ export function railwayDeployHelp(): string {
135
+ return `Usage: mercato deploy railway [options]
136
+
137
+ Deploy an Open Mercato standalone app to Railway.
138
+
139
+ --project <name> Railway project name
140
+ --env <name> Environment name (default: production)
141
+ --service <name> App service name (default: mercato-app)
142
+ --worker / --no-worker Enable or disable the dedicated worker
143
+ --source <auto|git|local> Source strategy (default: auto)
144
+ --region <id> Railway region
145
+ --env-file <path> Environment file
146
+ --domain <fqdn> Custom domain
147
+ --no-wait-domain Do not wait for DNS verification
148
+ --volume <mountPath> Create an attachments volume
149
+ --token <value> Railway Account token
150
+ --non-interactive Disable prompts
151
+ --dry-run Print the plan without mutations
152
+ --cleanup --yes Delete the recorded Railway project
153
+ --write-env Persist generated secrets locally
154
+ --no-track Use .mercato/railway.json.local
155
+ --force-rename Allow renaming a recorded project
156
+ --timeout <seconds> Deployment timeout (default: 900)
157
+ --allow-secret-passthrough <key> Allow one credential-like env value
158
+ --verbose Print redacted GraphQL diagnostics`
159
+ }
160
+
161
+ export function redactRailwayCliArgs(args: string[]): string[] {
162
+ return args.map((argument, index) => {
163
+ if (argument.startsWith('--token=')) return '--token=****'
164
+ if (args[index - 1] === '--token') return '****'
165
+ return argument
166
+ })
167
+ }
@@ -0,0 +1,65 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { redactText } from './redaction'
3
+
4
+ function runRailway(args: string[], cwd: string, token: string, secrets: string[]): string {
5
+ const result = spawnSync('railway', args, {
6
+ cwd,
7
+ encoding: 'utf8',
8
+ env: { ...process.env, RAILWAY_API_TOKEN: token },
9
+ stdio: ['ignore', 'pipe', 'pipe'],
10
+ })
11
+ const output = `${result.stdout ?? ''}\n${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
+
18
+ export function uploadLocalSource(input: {
19
+ cwd: string
20
+ projectId: string
21
+ environmentId: string
22
+ serviceId: string
23
+ serviceName: string
24
+ token: string
25
+ }): string | null {
26
+ runRailway(
27
+ [
28
+ 'link',
29
+ '--project',
30
+ input.projectId,
31
+ '--environment',
32
+ input.environmentId,
33
+ '--service',
34
+ input.serviceId,
35
+ ],
36
+ input.cwd,
37
+ input.token,
38
+ [input.token],
39
+ )
40
+ const output = runRailway(
41
+ [
42
+ 'up',
43
+ '--environment',
44
+ input.environmentId,
45
+ '--service',
46
+ input.serviceName,
47
+ '--detach',
48
+ '--json',
49
+ ],
50
+ input.cwd,
51
+ input.token,
52
+ [input.token],
53
+ )
54
+ try {
55
+ const parsed: unknown = JSON.parse(output)
56
+ if (parsed && typeof parsed === 'object') {
57
+ const record = parsed as Record<string, unknown>
58
+ const deploymentId = record.deploymentId ?? record.deployment_id ?? record.id
59
+ return typeof deploymentId === 'string' ? deploymentId : null
60
+ }
61
+ } catch {
62
+ return null
63
+ }
64
+ return null
65
+ }
@@ -0,0 +1,91 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ const SENSITIVE_KEY_PATTERN = /(secret|token|password|private|credential|encryption|fallback|api[_-]?key|database_url|redis_url)/i
4
+ const CREDENTIAL_PATTERNS = [
5
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
6
+ /\bsk-[A-Za-z0-9_-]{12,}\b/,
7
+ /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/,
8
+ /\bgh[pousr]_[A-Za-z0-9]{20,}\b/,
9
+ /\bAKIA[0-9A-Z]{16}\b/,
10
+ ]
11
+
12
+ export function fingerprintSecret(value: string): string {
13
+ return `sha256:${createHash('sha256').update(value).digest('hex').slice(0, 8)}`
14
+ }
15
+
16
+ export function isSensitiveKey(key: string): boolean {
17
+ return SENSITIVE_KEY_PATTERN.test(key)
18
+ }
19
+
20
+ export function detectCredentialValue(value: string, railwayToken?: string): boolean {
21
+ if (railwayToken && value.includes(railwayToken)) return true
22
+ return CREDENTIAL_PATTERNS.some((pattern) => pattern.test(value))
23
+ }
24
+
25
+ export function assertSafeVariables(
26
+ variables: Record<string, string>,
27
+ options: { railwayToken?: string; allowedKeys?: string[] } = {},
28
+ ): void {
29
+ const allowedKeys = new Set(options.allowedKeys ?? [])
30
+ for (const [key, value] of Object.entries(variables)) {
31
+ if (!allowedKeys.has(key) && detectCredentialValue(value, options.railwayToken)) {
32
+ throw new Error(
33
+ `Refusing to upload credential-like value from ${key}. Pass --allow-secret-passthrough ${key} to allow this exact key.`,
34
+ )
35
+ }
36
+ }
37
+ }
38
+
39
+ export function formatVariableValue(key: string, value: string, railwayToken?: string): string {
40
+ if (isSensitiveKey(key) || detectCredentialValue(value, railwayToken)) {
41
+ return `<redacted> (${fingerprintSecret(value)})`
42
+ }
43
+ return value
44
+ }
45
+
46
+ export function redactText(value: string, secrets: string[] = []): string {
47
+ let redacted = value
48
+ for (const secret of secrets.filter(Boolean).sort((left, right) => right.length - left.length)) {
49
+ redacted = redacted.split(secret).join('****')
50
+ }
51
+ for (const pattern of CREDENTIAL_PATTERNS) {
52
+ redacted = redacted.replace(new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`), '****')
53
+ }
54
+ return redacted.replace(/Bearer\s+[A-Za-z0-9._~+/-]+/gi, 'Bearer ****')
55
+ }
56
+
57
+ export function redactStructuredValue(value: unknown, secrets: string[] = []): unknown {
58
+ if (typeof value === 'string') return redactText(value, secrets)
59
+ if (Array.isArray(value)) {
60
+ return value.map((item) => redactStructuredValue(item, secrets))
61
+ }
62
+ if (!value || typeof value !== 'object') return value
63
+ return Object.fromEntries(
64
+ Object.entries(value as Record<string, unknown>).map(([key, nestedValue]) => [
65
+ key,
66
+ isSensitiveKey(key) ? '<redacted>' : redactStructuredValue(nestedValue, secrets),
67
+ ]),
68
+ )
69
+ }
70
+
71
+ export function collectSensitiveStructuredValues(value: unknown): string[] {
72
+ const collected = new Set<string>()
73
+
74
+ function visit(current: unknown): void {
75
+ if (Array.isArray(current)) {
76
+ current.forEach(visit)
77
+ return
78
+ }
79
+ if (!current || typeof current !== 'object') return
80
+ for (const [key, nestedValue] of Object.entries(current as Record<string, unknown>)) {
81
+ if (isSensitiveKey(key) && typeof nestedValue === 'string' && nestedValue.length > 0) {
82
+ collected.add(nestedValue)
83
+ } else {
84
+ visit(nestedValue)
85
+ }
86
+ }
87
+ }
88
+
89
+ visit(value)
90
+ return Array.from(collected)
91
+ }