@tarout/cli 0.10.0 → 0.11.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 +344 -116
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -62,7 +62,7 @@ import { Command } from "commander";
62
62
  // package.json
63
63
  var package_default = {
64
64
  name: "@tarout/cli",
65
- version: "0.10.0",
65
+ version: "0.11.0",
66
66
  description: "Tarout CLI \u2014 the Saudi cloud platform for coding agents",
67
67
  type: "module",
68
68
  bin: {
@@ -753,8 +753,12 @@ don't hand-edit infrastructure.
753
753
  - **Run locally** with cloud env vars: \`tarout dev\`.
754
754
  - **Full agent guide:** https://tarout.sa/docs/for-ai/onboarding
755
755
 
756
- Run Tarout commands with \`--json\` for machine-readable output. On a \`NEEDS_UPGRADE\`
757
- error, surface it to the user and follow the upgrade flow instead of auto-retrying.
756
+ Run Tarout commands with \`--json\` for machine-readable output. New apps and
757
+ databases automatically use your org's **subscribed tier** \u2014 don't pass
758
+ \`--plan free\` / \`--database-plan free\`. \`tarout login\` opens the browser for you;
759
+ the user just signs in. A \`NEEDS_UPGRADE\` error means the org is out of slots for
760
+ its tier: surface the two options (buy the add-on vs upgrade the plan) and let the
761
+ user pick \u2014 the chosen command opens the payment page and waits until it's confirmed.
758
762
  Ask before destructive actions (delete, rollback, revealing secrets).
759
763
  ${BLOCK_END}`;
760
764
  function markdownTargetFor(agent) {
@@ -3079,9 +3083,6 @@ function formatTime(ts) {
3079
3083
  });
3080
3084
  }
3081
3085
 
3082
- // src/commands/auth.ts
3083
- import open3 from "open";
3084
-
3085
3086
  // src/lib/auth-server.ts
3086
3087
  import { randomBytes, timingSafeEqual } from "crypto";
3087
3088
  import express from "express";
@@ -3240,12 +3241,34 @@ function canLaunchBrowser() {
3240
3241
  }
3241
3242
  return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
3242
3243
  }
3244
+ async function openInBrowser(url, opts) {
3245
+ if (!isJsonMode()) {
3246
+ log(
3247
+ colors.dim(opts?.hint ?? "If the browser didn't open, visit this URL:")
3248
+ );
3249
+ log(` ${colors.cyan(url)}`);
3250
+ }
3251
+ if (opts?.noOpen || !canLaunchBrowser()) return false;
3252
+ try {
3253
+ await open2(url);
3254
+ return true;
3255
+ } catch {
3256
+ return false;
3257
+ }
3258
+ }
3243
3259
  function paymentBrowserOpener(opts) {
3244
3260
  if (opts?.noOpen || !canLaunchBrowser()) return void 0;
3245
3261
  return async (url) => {
3246
3262
  try {
3247
3263
  await open2(url);
3248
3264
  } catch {
3265
+ if (!isJsonMode()) {
3266
+ log(
3267
+ colors.dim(
3268
+ "Couldn't open the browser automatically \u2014 use the payment link to complete checkout."
3269
+ )
3270
+ );
3271
+ }
3249
3272
  }
3250
3273
  };
3251
3274
  }
@@ -3255,21 +3278,24 @@ function shouldAutoConfirmPaidCheckout(amountDueHalalas) {
3255
3278
  }
3256
3279
 
3257
3280
  // src/commands/auth.ts
