@tarout/cli 0.6.0 → 0.7.1

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 (2) hide show
  1. package/dist/index.js +166 -112
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ import { Command } from "commander";
61
61
  // package.json
62
62
  var package_default = {
63
63
  name: "@tarout/cli",
64
- version: "0.6.0",
64
+ version: "0.7.1",
65
65
  description: "Tarout CLI \u2014 the Saudi cloud platform for coding agents",
66
66
  type: "module",
67
67
  bin: {
@@ -7133,87 +7133,91 @@ async function resolveDeploymentTarget(client, profile, appIdentifier, options =
7133
7133
  );
7134
7134
  throw new NotFoundError("Application", appIdentifier, suggestions);
7135
7135
  }
7136
- const details2 = await getApplicationDetails(client, app2.applicationId);
7136
+ const details = await getApplicationDetails(client, app2.applicationId);
7137
7137
  return {
7138
7138
  app: app2,
7139
7139
  createdApp: false,
7140
- hasConfiguredSource: hasConfiguredSource(details2),
7141
- shouldUploadSource: shouldUseLocalSource(details2, {
7140
+ hasConfiguredSource: hasConfiguredSource(details),
7141
+ shouldUploadSource: shouldUseLocalSource(details, {
7142
7142
  explicitApp: true
7143
7143
  })
7144
7144
  };
7145
7145
  }
7146
- const linkedProject = getProjectConfig();
7147
- if (linkedProject) {
7148
- const app2 = findApp3(apps, linkedProject.applicationId) ?? findApp3(apps, linkedProject.name);
7149
- if (app2) {
7150
- const details2 = await getApplicationDetails(client, app2.applicationId);
7151
- return {
7152
- app: app2,
7153
- createdApp: false,
7154
- hasConfiguredSource: hasConfiguredSource(details2),
7155
- shouldUploadSource: shouldUseLocalSource(details2, {
7156
- linkedProject: true
7157
- })
7158
- };
7159
- }
7160
- if (!isJsonMode()) {
7161
- log("");
7162
- log(
7163
- colors.warn(
7164
- `Linked application "${linkedProject.name}" was not found in this organization.`
7165
- )
7166
- );
7167
- }
7146
+ if (options.newApp) {
7147
+ assertConfiguredSourceAllowsCreate(sourcePreference);
7148
+ return createNewAppTarget(client, profile, options);
7168
7149
  }
7169
- if (isJsonMode()) {
7170
- throw new InvalidArgumentError(
7171
- "No linked application. Run 'tarout link', pass an app name, or run without --json to choose interactively."
7150
+ const linkedProject = getProjectConfig();
7151
+ const linkedApp = linkedProject ? findApp3(apps, linkedProject.applicationId) ?? findApp3(apps, linkedProject.name) : void 0;
7152
+ if (linkedProject && !linkedApp && !isJsonMode()) {
7153
+ log("");
7154
+ log(
7155
+ colors.warn(
7156
+ `Linked application "${linkedProject.name}" was not found in this organization.`
7157
+ )
7172
7158
  );
7173
7159
  }
7174
7160
  if (apps.length === 0) {
7175
- if (sourcePreference === "configured") {
7176
- throw new InvalidArgumentError(
7177
- 'No Tarout app exists to deploy from a configured source. Create or select an app with a configured Git provider first, or rerun with "--source upload".'
7178
- );
7179
- }
7180
- const app2 = await createAppFromCurrentDirectory(client, profile, options);
7181
- return {
7182
- app: app2,
7183
- createdApp: true,
7184
- hasConfiguredSource: false,
7185
- shouldUploadSource: true
7186
- };
7161
+ assertConfiguredSourceAllowsCreate(sourcePreference, true);
7162
+ return createNewAppTarget(client, profile, options);
7163
+ }
7164
+ if (shouldSkipConfirmation()) {
7165
+ if (linkedApp) return reuseAppTarget(client, profile, linkedApp);
7166
+ assertConfiguredSourceAllowsCreate(sourcePreference, true);
7167
+ return createNewAppTarget(client, profile, options);
7187
7168
  }
7188
7169
  const createValue = "__create__";
7189
- const selected = await select("Select an application to deploy:", [
7170
+ const orderedApps = linkedApp ? [linkedApp, ...apps.filter((a) => a.applicationId !== linkedApp.applicationId)] : apps;
7171
+ const selected = await select(
7172
+ "Create a new app or reuse an existing one?",
7173
+ [
7174
+ {
7175
+ name: `Create a new app from ${colors.cyan(basename(process.cwd()) || "this directory")}`,
7176
+ value: createValue
7177
+ },
7178
+ ...orderedApps.map((app2) => ({
7179
+ name: `Reuse ${app2.name} ${colors.dim(`(${app2.applicationId.slice(0, 8)})`)}${linkedApp && app2.applicationId === linkedApp.applicationId ? colors.dim(" \u2014 linked") : ""}`,
7180
+ value: app2.applicationId
7181
+ }))
7182
+ ],
7190
7183
  {
7191
- name: `Create a new app from ${colors.cyan(basename(process.cwd()) || "this directory")}`,
7192
- value: createValue
7193
- },
7194
- ...apps.map((app2) => ({
7195
- name: `${app2.name} ${colors.dim(`(${app2.applicationId.slice(0, 8)})`)}`,
7196
- value: app2.applicationId
7197
- }))
7198
- ]);
7199
- if (selected === createValue) {
7200
- if (sourcePreference === "configured") {
7201
- throw new InvalidArgumentError(
7202
- 'New apps do not have a configured Git provider source yet. Connect a Git provider first, or rerun with "--source upload".'
7203
- );
7184
+ field: "deploy_app",
7185
+ flag: "--new-app to create a new app, or --app <id|name> to reuse an existing one",
7186
+ context: {
7187
+ linkedApp: linkedApp ? { id: linkedApp.applicationId, name: linkedApp.name } : null,
7188
+ apps: orderedApps.map((a) => ({
7189
+ id: a.applicationId,
7190
+ name: a.name
7191
+ }))
7192
+ }
7204
7193
  }
7205
- const app2 = await createAppFromCurrentDirectory(client, profile, options);
7206
- return {
7207
- app: app2,
7208
- createdApp: true,
7209
- hasConfiguredSource: false,
7210
- shouldUploadSource: true
7211
- };
7194
+ );
7195
+ if (selected === createValue) {
7196
+ assertConfiguredSourceAllowsCreate(sourcePreference);
7197
+ return createNewAppTarget(client, profile, options);
7212
7198
  }
7213
7199
  const app = findApp3(apps, selected);
7214
7200
  if (!app) {
7215
7201
  throw new NotFoundError("Application", selected);
7216
7202
  }
7203
+ return reuseAppTarget(client, profile, app);
7204
+ }
7205
+ function assertConfiguredSourceAllowsCreate(sourcePreference, noAppsExist = false) {
7206
+ if (sourcePreference !== "configured") return;
7207
+ throw new InvalidArgumentError(
7208
+ noAppsExist ? 'No Tarout app exists to deploy from a configured source. Create or select an app with a configured Git provider first, or rerun with "--source upload".' : 'New apps do not have a configured Git provider source yet. Connect a Git provider first, or rerun with "--source upload".'
7209
+ );
7210
+ }
7211
+ async function createNewAppTarget(client, profile, options) {
7212
+ const app = await createAppFromCurrentDirectory(client, profile, options);
7213
+ return {
7214
+ app,
7215
+ createdApp: true,
7216
+ hasConfiguredSource: false,
7217
+ shouldUploadSource: true
7218
+ };
7219
+ }
7220
+ async function reuseAppTarget(client, profile, app) {
7217
7221
  setProjectConfig({
7218
7222
  applicationId: app.applicationId,
7219
7223
  name: app.name,
@@ -7282,9 +7286,30 @@ async function createAppFromCurrentDirectory(client, profile, options = {}) {
7282
7286
  sourceType: application.sourceType
7283
7287
  };
7284
7288
  }
7289
+ async function isOrgPaidSafely(client) {
7290
+ try {
7291
+ const opts = await client.application.getCreateOptions.query();
7292
+ return opts?.isPaid === true;
7293
+ } catch {
7294
+ return false;
7295
+ }
7296
+ }
7285
7297
  async function resolveAppPlanForCreate(client, options) {
7286
7298
  const explicitPlan = normalizeAppPlan(options.plan);
7287
- if (explicitPlan) return explicitPlan;
7299
+ if (explicitPlan && explicitPlan !== "FREE") return explicitPlan;
7300
+ if (explicitPlan === "FREE") {
7301
+ if (await isOrgPaidSafely(client)) {
7302
+ if (!isJsonMode()) {
7303
+ log(
7304
+ colors.dim(
7305
+ "Your org is on a paid plan \u2014 free apps aren't available, so this app will use your paid tier."
7306
+ )
7307
+ );
7308
+ }
7309
+ return void 0;
7310
+ }
7311
+ return "FREE";
7312
+ }
7288
7313
  if (isJsonMode() || shouldSkipConfirmation()) return void 0;
7289
7314
  let createOptions;
7290
7315
  try {
@@ -7564,20 +7589,50 @@ function extractEntitlementKeyFromError(err) {
7564
7589
  const m = msg.match(/Plan limit reached for ([\w.]+)/i);
7565
7590
  return m?.[1];
7566
7591
  }
7592
+ function buildRemedyOptions(remedy, requestedPlan, catalog) {
7593
+ if (remedy.kind === "addon" || remedy.kind === "plan_quantity") {
7594
+ const upgradePlan = nextPlanForRequested(requestedPlan);
7595
+ const planDef = (catalog?.plans ?? []).find(
7596
+ (p) => (p.planKey ?? p.key) === upgradePlan
7597
+ );
7598
+ return [
7599
+ {
7600
+ action: remedy.kind === "addon" ? "buy_addon" : "add_app_slot",
7601
+ label: remedy.kind === "addon" ? `Buy just the ${remedy.targetName ?? remedy.targetKey}` : "Add one more app slot",
7602
+ command: remedy.command
7603
+ },
7604
+ {
7605
+ action: "upgrade_plan",
7606
+ label: `Upgrade to ${planDef?.name ?? upgradePlan}`,
7607
+ command: `tarout billing upgrade ${upgradePlan} --wait`
7608
+ }
7609
+ ];
7610
+ }
7611
+ return [
7612
+ {
7613
+ action: "upgrade_plan",
7614
+ label: `Upgrade to ${remedy.targetName ?? remedy.targetKey}`,
7615
+ command: remedy.command
7616
+ }
7617
+ ];
7618
+ }
7567
7619
  async function emitNeedsUpgrade(client, err, requestedPlan, retryCommand) {
7568
7620
  const message = err instanceof Error ? err.message : "Plan upgrade required";
7569
7621
  const failedKey = extractEntitlementKeyFromError(err);
7570
7622
  const catalog = await fetchCatalogSafely(client);
7571
7623
  const remedy = resolveEntitlementRemedy(failedKey, catalog, { requestedPlan });
7624
+ const options = buildRemedyOptions(remedy, requestedPlan, catalog);
7625
+ const hint = options.length > 1 ? `Two ways to resolve this \u2014 ask the user which they prefer, do not choose for them: (1) ${options[0]?.label}: \`${options[0]?.command}\`; (2) ${options[1]?.label}: \`${options[1]?.command}\`. Then retry: ${retryCommand}.` : `${remedy.hint} Then retry: ${retryCommand}.`;
7572
7626
  outputError("NEEDS_UPGRADE", message, {
7573
7627
  failedEntitlementKey: failedKey,
7574
7628
  remedyKind: remedy.kind,
7575
- // `suggestedPlan` retained for back-compat; now correct for every gate
7576
- // (the plan key, addon key, or quantity-aware plan to act on).
7629
+ // `suggestedPlan`/`nextCommand` retained for back-compat (the recommended
7630
+ // targeted action); `options` is the authoritative choice list.
7577
7631
  suggestedPlan: remedy.targetKey,
7578
7632
  suggestedTarget: remedy.targetKey,
7579
7633
  nextCommand: remedy.command,
7580
- hint: `${remedy.hint} Then retry: ${retryCommand}.`
7634
+ options,
7635
+ hint
7581
7636
  });
7582
7637
  }
7583
7638
  async function listFreeDatabasesSafely(client) {
@@ -8656,6 +8711,12 @@ function registerDeployCommands(program2) {
8656
8711
  ).option(
8657
8712
  "--reuse-storage <ref>",
8658
8713
  "Reuse an existing storage bucket in this project: <id>, <name>, or 'auto' (exactly one match)"
8714
+ ).option(
8715
+ "--new-app",
8716
+ "Create a new app instead of being prompted to reuse an existing one"
8717
+ ).option(
8718
+ "--name <name>",
8719
+ "Name for a newly created app (defaults to the directory name)"
8659
8720
  ).option("--token <token>", "API token to use for this deployment").option("-r, --region <region>", "Deployment region", DEFAULT_REGION).option("--description <text>", "Description for a newly created app").option(
8660
8721
  "--framework-preset <preset>",
8661
8722
  "Framework preset override (e.g. nextjs, vite, astro)"
@@ -8744,7 +8805,7 @@ function registerDeployCommands(program2) {
8744
8805
  } catch (err) {
8745
8806
  if (isEntitlementError(err)) {
8746
8807
  const message = err instanceof Error ? err.message : "Plan upgrade required";
8747
- if (isJsonMode() || shouldSkipConfirmation()) {
8808
+ if (isJsonMode() || isNonInteractiveMode() || shouldSkipConfirmation()) {
8748
8809
  await emitNeedsUpgrade(
8749
8810
  getApiClient(),
8750
8811
  err,
@@ -19204,9 +19265,15 @@ function registerUpCommand(program2) {
19204
19265
  ).option(
19205
19266
  "--api-url <url>",
19206
19267
  "Custom API URL (defaults to saved profile or https://tarout.sa)"
19207
- ).option("--token <token>", "API token for this run").option("--name <name>", "Application name (defaults to directory name)").option("--plan <plan>", "App hosting plan: free, shared, or dedicated", "free").option("--source <source>", "Source: upload (default) or github", "upload").option(
19268
+ ).option("--token <token>", "API token for this run").option("--name <name>", "Application name (defaults to directory name)").option(
19269
+ "--plan <plan>",
19270
+ "App hosting plan: free, shared, or dedicated (defaults to your org's tier)"
19271
+ ).option("--source <source>", "Source: upload (default) or github", "upload").option(
19208
19272
  "--app <ref>",
19209
19273
  "Deploy to an existing app by id or name (skips create-or-pick prompt; 'auto' picks the lone match)"
19274
+ ).option(
19275
+ "--new-app",
19276
+ "Create a new app instead of being prompted to reuse an existing one"
19210
19277
  ).option("--repo <owner/repo>", "GitHub repository (when --source github)").option("--branch <branch>", "GitHub branch (with --source github)", "main").option("-r, --region <region>", "Deployment region", DEFAULT_REGION3).option(
19211
19278
  "--database <type>",
19212
19279
  "Provision and attach a database: none, postgres, or mysql (defaults to auto-detected)"
@@ -19268,9 +19335,7 @@ function registerUpCommand(program2) {
19268
19335
  }
19269
19336
  return allApps;
19270
19337
  };
19271
- if (options.app) {
19272
- const apps = await loadApps();
19273
- const picked = resolveAppRef(apps, options.app);
19338
+ const reuse = (picked, via) => {
19274
19339
  app = picked;
19275
19340
  reused = true;
19276
19341
  setProjectConfig({
@@ -19283,45 +19348,47 @@ function registerUpCommand(program2) {
19283
19348
  event: "app_reused",
19284
19349
  applicationId: picked.applicationId,
19285
19350
  name: picked.name,
19286
- via: "--app"
19351
+ via
19287
19352
  });
19288
- } else if (linked) {
19353
+ };
19354
+ if (options.app) {
19289
19355
  const apps = await loadApps();
19290
- const existing = findApp3(apps, linked.applicationId) ?? findApp3(apps, linked.name);
19291
- if (existing) {
19292
- app = existing;
19293
- reused = true;
19294
- emitEvent2({
19295
- event: "app_reused",
19296
- applicationId: app.applicationId,
19297
- name: app.name,
19298
- via: "linked"
19299
- });
19300
- }
19301
- }
19302
- if (!app && !isJsonMode() && !shouldSkipConfirmation()) {
19356
+ reuse(resolveAppRef(apps, options.app), "--app");
19357
+ } else if (!options.newApp) {
19303
19358
  const apps = await loadApps();
19304
- if (apps.length > 0) {
19359
+ const linkedApp = linked ? findApp3(apps, linked.applicationId) ?? findApp3(apps, linked.name) : void 0;
19360
+ if (shouldSkipConfirmation()) {
19361
+ if (linkedApp) reuse(linkedApp, "linked");
19362
+ } else if (apps.length > 0) {
19305
19363
  const createValue = "__create__";
19306
- log("");
19307
- log(
19308
- `Found ${apps.length} existing app${apps.length === 1 ? "" : "s"} in this organization.`
19309
- );
19364
+ const orderedApps = linkedApp ? [
19365
+ linkedApp,
19366
+ ...apps.filter(
19367
+ (a) => a.applicationId !== linkedApp.applicationId
19368
+ )
19369
+ ] : apps;
19310
19370
  const selected = await select(
19311
- "Deploy to an existing app or create a new one?",
19371
+ "Create a new app or reuse an existing one?",
19312
19372
  [
19313
19373
  {
19314
19374
  name: `Create a new app${options.name ? ` named "${options.name}"` : ""}`,
19315
19375
  value: createValue
19316
19376
  },
19317
- ...apps.map((existing) => ({
19318
- name: `${existing.name} ${colors.dim(`(${existing.applicationId.slice(0, 8)})`)}`,
19377
+ ...orderedApps.map((existing) => ({
19378
+ name: `Reuse ${existing.name} ${colors.dim(`(${existing.applicationId.slice(0, 8)})`)}${linkedApp && existing.applicationId === linkedApp.applicationId ? colors.dim(" \u2014 linked") : ""}`,
19319
19379
  value: existing.applicationId
19320
19380
  }))
19321
19381
  ],
19322
19382
  {
19323
- field: "deploy_target_app",
19324
- flag: "--app <id|name|auto>"
19383
+ field: "deploy_app",
19384
+ flag: "--new-app to create a new app, or --app <id|name> to reuse an existing one",
19385
+ context: {
19386
+ linkedApp: linkedApp ? { id: linkedApp.applicationId, name: linkedApp.name } : null,
19387
+ apps: orderedApps.map((a) => ({
19388
+ id: a.applicationId,
19389
+ name: a.name
19390
+ }))
19391
+ }
19325
19392
  }
19326
19393
  );
19327
19394
  if (selected !== createValue) {
@@ -19329,20 +19396,7 @@ function registerUpCommand(program2) {
19329
19396
  if (!picked) {
19330
19397
  throw new NotFoundError("Application", selected);
19331
19398
  }
19332
- app = picked;
19333
- reused = true;
19334
- setProjectConfig({
19335
- applicationId: picked.applicationId,
19336
- name: picked.name,
19337
- organizationId: profile.organizationId,
19338
- linkedAt: (/* @__PURE__ */ new Date()).toISOString()
19339
- });
19340
- emitEvent2({
19341
- event: "app_reused",
19342
- applicationId: picked.applicationId,
19343
- name: picked.name,
19344
- via: "interactive"
19345
- });
19399
+ reuse(picked, "interactive");
19346
19400
  }
19347
19401
  }
19348
19402
  }
@@ -19370,7 +19424,7 @@ function registerUpCommand(program2) {
19370
19424
  } catch (err) {
19371
19425
  if (isEntitlementError(err)) {
19372
19426
  const message = err instanceof Error ? err.message : "Plan upgrade required";
19373
- if (isJsonMode() || shouldSkipConfirmation()) {
19427
+ if (isJsonMode() || isNonInteractiveMode() || shouldSkipConfirmation()) {
19374
19428
  await emitNeedsUpgrade(client, err, options.plan, "tarout up");
19375
19429
  exit(ExitCode.PERMISSION_DENIED);
19376
19430
  }
@@ -19484,7 +19538,7 @@ function registerUpCommand(program2) {
19484
19538
  } catch (err) {
19485
19539
  if (isEntitlementError(err)) {
19486
19540
  const message = err instanceof Error ? err.message : "Plan upgrade required";
19487
- if (isJsonMode() || shouldSkipConfirmation()) {
19541
+ if (isJsonMode() || isNonInteractiveMode() || shouldSkipConfirmation()) {
19488
19542
  await emitNeedsUpgrade(
19489
19543
  getApiClient(),
19490
19544
  err,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarout/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Tarout CLI — the Saudi cloud platform for coding agents",
5
5
  "type": "module",
6
6
  "bin": {