@tarout/cli 0.2.1 → 0.3.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/README.md CHANGED
@@ -39,6 +39,28 @@ tarout deploy --wait --source upload
39
39
  # The first deploy prompts to create or link an app, and to create detected resources.
40
40
  ```
41
41
 
42
+ ## Call any API (`tarout call`)
43
+
44
+ Beyond the curated commands, `tarout call` reaches **every** platform procedure
45
+ directly — the same control surface exposed via REST and MCP:
46
+
47
+ ```bash
48
+ tarout call --list # discover all callable procedures + type
49
+ tarout call application.create --input '{"name":"my-app"}' --json
50
+ tarout call deployment.all --input '{"applicationId":"app_123"}'
51
+ ```
52
+
53
+ ## MCP (agent access)
54
+
55
+ Connect Claude Desktop / Cursor to Tarout via the bundled `tarout-mcp` bridge:
56
+
57
+ ```json
58
+ { "mcpServers": { "tarout": { "command": "tarout-mcp" } } }
59
+ ```
60
+
61
+ Hosted agents can hit the Streamable-HTTP endpoint `/api/mcp` directly with an
62
+ `x-api-key` header. See [`docs/MCP.md`](../docs/MCP.md).
63
+
42
64
  ## Commands
43
65
 
44
66
  ### Authentication
package/bin/tarout-mcp ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import('../dist/mcp/stdio.js');
@@ -2,8 +2,9 @@ import {
2
2
  createApiClient,
3
3
  getApiClient,
4
4
  resetApiClient
5
- } from "./chunk-FKX4CRPL.js";
6
- import "./chunk-FS74WWHV.js";
5
+ } from "./chunk-NHNK5ZQ5.js";
6
+ import "./chunk-5DAFGMBH.js";
7
+ import "./chunk-KL3JNPAY.js";
7
8
  export {
8
9
  createApiClient,
9
10
  getApiClient,
@@ -0,0 +1,12 @@
1
+ import {
2
+ pollCheckoutUntilTerminal,
3
+ registerBillingCommands
4
+ } from "./chunk-BS6DFVSU.js";
5
+ import "./chunk-NHNK5ZQ5.js";
6
+ import "./chunk-5DAFGMBH.js";
7
+ import "./chunk-CJMIX35A.js";
8
+ import "./chunk-KL3JNPAY.js";
9
+ export {
10
+ pollCheckoutUntilTerminal,
11
+ registerBillingCommands
12
+ };
@@ -0,0 +1,137 @@
1
+ // src/lib/config.ts
2
+ import Conf from "conf";
3
+ import {
4
+ chmodSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ rmSync,
9
+ writeFileSync
10
+ } from "fs";
11
+ import { join } from "path";
12
+ var config = new Conf({
13
+ projectName: "tarout",
14
+ configFileMode: 384,
15
+ defaults: {
16
+ currentProfile: "default",
17
+ profiles: {}
18
+ }
19
+ });
20
+ function getConfig() {
21
+ return config.store;
22
+ }
23
+ function getCurrentProfile() {
24
+ const cfg = getConfig();
25
+ return cfg.profiles[cfg.currentProfile] || null;
26
+ }
27
+ function setProfile(name, profile) {
28
+ config.set(`profiles.${name}`, profile);
29
+ }
30
+ function setCurrentProfile(name) {
31
+ config.set("currentProfile", name);
32
+ }
33
+ function clearConfig() {
34
+ config.clear();
35
+ }
36
+ function isLoggedIn() {
37
+ const profile = getCurrentProfile();
38
+ return profile !== null && !!profile.token || !!process.env.TAROUT_TOKEN;
39
+ }
40
+ function getToken() {
41
+ const profile = getCurrentProfile();
42
+ return profile?.token || process.env.TAROUT_TOKEN || null;
43
+ }
44
+ function getApiUrl() {
45
+ const profile = getCurrentProfile();
46
+ return profile?.apiUrl || process.env.TAROUT_API_URL || "https://tarout.sa";
47
+ }
48
+ function updateProfile(updates) {
49
+ const cfg = getConfig();
50
+ const currentProfileName = cfg.currentProfile;
51
+ const currentProfile = cfg.profiles[currentProfileName];
52
+ if (currentProfile) {
53
+ config.set(`profiles.${currentProfileName}`, {
54
+ ...currentProfile,
55
+ ...updates
56
+ });
57
+ }
58
+ }
59
+ var PROJECT_CONFIG_DIR = ".tarout";
60
+ var PROJECT_CONFIG_FILE = "project.json";
61
+ function chmodIfSupported(path, mode) {
62
+ try {
63
+ chmodSync(path, mode);
64
+ } catch {
65
+ }
66
+ }
67
+ function getProjectConfigDir(basePath) {
68
+ const base = basePath || process.cwd();
69
+ return join(base, PROJECT_CONFIG_DIR);
70
+ }
71
+ function getProjectConfigPath(basePath) {
72
+ return join(getProjectConfigDir(basePath), PROJECT_CONFIG_FILE);
73
+ }
74
+ function isProjectLinked(basePath) {
75
+ return existsSync(getProjectConfigPath(basePath));
76
+ }
77
+ function getProjectConfig(basePath) {
78
+ const configPath = getProjectConfigPath(basePath);
79
+ if (!existsSync(configPath)) {
80
+ return null;
81
+ }
82
+ try {
83
+ const content = readFileSync(configPath, "utf-8");
84
+ return JSON.parse(content);
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+ function setProjectConfig(config2, basePath) {
90
+ const configDir = getProjectConfigDir(basePath);
91
+ const configPath = getProjectConfigPath(basePath);
92
+ if (!existsSync(configDir)) {
93
+ mkdirSync(configDir, { recursive: true, mode: 448 });
94
+ }
95
+ chmodIfSupported(configDir, 448);
96
+ writeFileSync(configPath, JSON.stringify(config2, null, 2), {
97
+ encoding: "utf-8",
98
+ mode: 384
99
+ });
100
+ chmodIfSupported(configPath, 384);
101
+ const gitignorePath = join(configDir, ".gitignore");
102
+ if (!existsSync(gitignorePath)) {
103
+ writeFileSync(
104
+ gitignorePath,
105
+ "# Ignore local tarout config\n*\n!.gitignore\n",
106
+ { encoding: "utf-8", mode: 384 }
107
+ );
108
+ chmodIfSupported(gitignorePath, 384);
109
+ }
110
+ }
111
+ function removeProjectConfig(basePath) {
112
+ const configDir = getProjectConfigDir(basePath);
113
+ if (!existsSync(configDir)) {
114
+ return false;
115
+ }
116
+ try {
117
+ rmSync(configDir, { recursive: true, force: true });
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ export {
125
+ getCurrentProfile,
126
+ setProfile,
127
+ setCurrentProfile,
128
+ clearConfig,
129
+ isLoggedIn,
130
+ getToken,
131
+ getApiUrl,
132
+ updateProfile,
133
+ isProjectLinked,
134
+ getProjectConfig,
135
+ setProjectConfig,
136
+ removeProjectConfig
137
+ };
@@ -1,13 +1,15 @@
1
1
  import {
2
2
  AuthError,
3
3
  getApiClient,
4
- handleError,
4
+ handleError
5
+ } from "./chunk-NHNK5ZQ5.js";
6
+ import {
5
7
  isLoggedIn
6
- } from "./chunk-FKX4CRPL.js";
8
+ } from "./chunk-5DAFGMBH.js";
7
9
  import {
8
10
  confirm,
9
11
  select
10
- } from "./chunk-J3H7LTFT.js";
12
+ } from "./chunk-CJMIX35A.js";
11
13
  import {
12
14
  ExitCode,
13
15
  box,
@@ -20,9 +22,10 @@ import {
20
22
  outputJsonLine,
21
23
  shouldSkipConfirmation,
22
24
  table
23
- } from "./chunk-FS74WWHV.js";
25
+ } from "./chunk-KL3JNPAY.js";
24
26
 
25
27
  // src/commands/billing.ts
28
+ import { InvalidArgumentError } from "commander";
26
29
  import open from "open";
27
30
 
28
31
  // src/utils/spinner.ts
@@ -153,10 +156,22 @@ function registerBillingCommands(program) {
153
156
  handleError(err);
154
157
  }
155
158
  });
156
- billing.command("upgrade").argument("[plan]", "Plan key to switch to").description("Upgrade or change subscription plan").option(
159
+ billing.command("upgrade").argument("[plan]", "Plan key to switch to (alias: --plan)").description("Upgrade or change subscription plan").option(
160
+ "--plan <key>",
161
+ "Plan key (alias for the positional argument; useful for agent invocations)"
162
+ ).option(
157
163
  "-q, --quantity <n>",
158
164
  "Plan quantity (for multi-slot plans)",
159
165
  Number.parseInt
166
+ ).option(
167
+ "--billing-period <period>",
168
+ "Billing period: monthly or yearly (yearly = 10\xD7 monthly, 2 months free)",
169
+ parseBillingPeriod
170
+ ).option(
171
+ "--addon <key[:qty]>",
172
+ "Bundled addon to purchase with the plan change (repeatable, e.g. --addon db.standard:2)",
173
+ collectAddon,
174
+ []
160
175
  ).option(
161
176
  "-w, --wait",
162
177
  "After hosted-checkout opens, poll status until the payment is confirmed"
@@ -172,7 +187,9 @@ function registerBillingCommands(program) {
172
187
  try {
173
188
  if (!isLoggedIn()) throw new AuthError();
174
189
  const client = getApiClient();
175
- let targetPlan = planKey;
190
+ let targetPlan = planKey || options.plan;
191
+ const billingPeriod = options.billingPeriod;
192
+ const addons = Array.isArray(options.addon) && options.addon.length > 0 ? options.addon : void 0;
176
193
  if (!targetPlan) {
177
194
  const _spinner = startSpinner("Fetching plans...");
178
195
  const catalog = await client.subscription.getCatalog.query();
@@ -187,15 +204,30 @@ function registerBillingCommands(program) {
187
204
  plans.map((p) => ({
188
205
  name: `${p.planKey || p.key || p.name} ${p.priceHalalas ? `(${(p.priceHalalas / 100).toFixed(2)} SAR/mo)` : "(Free)"}`,
189
206
  value: p.planKey || p.key || p.name
190
- }))
207
+ })),
208
+ {
209
+ field: "plan",
210
+ flag: "--plan",
211
+ context: {
212
+ available: plans.map((p) => ({
213
+ key: p.planKey || p.key || p.name,
214
+ priceHalalas: p.priceHalalas ?? 0
215
+ }))
216
+ }
217
+ }
191
218
  );
192
219
  }
220
+ if (!targetPlan) {
221
+ throw new Error("No plan selected");
222
+ }
193
223
  const _previewSpinner = startSpinner("Calculating change...");
194
224
  let preview;
195
225
  try {
196
226
  preview = await client.subscription.previewPlanChange.query({
197
227
  planKey: targetPlan,
198
- planQuantity: options.quantity
228
+ planQuantity: options.quantity,
229
+ billingPeriod,
230
+ addons
199
231
  });
200
232
  succeedSpinner();
201
233
  } catch {
@@ -206,6 +238,12 @@ function registerBillingCommands(program) {
206
238
  log("");
207
239
  log(`Plan: ${colors.cyan(targetPlan)}`);
208
240
  if (options.quantity) log(`Quantity: ${options.quantity}`);
241
+ if (billingPeriod) log(`Billing period: ${billingPeriod}`);
242
+ if (addons && addons.length > 0) {
243
+ log(
244
+ `Addons: ${addons.map((a) => `${a.addonKey}\xD7${a.quantity}`).join(", ")}`
245
+ );
246
+ }
209
247
  if (preview?.amountDue !== void 0) {
210
248
  log(
211
249
  `Amount due now: ${colors.bold(`${(preview.amountDue / 100).toFixed(2)} SAR`)}`
@@ -214,7 +252,18 @@ function registerBillingCommands(program) {
214
252
  log("");
215
253
  const confirmed = await confirm(
216
254
  `Switch to plan "${targetPlan}"?`,
217
- false
255
+ false,
256
+ {
257
+ field: "confirm_upgrade",
258
+ flag: "--yes",
259
+ context: {
260
+ plan: targetPlan,
261
+ quantity: options.quantity,
262
+ billingPeriod,
263
+ addons,
264
+ amountDueHalalas: preview?.amountDue
265
+ }
266
+ }
218
267
  );
219
268
  if (!confirmed) {
220
269
  log("Cancelled.");
@@ -224,7 +273,9 @@ function registerBillingCommands(program) {
224
273
  const _changeSpinner = startSpinner("Changing plan...");
225
274
  const result = await client.subscription.changePlan.mutate({
226
275
  planKey: targetPlan,
227
- planQuantity: options.quantity
276
+ planQuantity: options.quantity,
277
+ billingPeriod,
278
+ addons
228
279
  });
229
280
  succeedSpinner("Plan changed!");
230
281
  if (result?.applied) {
@@ -369,6 +420,14 @@ function registerBillingCommands(program) {
369
420
  try {
370
421
  if (!isLoggedIn()) throw new AuthError();
371
422
  const client = getApiClient();
423
+ if (isJsonMode()) {
424
+ outputJsonLine({
425
+ type: "event",
426
+ event: "checkout_polling_started",
427
+ orderId,
428
+ timeoutSeconds: options.timeout
429
+ });
430
+ }
372
431
  const final = await pollCheckoutUntilTerminal(client, orderId, {
373
432
  timeoutMs: options.timeout * 1e3,
374
433
  intervalMs: 4e3
@@ -414,7 +473,8 @@ function registerBillingCommands(program) {
414
473
  log("");
415
474
  const confirmed = await confirm(
416
475
  "Are you sure you want to cancel your subscription?",
417
- false
476
+ false,
477
+ { field: "confirm_cancel", flag: "--yes" }
418
478
  );
419
479
  if (!confirmed) {
420
480
  log("Cancelled.");
@@ -472,7 +532,12 @@ function registerBillingCommands(program) {
472
532
  log("");
473
533
  const confirmed = await confirm(
474
534
  `Add addon "${addonKey}" \xD7 ${quantity}?`,
475
- false
535
+ false,
536
+ {
537
+ field: "confirm_addon_add",
538
+ flag: "--yes",
539
+ context: { addonKey, quantity }
540
+ }
476
541
  );
477
542
  if (!confirmed) {
478
543
  log("Cancelled.");
@@ -503,7 +568,15 @@ function registerBillingCommands(program) {
503
568
  try {
504
569
  if (!isLoggedIn()) throw new AuthError();
505
570
  if (!shouldSkipConfirmation()) {
506
- const confirmed = await confirm(`Remove addon "${addonKey}"?`, false);
571
+ const confirmed = await confirm(
572
+ `Remove addon "${addonKey}"?`,
573
+ false,
574
+ {
575
+ field: "confirm_addon_remove",
576
+ flag: "--yes",
577
+ context: { addonKey }
578
+ }
579
+ );
507
580
  if (!confirmed) {
508
581
  log("Cancelled.");
509
582
  return;
@@ -584,7 +657,11 @@ function registerBillingCommands(program) {
584
657
  if (!shouldSkipConfirmation()) {
585
658
  log(`