3258
- function refuseBrowserAuthForAgent(action) {
3259
- const message = `tarout ${action} requires a browser. Agents should ask the user to run \`tarout token <api-token>\` or set TAROUT_TOKEN \u2014 generate one at https://tarout.sa/dashboard/settings/profile.`;
3281
+ function announceAuthUrl(authUrl, callbackPort, launched) {
3260
3282
  if (isJsonMode()) {
3261
- outputError("AUTH_BOOTSTRAP_REQUIRED", message, {
3262
- action,
3263
- recommendedFlags: [
3264
- "export TAROUT_TOKEN=<token>",
3265
- "tarout token <api-token>"
3266
- ]
3283
+ outputJsonLine({
3284
+ type: "event",
3285
+ event: "auth_url",
3286
+ authUrl,
3287
+ browserLaunched: launched,
3288
+ callbackPort
3267
3289
  });
3268
- } else {
3269
- process.stderr.write(`error: ${message}
3270
- `);
3290
+ return;
3291
+ }
3292
+ if (!canLaunchBrowser()) {
3293
+ log(
3294
+ colors.dim(
3295
+ "On a remote/headless host? Run `tarout token <api-token>` instead \u2014 generate one at https://tarout.sa/dashboard/settings/profile."
3296
+ )
3297
+ );
3271
3298
  }
3272
- exit(ExitCode.AUTH_ERROR);
3273
3299
  }
3274
3300
  function registerAuthCommands(program2) {
3275
3301
  program2.command("login").description("Authenticate with Tarout via browser").option("--api-url <url>", "Custom API URL", "https://tarout.sa").action(async (options) => {
@@ -3292,21 +3318,16 @@ function registerAuthCommands(program2) {
3292
3318
  return;
3293
3319
  }
3294
3320
  }
3295
- if (isJsonMode() || !canLaunchBrowser()) {
3296
- refuseBrowserAuthForAgent("login");
3297
- }
3298
3321
  const apiUrl = options.apiUrl;
3299
3322
  log("");
3300
3323
  log("Opening browser to authenticate...");
3301
3324
  const authServer = await startAuthServer();
3302
3325
  const callbackUrl = `http://localhost:${authServer.port}/callback?state=${encodeURIComponent(authServer.state)}`;
3303
3326
  const authUrl = `${apiUrl}/cli-authorize?callback=${encodeURIComponent(callbackUrl)}`;
3304
- try {
3305
- await open3(authUrl);
3306
- } catch {
3307
- authServer.close();
3308
- refuseBrowserAuthForAgent("login");
3309
- }
3327
+ const launched = await openInBrowser(authUrl, {
3328
+ hint: "If the browser didn't open, visit this URL to authenticate:"
3329
+ });
3330
+ announceAuthUrl(authUrl, authServer.port, launched);
3310
3331
  const _spinner = startSpinner("Waiting for authentication...");
3311
3332
  try {
3312
3333
  const authData = await authServer.waitForCallback();
@@ -3406,21 +3427,16 @@ function registerAuthCommands(program2) {
3406
3427
  return;
3407
3428
  }
3408
3429
  }
3409
- if (isJsonMode() || !canLaunchBrowser()) {
3410
- refuseBrowserAuthForAgent("register");
3411
- }
3412
3430
  const apiUrl = options.apiUrl;
3413
3431
  log("");
3414
3432
  log("Opening browser to create your account...");
3415
3433
  const authServer = await startAuthServer();
3416
3434
  const callbackUrl = `http://localhost:${authServer.port}/callback?state=${encodeURIComponent(authServer.state)}`;
3417
3435
  const authUrl = `${apiUrl}/cli-authorize?action=register&callback=${encodeURIComponent(callbackUrl)}`;
3418
- try {
3419
- await open3(authUrl);
3420
- } catch {
3421
- authServer.close();
3422
- refuseBrowserAuthForAgent("register");
3423
- }
3436
+ const launched = await openInBrowser(authUrl, {
3437
+ hint: "If the browser didn't open, visit this URL to create your account:"
3438
+ });
3439
+ announceAuthUrl(authUrl, authServer.port, launched);
3424
3440
  const _spinner = startSpinner("Waiting for account creation...");
3425
3441
  try {
3426
3442
  const authData = await authServer.waitForCallback();
@@ -3964,6 +3980,18 @@ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
3964
3980
 
3965
3981
  // src/lib/billing-upgrade.ts
3966
3982
  var AGENT_BILLING_PERMISSION_HINT = `If your agent's permission system blocks this billing command, ask the user to allowlist Tarout billing once so you can run it directly (Claude Code: add "Bash(tarout billing:*)" to .claude/settings.json). Running an upgrade only opens the hosted payment page \u2014 the user still completes payment in the browser.`;
3983
+ function storageSlotTierForAddonKey(addonKey) {
3984
+ switch (addonKey) {
3985
+ case "storage.starter":
3986
+ return "STARTER";
3987
+ case "storage.standard":
3988
+ return "STANDARD";
3989
+ case "storage.pro":
3990
+ return "PRO";
3991
+ default:
3992
+ return null;
3993
+ }
3994
+ }
3967
3995
  function resolveTarget(input2) {
3968
3996
  if (input2.kind === "plan") return input2.planKey ?? "";
3969
3997
  if (input2.kind === "addon") {
@@ -3990,8 +4018,16 @@ async function performBillingChange(client, input2) {
3990
4018
  addons: input2.addons
3991
4019
  });
3992
4020
  } else if (kind === "addon") {
3993
- const items = input2.addons ?? (input2.addonKey ? [{ addonKey: input2.addonKey, quantity: input2.quantity ?? 1 }] : []);
3994
- result = await client.subscription.purchaseAddons.mutate({ items });
4021
+ const storageTier = !input2.addons && input2.addonKey ? storageSlotTierForAddonKey(input2.addonKey) : null;
4022
+ if (storageTier) {
4023
+ result = await client.storage.purchaseStorageSlot.mutate({
4024
+ tier: storageTier,
4025
+ quantity: input2.quantity ?? 1
4026
+ });
4027
+ } else {
4028
+ const items = input2.addons ?? (input2.addonKey ? [{ addonKey: input2.addonKey, quantity: input2.quantity ?? 1 }] : []);
4029
+ result = await client.subscription.purchaseAddons.mutate({ items });
4030
+ }
3995
4031
  } else {
3996
4032
  result = await client.subscription.setPlanQuantity.mutate({
3997
4033
  quantity: input2.quantity
@@ -4111,7 +4147,7 @@ function emitBillingResult(result, opts) {
4111
4147
  case "payment_required":
4112
4148
  outputData({
4113
4149
  ...envelope,
4114
- hint: `Open paymentUrl to complete checkout, then run \`${nextCommand}\` \u2014 or re-run with --wait.`
4150
+ hint: `Open paymentUrl to complete checkout, then run \`${nextCommand}\` \u2014 or re-run without --no-wait (waiting is the default).`
4115
4151
  });
4116
4152
  box("Payment required", [
4117
4153
  `${label}`,
@@ -4197,6 +4233,16 @@ function isPaidFamily(planKey) {
4197
4233
  const family = planFamily(planKey);
4198
4234
  return family === "SHARED" || family === "DEDICATED";
4199
4235
  }
4236
+ function dbAddonKeyForPlanFamily(planKey) {
4237
+ switch (planFamily(planKey)) {
4238
+ case "SHARED":
4239
+ return "db.standard";
4240
+ case "DEDICATED":
4241
+ return "db.pro";
4242
+ default:
4243
+ return null;
4244
+ }
4245
+ }
4200
4246
  function resourceAddonKeysForPlan(planKey) {
4201
4247
  switch (planFamily(planKey)) {
4202
4248
  case "SHARED":
@@ -4230,6 +4276,23 @@ function isConflictError(err) {
4230
4276
  const e = err;
4231
4277
  return (e?.code ?? e?.data?.code) === "CONFLICT";
4232
4278
  }
4279
+ async function previewAddonAmountDue(client, addonKey, quantity) {
4280
+ try {
4281
+ if (storageSlotTierForAddonKey(addonKey)) {
4282
+ const catalog = await client.subscription.getCatalog.query();
4283
+ const addon = (catalog?.addons ?? []).find(
4284
+ (a) => (a.key ?? a.addonKey) === addonKey
4285
+ );
4286
+ return typeof addon?.priceHalalas === "number" ? addon.priceHalalas * quantity : void 0;
4287
+ }
4288
+ const preview = await client.subscription.previewAddonsPurchase.query({
4289
+ items: [{ addonKey, quantity }]
4290
+ });
4291
+ return typeof preview?.totalProratedHalalas === "number" ? preview.totalProratedHalalas : void 0;
4292
+ } catch {
4293
+ return void 0;
4294
+ }
4295
+ }
4233
4296
  function registerBillingCommands(program2) {
4234
4297
  const billing = program2.command("billing").description("Manage subscription and billing");
4235
4298
  billing.command("status").description("Show current subscription and entitlements").action(async () => {
@@ -4328,8 +4391,8 @@ function registerBillingCommands(program2) {
4328
4391
  collectAddon,
4329
4392
  []
4330
4393
  ).option(
4331
- "-w, --wait",
4332
- "After hosted-checkout opens, poll status until the payment is confirmed"
4394
+ "--no-wait",
4395
+ "Return as soon as the hosted checkout opens, without polling for confirmation (default: wait until paid)"
4333
4396
  ).option(
4334
4397
  "--timeout <seconds>",
4335
4398
  "Maximum wait time in seconds (default 600)",
@@ -4606,8 +4669,8 @@ function registerBillingCommands(program2) {
4606
4669
  }
4607
4670
  });
4608
4671
  billing.command("addon:add").argument("<addon>", "Addon key to add").option("-q, --quantity <n>", "Addon quantity", Number.parseInt).option(
4609
- "-w, --wait",
4610
- "After hosted-checkout opens, poll status until the payment is confirmed"
4672
+ "--no-wait",
4673
+ "Return as soon as the hosted checkout opens, without polling for confirmation (default: wait until paid)"
4611
4674
  ).option(
4612
4675
  "--timeout <seconds>",
4613
4676
  "Maximum wait time in seconds (default 600)",
@@ -4617,26 +4680,38 @@ function registerBillingCommands(program2) {
4617
4680
  try {
4618
4681
  if (!isLoggedIn()) throw new AuthError();
4619
4682
  const quantity = options.quantity || 1;
4683
+ const client = getApiClient();
4620
4684
  if (!shouldSkipConfirmation()) {
4621
- log("");
4622
- log(`Addon: ${colors.cyan(addonKey)}`);
4623
- log(`Quantity: ${quantity}`);
4624
- log("");
4625
- const confirmed = await confirm(
4626
- `Add addon "${addonKey}" \xD7 ${quantity}?`,
4627
- false,
4628
- {
4629
- field: "confirm_addon_add",
4630
- flag: "--yes",
4631
- context: { addonKey, quantity }
4632
- }
4685
+ const amountDueHalalas = await previewAddonAmountDue(
4686
+ client,
4687
+ addonKey,
4688
+ quantity
4633
4689
  );
4634
- if (!confirmed) {
4635
- log("Cancelled.");
4636
- return;
4690
+ if (shouldAutoConfirmPaidCheckout(amountDueHalalas)) {
4691
+ log(
4692
+ `
4693
+ Adding ${colors.cyan(addonKey)} \xD7 ${quantity} \u2014 opening the secure payment page...`
4694
+ );
4695
+ } else {
4696
+ log("");
4697
+ log(`Addon: ${colors.cyan(addonKey)}`);
4698
+ log(`Quantity: ${quantity}`);
4699
+ log("");
4700
+ const confirmed = await confirm(
4701
+ `Add addon "${addonKey}" \xD7 ${quantity}?`,
4702
+ false,
4703
+ {
4704
+ field: "confirm_addon_add",
4705
+ flag: "--yes",
4706
+ context: { addonKey, quantity, amountDueHalalas }
4707
+ }
4708
+ );
4709
+ if (!confirmed) {
4710
+ log("Cancelled.");
4711
+ return;
4712
+ }
4637
4713
  }
4638
4714
  }
4639
- const client = getApiClient();
4640
4715
  const _spinner = startSpinner("Adding addon...");
4641
4716
  let raw;
4642
4717
  try {
@@ -4707,8 +4782,8 @@ function registerBillingCommands(program2) {
4707
4782
  }
4708
4783
  });
4709
4784
  billing.command("plan:quantity").argument("<quantity>", "New plan quantity", Number.parseInt).option(
4710
- "-w, --wait",
4711
- "After hosted-checkout opens, poll status until the payment is confirmed"
4785
+ "--no-wait",
4786
+ "Return as soon as the hosted checkout opens, without polling for confirmation (default: wait until paid)"
4712
4787
  ).option(
4713
4788
  "--timeout <seconds>",
4714
4789
  "Maximum wait time in seconds (default 600)",
@@ -4781,8 +4856,8 @@ function registerBillingCommands(program2) {
4781
4856
  }
4782
4857
  });
4783
4858
  billing.command("addon:buy").argument("<addon>", "Addon key").option("-q, --quantity <n>", "Quantity", Number.parseInt).option(
4784
- "-w, --wait",
4785
- "After hosted-checkout opens, poll status until the payment is confirmed"
4859
+ "--no-wait",
4860
+ "Return as soon as the hosted checkout opens, without polling for confirmation (default: wait until paid)"
4786
4861
  ).option(
4787
4862
  "--timeout <seconds>",
4788
4863
  "Maximum wait time in seconds (default 600)",
@@ -4792,20 +4867,32 @@ function registerBillingCommands(program2) {
4792
4867
  try {
4793
4868
  if (!isLoggedIn()) throw new AuthError();
4794
4869
  const quantity = options.quantity || 1;
4870
+ const client = getApiClient();
4795
4871
  if (!shouldSkipConfirmation()) {
4796
- log(`
4872
+ const amountDueHalalas = await previewAddonAmountDue(
4873
+ client,
4874
+ addonKey,
4875
+ quantity
4876
+ );
4877
+ if (shouldAutoConfirmPaidCheckout(amountDueHalalas)) {
4878
+ log(
4879
+ `
4880
+ Purchasing ${quantity}\xD7 ${colors.cyan(addonKey)} \u2014 opening the secure payment page...`
4881
+ );
4882
+ } else {
4883
+ log(`
4797
4884
  Purchase ${quantity}\xD7 ${colors.cyan(addonKey)}?`);
4798
- const confirmed = await confirm("Proceed?", false, {
4799
- field: "confirm_addon_buy",
4800
- flag: "--yes",
4801
- context: { addonKey, quantity }
4802
- });
4803
- if (!confirmed) {
4804
- log("Cancelled.");
4805
- return;
4885
+ const confirmed = await confirm("Proceed?", false, {
4886
+ field: "confirm_addon_buy",
4887
+ flag: "--yes",
4888
+ context: { addonKey, quantity, amountDueHalalas }
4889
+ });
4890
+ if (!confirmed) {
4891
+ log("Cancelled.");
4892
+ return;
4893
+ }
4806
4894
  }
4807
4895
  }
4808
- const client = getApiClient();
4809
4896
  const _spinner = startSpinner("Purchasing addon...");
4810
4897
  const result = await performBillingChange(client, {
4811
4898
  kind: "addon",
@@ -5735,7 +5822,7 @@ import {
5735
5822
  import { tmpdir } from "os";
5736
5823
  import { basename, dirname, join as join4 } from "path";
5737
5824
  import { promisify } from "util";
5738
- import open4 from "open";
5825
+ import open3 from "open";
5739
5826
 
5740
5827
  // src/lib/agent-setup.ts
5741
5828
  var SETUP_HINT = "Run `tarout agent init` to allowlist Bash(tarout:*) so tarout commands run without per-command approval prompts.";
@@ -5782,6 +5869,21 @@ function nextPlanForRequested(requested) {
5782
5869
  if (r === "dedicated_large") return "dedicated_large";
5783
5870
  return r;
5784
5871
  }
5872
+ function nextPlanForCurrent(currentPlanKey) {
5873
+ const family = planFamily(currentPlanKey);
5874
+ if (family === "SHARED") return "dedicated_small";
5875
+ if (family === "DEDICATED") {
5876
+ const k = (currentPlanKey ?? "").trim().toLowerCase();
5877
+ if (k === "dedicated_large") return "dedicated_large";
5878
+ if (k === "dedicated_medium") return "dedicated_large";
5879
+ return "dedicated_medium";
5880
+ }
5881
+ return "shared";
5882
+ }
5883
+ function upgradeTargetPlan(opts) {
5884
+ if (opts?.currentPlanKey) return nextPlanForCurrent(opts.currentPlanKey);
5885
+ return nextPlanForRequested(opts?.requestedPlan);
5886
+ }
5785
5887
  function resolveEntitlementRemedy(failedKey, catalog, opts) {
5786
5888
  const plans = catalog?.plans ?? [];
5787
5889
  const addons = catalog?.addons ?? [];
@@ -5801,7 +5903,7 @@ function resolveEntitlementRemedy(failedKey, catalog, opts) {
5801
5903
  }
5802
5904
  if (failedKey?.startsWith("app.dedicated") || failedKey?.startsWith("host.")) {
5803
5905
  const requested = opts?.requestedPlan;
5804
- const target2 = requested && requested.toLowerCase().startsWith("dedicated") ? nextPlanForRequested(requested) : "dedicated_small";
5906
+ const target2 = planFamily(opts?.currentPlanKey) === "DEDICATED" ? nextPlanForCurrent(opts?.currentPlanKey) : requested && requested.toLowerCase().startsWith("dedicated") ? nextPlanForRequested(requested) : "dedicated_small";
5805
5907
  const plan2 = plans.find((p) => planKeyOf(p) === target2);
5806
5908
  return {
5807
5909
  kind: "plan",
@@ -5814,7 +5916,7 @@ function resolveEntitlementRemedy(failedKey, catalog, opts) {
5814
5916
  };
5815
5917
  }
5816
5918
  if (failedKey === "db.free.slots" || failedKey === "storage.free.slots") {
5817
- const target2 = nextPlanForRequested(opts?.requestedPlan);
5919
+ const target2 = upgradeTargetPlan(opts);
5818
5920
  const plan2 = plans.find((p) => planKeyOf(p) === target2);
5819
5921
  const resource = failedKey === "db.free.slots" ? "database" : "storage bucket";
5820
5922
  return {
@@ -5842,7 +5944,7 @@ function resolveEntitlementRemedy(failedKey, catalog, opts) {
5842
5944
  hint: `Add a ${matched?.name ?? targetKey} slot with an addon purchase.`
5843
5945
  };
5844
5946
  }
5845
- const target = nextPlanForRequested(opts?.requestedPlan);
5947
+ const target = upgradeTargetPlan(opts);
5846
5948
  const plan = plans.find((p) => planKeyOf(p) === target);
5847
5949
  return {
5848
5950
  kind: "plan",
@@ -6018,7 +6120,9 @@ async function authenticateViaBrowser(action, apiUrl) {
6018
6120
  log(
6019
6121
  action === "register" ? "Opening browser to create your account..." : "Opening browser to authenticate..."
6020
6122
  );
6021
- await open4(authUrl);
6123
+ await openInBrowser(authUrl, {
6124
+ hint: action === "register" ? "If the browser didn't open, visit this URL to create your account:" : "If the browser didn't open, visit this URL to authenticate:"
6125
+ });
6022
6126
  const _spinner = startSpinner(
6023
6127
  action === "register" ? "Waiting for account creation..." : "Waiting for authentication..."
6024
6128
  );
@@ -6705,6 +6809,14 @@ async function getCurrentPlanQuantitySafely(client) {
6705
6809
  return 1;
6706
6810
  }
6707
6811
  }
6812
+ async function getCurrentPlanKeySafely(client) {
6813
+ try {
6814
+ const sub = await client.subscription.getCurrent.query();
6815
+ return sub?.planKey ?? "free";
6816
+ } catch {
6817
+ return void 0;
6818
+ }
6819
+ }
6708
6820
  async function runInlineTargetedRemedy(client, remedy) {
6709
6821
  let input2;
6710
6822
  let label;
@@ -6748,13 +6860,29 @@ async function runInlineTargetedRemedy(client, remedy) {
6748
6860
  }
6749
6861
  async function promptEntitlementRemedy(client, err, requestedPlan) {
6750
6862
  const failedKey = extractEntitlementKeyFromError(err);
6751
- const catalog = await fetchCatalogSafely(client);
6752
- const remedy = resolveEntitlementRemedy(failedKey, catalog, { requestedPlan });
6863
+ const [catalog, currentPlanKey] = await Promise.all([
6864
+ fetchCatalogSafely(client),
6865
+ getCurrentPlanKeySafely(client)
6866
+ ]);
6867
+ const remedy = resolveEntitlementRemedy(failedKey, catalog, {
6868
+ requestedPlan,
6869
+ currentPlanKey
6870
+ });
6753
6871
  if (remedy.kind === "plan") {
6754
- return promptUpgradeFromEntitlementError(client, err, requestedPlan);
6872
+ return promptUpgradeFromEntitlementError(
6873
+ client,
6874
+ err,
6875
+ requestedPlan,
6876
+ currentPlanKey
6877
+ );
6755
6878
  }
6756
6879
  const price = remedy.priceHalalas !== void 0 ? ` (${formatPlanPrice(remedy.priceHalalas)})` : "";
6757
6880
  const targetedLabel = remedy.kind === "plan_quantity" ? `Add one more app slot${price}` : `Add just the ${remedy.targetName ?? remedy.targetKey}${price}`;
6881
+ const upgradePlanKey = upgradeTargetPlan({ currentPlanKey, requestedPlan });
6882
+ const upgradePlanDef = (catalog?.plans ?? []).find(
6883
+ (p) => (p.planKey ?? p.key) === upgradePlanKey
6884
+ );
6885
+ const upgradeLabel = `Upgrade to ${upgradePlanDef?.name ?? upgradePlanKey}`;
6758
6886
  log("");
6759
6887
  log(colors.warn("That resource isn't included in your current plan."));
6760
6888
  log("");
@@ -6764,7 +6892,7 @@ async function promptEntitlementRemedy(client, err, requestedPlan) {
6764
6892
  const choice = await select(
6765
6893
  "How would you like to add it?",
6766
6894
  [
6767
- { name: "Upgrade the plan", value: UPGRADE },
6895
+ { name: upgradeLabel, value: UPGRADE },
6768
6896
  { name: targetedLabel, value: TARGETED },
6769
6897
  { name: "Cancel", value: CANCEL }
6770
6898
  ],
@@ -6775,13 +6903,19 @@ async function promptEntitlementRemedy(client, err, requestedPlan) {
6775
6903
  failedEntitlementKey: failedKey,
6776
6904
  remedyKind: remedy.kind,
6777
6905
  targetKey: remedy.targetKey,
6778
- command: remedy.command
6906
+ command: remedy.command,
6907
+ upgradePlanKey
6779
6908
  }
6780
6909
  }
6781
6910
  );
6782
6911
  if (choice === CANCEL) return false;
6783
6912
  if (choice === UPGRADE) {
6784
- return promptUpgradeFromEntitlementError(client, err, requestedPlan);
6913
+ return promptUpgradeFromEntitlementError(
6914
+ client,
6915
+ err,
6916
+ requestedPlan,
6917
+ currentPlanKey
6918
+ );
6785
6919
  }
6786
6920
  return runInlineTargetedRemedy(client, remedy);
6787
6921
  }
@@ -6875,9 +7009,9 @@ function extractEntitlementKeyFromError(err) {
6875
7009
  const m = msg.match(/Plan limit reached for ([\w.]+)/i);
6876
7010
  return m?.[1];
6877
7011
  }
6878
- function buildRemedyOptions(remedy, requestedPlan, catalog) {
7012
+ function buildRemedyOptions(remedy, requestedPlan, catalog, currentPlanKey) {
6879
7013
  if (remedy.kind === "addon" || remedy.kind === "plan_quantity") {
6880
- const upgradePlan = nextPlanForRequested(requestedPlan);
7014
+ const upgradePlan = upgradeTargetPlan({ currentPlanKey, requestedPlan });
6881
7015
  const planDef = (catalog?.plans ?? []).find(
6882
7016
  (p) => (p.planKey ?? p.key) === upgradePlan
6883
7017
  );
@@ -6905,10 +7039,21 @@ function buildRemedyOptions(remedy, requestedPlan, catalog) {
6905
7039
  async function emitNeedsUpgrade(client, err, requestedPlan, retryCommand) {
6906
7040
  const message = err instanceof Error ? err.message : "Plan upgrade required";
6907
7041
  const failedKey = extractEntitlementKeyFromError(err);
6908
- const catalog = await fetchCatalogSafely(client);
6909
- const remedy = resolveEntitlementRemedy(failedKey, catalog, { requestedPlan });
6910
- const options = buildRemedyOptions(remedy, requestedPlan, catalog);
6911
- 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}.`;
7042
+ const [catalog, currentPlanKey] = await Promise.all([
7043
+ fetchCatalogSafely(client),
7044
+ getCurrentPlanKeySafely(client)
7045
+ ]);
7046
+ const remedy = resolveEntitlementRemedy(failedKey, catalog, {
7047
+ requestedPlan,
7048
+ currentPlanKey
7049
+ });
7050
+ const options = buildRemedyOptions(
7051
+ remedy,
7052
+ requestedPlan,
7053
+ catalog,
7054
+ currentPlanKey
7055
+ );
7056
+ 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}\`. The chosen command opens the payment page and waits until it's confirmed, then retry: ${retryCommand}.` : `${remedy.hint} That command opens the payment page and waits until it's confirmed, then retry: ${retryCommand}.`;
6912
7057
  outputError("NEEDS_UPGRADE", message, {
6913
7058
  failedEntitlementKey: failedKey,
6914
7059
  remedyKind: remedy.kind,
@@ -6979,7 +7124,7 @@ async function explainFreeResourceSlotExhaustion(client, failedKey) {
6979
7124
  log(` \u2022 Upgrade: ${colors.cyan("tarout billing upgrade shared --wait")}`);
6980
7125
  log("");
6981
7126
  }
6982
- async function promptUpgradeFromEntitlementError(client, err, requestedPlan) {
7127
+ async function promptUpgradeFromEntitlementError(client, err, requestedPlan, currentPlanKey) {
6983
7128
  const catalog = await fetchCatalogSafely(client);
6984
7129
  const allPlans = catalog?.plans ?? [];
6985
7130
  const upgradeable = allPlans.filter(
@@ -6989,10 +7134,14 @@ async function promptUpgradeFromEntitlementError(client, err, requestedPlan) {
6989
7134
  return false;
6990
7135
  }
6991
7136
  const failedKey = extractEntitlementKeyFromError(err);
7137
+ const ladderKey = upgradeTargetPlan({ currentPlanKey, requestedPlan });
7138
+ const ladderPlan = upgradeable.find(
7139
+ (p) => (p.planKey || p.key) === ladderKey
7140
+ );
6992
7141
  const matchingPlan = failedKey ? upgradeable.find(
6993
7142
  (p) => p.grants?.some((g) => g.entitlementKey === failedKey)
6994
7143
  ) : void 0;
6995
- const recommendedKey = matchingPlan ? matchingPlan.planKey || matchingPlan.key : inferSuggestedPlan(requestedPlan);
7144
+ const recommendedKey = ladderPlan ? ladderKey : matchingPlan ? matchingPlan.planKey || matchingPlan.key : inferSuggestedPlan(requestedPlan);
6996
7145
  log("");
6997
7146
  log(colors.bold("Available upgrade plans:"));
6998
7147
  log("");
@@ -7122,7 +7271,7 @@ async function openGitProviderSetup() {
7122
7271
  log(
7123
7272
  `No GitHub account is required if you keep using ${colors.dim("--source upload")}.`
7124
7273
  );
7125
- await open4(url);
7274
+ await open3(url);
7126
7275
  }
7127
7276
  async function configureOptionalResources(client, profile, app, options, inspection) {
7128
7277
  const database = await resolveDatabaseChoice(options, inspection);
@@ -7275,6 +7424,57 @@ async function resolveResourcePlan(client, kind, value, message) {
7275
7424
  }
7276
7425
  });
7277
7426
  }
7427
+ function dbTierForAddonKey(addonKey) {
7428
+ if (addonKey === "db.pro") return "PRO";
7429
+ if (addonKey === "db.standard") return "STANDARD";
7430
+ return "STARTER";
7431
+ }
7432
+ async function ensureDatabasePlan(client, requested) {
7433
+ const currentPlanKey = await getCurrentPlanKeySafely(client);
7434
+ const addonKey = dbAddonKeyForPlanFamily(currentPlanKey);
7435
+ const orgIsPaid = addonKey !== null;
7436
+ if (requested && !(requested === "FREE" && orgIsPaid)) {
7437
+ return { ok: true, plan: requested };
7438
+ }
7439
+ const tiers = await loadResourceTiers(client, "database");
7440
+ const hasCreatable = tiers.some((t) => t.canCreate);
7441
+ if (hasCreatable || !orgIsPaid) {
7442
+ return { ok: true, plan: pickDefaultResourceTier(tiers) };
7443
+ }
7444
+ if (!isJsonMode()) {
7445
+ log("");
7446
+ log(
7447
+ colors.dim(
7448
+ `Your plan has no open database slot \u2014 adding the ${addonKey} add-on. Complete payment in the browser to continue.`
7449
+ )
7450
+ );
7451
+ }
7452
+ const result = await performBillingChange(client, {
7453
+ kind: "addon",
7454
+ addonKey,
7455
+ quantity: 1,
7456
+ wait: true,
7457
+ timeoutMs: 6e5,
7458
+ openBrowser: paymentBrowserOpener(),
7459
+ onCheckoutOpened: ({ orderId, paymentUrl }) => {
7460
+ if (!isJsonMode()) {
7461
+ log("");
7462
+ log("Open this URL to complete payment:");
7463
+ log(` ${colors.cyan(paymentUrl)}`);
7464
+ log(`Order ID: ${colors.dim(orderId)}`);
7465
+ }
7466
+ }
7467
+ });
7468
+ if (result.status === "applied" || result.status === "paid") {
7469
+ return { ok: true, plan: dbTierForAddonKey(addonKey) };
7470
+ }
7471
+ return { ok: false, result };
7472
+ }
7473
+ async function resolveDatabasePlanOrExit(client, requested) {
7474
+ const resolution = await ensureDatabasePlan(client, requested);
7475
+ if (resolution.ok) return resolution.plan;
7476
+ exit(emitBillingResult(resolution.result, { label: "Database add-on" }));
7477
+ }
7278
7478
  async function createAndAttachDatabase(client, profile, app, input2) {
7279
7479
  const appName = generateResourceSlug(app.name, input2.kind);
7280
7480
  const label = input2.kind === "postgres" ? "Postgres" : "MySQL";
@@ -7412,12 +7612,7 @@ async function resolveDatabaseProvisioning(client, profile, app, kind, options)
7412
7612
  }
7413
7613
  );
7414
7614
  if (picked === "__create__") {
7415
- const plan = requestedPlan ?? await resolveResourcePlan(
7416
- client,
7417
- "database",
7418
- void 0,
7419
- "Database plan:"
7420
- );
7615
+ const plan = await resolveDatabasePlanOrExit(client, requestedPlan);
7421
7616
  return {
7422
7617
  action: "create",
7423
7618
  plan,
@@ -7782,7 +7977,7 @@ async function attachExistingStorage(client, app, decision) {
7782
7977
  }
7783
7978
  }
7784
7979
  async function buildDatabaseCreateDecision(client, app, kind, requestedPlan) {
7785
- const plan = requestedPlan ?? await resolveResourcePlan(client, "database", void 0, "Database plan:");
7980
+ const plan = await resolveDatabasePlanOrExit(client, requestedPlan);
7786
7981
  return {
7787
7982
  action: "create",
7788
7983
  plan,
@@ -8879,10 +9074,7 @@ function normalizeDbPlan(value) {
8879
9074
  );
8880
9075
  }
8881
9076
  async function resolveDbPlan(client, explicit) {
8882
- const normalized = normalizeDbPlan(explicit);
8883
- if (normalized) return normalized;
8884
- const tiers = await loadResourceTiers(client, "database");
8885
- return pickDefaultResourceTier(tiers);
9077
+ return resolveDatabasePlanOrExit(client, normalizeDbPlan(explicit));
8886
9078
  }
8887
9079
  function registerDbCommands(program2) {
8888
9080
  const db = program2.command("db").description("Manage databases");
@@ -9033,6 +9225,39 @@ function registerDbCommands(program2) {
9033
9225
  }
9034
9226
  log("");
9035
9227
  } catch (err) {
9228
+ if (isEntitlementError(err)) {
9229
+ if (isJsonMode() || isNonInteractiveMode() || shouldSkipConfirmation()) {
9230
+ await emitNeedsUpgrade(
9231
+ getApiClient(),
9232
+ err,
9233
+ void 0,
9234
+ "tarout db create"
9235
+ );
9236
+ exit(ExitCode.PERMISSION_DENIED);
9237
+ }
9238
+ const message = err instanceof Error ? err.message : "Plan upgrade required";
9239
+ log("");
9240
+ log(colors.warn(message));
9241
+ const upgraded = await promptEntitlementRemedy(
9242
+ getApiClient(),
9243
+ err,
9244
+ void 0
9245
+ );
9246
+ if (!upgraded) {
9247
+ await emitNeedsUpgrade(
9248
+ getApiClient(),
9249
+ err,
9250
+ void 0,
9251
+ "tarout db create"
9252
+ );
9253
+ exit(ExitCode.PERMISSION_DENIED);
9254
+ }
9255
+ box("Billing updated", [
9256
+ colors.success("Subscription updated."),
9257
+ `Run ${colors.cyan("tarout db create")} again to create the database.`
9258
+ ]);
9259
+ return;
9260
+ }
9036
9261
  handleError(err);
9037
9262
  }
9038
9263
  });
@@ -13221,8 +13446,7 @@ function registerInitCommand(program2) {
13221
13446
  "Custom API URL (defaults to saved profile or https://tarout.sa)"
13222
13447
  ).option("--token <token>", "API token for this run").option("--name <name>", "Application name (defaults to directory name)").option(
13223
13448
  "--plan <plan>",
13224
- "App hosting plan: free, shared, or dedicated",
13225
- "free"
13449
+ "App hosting plan: free, shared, or dedicated (defaults to your org's subscribed tier)"
13226
13450
  ).option("-r, --region <region>", "Deployment region", DEFAULT_REGION2).option("--description <text>", "Description for the newly created app").option(
13227
13451
  "--database <type>",
13228
13452
  "Provision a database: none, postgres, or mysql (defaults to auto-detected)"
@@ -13291,13 +13515,12 @@ function registerInitCommand(program2) {
13291
13515
  });
13292
13516
  } catch (err) {
13293
13517
  if (isEntitlementError(err)) {
13294
- const message = err instanceof Error ? err.message : "Plan upgrade required";
13295
- outputError("NEEDS_UPGRADE", message, {
13296
- suggestedPlan: inferSuggestedPlan(options.plan),
13297
- failedEntitlementKey: extractEntitlementKeyFromError(err),
13298
- hint: "Run `tarout billing upgrade <plan> --wait` to add slots, then retry `tarout init`.",
13299
- permissionHint: AGENT_BILLING_PERMISSION_HINT
13300
- });
13518
+ await emitNeedsUpgrade(
13519
+ getApiClient(),
13520
+ err,
13521
+ options.plan,
13522
+ "tarout init"
13523
+ );
13301
13524
  exit(ExitCode.PERMISSION_DENIED);
13302
13525
  }
13303
13526
  throw err;
@@ -15527,7 +15750,7 @@ function registerProjectsCommands(program2) {
15527
15750
  }
15528
15751
 
15529
15752
  // src/commands/providers.ts
15530
- import open5 from "open";
15753
+ import open4 from "open";
15531
15754
  function registerProvidersCommands(program2) {
15532
15755
  const providers = program2.command("providers").description("Manage Git providers (GitHub, GitLab, Bitbucket)");
15533
15756
  providers.command("list").alias("ls").description("List all connected Git providers").action(async () => {
@@ -15644,7 +15867,7 @@ function registerProvidersCommands(program2) {
15644
15867
  log(
15645
15868
  "Complete the GitHub browser flow, then connect the repository to your app."
15646
15869
  );
15647
- await open5(url);
15870
+ await open4(url);
15648
15871
  } catch (err) {
15649
15872
  handleError(err);
15650
15873
  }
@@ -20112,11 +20335,16 @@ function registerWalletCommands(program2) {
20112
20335
  outputData(result);
20113
20336
  return;
20114
20337
  }
20338
+ const paymentUrl = result.paymentUrl || result.url || "";
20115
20339
  box("Wallet Top-Up", [
20116
20340
  `Order ID: ${colors.cyan(result.orderId || "")}`,
20117
20341
  `Amount: ${result.amount ? `${(result.amount / 100).toFixed(2)} SAR` : "Default"}`,
20118
- `Payment URL: ${colors.cyan(result.paymentUrl || result.url || "")}`
20342
+ `Payment URL: ${colors.cyan(paymentUrl)}`
20119
20343
  ]);
20344
+ if (paymentUrl) {
20345
+ const openPayment = paymentBrowserOpener();
20346
+ if (openPayment) await openPayment(paymentUrl);
20347
+ }
20120
20348
  log("Complete payment in your browser to credit the wallet.");
20121
20349
  log(
20122
20350
  `Confirm after payment: ${colors.dim(`tarout wallet confirm ${result.orderId || "<orderId>"}`)}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarout/cli",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Tarout CLI — the Saudi cloud platform for coding agents",
5
5
  "type": "module",
6
6
  "bin": {