@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/agentic/guides/core.customers.md +1 -1
- package/dist/agentic/shared/AGENTS.md.template +56 -33
- package/dist/agentic/shared/ai/skills/{auto-continue-pr → om-auto-continue-pr}/SKILL.md +19 -19
- package/dist/agentic/shared/ai/skills/{auto-create-pr → om-auto-continue-pr}/STANDALONE.md +1 -1
- package/dist/agentic/shared/ai/skills/{auto-continue-pr-loop → om-auto-continue-pr-loop}/SKILL.md +32 -32
- package/dist/agentic/shared/ai/skills/{auto-create-pr → om-auto-create-pr}/SKILL.md +24 -24
- package/dist/agentic/shared/ai/skills/{auto-fix-github → om-auto-create-pr}/STANDALONE.md +1 -1
- package/dist/agentic/shared/ai/skills/{auto-create-pr-loop → om-auto-create-pr-loop}/SKILL.md +40 -40
- package/dist/agentic/shared/ai/skills/{auto-fix-github → om-auto-fix-github}/SKILL.md +3 -3
- package/dist/agentic/shared/ai/skills/{auto-continue-pr → om-auto-fix-github}/STANDALONE.md +1 -1
- package/dist/agentic/shared/ai/skills/{auto-review-pr → om-auto-review-pr}/SKILL.md +5 -5
- package/dist/agentic/shared/ai/skills/{auto-review-pr → om-auto-review-pr}/STANDALONE.md +1 -1
- 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
- package/dist/agentic/shared/ai/skills/{backend-ui-design → om-backend-ui-design}/SKILL.md +2 -2
- package/dist/agentic/shared/ai/skills/{code-review → om-code-review}/SKILL.md +1 -1
- package/dist/agentic/shared/ai/skills/{code-review → om-code-review}/references/review-checklist.md +3 -0
- package/dist/agentic/shared/ai/skills/{data-model-design → om-data-model-design}/SKILL.md +4 -2
- package/dist/agentic/shared/ai/skills/{eject-and-customize → om-eject-and-customize}/SKILL.md +2 -2
- package/dist/agentic/shared/ai/skills/{implement-spec → om-implement-spec}/SKILL.md +6 -6
- package/dist/agentic/shared/ai/skills/{integration-builder → om-integration-builder}/SKILL.md +1 -1
- package/dist/agentic/shared/ai/skills/{integration-tests → om-integration-tests}/SKILL.md +1 -1
- package/dist/agentic/shared/ai/skills/{module-scaffold → om-module-scaffold}/SKILL.md +5 -3
- package/dist/agentic/shared/ai/skills/{module-scaffold → om-module-scaffold}/references/naming-conventions.md +7 -8
- package/dist/agentic/shared/ai/skills/om-prepare-issue/SKILL.md +202 -0
- package/dist/agentic/shared/ai/skills/{spec-writing → om-spec-writing}/SKILL.md +6 -6
- package/dist/agentic/shared/ai/skills/{spec-writing → om-spec-writing}/references/spec-checklist.md +3 -3
- package/dist/agentic/shared/ai/skills/{system-extension → om-system-extension}/SKILL.md +1 -1
- package/dist/agentic/shared/ai/skills/{trim-unused-modules → om-trim-unused-modules}/SKILL.md +1 -1
- package/dist/agentic/shared/ai/skills/{troubleshooter → om-troubleshooter}/SKILL.md +1 -1
- package/dist/bin.js +1 -0
- package/dist/bin.js.map +2 -2
- package/dist/lib/__integration__/TC-INT-008.spec.js +1 -1
- package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
- package/dist/lib/agentic-setup.js +52 -51
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.meta.js +7 -0
- package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.meta.js.map +7 -0
- package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.spec.js +63 -0
- package/dist/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.spec.js.map +7 -0
- package/dist/lib/deploy/railway/env.js +98 -0
- package/dist/lib/deploy/railway/env.js.map +7 -0
- package/dist/lib/deploy/railway/graphql-client.js +99 -0
- package/dist/lib/deploy/railway/graphql-client.js.map +7 -0
- package/dist/lib/deploy/railway/index.js +1115 -0
- package/dist/lib/deploy/railway/index.js.map +7 -0
- package/dist/lib/deploy/railway/operations.js +249 -0
- package/dist/lib/deploy/railway/operations.js.map +7 -0
- package/dist/lib/deploy/railway/options.js +155 -0
- package/dist/lib/deploy/railway/options.js.map +7 -0
- package/dist/lib/deploy/railway/railway-cli.js +61 -0
- package/dist/lib/deploy/railway/railway-cli.js.map +7 -0
- package/dist/lib/deploy/railway/redaction.js +88 -0
- package/dist/lib/deploy/railway/redaction.js.map +7 -0
- package/dist/lib/deploy/railway/source.js +128 -0
- package/dist/lib/deploy/railway/source.js.map +7 -0
- package/dist/lib/deploy/railway/state.js +41 -0
- package/dist/lib/deploy/railway/state.js.map +7 -0
- package/dist/lib/deploy/railway/token.js +46 -0
- package/dist/lib/deploy/railway/token.js.map +7 -0
- package/dist/lib/deploy/railway/types.js +1 -0
- package/dist/lib/deploy/railway/types.js.map +7 -0
- package/dist/lib/generators/openapi.js +4 -0
- package/dist/lib/generators/openapi.js.map +2 -2
- package/dist/lib/testing/integration.js +57 -26
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/lib/testing/runtime-utils.js +16 -8
- package/dist/lib/testing/runtime-utils.js.map +2 -2
- package/dist/mercato.js +23 -8
- package/dist/mercato.js.map +2 -2
- package/package.json +11 -12
- package/src/bin.ts +1 -0
- package/src/lib/__integration__/TC-INT-008.spec.ts +1 -1
- package/src/lib/agentic-setup.ts +52 -51
- package/src/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.meta.ts +3 -0
- package/src/lib/deploy/railway/__integration__/TC-CLI-RAILWAY-001.spec.ts +71 -0
- package/src/lib/deploy/railway/__tests__/dry-run.test.ts +61 -0
- package/src/lib/deploy/railway/__tests__/env.test.ts +55 -0
- package/src/lib/deploy/railway/__tests__/graphql-client.test.ts +80 -0
- package/src/lib/deploy/railway/__tests__/operations.test.ts +21 -0
- package/src/lib/deploy/railway/__tests__/options.test.ts +78 -0
- package/src/lib/deploy/railway/__tests__/redaction.test.ts +62 -0
- package/src/lib/deploy/railway/__tests__/source.test.ts +130 -0
- package/src/lib/deploy/railway/__tests__/state.test.ts +24 -0
- package/src/lib/deploy/railway/__tests__/token.test.ts +39 -0
- package/src/lib/deploy/railway/env.ts +112 -0
- package/src/lib/deploy/railway/graphql-client.ts +128 -0
- package/src/lib/deploy/railway/index.ts +1454 -0
- package/src/lib/deploy/railway/operations.ts +248 -0
- package/src/lib/deploy/railway/options.ts +167 -0
- package/src/lib/deploy/railway/railway-cli.ts +65 -0
- package/src/lib/deploy/railway/redaction.ts +91 -0
- package/src/lib/deploy/railway/source.ts +143 -0
- package/src/lib/deploy/railway/state.ts +45 -0
- package/src/lib/deploy/railway/token.ts +49 -0
- package/src/lib/deploy/railway/types.ts +87 -0
- package/src/lib/generators/openapi.ts +3 -0
- package/src/lib/testing/__tests__/integration.test.ts +79 -0
- package/src/lib/testing/__tests__/runtime-utils.test.ts +45 -1
- package/src/lib/testing/integration.ts +74 -28
- package/src/lib/testing/runtime-utils.ts +17 -8
- package/src/mercato.ts +33 -8
- /package/dist/agentic/shared/ai/skills/{backend-ui-design → om-backend-ui-design}/references/ui-components.md +0 -0
- /package/dist/agentic/shared/ai/skills/{data-model-design → om-data-model-design}/references/mikro-orm-cheatsheet.md +0 -0
- /package/dist/agentic/shared/ai/skills/{integration-builder → om-integration-builder}/references/adapter-contracts.md +0 -0
- /package/dist/agentic/shared/ai/skills/{module-scaffold → om-module-scaffold}/references/navigation-patterns.md +0 -0
- /package/dist/agentic/shared/ai/skills/{spec-writing → om-spec-writing}/references/spec-template.md +0 -0
- /package/dist/agentic/shared/ai/skills/{system-extension → om-system-extension}/references/extension-contracts.md +0 -0
- /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
|
+
}
|