@usequark/quark-create-app 1.25.2 → 1.25.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usequark/quark-create-app",
3
- "version": "1.25.2",
3
+ "version": "1.25.4",
4
4
  "description": "CLI to scaffold new Quark projects with Next.js, Prisma, BullMQ, and Railway deployment",
5
5
  "keywords": [
6
6
  "quark",
@@ -36,6 +36,9 @@
36
36
  "templates",
37
37
  "README.md"
38
38
  ],
39
+ "devDependencies": {
40
+ "esbuild": "^0.28.2"
41
+ },
39
42
  "dependencies": {
40
43
  "chalk": "^6.0.0",
41
44
  "commander": "^15.0.0",
@@ -2,6 +2,48 @@ import { execa } from "execa";
2
2
 
3
3
  const RAILWAY = "railway";
4
4
 
5
+ /**
6
+ * Every helper `railway/iac` exports that can appear in a generated body.
7
+ *
8
+ * The import list is derived from the helpers the body actually calls, rather
9
+ * than hardcoded. A helper that is called but not imported is a ReferenceError
10
+ * when the CLI evaluates the file, and that evaluation only happens later, in
11
+ * the user's `railway config apply` — so nothing in this repo ever surfaced it.
12
+ * Deriving the list makes the two impossible to drift apart.
13
+ */
14
+ const IAC_HELPERS = [
15
+ "bucket",
16
+ "database",
17
+ "defineRailway",
18
+ "group",
19
+ "image",
20
+ "postgres",
21
+ "preserve",
22
+ "project",
23
+ "redis",
24
+ "service",
25
+ "volume",
26
+ ];
27
+
28
+ /**
29
+ * Builds a Railway variable reference as a TypeScript *string literal*.
30
+ *
31
+ * Railway's `${{Service.VAR}}` syntax is only reference syntax inside a quoted
32
+ * string. Emitted bare it is not a valid expression — `${{` parses as an object
33
+ * literal that opens and never closes — so the file fails to parse before
34
+ * Railway ever sees it. The value must therefore carry its own quotes.
35
+ *
36
+ * Only for values embedded in generated TypeScript. A value passed to
37
+ * `railway variable set` on a command line wants the bare, unquoted form.
38
+ *
39
+ * @param {string} serviceName - Railway service the variable lives on
40
+ * @param {string} variableName - Variable name on that service
41
+ * @returns {string} A quoted reference, e.g. `"${{Postgres.DATABASE_URL}}"`
42
+ */
43
+ export function iacRef(serviceName, variableName) {
44
+ return `"$\{{${serviceName}.${variableName}}}"`;
45
+ }
46
+
5
47
  /**
6
48
  * Escapes a string for safe embedding in a TypeScript string literal.
7
49
  * Handles backslashes, quotes, newlines, and template literal interpolation.
@@ -53,6 +95,9 @@ function toIdentifier(value) {
53
95
  * @param {object} options.variableRefs - Map of variable key → Railway reference string
54
96
  * @param {object} options.serviceVars - Map of service name → extra env vars
55
97
  * @param {string} [options.projectName] - Railway project name (defaults to "quark-app")
98
+ * @returns {Promise<{content: string, changed: boolean, previous: string|null}>}
99
+ * `changed` is false when the file on disk already matched, in which case no
100
+ * write happened. `previous` is the content that was replaced, or null.
56
101
  */
57
102
  export async function generateIacFile({
58
103
  iacPath,
@@ -65,9 +110,6 @@ export async function generateIacFile({
65
110
  const { default: fs } = await import("fs-extra");
66
111
  const { dirname } = await import("node:path");
67
112
 
68
- const imports = new Set();
69
- imports.add(`import { defineRailway, project, service } from "railway/iac";`);
70
-
71
113
  const resourceLines = [];
72
114
 
73
115
  for (const svc of services) {
@@ -88,13 +130,24 @@ export async function generateIacFile({
88
130
 
89
131
  const preDeploy = "pnpm db:migrate:deploy";
90
132
 
133
+ // Each entry is one complete `key: value` field, and the object body is
134
+ // rendered by joining them. Concatenating pre-rendered blocks instead
135
+ // means each block has to guess whether it owns the separator between it
136
+ // and its neighbour — and when two blocks both assume they do, the file
137
+ // ships with `healthcheckTimeout: 120,,`, which does not parse. A field
138
+ // list makes that unrepresentable: there is exactly one join.
139
+ const fields = [
140
+ `build: "${escapeTsString(buildCmd)}"`,
141
+ `start: "${escapeTsString(startCmd)}"`,
142
+ `preDeploy: "${escapeTsString(preDeploy)}"`,
143
+ ];
144
+
91
145
  // Healthcheck (web only)
92
- const healthcheck =
93
- svc.kind === "web"
94
- ? `\n\t\thealthcheck: "/api/health",\n\t\thealthcheckTimeout: 120,`
95
- : "";
146
+ if (svc.kind === "web") {
147
+ fields.push('healthcheck: "/api/health"', "healthcheckTimeout: 120");
148
+ }
96
149
 
97
- // Build env vars — escape all string values
150
+ // Env vars — escape all string values
98
151
  const extraVars = serviceVars[svc.name] || {};
99
152
  const envEntries = [
100
153
  ...Object.entries(variableRefs),
@@ -102,33 +155,61 @@ export async function generateIacFile({
102
155
  ...Object.entries(extraVars),
103
156
  ];
104
157
 
105
- const envBlock =
106
- envEntries.length > 0
107
- ? `,\n\t\tenv: {\n${envEntries.map(([k, v]) => `\t\t\t${escapeTsString(k)}: ${v},`).join("\n")}\n\t\t}`
108
- : "";
158
+ if (envEntries.length > 0) {
159
+ const envLines = envEntries
160
+ .map(([k, v]) => `\t\t\t${escapeTsString(k)}: ${v},`)
161
+ .join("\n");
162
+ fields.push(`env: {\n${envLines}\n\t\t}`);
163
+ }
109
164
 
110
- resourceLines.push(`\tconst ${svcIdentifier} = service("${svcLabel}", {
111
- \t\tbuild: "${escapeTsString(buildCmd)}",
112
- \t\tstart: "${escapeTsString(startCmd)}",
113
- \t\tpreDeploy: "${escapeTsString(preDeploy)}",${healthcheck}${envBlock}
114
- \t});`);
165
+ resourceLines.push(
166
+ `\tconst ${svcIdentifier} = service("${svcLabel}", {\n\t\t${fields.join(",\n\t\t")},\n\t});`,
167
+ );
115
168
  }
116
169
 
117
170
  const resources = services.map((s) => toIdentifier(s.name)).join(", ");
118
171
 
119
- const content = `${Array.from(imports).join("\n")}
120
-
121
- export default defineRailway(() => {
172
+ const body = `export default defineRailway(() => {
122
173
  ${resourceLines.join("\n\n")}
123
174
 
124
- \treturn project("${escapeTsString(projectName)}", {
125
- \t\tresources: [${resources}],
126
- \t});
175
+ return project("${escapeTsString(projectName)}", {
176
+ resources: [${resources}],
177
+ });
127
178
  });
128
179
  `;
129
180
 
181
+ // Derive the import list from the body so a helper can never be called
182
+ // without being in scope. See IAC_HELPERS.
183
+ const used = IAC_HELPERS.filter((helper) =>
184
+ new RegExp(`\\b${helper}\\s*\\(`).test(body),
185
+ ).sort();
186
+
187
+ const content = `import { ${used.join(", ")} } from "railway/iac";
188
+
189
+ ${body}`;
190
+
130
191
  await fs.ensureDir(dirname(iacPath));
131
- await fs.writeFile(iacPath, content, "utf8");
192
+
193
+ // `.railway/railway.ts` is a tracked file that every scaffold ships with a
194
+ // hand-written copy of. Overwriting it silently is how a deploy once
195
+ // replaced a committed file with generated output and the user only found
196
+ // out from `git status` afterwards. So: skip the write when nothing changed,
197
+ // and hand the previous content back when something did, so the caller can
198
+ // show the diff. The file is tracked, so `git checkout` remains the escape
199
+ // hatch — this only makes the change visible at the moment it happens.
200
+ let previous = null;
201
+ try {
202
+ previous = await fs.readFile(iacPath, "utf8");
203
+ } catch {
204
+ previous = null;
205
+ }
206
+
207
+ const changed = previous !== content;
208
+ if (changed) {
209
+ await fs.writeFile(iacPath, content, "utf8");
210
+ }
211
+
212
+ return { content, changed, previous };
132
213
  }
133
214
 
134
215
  /**
@@ -153,18 +234,68 @@ export async function applyIacConfig({ cwd, timeout = 120_000 } = {}) {
153
234
  }
154
235
  }
155
236
 
237
+ /**
238
+ * Checks whether the `railway` package already resolves from `cwd`.
239
+ *
240
+ * Walks up from `cwd` the way Node's resolver does, so a hoisted install in a
241
+ * pnpm workspace root is found from a package directory too.
242
+ *
243
+ * @param {object} options
244
+ * @param {string} options.cwd - Project root directory
245
+ * @returns {Promise<boolean>}
246
+ */
247
+ export async function hasRailwaySdk({ cwd } = {}) {
248
+ const { default: fs } = await import("fs-extra");
249
+ const { dirname, join, parse, resolve } = await import("node:path");
250
+
251
+ const root = parse(resolve(cwd)).root;
252
+ let dir = resolve(cwd);
253
+
254
+ while (dir !== root) {
255
+ if (
256
+ await fs.pathExists(join(dir, "node_modules", "railway", "package.json"))
257
+ ) {
258
+ return true;
259
+ }
260
+ dir = dirname(dir);
261
+ }
262
+
263
+ return false;
264
+ }
265
+
156
266
  /**
157
267
  * Installs the Railway SDK (required for TypeScript IaC evaluation).
158
268
  *
269
+ * No-op when the package already resolves, so a repeat deploy does not touch
270
+ * package.json or the lockfile again.
271
+ *
272
+ * `-w` is passed explicitly because every scaffolded project is a pnpm
273
+ * workspace and the SDK is added at the root. Current pnpm versions accept the
274
+ * bare form, but older ones reject it with `ERR_PNPM_ADDING_TO_ROOT` and name
275
+ * the same command as the remedy, so being explicit is the only version this
276
+ * has to reason about.
277
+ *
159
278
  * @param {object} options
160
279
  * @param {string} options.cwd - Project root directory
161
280
  * @param {string} [options.packageManager="pnpm"] - Package manager to use
281
+ * @returns {Promise<{installed: boolean, alreadyPresent: boolean}>}
162
282
  */
163
283
  export async function installRailwaySdk({ cwd, packageManager = "pnpm" } = {}) {
164
- await execa(packageManager, ["add", "-D", "railway"], {
284
+ if (await hasRailwaySdk({ cwd })) {
285
+ return { installed: false, alreadyPresent: true };
286
+ }
287
+
288
+ const args = ["add", "-D"];
289
+ if (packageManager === "pnpm") {
290
+ args.push("-w");
291
+ }
292
+ args.push("railway");
293
+
294
+ await execa(packageManager, args, {
165
295
  cwd,
166
296
  timeout: 60_000,
167
297
  });
298
+ return { installed: true, alreadyPresent: false };
168
299
  }
169
300
 
170
301
  /**
@@ -3,6 +3,8 @@ export {
3
3
  escapeTsString,
4
4
  generateIacFile,
5
5
  getDeploymentStatus,
6
+ hasRailwaySdk,
7
+ iacRef,
6
8
  installRailwaySdk,
7
9
  } from "./iac.js";
8
10
  export {
@@ -21,7 +23,9 @@ export {
21
23
  listProjects,
22
24
  RAILWAY,
23
25
  RailwayError,
26
+ readLinkedProject,
24
27
  removeServiceDomain,
28
+ resolveProjectName,
25
29
  setProjectVariables,
26
30
  tryLinkProject,
27
31
  } from "./railway.js";
@@ -42,13 +42,8 @@ export async function checkRailwayLogin() {
42
42
  }
43
43
 
44
44
  export async function isProjectLinked(cwd) {
45
- const railwayDir = path.join(cwd, ".railway");
46
- try {
47
- const entries = await fs.readdir(railwayDir);
48
- return entries.some((e) => e.endsWith(".json"));
49
- } catch {
50
- return false;
51
- }
45
+ const { projectId } = await readLinkedProject({ cwd });
46
+ return projectId !== null;
52
47
  }
53
48
 
54
49
  export async function tryLinkProject(projectId, { cwd } = {}) {
@@ -76,6 +71,124 @@ export async function listProjects({ cwd } = {}) {
76
71
  }
77
72
  }
78
73
 
74
+ /**
75
+ * Reads the Railway project this directory is linked to.
76
+ *
77
+ * Two layouts are in play, because the CLI moved:
78
+ *
79
+ * - Railway CLI 3.x wrote `.railway/config.json` inside the project directory,
80
+ * holding only `projectId` — no name to be found in it.
81
+ * - Railway CLI 5.x (verified against 5.62.1) keeps every link in
82
+ * `~/.railway/config.json`, in a `projects` map keyed by absolute directory,
83
+ * with `project` (the id) and `name` alongside it. A linked project
84
+ * directory contains `.railway/railway.ts` and nothing else.
85
+ *
86
+ * Both are read, per-project first. The `~/.railway` file also holds the user's
87
+ * OAuth tokens; only the `projects` map is read and nothing else from it is
88
+ * ever returned or logged.
89
+ *
90
+ * @param {object} options
91
+ * @param {string} options.cwd - Project root directory
92
+ * @returns {Promise<{projectId: string|null, projectName: string|null}>}
93
+ */
94
+ export async function readLinkedProject({ cwd } = {}) {
95
+ const fromProjectDir = await readProjectDirLink(cwd);
96
+ if (fromProjectDir.projectId) return fromProjectDir;
97
+
98
+ return readUserConfigLink(cwd);
99
+ }
100
+
101
+ /** Railway CLI 3.x layout: `.railway/config.json` in the project directory. */
102
+ async function readProjectDirLink(cwd) {
103
+ const railwayDir = path.join(cwd, ".railway");
104
+ try {
105
+ const entries = await fs.readdir(railwayDir);
106
+ for (const entry of entries.filter((e) => e.endsWith(".json"))) {
107
+ try {
108
+ const parsed = JSON.parse(
109
+ await fs.readFile(path.join(railwayDir, entry), "utf8"),
110
+ );
111
+ const projectId = parsed?.projectId ?? null;
112
+ const projectName = parsed?.projectName ?? parsed?.name ?? null;
113
+ if (projectId || projectName) return { projectId, projectName };
114
+ } catch {
115
+ // Not the file we were after, or unparseable — try the next.
116
+ }
117
+ }
118
+ } catch {
119
+ // No .railway directory, or not readable.
120
+ }
121
+ return { projectId: null, projectName: null };
122
+ }
123
+
124
+ /**
125
+ * Railway CLI 5.x layout: `~/.railway/config.json`, `projects` keyed by
126
+ * absolute directory path.
127
+ */
128
+ async function readUserConfigLink(cwd) {
129
+ const empty = { projectId: null, projectName: null };
130
+
131
+ const home = process.env.HOME ?? process.env.USERPROFILE;
132
+ if (!home) return empty;
133
+
134
+ try {
135
+ const parsed = JSON.parse(
136
+ await fs.readFile(path.join(home, ".railway", "config.json"), "utf8"),
137
+ );
138
+ const projects = parsed?.projects;
139
+ if (!projects || typeof projects !== "object") return empty;
140
+
141
+ const target = path.resolve(cwd);
142
+ const entry =
143
+ projects[target] ??
144
+ Object.values(projects).find(
145
+ (v) =>
146
+ typeof v?.projectPath === "string" &&
147
+ path.resolve(v.projectPath) === target,
148
+ );
149
+ if (!entry) return empty;
150
+
151
+ return {
152
+ projectId: entry.project ?? null,
153
+ projectName: entry.name ?? null,
154
+ };
155
+ } catch {
156
+ // No user config, unparseable, or unreadable — treat as unlinked.
157
+ return empty;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Resolves a project id to its name via the API, for when the link record
163
+ * carries only an id.
164
+ *
165
+ * @param {string} projectId
166
+ * @param {object} options
167
+ * @param {string} options.cwd - Project root directory
168
+ * @returns {Promise<string|null>}
169
+ */
170
+ export async function resolveProjectName(projectId, { cwd } = {}) {
171
+ if (!projectId) return null;
172
+ const match = (await listProjects({ cwd })).find((p) => p.id === projectId);
173
+ return match?.name ?? null;
174
+ }
175
+
176
+ /**
177
+ * Best-effort name for a linked project: the link record when it carries one,
178
+ * otherwise resolved from its id.
179
+ *
180
+ * @param {object} options
181
+ * @param {string} options.cwd - Project root directory
182
+ * @returns {Promise<{projectId: string|null, projectName: string|null}>}
183
+ */
184
+ async function linkedProjectWithName({ cwd }) {
185
+ const linked = await readLinkedProject({ cwd });
186
+ if (linked.projectName || !linked.projectId) return linked;
187
+
188
+ const projectName = await resolveProjectName(linked.projectId, { cwd });
189
+ return { projectId: linked.projectId, projectName };
190
+ }
191
+
79
192
  export async function ensureRailwayProject({
80
193
  projectName,
81
194
  projectId,
@@ -86,11 +199,26 @@ export async function ensureRailwayProject({
86
199
  if (alreadyLinked) {
87
200
  try {
88
201
  await execa(RAILWAY, ["status"], { cwd, timeout: 15_000 });
89
- return { created: false, linked: true, projectName: null };
202
+ // Report the linked name rather than null: callers label generated
203
+ // IaC and app metadata from it, and falling back to the local
204
+ // directory name is how a project called `quark-site` ended up
205
+ // advertising itself as whatever directory it was deployed from.
206
+ const linked = await linkedProjectWithName({ cwd });
207
+ return {
208
+ created: false,
209
+ linked: true,
210
+ projectName: linked.projectName,
211
+ };
90
212
  } catch {
91
- // Stale link - clean up so we don't create an orphan project
92
- const railwayDir = path.join(cwd, ".railway");
93
- await fs.rm(railwayDir, { recursive: true, force: true });
213
+ // Stale link - clear it so we don't create an orphan project.
214
+ // `railway unlink` is what actually removes the link; it lives in
215
+ // ~/.railway on CLI 5.x, so deleting .railway/ here would only
216
+ // destroy the IaC file while leaving the stale link in place.
217
+ try {
218
+ await execa(RAILWAY, ["unlink"], { cwd, timeout: 15_000 });
219
+ } catch {
220
+ // Nothing to unlink, or it failed — fall through either way.
221
+ }
94
222
  }
95
223
  }
96
224
 
@@ -99,7 +227,8 @@ export async function ensureRailwayProject({
99
227
  cwd,
100
228
  timeout: 30_000,
101
229
  });
102
- return { created: false, linked: true, projectName: null };
230
+ const projectName = await resolveProjectName(projectId, { cwd });
231
+ return { created: false, linked: true, projectName };
103
232
  }
104
233
 
105
234
  if (!projectName) {
@@ -12,6 +12,8 @@ import {
12
12
  getDeploymentStatus,
13
13
  getExistingVariable,
14
14
  getServiceUrl,
15
+ hasRailwaySdk,
16
+ iacRef,
15
17
  installRailwaySdk,
16
18
  removeServiceDomain,
17
19
  setProjectVariables,
@@ -99,13 +101,17 @@ export async function deployToRailway(options = {}) {
99
101
 
100
102
  // --- Step 4: Ensure Railway project ---
101
103
  console.log(chalk.cyan(" Ensuring Railway project..."));
104
+ let resolvedProjectName = projectName || null;
102
105
  try {
103
106
  const project = await ensureRailwayProject({ projectName, projectId, cwd });
107
+ // Prefer the name Railway actually resolved. The local directory name is
108
+ // only a fallback for naming a brand new project — it must not become the
109
+ // project's identity just because the deploy ran from a differently-named
110
+ // checkout.
111
+ resolvedProjectName = project.projectName || resolvedProjectName;
104
112
  if (project.created) {
105
113
  console.log(
106
- chalk.green(
107
- ` ✔ Created Railway project "${project.projectName || projectName || ""}"`,
108
- ),
114
+ chalk.green(` ✔ Created Railway project "${resolvedProjectName}"`),
109
115
  );
110
116
  } else {
111
117
  console.log(chalk.green(" ✔ Railway project linked"));
@@ -141,7 +147,7 @@ export async function deployToRailway(options = {}) {
141
147
  }
142
148
 
143
149
  // --- Step 6: Preserve existing secrets across deploys ---
144
- const projectLabel = path.basename(cwd);
150
+ const projectLabel = resolvedProjectName || path.basename(cwd);
145
151
  let authSecret;
146
152
  let nextAuthSecret;
147
153
 
@@ -170,14 +176,18 @@ export async function deployToRailway(options = {}) {
170
176
  console.log(chalk.cyan(" Generating IaC configuration..."));
171
177
  const iacPath = `${cwd}/.railway/railway.ts`;
172
178
 
173
- await generateIacFile({
179
+ const iacResult = await generateIacFile({
174
180
  iacPath,
175
181
  services: discovery.services,
176
182
  // Pass raw name — generateIacFile handles escaping internally
177
- projectName: projectName || projectLabel,
183
+ projectName: projectLabel,
178
184
  variableRefs: {
179
- DATABASE_URL: `\${{${pgServiceName}.DATABASE_URL}}`,
180
- REDIS_URL: `\${{${redisServiceName}.REDIS_URL}}`,
185
+ // Quoted: these land inside generated TypeScript as object values,
186
+ // where `${{...}}` must be a string literal to parse at all. The
187
+ // equivalent bare form is still used in step 9, where the value is a
188
+ // command-line argument rather than an expression.
189
+ DATABASE_URL: iacRef(pgServiceName, "DATABASE_URL"),
190
+ REDIS_URL: iacRef(redisServiceName, "REDIS_URL"),
181
191
  NODE_ENV: '"production"',
182
192
  APP_NAME: `"${escapeTsString(projectLabel)}"`,
183
193
  APP_DESCRIPTION: `"${escapeTsString(`${projectLabel} - Quark application`)}"`,
@@ -197,23 +207,34 @@ export async function deployToRailway(options = {}) {
197
207
  },
198
208
  },
199
209
  });
210
+ if (iacResult.changed) {
211
+ console.log(
212
+ chalk.yellow(
213
+ " · .railway/railway.ts rewritten — review with `git diff .railway/railway.ts`",
214
+ ),
215
+ );
216
+ }
200
217
  console.log(chalk.green(` ✔ IaC file generated at .railway/railway.ts`));
201
218
 
202
219
  // --- Step 8: Install Railway SDK (required for IaC evaluation) ---
203
- console.log(chalk.cyan(" Installing Railway SDK..."));
204
- try {
205
- await installRailwaySdk({ cwd });
206
- console.log(chalk.green(" ✔ Railway SDK installed"));
207
- } catch (error) {
208
- // SDK is mandatory — railway config apply cannot evaluate .railway/railway.ts without it
209
- console.error(
210
- chalk.red(` ✖ Failed to install Railway SDK: ${error.message}`),
211
- );
212
- throw new Error(
213
- `Railway SDK installation failed. The generated IaC file requires the "railway" package.\n` +
214
- `Install it manually: pnpm add -D railway\n` +
215
- `Original error: ${error.message}`,
216
- );
220
+ if (await hasRailwaySdk({ cwd })) {
221
+ console.log(chalk.dim(" · Railway SDK already installed"));
222
+ } else {
223
+ console.log(chalk.cyan(" Installing Railway SDK..."));
224
+ try {
225
+ await installRailwaySdk({ cwd });
226
+ console.log(chalk.green(" ✔ Railway SDK installed"));
227
+ } catch (error) {
228
+ // SDK is mandatory — railway config apply cannot evaluate .railway/railway.ts without it
229
+ console.error(
230
+ chalk.red(` ✖ Failed to install Railway SDK: ${error.message}`),
231
+ );
232
+ throw new Error(
233
+ `Railway SDK installation failed. The generated IaC file requires the "railway" package.\n` +
234
+ `Install it manually: pnpm add -D -w railway\n` +
235
+ `Original error: ${error.message}`,
236
+ );
237
+ }
217
238
  }
218
239
 
219
240
  // --- Step 9: Set dynamic variable references (infrastructure wiring) ---
@@ -12,13 +12,17 @@ export {
12
12
  getDeploymentStatus,
13
13
  getExistingVariable,
14
14
  getServiceUrl,
15
+ hasRailwaySdk,
16
+ iacRef,
15
17
  installRailwaySdk,
16
18
  isProjectLinked,
17
19
  listProjects,
18
20
  RAILWAY,
19
21
  RAILWAY_DIAGNOSTIC_CODES,
20
22
  RailwayError,
23
+ readLinkedProject,
21
24
  removeServiceDomain,
25
+ resolveProjectName,
22
26
  setProjectVariables,
23
27
  tryLinkProject,
24
28
  } from "./adapters/index.js";
@@ -1,4 +1,4 @@
1
- import { defineRailway, project, service } from "railway/iac";
1
+ import { defineRailway, preserve, project, service } from "railway/iac";
2
2
 
3
3
  export default defineRailway(() => {
4
4
  const web = service("web", {