betterstart-cli 0.0.73 → 0.0.75

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/dist/cli.js CHANGED
@@ -115,7 +115,7 @@ function createInitCommand(runtime) {
115
115
  ).option("--database-provider <provider>", "Database provider: vercel, railway, or manual").option(
116
116
  "--storage-provider <provider>",
117
117
  "Storage provider: vercel-blob, railway-bucket, r2, or local"
118
- ).option("--deploy-provider <provider>", "Deploy provider: vercel, railway, or none").option("--database-plan <id>", "Neon plan id for a Vercel-provisioned database").option("--railway-workspace <id-or-name>", "Railway workspace for a new project").option("--railway-project <id-or-name>", "Railway project to reuse").option("--railway-environment <id-or-name>", "Railway environment (default: production)").option("--railway-service <id-or-name>", "Railway app service (default: web)").option(
118
+ ).option("--deploy-provider <provider>", "Deploy provider: vercel, railway, or none").option("--database-plan <id>", "Neon plan id for a Vercel-provisioned database").option("--railway-workspace <id-or-name>", "Railway workspace for a new project").option("--railway-project <id-or-name>", "Existing Railway project to intentionally reuse").option("--railway-environment <id-or-name>", "Railway environment (default: production)").option("--railway-service <id-or-name>", "Railway app service (default: web)").option(
119
119
  "--railway-bucket-region <region>",
120
120
  "Railway bucket region: sjc, iad, ams, or sin (default: iad)"
121
121
  ).option("--force", "Overwrite all existing Admin files (nuclear option)").action(
@@ -20138,6 +20138,10 @@ function spinner2(options) {
20138
20138
  };
20139
20139
  return {
20140
20140
  start: (message = "") => {
20141
+ if (started) {
20142
+ inner.message(fitSpinnerMessage(message));
20143
+ return;
20144
+ }
20141
20145
  setStarted(true);
20142
20146
  inner.start(fitSpinnerMessage(message));
20143
20147
  },
@@ -22971,6 +22975,16 @@ function parseProjectSummary(value) {
22971
22975
  }
22972
22976
  return { id: project2.id, name: project2.name, workspace };
22973
22977
  }
22978
+ var MAX_PROJECT_NAME_ATTEMPTS = 10;
22979
+ function versionedRailwayProjectName(base, attempt) {
22980
+ return attempt <= 1 ? base : `${base}-v${attempt}`;
22981
+ }
22982
+ function railwayProjectNameIsTaken(projects, candidate) {
22983
+ return projects.some((project2) => project2.name.toLowerCase() === candidate.toLowerCase());
22984
+ }
22985
+ function isRailwayProjectNameConflict(detail) {
22986
+ return /already exists|name.*taken|duplicate.*name/i.test(detail);
22987
+ }
22974
22988
  async function readRailwayProjectContext(runner, cwd, environment, env) {
22975
22989
  const result = await runRailway(runner, ["status", "--environment", environment, "--json"], {
22976
22990
  cwd,
@@ -23039,18 +23053,26 @@ ${result.stderr}`) ?? `Could not link Railway project ${project2}.`
23039
23053
  );
23040
23054
  }
23041
23055
  }
23042
- async function createRailwayProject(runner, cwd, projectName, options) {
23043
- const args = ["init", "--name", projectName, "--json"];
23044
- if (options.workspace) args.push("--workspace", options.workspace);
23045
- const result = await runRailway(runner, args, { cwd, mode: "capture", env: options.env });
23046
- const project2 = result.success ? parseProjectSummary(parseRailwayJson(result.stdout)) : void 0;
23047
- if (!project2) {
23048
- throw new Error(
23049
- railwayOutputTail(`${result.stdout}
23050
- ${result.stderr}`) ?? `Could not create Railway project ${projectName}.`
23051
- );
23056
+ async function createRailwayProject(runner, cwd, projectName, existingProjects, options) {
23057
+ const knownProjects = [...existingProjects];
23058
+ for (let attempt = 1; attempt <= MAX_PROJECT_NAME_ATTEMPTS; attempt++) {
23059
+ const candidate = versionedRailwayProjectName(projectName, attempt);
23060
+ if (railwayProjectNameIsTaken(knownProjects, candidate)) continue;
23061
+ const args = ["init", "--name", candidate, "--json"];
23062
+ if (options.workspace) args.push("--workspace", options.workspace);
23063
+ const result = await runRailway(runner, args, { cwd, mode: "capture", env: options.env });
23064
+ const project2 = result.success ? parseProjectSummary(parseRailwayJson(result.stdout)) : void 0;
23065
+ if (project2) return project2;
23066
+ const detail = railwayOutputTail(`${result.stdout}
23067
+ ${result.stderr}`);
23068
+ if (!detail || !isRailwayProjectNameConflict(detail)) {
23069
+ throw new Error(detail ?? `Could not create Railway project ${candidate}.`);
23070
+ }
23071
+ knownProjects.push({ id: candidate, name: candidate });
23052
23072
  }
23053
- return project2;
23073
+ throw new Error(
23074
+ `Could not create a unique Railway project name after ${MAX_PROJECT_NAME_ATTEMPTS} attempts.`
23075
+ );
23054
23076
  }
23055
23077
  async function ensureRailwayProject(runner, options) {
23056
23078
  const environment = options.environment?.trim() || "production";
@@ -23063,77 +23085,47 @@ async function ensureRailwayProject(runner, options) {
23063
23085
  env: options.env
23064
23086
  });
23065
23087
  } else {
23066
- const linked = await readRailwayProjectContext(runner, options.cwd, environment, options.env);
23067
- if (linked) {
23088
+ let createWorkspace = options.workspace;
23089
+ if (!createWorkspace && options.workspaces?.length === 1) {
23090
+ createWorkspace = options.workspaces[0]?.id;
23091
+ } else if (!createWorkspace && options.interactive && options.workspaces?.length) {
23068
23092
  projectSpinner.clear();
23069
- return linked;
23070
- }
23071
- const projects = options.interactive ? await listRailwayProjects(runner, options.cwd, options.env) : [];
23072
- projectSpinner.clear();
23073
- let selectedProject;
23074
- if (options.interactive && projects.length > 0) {
23075
- const createValue = "__betterstart_create_railway_project__";
23076
- const choice = await p15.select({
23077
- message: "Choose a Railway project",
23078
- options: [
23079
- {
23080
- value: createValue,
23081
- label: `Create ${options.projectName}`,
23082
- hint: "new project"
23083
- },
23084
- ...projects.map((project2) => ({
23085
- value: project2.id,
23086
- label: project2.name,
23087
- hint: project2.workspace?.name
23088
- }))
23089
- ],
23090
- initialValue: createValue
23093
+ const workspace = await p15.select({
23094
+ message: "Choose a Railway workspace",
23095
+ options: options.workspaces.map((candidate) => ({
23096
+ value: candidate.id,
23097
+ label: candidate.name
23098
+ }))
23091
23099
  });
23092
- if (p15.isCancel(choice)) {
23100
+ if (p15.isCancel(workspace)) {
23093
23101
  p15.cancel("Setup cancelled.");
23094
23102
  process.exit(0);
23095
23103
  }
23096
- selectedProject = projects.find((project2) => project2.id === choice);
23097
- }
23098
- projectSpinner.start(
23099
- selectedProject ? `Linking Railway project ${pc2.cyan(selectedProject.name)}` : `Creating Railway project ${pc2.cyan(options.projectName)}`
23100
- );
23101
- if (selectedProject) {
23102
- await linkRailwayProject(runner, options.cwd, selectedProject.id, environment, {
23103
- workspace: selectedProject.workspace?.id ?? options.workspace,
23104
- env: options.env
23105
- });
23106
- } else {
23107
- let createWorkspace = options.workspace;
23108
- if (!createWorkspace && options.workspaces?.length === 1) {
23109
- createWorkspace = options.workspaces[0]?.id;
23110
- } else if (!createWorkspace && options.interactive && options.workspaces?.length) {
23111
- projectSpinner.clear();
23112
- const workspace = await p15.select({
23113
- message: "Choose a Railway workspace",
23114
- options: options.workspaces.map((candidate) => ({
23115
- value: candidate.id,
23116
- label: candidate.name
23117
- }))
23118
- });
23119
- if (p15.isCancel(workspace)) {
23120
- p15.cancel("Setup cancelled.");
23121
- process.exit(0);
23122
- }
23123
- createWorkspace = workspace;
23124
- projectSpinner.start(`Creating Railway project ${pc2.cyan(options.projectName)}`);
23125
- }
23126
- const created = await createRailwayProject(runner, options.cwd, options.projectName, {
23104
+ createWorkspace = workspace;
23105
+ projectSpinner.start("Checking Railway project names");
23106
+ }
23107
+ const projects = await listRailwayProjects(runner, options.cwd, options.env);
23108
+ projectSpinner.message(`Creating Railway project ${pc2.cyan(options.projectName)}`);
23109
+ const created = await createRailwayProject(
23110
+ runner,
23111
+ options.cwd,
23112
+ options.projectName,
23113
+ projects,
23114
+ {
23127
23115
  workspace: createWorkspace,
23128
23116
  env: options.env
23129
- });
23130
- if (environment.toLowerCase() !== "production") {
23131
- await linkRailwayProject(runner, options.cwd, created.id, environment, {
23132
- workspace: created.workspace?.id ?? options.workspace,
23133
- env: options.env
23134
- });
23135
23117
  }
23118
+ );
23119
+ await linkRailwayProject(runner, options.cwd, created.id, environment, {
23120
+ workspace: created.workspace?.id ?? createWorkspace,
23121
+ env: options.env
23122
+ });
23123
+ const context2 = await readRailwayProjectContext(runner, options.cwd, environment, options.env);
23124
+ if (!context2 || context2.id !== created.id) {
23125
+ throw new Error("Railway did not link the newly created project.");
23136
23126
  }
23127
+ projectSpinner.clear();
23128
+ return context2;
23137
23129
  }
23138
23130
  const context = await readRailwayProjectContext(runner, options.cwd, environment, options.env);
23139
23131
  projectSpinner.clear();
@@ -23255,41 +23247,48 @@ async function waitForPostgresVariables(session, cwd, service) {
23255
23247
  return {};
23256
23248
  }
23257
23249
  async function ensureRailwayPostgres(session, cwd) {
23258
- const services = await listRailwayServices(session, cwd);
23259
- const candidates = services.filter((service2) => /^postgres(?:[-_ ].*)?$/i.test(service2.name));
23260
- for (const candidate of candidates) {
23261
- const variables2 = await readRailwayServiceVariables(session, cwd, candidate.name);
23262
- if (variables2.DATABASE_PUBLIC_URL) {
23263
- return { service: candidate, databaseUrl: variables2.DATABASE_PUBLIC_URL };
23250
+ const provisionSpinner = spinner2();
23251
+ provisionSpinner.start("Creating your Railway Postgres database");
23252
+ try {
23253
+ const services = await listRailwayServices(session, cwd);
23254
+ const candidates = services.filter((service2) => /^postgres(?:[-_ ].*)?$/i.test(service2.name));
23255
+ for (const candidate of candidates) {
23256
+ const variables2 = await readRailwayServiceVariables(session, cwd, candidate.name);
23257
+ if (variables2.DATABASE_PUBLIC_URL) {
23258
+ return { service: candidate, databaseUrl: variables2.DATABASE_PUBLIC_URL };
23259
+ }
23264
23260
  }
23265
- }
23266
- if (candidates.length > 0) {
23267
- throw new Error(
23268
- `Railway Postgres service ${candidates.map((candidate) => candidate.name).join(", ")} exists, but DATABASE_PUBLIC_URL is not available. Rerun init after the database is ready.`
23269
- );
23270
- }
23271
- const result = await runRailway(session.runner, ["add", "--database", "postgres", "--json"], {
23272
- cwd,
23273
- mode: "capture",
23274
- env: session.env
23275
- });
23276
- const payload = result.success ? parseRailwayJson(result.stdout) : void 0;
23277
- const serviceName = payload && isNonEmptyString(payload.serviceName) ? payload.serviceName : null;
23278
- const serviceId = payload && isNonEmptyString(payload.serviceId) ? payload.serviceId : null;
23279
- if (!result.success || !serviceName) {
23280
- throw new Error(
23281
- railwayOutputTail(`${result.stdout}
23261
+ if (candidates.length > 0) {
23262
+ throw new Error(
23263
+ `Railway Postgres service ${candidates.map((candidate) => candidate.name).join(", ")} exists, but DATABASE_PUBLIC_URL is not available. Rerun init after the database is ready.`
23264
+ );
23265
+ }
23266
+ const result = await runRailway(session.runner, ["add", "--database", "postgres", "--json"], {
23267
+ cwd,
23268
+ mode: "capture",
23269
+ env: session.env
23270
+ });
23271
+ const payload = result.success ? parseRailwayJson(result.stdout) : void 0;
23272
+ const serviceName = payload && isNonEmptyString(payload.serviceName) ? payload.serviceName : null;
23273
+ const serviceId = payload && isNonEmptyString(payload.serviceId) ? payload.serviceId : null;
23274
+ if (!result.success || !serviceName) {
23275
+ throw new Error(
23276
+ railwayOutputTail(`${result.stdout}
23282
23277
  ${result.stderr}`) ?? "Could not provision Railway Postgres."
23283
- );
23284
- }
23285
- const service = { id: serviceId ?? serviceName, name: serviceName };
23286
- const variables = await waitForPostgresVariables(session, cwd, service);
23287
- if (!variables.DATABASE_PUBLIC_URL) {
23288
- throw new Error(
23289
- `Railway created ${service.name}, but DATABASE_PUBLIC_URL was not available. Rerun init after the database is ready.`
23290
- );
23278
+ );
23279
+ }
23280
+ const service = { id: serviceId ?? serviceName, name: serviceName };
23281
+ provisionSpinner.message("Waiting for the database connection string");
23282
+ const variables = await waitForPostgresVariables(session, cwd, service);
23283
+ if (!variables.DATABASE_PUBLIC_URL) {
23284
+ throw new Error(
23285
+ `Railway created ${service.name}, but DATABASE_PUBLIC_URL was not available. Rerun init after the database is ready.`
23286
+ );
23287
+ }
23288
+ return { service, databaseUrl: variables.DATABASE_PUBLIC_URL };
23289
+ } finally {
23290
+ provisionSpinner.clear();
23291
23291
  }
23292
- return { service, databaseUrl: variables.DATABASE_PUBLIC_URL };
23293
23292
  }
23294
23293
  function parseBucket(value) {
23295
23294
  if (!value || typeof value !== "object") return void 0;
@@ -23320,6 +23319,15 @@ function parseBucketCredentials(value) {
23320
23319
  async function ensureRailwayBucket(session, cwd, options) {
23321
23320
  const name = options.name ?? "media";
23322
23321
  const environmentArgs = ["--environment", session.project.environmentName];
23322
+ const provisionSpinner = spinner2();
23323
+ provisionSpinner.start("Creating your Railway bucket");
23324
+ try {
23325
+ return await provisionRailwayBucketResource(session, cwd, name, options.region, environmentArgs);
23326
+ } finally {
23327
+ provisionSpinner.clear();
23328
+ }
23329
+ }
23330
+ async function provisionRailwayBucketResource(session, cwd, name, region, environmentArgs) {
23323
23331
  const list = await runRailway(session.runner, ["bucket", "list", ...environmentArgs, "--json"], {
23324
23332
  cwd,
23325
23333
  mode: "capture",
@@ -23344,7 +23352,7 @@ ${list.stderr}`) ?? "Could not list Railway buckets."
23344
23352
  if (!bucket) {
23345
23353
  const create = await runRailway(
23346
23354
  session.runner,
23347
- ["bucket", "create", name, "--region", options.region, ...environmentArgs, "--json"],
23355
+ ["bucket", "create", name, "--region", region, ...environmentArgs, "--json"],
23348
23356
  { cwd, mode: "capture", env: session.env }
23349
23357
  );
23350
23358
  const createPayload = create.success ? parseRailwayJson(create.stdout) : void 0;
@@ -27301,9 +27309,6 @@ async function runInitCommand(name, options) {
27301
27309
  databaseUrl = await promptConnectionString();
27302
27310
  }
27303
27311
  } else {
27304
- p22.log.warn(
27305
- "Railway Postgres is container-backed. Plan and test your own backups, upgrades, and high-availability strategy before production use."
27306
- );
27307
27312
  try {
27308
27313
  const session = await getRailwaySession();
27309
27314
  const postgres = await ensureRailwayPostgres(session, cwd);