586
659
  Purchase ${quantity}\xD7 ${colors.cyan(addonKey)}?`);
587
- const confirmed = await confirm("Proceed?", false);
660
+ const confirmed = await confirm("Proceed?", false, {
661
+ field: "confirm_addon_buy",
662
+ flag: "--yes",
663
+ context: { addonKey, quantity }
664
+ });
588
665
  if (!confirmed) {
589
666
  log("Cancelled.");
590
667
  return;
@@ -614,7 +691,8 @@ Payment required: ${colors.cyan(result.paymentUrl)}
614
691
  if (!shouldSkipConfirmation()) {
615
692
  const confirmed = await confirm(
616
693
  "Cancel the pending plan change?",
617
- false
694
+ false,
695
+ { field: "confirm_pending_cancel", flag: "--yes" }
618
696
  );
619
697
  if (!confirmed) {
620
698
  log("Cancelled.");
@@ -867,6 +945,29 @@ async function pollCheckoutUntilTerminal(client, orderId, opts) {
867
945
  failureReason: r.failureReason
868
946
  };
869
947
  }
948
+ function collectAddon(value, previous) {
949
+ const [rawKey, rawQty] = value.split(":");
950
+ const addonKey = (rawKey || "").trim();
951
+ if (!addonKey) {
952
+ throw new InvalidArgumentError(
953
+ `Invalid --addon value "${value}". Expected key[:qty] (e.g. db.standard:2).`
954
+ );
955
+ }
956
+ const quantity = rawQty === void 0 ? 1 : Number.parseInt(rawQty, 10);
957
+ if (!Number.isFinite(quantity) || quantity <= 0) {
958
+ throw new InvalidArgumentError(
959
+ `Invalid --addon quantity in "${value}". Expected a positive integer.`
960
+ );
961
+ }
962
+ return [...previous, { addonKey, quantity }];
963
+ }
964
+ function parseBillingPeriod(raw) {
965
+ const v = raw.toLowerCase();
966
+ if (v === "monthly" || v === "yearly") return v;
967
+ throw new InvalidArgumentError(
968
+ `Invalid --billing-period "${raw}". Expected "monthly" or "yearly".`
969
+ );
970
+ }
870
971
  function formatSubStatus(status) {
871
972
  const map = {
872
973
  active: colors.success("active"),
@@ -0,0 +1,127 @@
1
+ import {
2
+ ExitCode,
3
+ exit,
4
+ isJsonMode,
5
+ isNonInteractiveMode,
6
+ outputNeedsInput
7
+ } from "./chunk-KL3JNPAY.js";
8
+
9
+ // src/utils/prompts.ts
10
+ import inquirer from "inquirer";
11
+ function emitNeedsInputAndExit(descriptor, rest) {
12
+ outputNeedsInput({ ...descriptor, ...rest });
13
+ exit(ExitCode.NEEDS_INPUT);
14
+ }
15
+ function shouldEmitNeedsInput() {
16
+ return isJsonMode() || isNonInteractiveMode();
17
+ }
18
+ function emitUnannotatedPromptError(kind, question) {
19
+ outputNeedsInput({
20
+ field: "unannotated_prompt",
21
+ kind,
22
+ question,
23
+ flag: "--yes",
24
+ context: {
25
+ hint: "This prompt site does not yet expose a CLI flag. Re-invoke with --yes (for confirms) or provide the value as a positional/flag argument, or file an issue to ask for explicit flag support for this command."
26
+ }
27
+ });
28
+ exit(ExitCode.NEEDS_INPUT);
29
+ }
30
+ async function confirm(message, defaultValue = false, descriptor) {
31
+ if (descriptor && shouldEmitNeedsInput()) {
32
+ emitNeedsInputAndExit(descriptor, {
33
+ kind: "confirm",
34
+ question: message,
35
+ default: defaultValue
36
+ });
37
+ }
38
+ if (!descriptor && shouldEmitNeedsInput()) {
39
+ emitUnannotatedPromptError("confirm", message);
40
+ }
41
+ const { confirmed } = await inquirer.prompt([
42
+ {
43
+ type: "confirm",
44
+ name: "confirmed",
45
+ message,
46
+ default: defaultValue
47
+ }
48
+ ]);
49
+ return confirmed;
50
+ }
51
+ async function input(message, defaultValue, descriptor) {
52
+ if (descriptor && shouldEmitNeedsInput()) {
53
+ emitNeedsInputAndExit(descriptor, {
54
+ kind: "input",
55
+ question: message,
56
+ default: defaultValue
57
+ });
58
+ }
59
+ if (!descriptor && shouldEmitNeedsInput()) {
60
+ emitUnannotatedPromptError("input", message);
61
+ }
62
+ const { value } = await inquirer.prompt([
63
+ {
64
+ type: "input",
65
+ name: "value",
66
+ message,
67
+ default: defaultValue
68
+ }
69
+ ]);
70
+ return value;
71
+ }
72
+ async function select(message, choices, descriptor) {
73
+ if (descriptor && shouldEmitNeedsInput()) {
74
+ emitNeedsInputAndExit(descriptor, {
75
+ kind: "select",
76
+ question: message,
77
+ choices: choices.map((c) => ({ label: c.name, value: c.value }))
78
+ });
79
+ }
80
+ if (!descriptor && shouldEmitNeedsInput()) {
81
+ emitUnannotatedPromptError("select", message);
82
+ }
83
+ const { value } = await inquirer.prompt([
84
+ {
85
+ type: "list",
86
+ name: "value",
87
+ message,
88
+ choices
89
+ }
90
+ ]);
91
+ return value;
92
+ }
93
+ async function password(message, descriptor) {
94
+ if (descriptor && shouldEmitNeedsInput()) {
95
+ emitNeedsInputAndExit(
96
+ { ...descriptor, sensitive: true },
97
+ { kind: "password", question: message }
98
+ );
99
+ }
100
+ if (!descriptor && shouldEmitNeedsInput()) {
101
+ emitUnannotatedPromptError("password", message);
102
+ }
103
+ const { value } = await inquirer.prompt([
104
+ {
105
+ type: "password",
106
+ name: "value",
107
+ message,
108
+ mask: "*"
109
+ }
110
+ ]);
111
+ return value;
112
+ }
113
+ async function promptOrEmit(req, fallback) {
114
+ if (shouldEmitNeedsInput()) {
115
+ outputNeedsInput(req);
116
+ exit(ExitCode.NEEDS_INPUT);
117
+ }
118
+ return fallback();
119
+ }
120
+
121
+ export {
122
+ confirm,
123
+ input,
124
+ select,
125
+ password,
126
+ promptOrEmit
127
+ };
@@ -43,7 +43,7 @@ function jsonError(code, message, suggestions, details) {
43
43
  };
44
44
  }
45
45
  function outputJson(response) {
46
- console.log(JSON.stringify(response, null, 2));
46
+ console.log(JSON.stringify(response));
47
47
  }
48
48
 
49
49
  // src/lib/output.ts
@@ -52,7 +52,8 @@ var globalOptions = {
52
52
  quiet: false,
53
53
  verbose: false,
54
54
  noColor: false,
55
- yes: false
55
+ yes: false,
56
+ nonInteractive: false
56
57
  };
57
58
  function setGlobalOptions(options) {
58
59
  globalOptions = { ...globalOptions, ...options };
@@ -63,6 +64,9 @@ function isJsonMode() {
63
64
  function shouldSkipConfirmation() {
64
65
  return globalOptions.yes;
65
66
  }
67
+ function isNonInteractiveMode() {
68
+ return globalOptions.nonInteractive;
69
+ }
66
70
  function c(colorFn, str) {
67
71
  return globalOptions.noColor ? str : colorFn(str);
68
72
  }
@@ -183,6 +187,7 @@ export {
183
187
  setGlobalOptions,
184
188
  isJsonMode,
185
189
  shouldSkipConfirmation,
190
+ isNonInteractiveMode,
186
191
  colors,
187
192
  getStatusBadge,
188
193
  log,