@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.
- package/dist/index.js +313 -76
- 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.
|
|
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
|
-
|
|
3292
|
-
|
|
3293
|
-
}
|
|
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
|
-
|
|
3406
|
-
|
|
3407
|
-
}
|
|
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
|
|
3981
|
-
|
|
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
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
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 (
|
|
4622
|
-
log(
|
|
4623
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
6706
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
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 =
|
|
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
|
|
6863
|
-
|
|
6864
|
-
|
|
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
|
|
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").
|
|
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").
|
|
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
|
-
|
|
13241
|
-
|
|
13242
|
-
|
|
13243
|
-
|
|
13244
|
-
|
|
13245
|
-
|
|
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
|
|
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
|
|
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(
|
|
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);
|