@tarout/cli 0.5.0 → 0.7.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.
- package/dist/index.js +344 -114
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -61,7 +61,7 @@ import { Command } from "commander";
|
|
|
61
61
|
// package.json
|
|
62
62
|
var package_default = {
|
|
63
63
|
name: "@tarout/cli",
|
|
64
|
-
version: "0.
|
|
64
|
+
version: "0.7.0",
|
|
65
65
|
description: "Tarout CLI \u2014 the Saudi cloud platform for coding agents",
|
|
66
66
|
type: "module",
|
|
67
67
|
bin: {
|
|
@@ -6529,6 +6529,20 @@ function resolveEntitlementRemedy(failedKey, catalog, opts) {
|
|
|
6529
6529
|
hint: `A dedicated host slot is required \u2014 upgrade to ${plan2?.name ?? target2}.`
|
|
6530
6530
|
};
|
|
6531
6531
|
}
|
|
6532
|
+
if (failedKey === "db.free.slots" || failedKey === "storage.free.slots") {
|
|
6533
|
+
const target2 = nextPlanForRequested(opts?.requestedPlan);
|
|
6534
|
+
const plan2 = plans.find((p) => planKeyOf(p) === target2);
|
|
6535
|
+
const resource = failedKey === "db.free.slots" ? "database" : "storage bucket";
|
|
6536
|
+
return {
|
|
6537
|
+
kind: "plan",
|
|
6538
|
+
failedKey,
|
|
6539
|
+
targetKey: target2,
|
|
6540
|
+
targetName: plan2?.name,
|
|
6541
|
+
priceHalalas: plan2?.priceHalalas,
|
|
6542
|
+
command: `tarout billing upgrade ${target2} --wait`,
|
|
6543
|
+
hint: `The free plan includes a single ${resource} for the whole org \u2014 delete the existing free ${resource}, or upgrade to ${plan2?.name ?? target2} to add more.`
|
|
6544
|
+
};
|
|
6545
|
+
}
|
|
6532
6546
|
if (failedKey?.startsWith("db.") || failedKey?.startsWith("storage.") || failedKey?.startsWith("domain") || failedKey?.startsWith("email")) {
|
|
6533
6547
|
const matched = addons.find(
|
|
6534
6548
|
(a) => a.grants?.some((g) => g.entitlementKey === failedKey)
|
|
@@ -7119,87 +7133,91 @@ async function resolveDeploymentTarget(client, profile, appIdentifier, options =
|
|
|
7119
7133
|
);
|
|
7120
7134
|
throw new NotFoundError("Application", appIdentifier, suggestions);
|
|
7121
7135
|
}
|
|
7122
|
-
const
|
|
7136
|
+
const details = await getApplicationDetails(client, app2.applicationId);
|
|
7123
7137
|
return {
|
|
7124
7138
|
app: app2,
|
|
7125
7139
|
createdApp: false,
|
|
7126
|
-
hasConfiguredSource: hasConfiguredSource(
|
|
7127
|
-
shouldUploadSource: shouldUseLocalSource(
|
|
7140
|
+
hasConfiguredSource: hasConfiguredSource(details),
|
|
7141
|
+
shouldUploadSource: shouldUseLocalSource(details, {
|
|
7128
7142
|
explicitApp: true
|
|
7129
7143
|
})
|
|
7130
7144
|
};
|
|
7131
7145
|
}
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
if (app2) {
|
|
7136
|
-
const details2 = await getApplicationDetails(client, app2.applicationId);
|
|
7137
|
-
return {
|
|
7138
|
-
app: app2,
|
|
7139
|
-
createdApp: false,
|
|
7140
|
-
hasConfiguredSource: hasConfiguredSource(details2),
|
|
7141
|
-
shouldUploadSource: shouldUseLocalSource(details2, {
|
|
7142
|
-
linkedProject: true
|
|
7143
|
-
})
|
|
7144
|
-
};
|
|
7145
|
-
}
|
|
7146
|
-
if (!isJsonMode()) {
|
|
7147
|
-
log("");
|
|
7148
|
-
log(
|
|
7149
|
-
colors.warn(
|
|
7150
|
-
`Linked application "${linkedProject.name}" was not found in this organization.`
|
|
7151
|
-
)
|
|
7152
|
-
);
|
|
7153
|
-
}
|
|
7146
|
+
if (options.newApp) {
|
|
7147
|
+
assertConfiguredSourceAllowsCreate(sourcePreference);
|
|
7148
|
+
return createNewAppTarget(client, profile, options);
|
|
7154
7149
|
}
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7150
|
+
const linkedProject = getProjectConfig();
|
|
7151
|
+
const linkedApp = linkedProject ? findApp3(apps, linkedProject.applicationId) ?? findApp3(apps, linkedProject.name) : void 0;
|
|
7152
|
+
if (linkedProject && !linkedApp && !isJsonMode()) {
|
|
7153
|
+
log("");
|
|
7154
|
+
log(
|
|
7155
|
+
colors.warn(
|
|
7156
|
+
`Linked application "${linkedProject.name}" was not found in this organization.`
|
|
7157
|
+
)
|
|
7158
7158
|
);
|
|
7159
7159
|
}
|
|
7160
7160
|
if (apps.length === 0) {
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
return
|
|
7168
|
-
app: app2,
|
|
7169
|
-
createdApp: true,
|
|
7170
|
-
hasConfiguredSource: false,
|
|
7171
|
-
shouldUploadSource: true
|
|
7172
|
-
};
|
|
7161
|
+
assertConfiguredSourceAllowsCreate(sourcePreference, true);
|
|
7162
|
+
return createNewAppTarget(client, profile, options);
|
|
7163
|
+
}
|
|
7164
|
+
if (shouldSkipConfirmation()) {
|
|
7165
|
+
if (linkedApp) return reuseAppTarget(client, profile, linkedApp);
|
|
7166
|
+
assertConfiguredSourceAllowsCreate(sourcePreference, true);
|
|
7167
|
+
return createNewAppTarget(client, profile, options);
|
|
7173
7168
|
}
|
|
7174
7169
|
const createValue = "__create__";
|
|
7175
|
-
const
|
|
7170
|
+
const orderedApps = linkedApp ? [linkedApp, ...apps.filter((a) => a.applicationId !== linkedApp.applicationId)] : apps;
|
|
7171
|
+
const selected = await select(
|
|
7172
|
+
"Create a new app or reuse an existing one?",
|
|
7173
|
+
[
|
|
7174
|
+
{
|
|
7175
|
+
name: `Create a new app from ${colors.cyan(basename(process.cwd()) || "this directory")}`,
|
|
7176
|
+
value: createValue
|
|
7177
|
+
},
|
|
7178
|
+
...orderedApps.map((app2) => ({
|
|
7179
|
+
name: `Reuse ${app2.name} ${colors.dim(`(${app2.applicationId.slice(0, 8)})`)}${linkedApp && app2.applicationId === linkedApp.applicationId ? colors.dim(" \u2014 linked") : ""}`,
|
|
7180
|
+
value: app2.applicationId
|
|
7181
|
+
}))
|
|
7182
|
+
],
|
|
7176
7183
|
{
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
if (sourcePreference === "configured") {
|
|
7187
|
-
throw new InvalidArgumentError(
|
|
7188
|
-
'New apps do not have a configured Git provider source yet. Connect a Git provider first, or rerun with "--source upload".'
|
|
7189
|
-
);
|
|
7184
|
+
field: "deploy_app",
|
|
7185
|
+
flag: "--new-app to create a new app, or --app <id|name> to reuse an existing one",
|
|
7186
|
+
context: {
|
|
7187
|
+
linkedApp: linkedApp ? { id: linkedApp.applicationId, name: linkedApp.name } : null,
|
|
7188
|
+
apps: orderedApps.map((a) => ({
|
|
7189
|
+
id: a.applicationId,
|
|
7190
|
+
name: a.name
|
|
7191
|
+
}))
|
|
7192
|
+
}
|
|
7190
7193
|
}
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
hasConfiguredSource: false,
|
|
7196
|
-
shouldUploadSource: true
|
|
7197
|
-
};
|
|
7194
|
+
);
|
|
7195
|
+
if (selected === createValue) {
|
|
7196
|
+
assertConfiguredSourceAllowsCreate(sourcePreference);
|
|
7197
|
+
return createNewAppTarget(client, profile, options);
|
|
7198
7198
|
}
|
|
7199
7199
|
const app = findApp3(apps, selected);
|
|
7200
7200
|
if (!app) {
|
|
7201
7201
|
throw new NotFoundError("Application", selected);
|
|
7202
7202
|
}
|
|
7203
|
+
return reuseAppTarget(client, profile, app);
|
|
7204
|
+
}
|
|
7205
|
+
function assertConfiguredSourceAllowsCreate(sourcePreference, noAppsExist = false) {
|
|
7206
|
+
if (sourcePreference !== "configured") return;
|
|
7207
|
+
throw new InvalidArgumentError(
|
|
7208
|
+
noAppsExist ? 'No Tarout app exists to deploy from a configured source. Create or select an app with a configured Git provider first, or rerun with "--source upload".' : 'New apps do not have a configured Git provider source yet. Connect a Git provider first, or rerun with "--source upload".'
|
|
7209
|
+
);
|
|
7210
|
+
}
|
|
7211
|
+
async function createNewAppTarget(client, profile, options) {
|
|
7212
|
+
const app = await createAppFromCurrentDirectory(client, profile, options);
|
|
7213
|
+
return {
|
|
7214
|
+
app,
|
|
7215
|
+
createdApp: true,
|
|
7216
|
+
hasConfiguredSource: false,
|
|
7217
|
+
shouldUploadSource: true
|
|
7218
|
+
};
|
|
7219
|
+
}
|
|
7220
|
+
async function reuseAppTarget(client, profile, app) {
|
|
7203
7221
|
setProjectConfig({
|
|
7204
7222
|
applicationId: app.applicationId,
|
|
7205
7223
|
name: app.name,
|
|
@@ -7369,6 +7387,97 @@ async function runInlineUpgrade(client, planKey) {
|
|
|
7369
7387
|
}
|
|
7370
7388
|
return false;
|
|
7371
7389
|
}
|
|
7390
|
+
async function getCurrentPlanQuantitySafely(client) {
|
|
7391
|
+
try {
|
|
7392
|
+
const sub = await client.subscription.getCurrent.query();
|
|
7393
|
+
const q = Number(sub?.planQuantity);
|
|
7394
|
+
return Number.isFinite(q) && q > 0 ? q : 1;
|
|
7395
|
+
} catch {
|
|
7396
|
+
return 1;
|
|
7397
|
+
}
|
|
7398
|
+
}
|
|
7399
|
+
async function runInlineTargetedRemedy(client, remedy) {
|
|
7400
|
+
let input2;
|
|
7401
|
+
let label;
|
|
7402
|
+
if (remedy.kind === "addon") {
|
|
7403
|
+
input2 = { kind: "addon", addonKey: remedy.targetKey, quantity: 1 };
|
|
7404
|
+
label = remedy.targetName ?? remedy.targetKey;
|
|
7405
|
+
} else {
|
|
7406
|
+
const next = await getCurrentPlanQuantitySafely(client) + 1;
|
|
7407
|
+
input2 = { kind: "plan_quantity", planKey: remedy.targetKey, quantity: next };
|
|
7408
|
+
label = `${remedy.targetName ?? remedy.targetKey} \xD7${next}`;
|
|
7409
|
+
}
|
|
7410
|
+
const _spinner = startSpinner(`Adding ${label}...`);
|
|
7411
|
+
const result = await performBillingChange(client, {
|
|
7412
|
+
...input2,
|
|
7413
|
+
wait: true,
|
|
7414
|
+
timeoutMs: 6e5,
|
|
7415
|
+
openBrowser: isJsonMode() ? void 0 : async (url) => {
|
|
7416
|
+
await open4(url);
|
|
7417
|
+
},
|
|
7418
|
+
onCheckoutOpened: ({ orderId, paymentUrl }) => {
|
|
7419
|
+
log("");
|
|
7420
|
+
log("Open this URL to complete payment:");
|
|
7421
|
+
log(` ${colors.cyan(paymentUrl)}`);
|
|
7422
|
+
log(`Order ID: ${colors.dim(orderId)}`);
|
|
7423
|
+
}
|
|
7424
|
+
});
|
|
7425
|
+
if (result.status === "applied" || result.status === "paid") {
|
|
7426
|
+
succeedSpinner(`${label} added.`);
|
|
7427
|
+
return true;
|
|
7428
|
+
}
|
|
7429
|
+
failSpinner(
|
|
7430
|
+
result.status === "deferred" ? "Change did not require an immediate payment." : result.status === "pending_timeout" ? "Payment still pending after 10 minutes." : `Payment ${result.status}.`
|
|
7431
|
+
);
|
|
7432
|
+
if (result.failureReason) log(colors.error(result.failureReason));
|
|
7433
|
+
if (result.orderId) {
|
|
7434
|
+
log(
|
|
7435
|
+
colors.dim(
|
|
7436
|
+
`Resume later with: tarout billing wait ${result.orderId.slice(0, 8)} --timeout 600`
|
|
7437
|
+
)
|
|
7438
|
+
);
|
|
7439
|
+
}
|
|
7440
|
+
return false;
|
|
7441
|
+
}
|
|
7442
|
+
async function promptEntitlementRemedy(client, err, requestedPlan) {
|
|
7443
|
+
const failedKey = extractEntitlementKeyFromError(err);
|
|
7444
|
+
const catalog = await fetchCatalogSafely(client);
|
|
7445
|
+
const remedy = resolveEntitlementRemedy(failedKey, catalog, { requestedPlan });
|
|
7446
|
+
if (remedy.kind === "plan") {
|
|
7447
|
+
return promptUpgradeFromEntitlementError(client, err, requestedPlan);
|
|
7448
|
+
}
|
|
7449
|
+
const price = remedy.priceHalalas !== void 0 ? ` (${formatPlanPrice(remedy.priceHalalas)})` : "";
|
|
7450
|
+
const targetedLabel = remedy.kind === "plan_quantity" ? `Add one more app slot${price}` : `Add just the ${remedy.targetName ?? remedy.targetKey}${price}`;
|
|
7451
|
+
log("");
|
|
7452
|
+
log(colors.warn("That resource isn't included in your current plan."));
|
|
7453
|
+
log("");
|
|
7454
|
+
const UPGRADE = "__upgrade__";
|
|
7455
|
+
const TARGETED = "__targeted__";
|
|
7456
|
+
const CANCEL = "__cancel__";
|
|
7457
|
+
const choice = await select(
|
|
7458
|
+
"How would you like to add it?",
|
|
7459
|
+
[
|
|
7460
|
+
{ name: "Upgrade the plan", value: UPGRADE },
|
|
7461
|
+
{ name: targetedLabel, value: TARGETED },
|
|
7462
|
+
{ name: "Cancel", value: CANCEL }
|
|
7463
|
+
],
|
|
7464
|
+
{
|
|
7465
|
+
field: "entitlement_remedy",
|
|
7466
|
+
flag: "--plan <key> (upgrade) | tarout billing addon:buy <key> (addon)",
|
|
7467
|
+
context: {
|
|
7468
|
+
failedEntitlementKey: failedKey,
|
|
7469
|
+
remedyKind: remedy.kind,
|
|
7470
|
+
targetKey: remedy.targetKey,
|
|
7471
|
+
command: remedy.command
|
|
7472
|
+
}
|
|
7473
|
+
}
|
|
7474
|
+
);
|
|
7475
|
+
if (choice === CANCEL) return false;
|
|
7476
|
+
if (choice === UPGRADE) {
|
|
7477
|
+
return promptUpgradeFromEntitlementError(client, err, requestedPlan);
|
|
7478
|
+
}
|
|
7479
|
+
return runInlineTargetedRemedy(client, remedy);
|
|
7480
|
+
}
|
|
7372
7481
|
async function getAppPlanChoices(client, preloadedOptions) {
|
|
7373
7482
|
try {
|
|
7374
7483
|
const options = preloadedOptions ?? await client.application.getCreateOptions.query();
|
|
@@ -7459,22 +7568,109 @@ function extractEntitlementKeyFromError(err) {
|
|
|
7459
7568
|
const m = msg.match(/Plan limit reached for ([\w.]+)/i);
|
|
7460
7569
|
return m?.[1];
|
|
7461
7570
|
}
|
|
7571
|
+
function buildRemedyOptions(remedy, requestedPlan, catalog) {
|
|
7572
|
+
if (remedy.kind === "addon" || remedy.kind === "plan_quantity") {
|
|
7573
|
+
const upgradePlan = nextPlanForRequested(requestedPlan);
|
|
7574
|
+
const planDef = (catalog?.plans ?? []).find(
|
|
7575
|
+
(p) => (p.planKey ?? p.key) === upgradePlan
|
|
7576
|
+
);
|
|
7577
|
+
return [
|
|
7578
|
+
{
|
|
7579
|
+
action: remedy.kind === "addon" ? "buy_addon" : "add_app_slot",
|
|
7580
|
+
label: remedy.kind === "addon" ? `Buy just the ${remedy.targetName ?? remedy.targetKey}` : "Add one more app slot",
|
|
7581
|
+
command: remedy.command
|
|
7582
|
+
},
|
|
7583
|
+
{
|
|
7584
|
+
action: "upgrade_plan",
|
|
7585
|
+
label: `Upgrade to ${planDef?.name ?? upgradePlan}`,
|
|
7586
|
+
command: `tarout billing upgrade ${upgradePlan} --wait`
|
|
7587
|
+
}
|
|
7588
|
+
];
|
|
7589
|
+
}
|
|
7590
|
+
return [
|
|
7591
|
+
{
|
|
7592
|
+
action: "upgrade_plan",
|
|
7593
|
+
label: `Upgrade to ${remedy.targetName ?? remedy.targetKey}`,
|
|
7594
|
+
command: remedy.command
|
|
7595
|
+
}
|
|
7596
|
+
];
|
|
7597
|
+
}
|
|
7462
7598
|
async function emitNeedsUpgrade(client, err, requestedPlan, retryCommand) {
|
|
7463
7599
|
const message = err instanceof Error ? err.message : "Plan upgrade required";
|
|
7464
7600
|
const failedKey = extractEntitlementKeyFromError(err);
|
|
7465
7601
|
const catalog = await fetchCatalogSafely(client);
|
|
7466
7602
|
const remedy = resolveEntitlementRemedy(failedKey, catalog, { requestedPlan });
|
|
7603
|
+
const options = buildRemedyOptions(remedy, requestedPlan, catalog);
|
|
7604
|
+
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}.`;
|
|
7467
7605
|
outputError("NEEDS_UPGRADE", message, {
|
|
7468
7606
|
failedEntitlementKey: failedKey,
|
|
7469
7607
|
remedyKind: remedy.kind,
|
|
7470
|
-
// `suggestedPlan` retained for back-compat
|
|
7471
|
-
//
|
|
7608
|
+
// `suggestedPlan`/`nextCommand` retained for back-compat (the recommended
|
|
7609
|
+
// targeted action); `options` is the authoritative choice list.
|
|
7472
7610
|
suggestedPlan: remedy.targetKey,
|
|
7473
7611
|
suggestedTarget: remedy.targetKey,
|
|
7474
7612
|
nextCommand: remedy.command,
|
|
7475
|
-
|
|
7613
|
+
options,
|
|
7614
|
+
hint
|
|
7476
7615
|
});
|
|
7477
7616
|
}
|
|
7617
|
+
async function listFreeDatabasesSafely(client) {
|
|
7618
|
+
try {
|
|
7619
|
+
const [pgRows, myRows] = await Promise.all([
|
|
7620
|
+
client.postgres.allByOrganization.query().catch(() => []),
|
|
7621
|
+
client.mysql.allByOrganization.query().catch(() => [])
|
|
7622
|
+
]);
|
|
7623
|
+
const out = [];
|
|
7624
|
+
for (const r of pgRows ?? []) {
|
|
7625
|
+
if ((r?.plan ?? "FREE") === "FREE" && r?.postgresId) {
|
|
7626
|
+
out.push({ kind: "postgres", id: r.postgresId, name: r.name });
|
|
7627
|
+
}
|
|
7628
|
+
}
|
|
7629
|
+
for (const r of myRows ?? []) {
|
|
7630
|
+
if ((r?.plan ?? "FREE") === "FREE" && r?.mysqlId) {
|
|
7631
|
+
out.push({ kind: "mysql", id: r.mysqlId, name: r.name });
|
|
7632
|
+
}
|
|
7633
|
+
}
|
|
7634
|
+
return out;
|
|
7635
|
+
} catch {
|
|
7636
|
+
return [];
|
|
7637
|
+
}
|
|
7638
|
+
}
|
|
7639
|
+
async function explainFreeResourceSlotExhaustion(client, failedKey) {
|
|
7640
|
+
const isDb = failedKey === "db.free.slots";
|
|
7641
|
+
const resource = isDb ? "database" : "storage bucket";
|
|
7642
|
+
log("");
|
|
7643
|
+
log(
|
|
7644
|
+
colors.warn(
|
|
7645
|
+
`The free plan includes one ${resource} for the whole organization (across all environments).`
|
|
7646
|
+
)
|
|
7647
|
+
);
|
|
7648
|
+
if (isDb) {
|
|
7649
|
+
const existing = await listFreeDatabasesSafely(client);
|
|
7650
|
+
if (existing.length > 0) {
|
|
7651
|
+
log("You already have:");
|
|
7652
|
+
for (const d of existing) {
|
|
7653
|
+
log(
|
|
7654
|
+
` \u2022 ${d.name} ${colors.dim(`(${d.kind} \xB7 ${d.id.slice(0, 8)})`)}`
|
|
7655
|
+
);
|
|
7656
|
+
}
|
|
7657
|
+
} else {
|
|
7658
|
+
log(
|
|
7659
|
+
colors.dim(
|
|
7660
|
+
"Your existing free database may be in another environment \u2014 run `tarout db list`."
|
|
7661
|
+
)
|
|
7662
|
+
);
|
|
7663
|
+
}
|
|
7664
|
+
}
|
|
7665
|
+
log("");
|
|
7666
|
+
log("To continue, pick one:");
|
|
7667
|
+
if (isDb) {
|
|
7668
|
+
log(` \u2022 Reuse it: ${colors.cyan("tarout deploy --reuse-database auto")}`);
|
|
7669
|
+
log(` \u2022 Delete it: ${colors.cyan("tarout db delete <id>")}`);
|
|
7670
|
+
}
|
|
7671
|
+
log(` \u2022 Upgrade: ${colors.cyan("tarout billing upgrade shared --wait")}`);
|
|
7672
|
+
log("");
|
|
7673
|
+
}
|
|
7478
7674
|
async function promptUpgradeFromEntitlementError(client, err, requestedPlan) {
|
|
7479
7675
|
const catalog = await fetchCatalogSafely(client);
|
|
7480
7676
|
const allPlans = catalog?.plans ?? [];
|
|
@@ -8494,6 +8690,12 @@ function registerDeployCommands(program2) {
|
|
|
8494
8690
|
).option(
|
|
8495
8691
|
"--reuse-storage <ref>",
|
|
8496
8692
|
"Reuse an existing storage bucket in this project: <id>, <name>, or 'auto' (exactly one match)"
|
|
8693
|
+
).option(
|
|
8694
|
+
"--new-app",
|
|
8695
|
+
"Create a new app instead of being prompted to reuse an existing one"
|
|
8696
|
+
).option(
|
|
8697
|
+
"--name <name>",
|
|
8698
|
+
"Name for a newly created app (defaults to the directory name)"
|
|
8497
8699
|
).option("--token <token>", "API token to use for this deployment").option("-r, --region <region>", "Deployment region", DEFAULT_REGION).option("--description <text>", "Description for a newly created app").option(
|
|
8498
8700
|
"--framework-preset <preset>",
|
|
8499
8701
|
"Framework preset override (e.g. nextjs, vite, astro)"
|
|
@@ -8582,7 +8784,7 @@ function registerDeployCommands(program2) {
|
|
|
8582
8784
|
} catch (err) {
|
|
8583
8785
|
if (isEntitlementError(err)) {
|
|
8584
8786
|
const message = err instanceof Error ? err.message : "Plan upgrade required";
|
|
8585
|
-
if (isJsonMode() || shouldSkipConfirmation()) {
|
|
8787
|
+
if (isJsonMode() || isNonInteractiveMode() || shouldSkipConfirmation()) {
|
|
8586
8788
|
await emitNeedsUpgrade(
|
|
8587
8789
|
getApiClient(),
|
|
8588
8790
|
err,
|
|
@@ -8593,7 +8795,11 @@ function registerDeployCommands(program2) {
|
|
|
8593
8795
|
}
|
|
8594
8796
|
log("");
|
|
8595
8797
|
log(colors.warn(message));
|
|
8596
|
-
const
|
|
8798
|
+
const failedKey = extractEntitlementKeyFromError(err);
|
|
8799
|
+
if (failedKey === "db.free.slots" || failedKey === "storage.free.slots") {
|
|
8800
|
+
await explainFreeResourceSlotExhaustion(getApiClient(), failedKey);
|
|
8801
|
+
}
|
|
8802
|
+
const upgraded = await promptEntitlementRemedy(
|
|
8597
8803
|
getApiClient(),
|
|
8598
8804
|
err,
|
|
8599
8805
|
options.plan
|
|
@@ -8607,7 +8813,7 @@ function registerDeployCommands(program2) {
|
|
|
8607
8813
|
);
|
|
8608
8814
|
exit(ExitCode.PERMISSION_DENIED);
|
|
8609
8815
|
}
|
|
8610
|
-
box("
|
|
8816
|
+
box("Billing updated", [
|
|
8611
8817
|
colors.success("Subscription updated."),
|
|
8612
8818
|
`Run ${colors.cyan("tarout deploy")} again to deploy on the new plan.`
|
|
8613
8819
|
]);
|
|
@@ -19041,6 +19247,9 @@ function registerUpCommand(program2) {
|
|
|
19041
19247
|
).option("--token <token>", "API token for this run").option("--name <name>", "Application name (defaults to directory name)").option("--plan <plan>", "App hosting plan: free, shared, or dedicated", "free").option("--source <source>", "Source: upload (default) or github", "upload").option(
|
|
19042
19248
|
"--app <ref>",
|
|
19043
19249
|
"Deploy to an existing app by id or name (skips create-or-pick prompt; 'auto' picks the lone match)"
|
|
19250
|
+
).option(
|
|
19251
|
+
"--new-app",
|
|
19252
|
+
"Create a new app instead of being prompted to reuse an existing one"
|
|
19044
19253
|
).option("--repo <owner/repo>", "GitHub repository (when --source github)").option("--branch <branch>", "GitHub branch (with --source github)", "main").option("-r, --region <region>", "Deployment region", DEFAULT_REGION3).option(
|
|
19045
19254
|
"--database <type>",
|
|
19046
19255
|
"Provision and attach a database: none, postgres, or mysql (defaults to auto-detected)"
|
|
@@ -19102,9 +19311,7 @@ function registerUpCommand(program2) {
|
|
|
19102
19311
|
}
|
|
19103
19312
|
return allApps;
|
|
19104
19313
|
};
|
|
19105
|
-
|
|
19106
|
-
const apps = await loadApps();
|
|
19107
|
-
const picked = resolveAppRef(apps, options.app);
|
|
19314
|
+
const reuse = (picked, via) => {
|
|
19108
19315
|
app = picked;
|
|
19109
19316
|
reused = true;
|
|
19110
19317
|
setProjectConfig({
|
|
@@ -19117,45 +19324,47 @@ function registerUpCommand(program2) {
|
|
|
19117
19324
|
event: "app_reused",
|
|
19118
19325
|
applicationId: picked.applicationId,
|
|
19119
19326
|
name: picked.name,
|
|
19120
|
-
via
|
|
19327
|
+
via
|
|
19121
19328
|
});
|
|
19122
|
-
}
|
|
19329
|
+
};
|
|
19330
|
+
if (options.app) {
|
|
19123
19331
|
const apps = await loadApps();
|
|
19124
|
-
|
|
19125
|
-
|
|
19126
|
-
app = existing;
|
|
19127
|
-
reused = true;
|
|
19128
|
-
emitEvent2({
|
|
19129
|
-
event: "app_reused",
|
|
19130
|
-
applicationId: app.applicationId,
|
|
19131
|
-
name: app.name,
|
|
19132
|
-
via: "linked"
|
|
19133
|
-
});
|
|
19134
|
-
}
|
|
19135
|
-
}
|
|
19136
|
-
if (!app && !isJsonMode() && !shouldSkipConfirmation()) {
|
|
19332
|
+
reuse(resolveAppRef(apps, options.app), "--app");
|
|
19333
|
+
} else if (!options.newApp) {
|
|
19137
19334
|
const apps = await loadApps();
|
|
19138
|
-
|
|
19335
|
+
const linkedApp = linked ? findApp3(apps, linked.applicationId) ?? findApp3(apps, linked.name) : void 0;
|
|
19336
|
+
if (shouldSkipConfirmation()) {
|
|
19337
|
+
if (linkedApp) reuse(linkedApp, "linked");
|
|
19338
|
+
} else if (apps.length > 0) {
|
|
19139
19339
|
const createValue = "__create__";
|
|
19140
|
-
|
|
19141
|
-
|
|
19142
|
-
|
|
19143
|
-
|
|
19340
|
+
const orderedApps = linkedApp ? [
|
|
19341
|
+
linkedApp,
|
|
19342
|
+
...apps.filter(
|
|
19343
|
+
(a) => a.applicationId !== linkedApp.applicationId
|
|
19344
|
+
)
|
|
19345
|
+
] : apps;
|
|
19144
19346
|
const selected = await select(
|
|
19145
|
-
"
|
|
19347
|
+
"Create a new app or reuse an existing one?",
|
|
19146
19348
|
[
|
|
19147
19349
|
{
|
|
19148
19350
|
name: `Create a new app${options.name ? ` named "${options.name}"` : ""}`,
|
|
19149
19351
|
value: createValue
|
|
19150
19352
|
},
|
|
19151
|
-
...
|
|
19152
|
-
name:
|
|
19353
|
+
...orderedApps.map((existing) => ({
|
|
19354
|
+
name: `Reuse ${existing.name} ${colors.dim(`(${existing.applicationId.slice(0, 8)})`)}${linkedApp && existing.applicationId === linkedApp.applicationId ? colors.dim(" \u2014 linked") : ""}`,
|
|
19153
19355
|
value: existing.applicationId
|
|
19154
19356
|
}))
|
|
19155
19357
|
],
|
|
19156
19358
|
{
|
|
19157
|
-
field: "
|
|
19158
|
-
flag: "--app <id|name
|
|
19359
|
+
field: "deploy_app",
|
|
19360
|
+
flag: "--new-app to create a new app, or --app <id|name> to reuse an existing one",
|
|
19361
|
+
context: {
|
|
19362
|
+
linkedApp: linkedApp ? { id: linkedApp.applicationId, name: linkedApp.name } : null,
|
|
19363
|
+
apps: orderedApps.map((a) => ({
|
|
19364
|
+
id: a.applicationId,
|
|
19365
|
+
name: a.name
|
|
19366
|
+
}))
|
|
19367
|
+
}
|
|
19159
19368
|
}
|
|
19160
19369
|
);
|
|
19161
19370
|
if (selected !== createValue) {
|
|
@@ -19163,20 +19372,7 @@ function registerUpCommand(program2) {
|
|
|
19163
19372
|
if (!picked) {
|
|
19164
19373
|
throw new NotFoundError("Application", selected);
|
|
19165
19374
|
}
|
|
19166
|
-
|
|
19167
|
-
reused = true;
|
|
19168
|
-
setProjectConfig({
|
|
19169
|
-
applicationId: picked.applicationId,
|
|
19170
|
-
name: picked.name,
|
|
19171
|
-
organizationId: profile.organizationId,
|
|
19172
|
-
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
19173
|
-
});
|
|
19174
|
-
emitEvent2({
|
|
19175
|
-
event: "app_reused",
|
|
19176
|
-
applicationId: picked.applicationId,
|
|
19177
|
-
name: picked.name,
|
|
19178
|
-
via: "interactive"
|
|
19179
|
-
});
|
|
19375
|
+
reuse(picked, "interactive");
|
|
19180
19376
|
}
|
|
19181
19377
|
}
|
|
19182
19378
|
}
|
|
@@ -19204,13 +19400,13 @@ function registerUpCommand(program2) {
|
|
|
19204
19400
|
} catch (err) {
|
|
19205
19401
|
if (isEntitlementError(err)) {
|
|
19206
19402
|
const message = err instanceof Error ? err.message : "Plan upgrade required";
|
|
19207
|
-
if (isJsonMode() || shouldSkipConfirmation()) {
|
|
19403
|
+
if (isJsonMode() || isNonInteractiveMode() || shouldSkipConfirmation()) {
|
|
19208
19404
|
await emitNeedsUpgrade(client, err, options.plan, "tarout up");
|
|
19209
19405
|
exit(ExitCode.PERMISSION_DENIED);
|
|
19210
19406
|
}
|
|
19211
19407
|
log("");
|
|
19212
19408
|
log(colors.warn(message));
|
|
19213
|
-
const upgraded = await
|
|
19409
|
+
const upgraded = await promptEntitlementRemedy(
|
|
19214
19410
|
client,
|
|
19215
19411
|
err,
|
|
19216
19412
|
options.plan
|
|
@@ -19219,7 +19415,7 @@ function registerUpCommand(program2) {
|
|
|
19219
19415
|
await emitNeedsUpgrade(client, err, options.plan, "tarout up");
|
|
19220
19416
|
exit(ExitCode.PERMISSION_DENIED);
|
|
19221
19417
|
}
|
|
19222
|
-
box("
|
|
19418
|
+
box("Billing updated", [
|
|
19223
19419
|
colors.success("Subscription updated."),
|
|
19224
19420
|
`Run ${colors.cyan("tarout up")} again to deploy on the new plan.`
|
|
19225
19421
|
]);
|
|
@@ -19316,6 +19512,39 @@ function registerUpCommand(program2) {
|
|
|
19316
19512
|
app.applicationId
|
|
19317
19513
|
);
|
|
19318
19514
|
} catch (err) {
|
|
19515
|
+
if (isEntitlementError(err)) {
|
|
19516
|
+
const message = err instanceof Error ? err.message : "Plan upgrade required";
|
|
19517
|
+
if (isJsonMode() || isNonInteractiveMode() || shouldSkipConfirmation()) {
|
|
19518
|
+
await emitNeedsUpgrade(
|
|
19519
|
+
getApiClient(),
|
|
19520
|
+
err,
|
|
19521
|
+
options.plan,
|
|
19522
|
+
"tarout up"
|
|
19523
|
+
);
|
|
19524
|
+
exit(ExitCode.PERMISSION_DENIED);
|
|
19525
|
+
}
|
|
19526
|
+
log("");
|
|
19527
|
+
log(colors.warn(message));
|
|
19528
|
+
const upgraded = await promptEntitlementRemedy(
|
|
19529
|
+
getApiClient(),
|
|
19530
|
+
err,
|
|
19531
|
+
options.plan
|
|
19532
|
+
);
|
|
19533
|
+
if (!upgraded) {
|
|
19534
|
+
await emitNeedsUpgrade(
|
|
19535
|
+
getApiClient(),
|
|
19536
|
+
err,
|
|
19537
|
+
options.plan,
|
|
19538
|
+
"tarout up"
|
|
19539
|
+
);
|
|
19540
|
+
exit(ExitCode.PERMISSION_DENIED);
|
|
19541
|
+
}
|
|
19542
|
+
box("Billing updated", [
|
|
19543
|
+
colors.success("Subscription updated."),
|
|
19544
|
+
`Run ${colors.cyan("tarout up")} again to deploy on the new plan.`
|
|
19545
|
+
]);
|
|
19546
|
+
return;
|
|
19547
|
+
}
|
|
19319
19548
|
if (err instanceof Error && err.message.startsWith("Invalid --source")) {
|
|
19320
19549
|
outputError("INVALID_ARGUMENTS", err.message);
|
|
19321
19550
|
if (!isJsonMode()) log(colors.error(err.message));
|
|
@@ -19544,10 +19773,11 @@ program.name("tarout").description("Tarout PaaS Command Line Interface").version
|
|
|
19544
19773
|
"Fail fast on missing input (emit needs_input + exit 6 instead of prompting on TTY)"
|
|
19545
19774
|
).option("-q, --quiet", "Minimal output").option("-v, --verbose", "Extra debug information").option("--no-color", "Disable colored output").hook("preAction", (thisCommand) => {
|
|
19546
19775
|
const opts = thisCommand.opts();
|
|
19776
|
+
const stdinIsTTY = Boolean(process.stdin.isTTY);
|
|
19547
19777
|
setGlobalOptions({
|
|
19548
19778
|
json: opts.json || false,
|
|
19549
19779
|
yes: opts.yes || false,
|
|
19550
|
-
nonInteractive: opts.nonInteractive ||
|
|
19780
|
+
nonInteractive: opts.nonInteractive || !stdinIsTTY,
|
|
19551
19781
|
quiet: opts.quiet || false,
|
|
19552
19782
|
verbose: opts.verbose || false,
|
|
19553
19783
|
noColor: opts.color === false
|