@tarout/cli 0.4.0 → 0.6.0

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 +278 -14
  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.4.0",
64
+ version: "0.6.0",
65
65
  description: "Tarout CLI \u2014 the Saudi cloud platform for coding agents",
66
66
  type: "module",
67
67
  bin: {
@@ -3758,8 +3758,8 @@ async function performBillingChange(client, input2) {
3758
3758
  addons: input2.addons
3759
3759
  });
3760
3760
  } else if (kind === "addon") {
3761
- const addons = input2.addons ?? (input2.addonKey ? [{ addonKey: input2.addonKey, quantity: input2.quantity ?? 1 }] : []);
3762
- result = await client.subscription.purchaseAddons.mutate({ addons });
3761
+ const items = input2.addons ?? (input2.addonKey ? [{ addonKey: input2.addonKey, quantity: input2.quantity ?? 1 }] : []);
3762
+ result = await client.subscription.purchaseAddons.mutate({ items });
3763
3763
  } else {
3764
3764
  result = await client.subscription.setPlanQuantity.mutate({
3765
3765
  quantity: input2.quantity
@@ -3949,6 +3949,46 @@ async function pollCheckoutUntilTerminal(client, orderId, opts) {
3949
3949
  };
3950
3950
  }
3951
3951
 
3952
+ // src/lib/plan-cart.ts
3953
+ function planFamily(planKey) {
3954
+ if (!planKey) return null;
3955
+ if (planKey === "free") return "FREE";
3956
+ if (planKey === "shared" || planKey.startsWith("shared_") || planKey.startsWith("bundle_")) {
3957
+ return "SHARED";
3958
+ }
3959
+ if (planKey === "dedicated" || planKey.startsWith("dedicated_")) {
3960
+ return "DEDICATED";
3961
+ }
3962
+ return null;
3963
+ }
3964
+ function isPaidFamily(planKey) {
3965
+ const family = planFamily(planKey);
3966
+ return family === "SHARED" || family === "DEDICATED";
3967
+ }
3968
+ function resourceAddonKeysForPlan(planKey) {
3969
+ switch (planFamily(planKey)) {
3970
+ case "SHARED":
3971
+ return { dbAddonKey: "db.starter", storageAddonKey: "storage.gb" };
3972
+ case "DEDICATED":
3973
+ return { dbAddonKey: "db.pro", storageAddonKey: "storage.gb" };
3974
+ default:
3975
+ return { dbAddonKey: null, storageAddonKey: null };
3976
+ }
3977
+ }
3978
+ function buildPlanAddonCart(planKey, resources) {
3979
+ const { dbAddonKey, storageAddonKey } = resourceAddonKeysForPlan(planKey);
3980
+ const databases = Math.max(0, Math.floor(resources.databases ?? 0));
3981
+ const storageGb = Math.max(0, Math.floor(resources.storageGb ?? 0));
3982
+ const cart = [];
3983
+ if (dbAddonKey && databases > 0) {
3984
+ cart.push({ addonKey: dbAddonKey, quantity: databases });
3985
+ }
3986
+ if (storageAddonKey && storageGb > 0) {
3987
+ cart.push({ addonKey: storageAddonKey, quantity: storageGb });
3988
+ }
3989
+ return cart;
3990
+ }
3991
+
3952
3992
  // src/commands/billing.ts
3953
3993
  function reportBillingResult(result, label) {
3954
3994
  const code = emitBillingResult(result, { label });
@@ -4078,7 +4118,8 @@ function registerBillingCommands(program2) {
4078
4118
  const client = getApiClient();
4079
4119
  let targetPlan = planKey || options.plan;
4080
4120
  const billingPeriod = options.billingPeriod;
4081
- const addons = Array.isArray(options.addon) && options.addon.length > 0 ? options.addon : void 0;
4121
+ let planQuantity = options.quantity;
4122
+ let addons = Array.isArray(options.addon) && options.addon.length > 0 ? options.addon : void 0;
4082
4123
  if (!targetPlan) {
4083
4124
  const _spinner = startSpinner("Fetching plans...");
4084
4125
  const catalog = await client.subscription.getCatalog.query();
@@ -4109,12 +4150,31 @@ function registerBillingCommands(program2) {
4109
4150
  if (!targetPlan) {
4110
4151
  throw new Error("No plan selected");
4111
4152
  }
4153
+ if (!isJsonMode() && !isNonInteractiveMode() && !shouldSkipConfirmation() && !addons && isPaidFamily(targetPlan)) {
4154
+ if (planQuantity === void 0 && planFamily(targetPlan) === "SHARED") {
4155
+ const apps = parsePositiveInt(
4156
+ await input("How many apps (app slots)?", "1"),
4157
+ 1
4158
+ );
4159
+ planQuantity = Math.max(1, apps);
4160
+ }
4161
+ const databases = parsePositiveInt(
4162
+ await input("How many databases to include?", "1"),
4163
+ 0
4164
+ );
4165
+ const storageGb = parsePositiveInt(
4166
+ await input("Object storage to include (GB, 0 for none)?", "5"),
4167
+ 0
4168
+ );
4169
+ const cart = buildPlanAddonCart(targetPlan, { databases, storageGb });
4170
+ if (cart.length > 0) addons = cart;
4171
+ }
4112
4172
  const _previewSpinner = startSpinner("Calculating change...");
4113
4173
  let preview;
4114
4174
  try {
4115
4175
  preview = await client.subscription.previewPlanChange.query({
4116
4176
  planKey: targetPlan,
4117
- planQuantity: options.quantity,
4177
+ planQuantity,
4118
4178
  billingPeriod,
4119
4179
  addons
4120
4180
  });
@@ -4126,7 +4186,7 @@ function registerBillingCommands(program2) {
4126
4186
  if (!shouldSkipConfirmation()) {
4127
4187
  log("");
4128
4188
  log(`Plan: ${colors.cyan(targetPlan)}`);
4129
- if (options.quantity) log(`Quantity: ${options.quantity}`);
4189
+ if (planQuantity) log(`Quantity: ${planQuantity}`);
4130
4190
  if (billingPeriod) log(`Billing period: ${billingPeriod}`);
4131
4191
  if (addons && addons.length > 0) {
4132
4192
  log(
@@ -4153,7 +4213,7 @@ function registerBillingCommands(program2) {
4153
4213
  flag: "--yes",
4154
4214
  context: {
4155
4215
  plan: targetPlan,
4156
- quantity: options.quantity,
4216
+ quantity: planQuantity,
4157
4217
  billingPeriod,
4158
4218
  addons,
4159
4219
  amountDueHalalas
@@ -4169,7 +4229,7 @@ function registerBillingCommands(program2) {
4169
4229
  const result = await performBillingChange(client, {
4170
4230
  kind: "plan",
4171
4231
  planKey: targetPlan,
4172
- quantity: options.quantity,
4232
+ quantity: planQuantity,
4173
4233
  billingPeriod,
4174
4234
  addons,
4175
4235
  wait: options.wait,
@@ -4461,7 +4521,7 @@ function registerBillingCommands(program2) {
4461
4521
  const client = getApiClient();
4462
4522
  const _spinner = startSpinner("Calculating preview...");
4463
4523
  const preview = await client.subscription.previewAddonsPurchase.query({
4464
- addons: [{ addonKey, quantity: options.quantity || 1 }]
4524
+ items: [{ addonKey, quantity: options.quantity || 1 }]
4465
4525
  });
4466
4526
  succeedSpinner();
4467
4527
  if (isJsonMode()) {
@@ -4771,6 +4831,10 @@ function collectAddon(value, previous) {
4771
4831
  }
4772
4832
  return [...previous, { addonKey, quantity }];
4773
4833
  }
4834
+ function parsePositiveInt(raw, fallback) {
4835
+ const n = Number.parseInt(String(raw).trim(), 10);
4836
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
4837
+ }
4774
4838
  function parseBillingPeriod(raw) {
4775
4839
  const v = raw.toLowerCase();
4776
4840
  if (v === "monthly" || v === "yearly") return v;
@@ -6465,6 +6529,20 @@ function resolveEntitlementRemedy(failedKey, catalog, opts) {
6465
6529
  hint: `A dedicated host slot is required \u2014 upgrade to ${plan2?.name ?? target2}.`
6466
6530
  };
6467
6531
  }
6532
+ if (failedKey === "db.free.slots" || failedKey === "storage.free.slots") {
6533
+ const target2 = nextPlanForRequested(opts?.requestedPlan);
6534
+ const plan2 = plans.find((p) => planKeyOf(p) === target2);
6535
+ const resource = failedKey === "db.free.slots" ? "database" : "storage bucket";
6536
+ return {
6537
+ kind: "plan",
6538
+ failedKey,
6539
+ targetKey: target2,
6540
+ targetName: plan2?.name,
6541
+ priceHalalas: plan2?.priceHalalas,
6542
+ command: `tarout billing upgrade ${target2} --wait`,
6543
+ hint: `The free plan includes a single ${resource} for the whole org \u2014 delete the existing free ${resource}, or upgrade to ${plan2?.name ?? target2} to add more.`
6544
+ };
6545
+ }
6468
6546
  if (failedKey?.startsWith("db.") || failedKey?.startsWith("storage.") || failedKey?.startsWith("domain") || failedKey?.startsWith("email")) {
6469
6547
  const matched = addons.find(
6470
6548
  (a) => a.grants?.some((g) => g.entitlementKey === failedKey)
@@ -7305,6 +7383,97 @@ async function runInlineUpgrade(client, planKey) {
7305
7383
  }
7306
7384
  return false;
7307
7385
  }
7386
+ async function getCurrentPlanQuantitySafely(client) {
7387
+ try {
7388
+ const sub = await client.subscription.getCurrent.query();
7389
+ const q = Number(sub?.planQuantity);
7390
+ return Number.isFinite(q) && q > 0 ? q : 1;
7391
+ } catch {
7392
+ return 1;
7393
+ }
7394
+ }
7395
+ async function runInlineTargetedRemedy(client, remedy) {
7396
+ let input2;
7397
+ let label;
7398
+ if (remedy.kind === "addon") {
7399
+ input2 = { kind: "addon", addonKey: remedy.targetKey, quantity: 1 };
7400
+ label = remedy.targetName ?? remedy.targetKey;
7401
+ } else {
7402
+ const next = await getCurrentPlanQuantitySafely(client) + 1;
7403
+ input2 = { kind: "plan_quantity", planKey: remedy.targetKey, quantity: next };
7404
+ label = `${remedy.targetName ?? remedy.targetKey} \xD7${next}`;
7405
+ }
7406
+ const _spinner = startSpinner(`Adding ${label}...`);
7407
+ const result = await performBillingChange(client, {
7408
+ ...input2,
7409
+ wait: true,
7410
+ timeoutMs: 6e5,
7411
+ openBrowser: isJsonMode() ? void 0 : async (url) => {
7412
+ await open4(url);
7413
+ },
7414
+ onCheckoutOpened: ({ orderId, paymentUrl }) => {
7415
+ log("");
7416
+ log("Open this URL to complete payment:");
7417
+ log(` ${colors.cyan(paymentUrl)}`);
7418
+ log(`Order ID: ${colors.dim(orderId)}`);
7419
+ }
7420
+ });
7421
+ if (result.status === "applied" || result.status === "paid") {
7422
+ succeedSpinner(`${label} added.`);
7423
+ return true;
7424
+ }
7425
+ failSpinner(
7426
+ result.status === "deferred" ? "Change did not require an immediate payment." : result.status === "pending_timeout" ? "Payment still pending after 10 minutes." : `Payment ${result.status}.`
7427
+ );
7428
+ if (result.failureReason) log(colors.error(result.failureReason));
7429
+ if (result.orderId) {
7430
+ log(
7431
+ colors.dim(
7432
+ `Resume later with: tarout billing wait ${result.orderId.slice(0, 8)} --timeout 600`
7433
+ )
7434
+ );
7435
+ }
7436
+ return false;
7437
+ }
7438
+ async function promptEntitlementRemedy(client, err, requestedPlan) {
7439
+ const failedKey = extractEntitlementKeyFromError(err);
7440
+ const catalog = await fetchCatalogSafely(client);
7441
+ const remedy = resolveEntitlementRemedy(failedKey, catalog, { requestedPlan });
7442
+ if (remedy.kind === "plan") {
7443
+ return promptUpgradeFromEntitlementError(client, err, requestedPlan);
7444
+ }
7445
+ const price = remedy.priceHalalas !== void 0 ? ` (${formatPlanPrice(remedy.priceHalalas)})` : "";
7446
+ const targetedLabel = remedy.kind === "plan_quantity" ? `Add one more app slot${price}` : `Add just the ${remedy.targetName ?? remedy.targetKey}${price}`;
7447
+ log("");
7448
+ log(colors.warn("That resource isn't included in your current plan."));
7449
+ log("");
7450
+ const UPGRADE = "__upgrade__";
7451
+ const TARGETED = "__targeted__";
7452
+ const CANCEL = "__cancel__";
7453
+ const choice = await select(
7454
+ "How would you like to add it?",
7455
+ [
7456
+ { name: "Upgrade the plan", value: UPGRADE },
7457
+ { name: targetedLabel, value: TARGETED },
7458
+ { name: "Cancel", value: CANCEL }
7459
+ ],
7460
+ {
7461
+ field: "entitlement_remedy",
7462
+ flag: "--plan <key> (upgrade) | tarout billing addon:buy <key> (addon)",
7463
+ context: {
7464
+ failedEntitlementKey: failedKey,
7465
+ remedyKind: remedy.kind,
7466
+ targetKey: remedy.targetKey,
7467
+ command: remedy.command
7468
+ }
7469
+ }
7470
+ );
7471
+ if (choice === CANCEL) return false;
7472
+ if (choice === UPGRADE) {
7473
+ return promptUpgradeFromEntitlementError(client, err, requestedPlan);
7474
+ }
7475
+ return runInlineTargetedRemedy(client, remedy);
7476
+ }
7308
7477
  async function getAppPlanChoices(client, preloadedOptions) {
7309
7478
  try {
7310
7479
  const options = preloadedOptions ?? await client.application.getCreateOptions.query();
@@ -7411,6 +7580,63 @@ async function emitNeedsUpgrade(client, err, requestedPlan, retryCommand) {
7411
7580
  hint: `${remedy.hint} Then retry: ${retryCommand}.`
7412
7581
  });
7413
7582
  }
7583
+ async function listFreeDatabasesSafely(client) {
7584
+ try {
7585
+ const [pgRows, myRows] = await Promise.all([
7586
+ client.postgres.allByOrganization.query().catch(() => []),
7587
+ client.mysql.allByOrganization.query().catch(() => [])
7588
+ ]);
7589
+ const out = [];
7590
+ for (const r of pgRows ?? []) {
7591
+ if ((r?.plan ?? "FREE") === "FREE" && r?.postgresId) {
7592
+ out.push({ kind: "postgres", id: r.postgresId, name: r.name });
7593
+ }
7594
+ }
7595
+ for (const r of myRows ?? []) {
7596
+ if ((r?.plan ?? "FREE") === "FREE" && r?.mysqlId) {
7597
+ out.push({ kind: "mysql", id: r.mysqlId, name: r.name });
7598
+ }
7599
+ }
7600
+ return out;
7601
+ } catch {
7602
+ return [];
7603
+ }
7604
+ }
7605
+ async function explainFreeResourceSlotExhaustion(client, failedKey) {
7606
+ const isDb = failedKey === "db.free.slots";
7607
+ const resource = isDb ? "database" : "storage bucket";
7608
+ log("");
7609
+ log(
7610
+ colors.warn(
7611
+ `The free plan includes one ${resource} for the whole organization (across all environments).`
7612
+ )
7613
+ );
7614
+ if (isDb) {
7615
+ const existing = await listFreeDatabasesSafely(client);
7616
+ if (existing.length > 0) {
7617
+ log("You already have:");
7618
+ for (const d of existing) {
7619
+ log(
7620
+ ` \u2022 ${d.name} ${colors.dim(`(${d.kind} \xB7 ${d.id.slice(0, 8)})`)}`
7621
+ );
7622
+ }
7623
+ } else {
7624
+ log(
7625
+ colors.dim(
7626
+ "Your existing free database may be in another environment \u2014 run `tarout db list`."
7627
+ )
7628
+ );
7629
+ }
7630
+ }
7631
+ log("");
7632
+ log("To continue, pick one:");
7633
+ if (isDb) {
7634
+ log(` \u2022 Reuse it: ${colors.cyan("tarout deploy --reuse-database auto")}`);
7635
+ log(` \u2022 Delete it: ${colors.cyan("tarout db delete <id>")}`);
7636
+ }
7637
+ log(` \u2022 Upgrade: ${colors.cyan("tarout billing upgrade shared --wait")}`);
7638
+ log("");
7639
+ }
7414
7640
  async function promptUpgradeFromEntitlementError(client, err, requestedPlan) {
7415
7641
  const catalog = await fetchCatalogSafely(client);
7416
7642
  const allPlans = catalog?.plans ?? [];
@@ -8529,7 +8755,11 @@ function registerDeployCommands(program2) {
8529
8755
  }
8530
8756
  log("");
8531
8757
  log(colors.warn(message));
8532
- const upgraded = await promptUpgradeFromEntitlementError(
8758
+ const failedKey = extractEntitlementKeyFromError(err);
8759
+ if (failedKey === "db.free.slots" || failedKey === "storage.free.slots") {
8760
+ await explainFreeResourceSlotExhaustion(getApiClient(), failedKey);
8761
+ }
8762
+ const upgraded = await promptEntitlementRemedy(
8533
8763
  getApiClient(),
8534
8764
  err,
8535
8765
  options.plan
@@ -8543,7 +8773,7 @@ function registerDeployCommands(program2) {
8543
8773
  );
8544
8774
  exit(ExitCode.PERMISSION_DENIED);
8545
8775
  }
8546
- box("Upgrade complete", [
8776
+ box("Billing updated", [
8547
8777
  colors.success("Subscription updated."),
8548
8778
  `Run ${colors.cyan("tarout deploy")} again to deploy on the new plan.`
8549
8779
  ]);
@@ -19146,7 +19376,7 @@ function registerUpCommand(program2) {
19146
19376
  }
19147
19377
  log("");
19148
19378
  log(colors.warn(message));
19149
- const upgraded = await promptUpgradeFromEntitlementError(
19379
+ const upgraded = await promptEntitlementRemedy(
19150
19380
  client,
19151
19381
  err,
19152
19382
  options.plan
@@ -19155,7 +19385,7 @@ function registerUpCommand(program2) {
19155
19385
  await emitNeedsUpgrade(client, err, options.plan, "tarout up");
19156
19386
  exit(ExitCode.PERMISSION_DENIED);
19157
19387
  }
19158
- box("Upgrade complete", [
19388
+ box("Billing updated", [
19159
19389
  colors.success("Subscription updated."),
19160
19390
  `Run ${colors.cyan("tarout up")} again to deploy on the new plan.`
19161
19391
  ]);
@@ -19252,6 +19482,39 @@ function registerUpCommand(program2) {
19252
19482
  app.applicationId
19253
19483
  );
19254
19484
  } catch (err) {
19485
+ if (isEntitlementError(err)) {
19486
+ const message = err instanceof Error ? err.message : "Plan upgrade required";
19487
+ if (isJsonMode() || shouldSkipConfirmation()) {
19488
+ await emitNeedsUpgrade(
19489
+ getApiClient(),
19490
+ err,
19491
+ options.plan,
19492
+ "tarout up"
19493
+ );
19494
+ exit(ExitCode.PERMISSION_DENIED);
19495
+ }
19496
+ log("");
19497
+ log(colors.warn(message));
19498
+ const upgraded = await promptEntitlementRemedy(
19499
+ getApiClient(),
19500
+ err,
19501
+ options.plan
19502
+ );
19503
+ if (!upgraded) {
19504
+ await emitNeedsUpgrade(
19505
+ getApiClient(),
19506
+ err,
19507
+ options.plan,
19508
+ "tarout up"
19509
+ );
19510
+ exit(ExitCode.PERMISSION_DENIED);
19511
+ }
19512
+ box("Billing updated", [
19513
+ colors.success("Subscription updated."),
19514
+ `Run ${colors.cyan("tarout up")} again to deploy on the new plan.`
19515
+ ]);
19516
+ return;
19517
+ }
19255
19518
  if (err instanceof Error && err.message.startsWith("Invalid --source")) {
19256
19519
  outputError("INVALID_ARGUMENTS", err.message);
19257
19520
  if (!isJsonMode()) log(colors.error(err.message));
@@ -19480,10 +19743,11 @@ program.name("tarout").description("Tarout PaaS Command Line Interface").version
19480
19743
  "Fail fast on missing input (emit needs_input + exit 6 instead of prompting on TTY)"
19481
19744
  ).option("-q, --quiet", "Minimal output").option("-v, --verbose", "Extra debug information").option("--no-color", "Disable colored output").hook("preAction", (thisCommand) => {
19482
19745
  const opts = thisCommand.opts();
19746
+ const stdinIsTTY = Boolean(process.stdin.isTTY);
19483
19747
  setGlobalOptions({
19484
19748
  json: opts.json || false,
19485
19749
  yes: opts.yes || false,
19486
- nonInteractive: opts.nonInteractive || false,
19750
+ nonInteractive: opts.nonInteractive || !stdinIsTTY,
19487
19751
  quiet: opts.quiet || false,
19488
19752
  verbose: opts.verbose || false,
19489
19753
  noColor: opts.color === false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarout/cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Tarout CLI — the Saudi cloud platform for coding agents",
5
5
  "type": "module",
6
6
  "bin": {