@tarout/cli 0.9.0 → 0.10.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 +313 -76
  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.9.0",
65
+ version: "0.10.1",
66
66
  description: "Tarout CLI \u2014 the Saudi cloud platform for coding agents",
67
67
  type: "module",
68
68
  bin: {
@@ -787,6 +787,19 @@ function upsertMarkdownBlock(filePath, block) {
787
787
  `, "utf-8");
788
788
  return "appended";
789
789
  }
790
+ function hasTaroutAllowlist(cwd) {
791
+ const settingsPath = join(cwd, ".claude", "settings.local.json");
792
+ if (!existsSync(settingsPath)) return false;
793
+ try {
794
+ const settings = JSON.parse(
795
+ readFileSync(settingsPath, "utf-8")
796
+ );
797
+ const allow = settings?.permissions?.allow;
798
+ return Array.isArray(allow) && allow.includes(TAROUT_ALLOW_ENTRY);
799
+ } catch {
800
+ return false;
801
+ }
802
+ }
790
803
  function mergeClaudeSettings(claudeDir) {
791
804
  const settingsPath = join(claudeDir, "settings.local.json");
792
805
  const relPath = join(".claude", "settings.local.json");
@@ -3066,9 +3079,6 @@ function formatTime(ts) {
3066
3079
  });
3067
3080
  }
3068
3081
 
3069
- // src/commands/auth.ts
3070
- import open3 from "open";
3071
-
3072
3082
  // src/lib/auth-server.ts
3073
3083
  import { randomBytes, timingSafeEqual } from "crypto";
3074
3084
  import express from "express";
@@ -3227,12 +3237,34 @@ function canLaunchBrowser() {
3227
3237
  }
3228
3238
  return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
3229
3239
  }
3240
+ async function openInBrowser(url, opts) {
3241
+ if (!isJsonMode()) {
3242
+ log(
3243
+ colors.dim(opts?.hint ?? "If the browser didn't open, visit this URL:")
3244
+ );
3245
+ log(` ${colors.cyan(url)}`);
3246
+ }
3247
+ if (opts?.noOpen || !canLaunchBrowser()) return false;
3248
+ try {
3249
+ await open2(url);
3250
+ return true;
3251
+ } catch {
3252
+ return false;
3253
+ }
3254
+ }
3230
3255
  function paymentBrowserOpener(opts) {
3231
3256
  if (opts?.noOpen || !canLaunchBrowser()) return void 0;
3232
3257
  return async (url) => {
3233
3258
  try {
3234
3259
  await open2(url);
3235
3260
  } catch {
3261
+ if (!isJsonMode()) {
3262
+ log(
3263
+ colors.dim(
3264
+ "Couldn't open the browser automatically \u2014 use the payment link to complete checkout."
3265
+ )
3266
+ );
3267
+ }
3236
3268
  }
3237
3269
  };
3238
3270
  }
@@ -3288,12 +3320,9 @@ function registerAuthCommands(program2) {
3288
3320
  const authServer = await startAuthServer();
3289
3321
  const callbackUrl = `http://localhost:${authServer.port}/callback?state=${encodeURIComponent(authServer.state)}`;
3290
3322
  const authUrl = `${apiUrl}/cli-authorize?callback=${encodeURIComponent(callbackUrl)}`;
3291
- try {
3292
- await open3(authUrl);
3293
- } catch {
3294
- authServer.close();
3295
- refuseBrowserAuthForAgent("login");
3296
- }
3323
+ await openInBrowser(authUrl, {
3324
+ hint: "If the browser didn't open, visit this URL to authenticate:"
3325
+ });
3297
3326
  const _spinner = startSpinner("Waiting for authentication...");
3298
3327
  try {
3299
3328
  const authData = await authServer.waitForCallback();
@@ -3402,12 +3431,9 @@ function registerAuthCommands(program2) {
3402
3431
  const authServer = await startAuthServer();
3403
3432
  const callbackUrl = `http://localhost:${authServer.port}/callback?state=${encodeURIComponent(authServer.state)}`;
3404
3433
  const authUrl = `${apiUrl}/cli-authorize?action=register&callback=${encodeURIComponent(callbackUrl)}`;
3405
- try {
3406
- await open3(authUrl);
3407
- } catch {
3408
- authServer.close();
3409
- refuseBrowserAuthForAgent("register");
3410
- }
3434
+ await openInBrowser(authUrl, {
3435
+ hint: "If the browser didn't open, visit this URL to create your account:"
3436
+ });
3411
3437
  const _spinner = startSpinner("Waiting for account creation...");
3412
3438
  try {
3413
3439
  const authData = await authServer.waitForCallback();
@@ -3951,6 +3977,18 @@ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
3951
3977
 
3952
3978
  // src/lib/billing-upgrade.ts
3953
3979
  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.`;
3980
+ function storageSlotTierForAddonKey(addonKey) {
3981
+ switch (addonKey) {
3982
+ case "storage.starter":
3983
+ return "STARTER";
3984
+ case "storage.standard":
3985
+ return "STANDARD";
3986
+ case "storage.pro":
3987
+ return "PRO";
3988
+ default:
3989
+ return null;
3990
+ }
3991
+ }
3954
3992
  function resolveTarget(input2) {
3955
3993
  if (input2.kind === "plan") return input2.planKey ?? "";
3956
3994
  if (input2.kind === "addon") {
@@ -3977,8 +4015,16 @@ async function performBillingChange(client, input2) {
3977
4015
  addons: input2.addons
3978
4016
  });
3979
4017
  } else if (kind === "addon") {
3980
- const items = input2.addons ?? (input2.addonKey ? [{ addonKey: input2.addonKey, quantity: input2.quantity ?? 1 }] : []);
3981
- result = await client.subscription.purchaseAddons.mutate({ items });
4018
+ const storageTier = !input2.addons && input2.addonKey ? storageSlotTierForAddonKey(input2.addonKey) : null;
4019
+ if (storageTier) {
4020
+ result = await client.storage.purchaseStorageSlot.mutate({
4021
+ tier: storageTier,
4022
+ quantity: input2.quantity ?? 1
4023
+ });
4024
+ } else {
4025
+ const items = input2.addons ?? (input2.addonKey ? [{ addonKey: input2.addonKey, quantity: input2.quantity ?? 1 }] : []);
4026
+ result = await client.subscription.purchaseAddons.mutate({ items });
4027
+ }
3982
4028
  } else {
3983
4029
  result = await client.subscription.setPlanQuantity.mutate({
3984
4030
  quantity: input2.quantity
@@ -4217,6 +4263,23 @@ function isConflictError(err) {
4217
4263
  const e = err;
4218
4264
  return (e?.code ?? e?.data?.code) === "CONFLICT";
4219
4265
  }
4266
+ async function previewAddonAmountDue(client, addonKey, quantity) {
4267
+ try {
4268
+ if (storageSlotTierForAddonKey(addonKey)) {
4269
+ const catalog = await client.subscription.getCatalog.query();
4270
+ const addon = (catalog?.addons ?? []).find(
4271
+ (a) => (a.key ?? a.addonKey) === addonKey
4272
+ );
4273
+ return typeof addon?.priceHalalas === "number" ? addon.priceHalalas * quantity : void 0;
4274
+ }
4275
+ const preview = await client.subscription.previewAddonsPurchase.query({
4276
+ items: [{ addonKey, quantity }]
4277
+ });
4278
+ return typeof preview?.totalProratedHalalas === "number" ? preview.totalProratedHalalas : void 0;
4279
+ } catch {
4280
+ return void 0;
4281
+ }
4282
+ }
4220
4283
  function registerBillingCommands(program2) {
4221
4284
  const billing = program2.command("billing").description("Manage subscription and billing");
4222
4285
  billing.command("status").description("Show current subscription and entitlements").action(async () => {
@@ -4604,26 +4667,38 @@ function registerBillingCommands(program2) {
4604
4667
  try {
4605
4668
  if (!isLoggedIn()) throw new AuthError();
4606
4669
  const quantity = options.quantity || 1;
4670
+ const client = getApiClient();
4607
4671
  if (!shouldSkipConfirmation()) {
4608
- log("");
4609
- log(`Addon: ${colors.cyan(addonKey)}`);
4610
- log(`Quantity: ${quantity}`);
4611
- log("");
4612
- const confirmed = await confirm(
4613
- `Add addon "${addonKey}" \xD7 ${quantity}?`,
4614
- false,
4615
- {
4616
- field: "confirm_addon_add",
4617
- flag: "--yes",
4618
- context: { addonKey, quantity }
4619
- }
4672
+ const amountDueHalalas = await previewAddonAmountDue(
4673
+ client,
4674
+ addonKey,
4675
+ quantity
4620
4676
  );
4621
- if (!confirmed) {
4622
- log("Cancelled.");
4623
- return;
4677
+ if (shouldAutoConfirmPaidCheckout(amountDueHalalas)) {
4678
+ log(
4679
+ `
4680
+ Adding ${colors.cyan(addonKey)} \xD7 ${quantity} \u2014 opening the secure payment page...`
4681
+ );
4682
+ } else {
4683
+ log("");
4684
+ log(`Addon: ${colors.cyan(addonKey)}`);
4685
+ log(`Quantity: ${quantity}`);
4686
+ log("");
4687
+ const confirmed = await confirm(
4688
+ `Add addon "${addonKey}" \xD7 ${quantity}?`,
4689
+ false,
4690
+ {
4691
+ field: "confirm_addon_add",
4692
+ flag: "--yes",
4693
+ context: { addonKey, quantity, amountDueHalalas }
4694
+ }
4695
+ );
4696
+ if (!confirmed) {
4697
+ log("Cancelled.");
4698
+ return;
4699
+ }
4624
4700
  }
4625
4701
  }
4626
- const client = getApiClient();
4627
4702
  const _spinner = startSpinner("Adding addon...");
4628
4703
  let raw;
4629
4704
  try {
@@ -4779,20 +4854,32 @@ function registerBillingCommands(program2) {
4779
4854
  try {
4780
4855
  if (!isLoggedIn()) throw new AuthError();
4781
4856
  const quantity = options.quantity || 1;
4857
+ const client = getApiClient();
4782
4858
  if (!shouldSkipConfirmation()) {
4783
- log(`
4859
+ const amountDueHalalas = await previewAddonAmountDue(
4860
+ client,
4861
+ addonKey,
4862
+ quantity
4863
+ );
4864
+ if (shouldAutoConfirmPaidCheckout(amountDueHalalas)) {
4865
+ log(
4866
+ `
4867
+ Purchasing ${quantity}\xD7 ${colors.cyan(addonKey)} \u2014 opening the secure payment page...`
4868
+ );
4869
+ } else {
4870
+ log(`
4784
4871
  Purchase ${quantity}\xD7 ${colors.cyan(addonKey)}?`);
4785
- const confirmed = await confirm("Proceed?", false, {
4786
- field: "confirm_addon_buy",
4787
- flag: "--yes",
4788
- context: { addonKey, quantity }
4789
- });
4790
- if (!confirmed) {
4791
- log("Cancelled.");
4792
- return;
4872
+ const confirmed = await confirm("Proceed?", false, {
4873
+ field: "confirm_addon_buy",
4874
+ flag: "--yes",
4875
+ context: { addonKey, quantity, amountDueHalalas }
4876
+ });
4877
+ if (!confirmed) {
4878
+ log("Cancelled.");
4879
+ return;
4880
+ }
4793
4881
  }
4794
4882
  }
4795
- const client = getApiClient();
4796
4883
  const _spinner = startSpinner("Purchasing addon...");
4797
4884
  const result = await performBillingChange(client, {
4798
4885
  kind: "addon",
@@ -5722,7 +5809,40 @@ import {
5722
5809
  import { tmpdir } from "os";
5723
5810
  import { basename, dirname, join as join4 } from "path";
5724
5811
  import { promisify } from "util";
5725
- import open4 from "open";
5812
+ import open3 from "open";
5813
+
5814
+ // src/lib/agent-setup.ts
5815
+ var SETUP_HINT = "Run `tarout agent init` to allowlist Bash(tarout:*) so tarout commands run without per-command approval prompts.";
5816
+ function isAgentDriven() {
5817
+ return isJsonMode() || isNonInteractiveMode();
5818
+ }
5819
+ function ensureAgentSetup(cwd, disabled = false) {
5820
+ if (disabled || !isAgentDriven() || hasTaroutAllowlist(cwd)) return;
5821
+ const result = scaffoldAgentConfig({ cwd, agent: "claude" });
5822
+ if (isJsonMode()) {
5823
+ outputJsonLine({
5824
+ type: "event",
5825
+ event: "agent_setup_done",
5826
+ files: result.files
5827
+ });
5828
+ } else {
5829
+ for (const file of result.files) {
5830
+ log(colors.dim(` agent setup: ${file.action} ${file.path}`));
5831
+ }
5832
+ }
5833
+ }
5834
+ function emitAgentSetupHint(cwd) {
5835
+ if (!isAgentDriven() || hasTaroutAllowlist(cwd)) return;
5836
+ if (isJsonMode()) {
5837
+ outputJsonLine({
5838
+ type: "event",
5839
+ event: "agent_setup_required",
5840
+ hint: SETUP_HINT
5841
+ });
5842
+ } else {
5843
+ warn(SETUP_HINT);
5844
+ }
5845
+ }
5726
5846
 
5727
5847
  // src/lib/entitlement-remedy.ts
5728
5848
  var planKeyOf = (p) => p.planKey ?? p.key ?? "";
@@ -5736,6 +5856,21 @@ function nextPlanForRequested(requested) {
5736
5856
  if (r === "dedicated_large") return "dedicated_large";
5737
5857
  return r;
5738
5858
  }
5859
+ function nextPlanForCurrent(currentPlanKey) {
5860
+ const family = planFamily(currentPlanKey);
5861
+ if (family === "SHARED") return "dedicated_small";
5862
+ if (family === "DEDICATED") {
5863
+ const k = (currentPlanKey ?? "").trim().toLowerCase();
5864
+ if (k === "dedicated_large") return "dedicated_large";
5865
+ if (k === "dedicated_medium") return "dedicated_large";
5866
+ return "dedicated_medium";
5867
+ }
5868
+ return "shared";
5869
+ }
5870
+ function upgradeTargetPlan(opts) {
5871
+ if (opts?.currentPlanKey) return nextPlanForCurrent(opts.currentPlanKey);
5872
+ return nextPlanForRequested(opts?.requestedPlan);
5873
+ }
5739
5874
  function resolveEntitlementRemedy(failedKey, catalog, opts) {
5740
5875
  const plans = catalog?.plans ?? [];
5741
5876
  const addons = catalog?.addons ?? [];
@@ -5755,7 +5890,7 @@ function resolveEntitlementRemedy(failedKey, catalog, opts) {
5755
5890
  }
5756
5891
  if (failedKey?.startsWith("app.dedicated") || failedKey?.startsWith("host.")) {
5757
5892
  const requested = opts?.requestedPlan;
5758
- const target2 = requested && requested.toLowerCase().startsWith("dedicated") ? nextPlanForRequested(requested) : "dedicated_small";
5893
+ const target2 = planFamily(opts?.currentPlanKey) === "DEDICATED" ? nextPlanForCurrent(opts?.currentPlanKey) : requested && requested.toLowerCase().startsWith("dedicated") ? nextPlanForRequested(requested) : "dedicated_small";
5759
5894
  const plan2 = plans.find((p) => planKeyOf(p) === target2);
5760
5895
  return {
5761
5896
  kind: "plan",
@@ -5768,7 +5903,7 @@ function resolveEntitlementRemedy(failedKey, catalog, opts) {
5768
5903
  };
5769
5904
  }
5770
5905
  if (failedKey === "db.free.slots" || failedKey === "storage.free.slots") {
5771
- const target2 = nextPlanForRequested(opts?.requestedPlan);
5906
+ const target2 = upgradeTargetPlan(opts);
5772
5907
  const plan2 = plans.find((p) => planKeyOf(p) === target2);
5773
5908
  const resource = failedKey === "db.free.slots" ? "database" : "storage bucket";
5774
5909
  return {
@@ -5796,7 +5931,7 @@ function resolveEntitlementRemedy(failedKey, catalog, opts) {
5796
5931
  hint: `Add a ${matched?.name ?? targetKey} slot with an addon purchase.`
5797
5932
  };
5798
5933
  }
5799
- const target = nextPlanForRequested(opts?.requestedPlan);
5934
+ const target = upgradeTargetPlan(opts);
5800
5935
  const plan = plans.find((p) => planKeyOf(p) === target);
5801
5936
  return {
5802
5937
  kind: "plan",
@@ -5972,7 +6107,9 @@ async function authenticateViaBrowser(action, apiUrl) {
5972
6107
  log(
5973
6108
  action === "register" ? "Opening browser to create your account..." : "Opening browser to authenticate..."
5974
6109
  );
5975
- await open4(authUrl);
6110
+ await openInBrowser(authUrl, {
6111
+ 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:"
6112
+ });
5976
6113
  const _spinner = startSpinner(
5977
6114
  action === "register" ? "Waiting for account creation..." : "Waiting for authentication..."
5978
6115
  );
@@ -6659,6 +6796,14 @@ async function getCurrentPlanQuantitySafely(client) {
6659
6796
  return 1;
6660
6797
  }
6661
6798
  }
6799
+ async function getCurrentPlanKeySafely(client) {
6800
+ try {
6801
+ const sub = await client.subscription.getCurrent.query();
6802
+ return sub?.planKey ?? "free";
6803
+ } catch {
6804
+ return void 0;
6805
+ }
6806
+ }
6662
6807
  async function runInlineTargetedRemedy(client, remedy) {
6663
6808
  let input2;
6664
6809
  let label;
@@ -6702,13 +6847,29 @@ async function runInlineTargetedRemedy(client, remedy) {
6702
6847
  }
6703
6848
  async function promptEntitlementRemedy(client, err, requestedPlan) {
6704
6849
  const failedKey = extractEntitlementKeyFromError(err);
6705
- const catalog = await fetchCatalogSafely(client);
6706
- const remedy = resolveEntitlementRemedy(failedKey, catalog, { requestedPlan });
6850
+ const [catalog, currentPlanKey] = await Promise.all([
6851
+ fetchCatalogSafely(client),
6852
+ getCurrentPlanKeySafely(client)
6853
+ ]);
6854
+ const remedy = resolveEntitlementRemedy(failedKey, catalog, {
6855
+ requestedPlan,
6856
+ currentPlanKey
6857
+ });
6707
6858
  if (remedy.kind === "plan") {
6708
- return promptUpgradeFromEntitlementError(client, err, requestedPlan);
6859
+ return promptUpgradeFromEntitlementError(
6860
+ client,
6861
+ err,
6862
+ requestedPlan,
6863
+ currentPlanKey
6864
+ );
6709
6865
  }
6710
6866
  const price = remedy.priceHalalas !== void 0 ? ` (${formatPlanPrice(remedy.priceHalalas)})` : "";
6711
6867
  const targetedLabel = remedy.kind === "plan_quantity" ? `Add one more app slot${price}` : `Add just the ${remedy.targetName ?? remedy.targetKey}${price}`;
6868
+ const upgradePlanKey = upgradeTargetPlan({ currentPlanKey, requestedPlan });
6869
+ const upgradePlanDef = (catalog?.plans ?? []).find(
6870
+ (p) => (p.planKey ?? p.key) === upgradePlanKey
6871
+ );
6872
+ const upgradeLabel = `Upgrade to ${upgradePlanDef?.name ?? upgradePlanKey}`;
6712
6873
  log("");
6713
6874
  log(colors.warn("That resource isn't included in your current plan."));
6714
6875
  log("");
@@ -6718,7 +6879,7 @@ async function promptEntitlementRemedy(client, err, requestedPlan) {
6718
6879
  const choice = await select(
6719
6880
  "How would you like to add it?",
6720
6881
  [
6721
- { name: "Upgrade the plan", value: UPGRADE },
6882
+ { name: upgradeLabel, value: UPGRADE },
6722
6883
  { name: targetedLabel, value: TARGETED },
6723
6884
  { name: "Cancel", value: CANCEL }
6724
6885
  ],
@@ -6729,13 +6890,19 @@ async function promptEntitlementRemedy(client, err, requestedPlan) {
6729
6890
  failedEntitlementKey: failedKey,
6730
6891
  remedyKind: remedy.kind,
6731
6892
  targetKey: remedy.targetKey,
6732
- command: remedy.command
6893
+ command: remedy.command,
6894
+ upgradePlanKey
6733
6895
  }
6734
6896
  }
6735
6897
  );
6736
6898
  if (choice === CANCEL) return false;
6737
6899
  if (choice === UPGRADE) {
6738
- return promptUpgradeFromEntitlementError(client, err, requestedPlan);
6900
+ return promptUpgradeFromEntitlementError(
6901
+ client,
6902
+ err,
6903
+ requestedPlan,
6904
+ currentPlanKey
6905
+ );
6739
6906
  }
6740
6907
  return runInlineTargetedRemedy(client, remedy);
6741
6908
  }
@@ -6829,9 +6996,9 @@ function extractEntitlementKeyFromError(err) {
6829
6996
  const m = msg.match(/Plan limit reached for ([\w.]+)/i);
6830
6997
  return m?.[1];
6831
6998
  }
6832
- function buildRemedyOptions(remedy, requestedPlan, catalog) {
6999
+ function buildRemedyOptions(remedy, requestedPlan, catalog, currentPlanKey) {
6833
7000
  if (remedy.kind === "addon" || remedy.kind === "plan_quantity") {
6834
- const upgradePlan = nextPlanForRequested(requestedPlan);
7001
+ const upgradePlan = upgradeTargetPlan({ currentPlanKey, requestedPlan });
6835
7002
  const planDef = (catalog?.plans ?? []).find(
6836
7003
  (p) => (p.planKey ?? p.key) === upgradePlan
6837
7004
  );
@@ -6859,9 +7026,20 @@ function buildRemedyOptions(remedy, requestedPlan, catalog) {
6859
7026
  async function emitNeedsUpgrade(client, err, requestedPlan, retryCommand) {
6860
7027
  const message = err instanceof Error ? err.message : "Plan upgrade required";
6861
7028
  const failedKey = extractEntitlementKeyFromError(err);
6862
- const catalog = await fetchCatalogSafely(client);
6863
- const remedy = resolveEntitlementRemedy(failedKey, catalog, { requestedPlan });
6864
- const options = buildRemedyOptions(remedy, requestedPlan, catalog);
7029
+ const [catalog, currentPlanKey] = await Promise.all([
7030
+ fetchCatalogSafely(client),
7031
+ getCurrentPlanKeySafely(client)
7032
+ ]);
7033
+ const remedy = resolveEntitlementRemedy(failedKey, catalog, {
7034
+ requestedPlan,
7035
+ currentPlanKey
7036
+ });
7037
+ const options = buildRemedyOptions(
7038
+ remedy,
7039
+ requestedPlan,
7040
+ catalog,
7041
+ currentPlanKey
7042
+ );
6865
7043
  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}.`;
6866
7044
  outputError("NEEDS_UPGRADE", message, {
6867
7045
  failedEntitlementKey: failedKey,
@@ -6933,7 +7111,7 @@ async function explainFreeResourceSlotExhaustion(client, failedKey) {
6933
7111
  log(` \u2022 Upgrade: ${colors.cyan("tarout billing upgrade shared --wait")}`);
6934
7112
  log("");
6935
7113
  }
6936
- async function promptUpgradeFromEntitlementError(client, err, requestedPlan) {
7114
+ async function promptUpgradeFromEntitlementError(client, err, requestedPlan, currentPlanKey) {
6937
7115
  const catalog = await fetchCatalogSafely(client);
6938
7116
  const allPlans = catalog?.plans ?? [];
6939
7117
  const upgradeable = allPlans.filter(
@@ -6943,10 +7121,14 @@ async function promptUpgradeFromEntitlementError(client, err, requestedPlan) {
6943
7121
  return false;
6944
7122
  }
6945
7123
  const failedKey = extractEntitlementKeyFromError(err);
7124
+ const ladderKey = upgradeTargetPlan({ currentPlanKey, requestedPlan });
7125
+ const ladderPlan = upgradeable.find(
7126
+ (p) => (p.planKey || p.key) === ladderKey
7127
+ );
6946
7128
  const matchingPlan = failedKey ? upgradeable.find(
6947
7129
  (p) => p.grants?.some((g) => g.entitlementKey === failedKey)
6948
7130
  ) : void 0;
6949
- const recommendedKey = matchingPlan ? matchingPlan.planKey || matchingPlan.key : inferSuggestedPlan(requestedPlan);
7131
+ const recommendedKey = ladderPlan ? ladderKey : matchingPlan ? matchingPlan.planKey || matchingPlan.key : inferSuggestedPlan(requestedPlan);
6950
7132
  log("");
6951
7133
  log(colors.bold("Available upgrade plans:"));
6952
7134
  log("");
@@ -7076,7 +7258,7 @@ async function openGitProviderSetup() {
7076
7258
  log(
7077
7259
  `No GitHub account is required if you keep using ${colors.dim("--source upload")}.`
7078
7260
  );
7079
- await open4(url);
7261
+ await open3(url);
7080
7262
  }
7081
7263
  async function configureOptionalResources(client, profile, app, options, inspection) {
7082
7264
  const database = await resolveDatabaseChoice(options, inspection);
@@ -8022,8 +8204,12 @@ function registerDeployCommands(program2) {
8022
8204
  ).option("--install-command <cmd>", "Custom install command").option("--build-command <cmd>", "Custom build command").option(
8023
8205
  "--output-directory <path>",
8024
8206
  "Build output directory (static assets)"
8025
- ).option("--start-command <cmd>", "Custom start command").option("-w, --wait", "Wait for deployment to complete and stream logs").option("--watch", "Alias for --wait").action(async (appIdentifier, options) => {
8207
+ ).option("--start-command <cmd>", "Custom start command").option("-w, --wait", "Wait for deployment to complete and stream logs").option("--watch", "Alias for --wait").option(
8208
+ "--no-agent-setup",
8209
+ "Don't auto-write the agent permission allowlist (CLAUDE.md / .claude/settings.local.json) on first run"
8210
+ ).action(async (appIdentifier, options) => {
8026
8211
  try {
8212
+ ensureAgentSetup(process.cwd(), options.agentSetup === false);
8027
8213
  const inspection = inspectCurrentProject();
8028
8214
  printProjectInspection(inspection);
8029
8215
  const profile = await ensureAuthenticatedForDeploy(options);
@@ -8983,6 +9169,39 @@ function registerDbCommands(program2) {
8983
9169
  }
8984
9170
  log("");
8985
9171
  } catch (err) {
9172
+ if (isEntitlementError(err)) {
9173
+ if (isJsonMode() || isNonInteractiveMode() || shouldSkipConfirmation()) {
9174
+ await emitNeedsUpgrade(
9175
+ getApiClient(),
9176
+ err,
9177
+ void 0,
9178
+ "tarout db create"
9179
+ );
9180
+ exit(ExitCode.PERMISSION_DENIED);
9181
+ }
9182
+ const message = err instanceof Error ? err.message : "Plan upgrade required";
9183
+ log("");
9184
+ log(colors.warn(message));
9185
+ const upgraded = await promptEntitlementRemedy(
9186
+ getApiClient(),
9187
+ err,
9188
+ void 0
9189
+ );
9190
+ if (!upgraded) {
9191
+ await emitNeedsUpgrade(
9192
+ getApiClient(),
9193
+ err,
9194
+ void 0,
9195
+ "tarout db create"
9196
+ );
9197
+ exit(ExitCode.PERMISSION_DENIED);
9198
+ }
9199
+ box("Billing updated", [
9200
+ colors.success("Subscription updated."),
9201
+ `Run ${colors.cyan("tarout db create")} again to create the database.`
9202
+ ]);
9203
+ return;
9204
+ }
8986
9205
  handleError(err);
8987
9206
  }
8988
9207
  });
@@ -13176,10 +13395,14 @@ function registerInitCommand(program2) {
13176
13395
  ).option("-r, --region <region>", "Deployment region", DEFAULT_REGION2).option("--description <text>", "Description for the newly created app").option(
13177
13396
  "--database <type>",
13178
13397
  "Provision a database: none, postgres, or mysql (defaults to auto-detected)"
13179
- ).option("--database-plan <plan>", "Database plan (e.g. free, starter)").option("--storage", "Provision file storage").option("--storage-plan <plan>", "Storage plan (e.g. free, starter)").option("--scaffold", "Write a minimal starter app if the directory is empty").option("--no-env-write", "Do not write a local .env file with connection strings").action(async (cwdArg, options) => {
13398
+ ).option("--database-plan <plan>", "Database plan (e.g. free, starter)").option("--storage", "Provision file storage").option("--storage-plan <plan>", "Storage plan (e.g. free, starter)").option("--scaffold", "Write a minimal starter app if the directory is empty").option("--no-env-write", "Do not write a local .env file with connection strings").option(
13399
+ "--no-agent-setup",
13400
+ "Don't auto-write the agent permission allowlist (CLAUDE.md / .claude/settings.local.json) on first run"
13401
+ ).action(async (cwdArg, options) => {
13180
13402
  try {
13181
13403
  const cwd = cwdArg ? resolve2(cwdArg) : process.cwd();
13182
13404
  if (cwdArg) process.chdir(cwd);
13405
+ ensureAgentSetup(cwd, options.agentSetup === false);
13183
13406
  if (options.scaffold) {
13184
13407
  const files = scaffoldStarter(cwd);
13185
13408
  emitEvent({ event: "scaffold_done", files });
@@ -13237,13 +13460,12 @@ function registerInitCommand(program2) {
13237
13460
  });
13238
13461
  } catch (err) {
13239
13462
  if (isEntitlementError(err)) {
13240
- const message = err instanceof Error ? err.message : "Plan upgrade required";
13241
- outputError("NEEDS_UPGRADE", message, {
13242
- suggestedPlan: inferSuggestedPlan(options.plan),
13243
- failedEntitlementKey: extractEntitlementKeyFromError(err),
13244
- hint: "Run `tarout billing upgrade <plan> --wait` to add slots, then retry `tarout init`.",
13245
- permissionHint: AGENT_BILLING_PERMISSION_HINT
13246
- });
13463
+ await emitNeedsUpgrade(
13464
+ getApiClient(),
13465
+ err,
13466
+ options.plan,
13467
+ "tarout init"
13468
+ );
13247
13469
  exit(ExitCode.PERMISSION_DENIED);
13248
13470
  }
13249
13471
  throw err;
@@ -15473,7 +15695,7 @@ function registerProjectsCommands(program2) {
15473
15695
  }
15474
15696
 
15475
15697
  // src/commands/providers.ts
15476
- import open5 from "open";
15698
+ import open4 from "open";
15477
15699
  function registerProvidersCommands(program2) {
15478
15700
  const providers = program2.command("providers").description("Manage Git providers (GitHub, GitLab, Bitbucket)");
15479
15701
  providers.command("list").alias("ls").description("List all connected Git providers").action(async () => {
@@ -15590,7 +15812,7 @@ function registerProvidersCommands(program2) {
15590
15812
  log(
15591
15813
  "Complete the GitHub browser flow, then connect the repository to your app."
15592
15814
  );
15593
- await open5(url);
15815
+ await open4(url);
15594
15816
  } catch (err) {
15595
15817
  handleError(err);
15596
15818
  }
@@ -19625,10 +19847,14 @@ function registerUpCommand(program2) {
19625
19847
  ).option("--start-command <cmd>", "Custom start command").option(
19626
19848
  "--idempotency-key <key>",
19627
19849
  "Idempotency key for safe retries (Phase 2; logged only in v1)"
19850
+ ).option(
19851
+ "--no-agent-setup",
19852
+ "Don't auto-write the agent permission allowlist (CLAUDE.md / .claude/settings.local.json) on first run"
19628
19853
  ).action(async (cwdArg, options) => {
19629
19854
  try {
19630
19855
  const cwd = cwdArg ? resolve3(cwdArg) : process.cwd();
19631
19856
  if (cwdArg) process.chdir(cwd);
19857
+ ensureAgentSetup(cwd, options.agentSetup === false);
19632
19858
  const source = normalizeSource(options.source);
19633
19859
  const idempotencyKey = options.idempotencyKey?.trim();
19634
19860
  if (idempotencyKey) {
@@ -20054,11 +20280,16 @@ function registerWalletCommands(program2) {
20054
20280
  outputData(result);
20055
20281
  return;
20056
20282
  }
20283
+ const paymentUrl = result.paymentUrl || result.url || "";
20057
20284
  box("Wallet Top-Up", [
20058
20285
  `Order ID: ${colors.cyan(result.orderId || "")}`,
20059
20286
  `Amount: ${result.amount ? `${(result.amount / 100).toFixed(2)} SAR` : "Default"}`,
20060
- `Payment URL: ${colors.cyan(result.paymentUrl || result.url || "")}`
20287
+ `Payment URL: ${colors.cyan(paymentUrl)}`
20061
20288
  ]);
20289
+ if (paymentUrl) {
20290
+ const openPayment = paymentBrowserOpener();
20291
+ if (openPayment) await openPayment(paymentUrl);
20292
+ }
20062
20293
  log("Complete payment in your browser to credit the wallet.");
20063
20294
  log(
20064
20295
  `Confirm after payment: ${colors.dim(`tarout wallet confirm ${result.orderId || "<orderId>"}`)}`
@@ -20128,7 +20359,7 @@ var program = new Command();
20128
20359
  program.name("tarout").description("Tarout PaaS Command Line Interface").version(package_default.version).option("--json", "Output as JSON (machine-readable)").option("-y, --yes", "Skip all confirmation prompts").option(
20129
20360
  "--non-interactive",
20130
20361
  "Fail fast on missing input (emit needs_input + exit 6 instead of prompting on TTY)"
20131
- ).option("-q, --quiet", "Minimal output").option("-v, --verbose", "Extra debug information").option("--no-color", "Disable colored output").hook("preAction", (thisCommand) => {
20362
+ ).option("-q, --quiet", "Minimal output").option("-v, --verbose", "Extra debug information").option("--no-color", "Disable colored output").hook("preAction", (thisCommand, actionCommand) => {
20132
20363
  const opts = thisCommand.opts();
20133
20364
  const stdinIsTTY = Boolean(process.stdin.isTTY);
20134
20365
  setGlobalOptions({
@@ -20139,6 +20370,12 @@ program.name("tarout").description("Tarout PaaS Command Line Interface").version
20139
20370
  verbose: opts.verbose || false,
20140
20371
  noColor: opts.color === false
20141
20372
  });
20373
+ const sub = actionCommand?.name();
20374
+ const isAgentNamespace = actionCommand?.parent?.name() === "agent";
20375
+ const autoRunsSetup = !!sub && ["up", "deploy", "init"].includes(sub);
20376
+ if (!isAgentNamespace && !autoRunsSetup) {
20377
+ emitAgentSetupHint(process.cwd());
20378
+ }
20142
20379
  });
20143
20380
  registerAuthCommands(program);
20144
20381
  registerAppsCommands(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarout/cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Tarout CLI — the Saudi cloud platform for coding agents",
5
5
  "type": "module",
6
6
  "bin": {