@tarout/cli 0.2.2 → 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/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  stopSpinner,
7
7
  succeedSpinner,
8
8
  updateSpinner
9
- } from "./chunk-5XBVQICT.js";
9
+ } from "./chunk-BS6DFVSU.js";
10
10
  import {
11
11
  AuthError,
12
12
  BuildFailedError,
@@ -16,24 +16,26 @@ import {
16
16
  InvalidArgumentError,
17
17
  NotFoundError,
18
18
  analyzeDeploymentError,
19
- clearConfig,
20
19
  findSimilar,
21
20
  getApiClient,
21
+ handleError,
22
+ platformFetch,
23
+ resetApiClient
24
+ } from "./chunk-NHNK5ZQ5.js";
25
+ import {
26
+ clearConfig,
22
27
  getApiUrl,
23
28
  getCurrentProfile,
24
29
  getProjectConfig,
25
30
  getToken,
26
- handleError,
27
31
  isLoggedIn,
28
32
  isProjectLinked,
29
- platformFetch,
30
33
  removeProjectConfig,
31
- resetApiClient,
32
34
  setCurrentProfile,
33
35
  setProfile,
34
36
  setProjectConfig,
35
37
  updateProfile
36
- } from "./chunk-7YS2WBLB.js";
38
+ } from "./chunk-5DAFGMBH.js";
37
39
  import {
38
40
  confirm,
39
41
  input,
@@ -67,11 +69,12 @@ import { Command } from "commander";
67
69
  // package.json
68
70
  var package_default = {
69
71
  name: "@tarout/cli",
70
- version: "0.2.2",
72
+ version: "0.3.0",
71
73
  description: "Tarout CLI \u2014 the Saudi cloud platform for coding agents",
72
74
  type: "module",
73
75
  bin: {
74
- tarout: "./bin/tarout"
76
+ tarout: "./bin/tarout",
77
+ "tarout-mcp": "./bin/tarout-mcp"
75
78
  },
76
79
  main: "./dist/index.js",
77
80
  types: "./dist/index.d.ts",
@@ -80,14 +83,15 @@ var package_default = {
80
83
  "dist"
81
84
  ],
82
85
  scripts: {
83
- build: "tsup src/index.ts --format esm --dts --clean",
84
- dev: "tsup src/index.ts --format esm --watch",
86
+ build: "tsup src/index.ts src/mcp/stdio.ts --format esm --dts --clean",
87
+ dev: "tsup src/index.ts src/mcp/stdio.ts --format esm --watch",
85
88
  typecheck: "tsc --noEmit",
86
89
  lint: "biome check src/",
87
90
  start: "node dist/index.js",
88
91
  prepublishOnly: "bun run build"
89
92
  },
90
93
  dependencies: {
94
+ "@modelcontextprotocol/sdk": "^1.29.0",
91
95
  "@trpc/client": "^10.45.2",
92
96
  "@trpc/server": "^10.45.2",
93
97
  chalk: "^5.3.0",
@@ -130,6 +134,83 @@ var package_default = {
130
134
  license: "MIT"
131
135
  };
132
136
 
137
+ // src/lib/auth-profile.ts
138
+ import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
139
+ import superjson from "superjson";
140
+ function createCredentialClient(apiUrl, token) {
141
+ return createTRPCProxyClient({
142
+ transformer: superjson,
143
+ links: [
144
+ httpBatchLink({
145
+ url: `${apiUrl.replace(/\/+$/, "")}/api/trpc`,
146
+ headers: () => ({ "x-api-key": token }),
147
+ fetch: platformFetch
148
+ })
149
+ ]
150
+ });
151
+ }
152
+ function pickUser(memberOrUser) {
153
+ return memberOrUser?.user ?? memberOrUser;
154
+ }
155
+ function pickEnvironmentName(environment, fallback) {
156
+ return environment?.displayName || environment?.name || environment?.slug || fallback || "production";
157
+ }
158
+ async function queryOrNull(query) {
159
+ try {
160
+ return await query();
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+ async function resolveProfileFromCredential(params) {
166
+ const apiUrl = params.apiUrl.replace(/\/+$/, "");
167
+ const client = createCredentialClient(apiUrl, params.token);
168
+ const member = await client.user.get.query();
169
+ if (!member) {
170
+ throw new Error(
171
+ "The credential is valid, but it is not scoped to an organization."
172
+ );
173
+ }
174
+ const user = pickUser(member);
175
+ const organizations = await client.organization.all.query();
176
+ const [activeProject, activeEnvironment] = await Promise.all([
177
+ queryOrNull(() => client.project.getActive.query()),
178
+ queryOrNull(() => client.environment.getActive.query())
179
+ ]);
180
+ const project = activeProject;
181
+ const environment = activeEnvironment;
182
+ const organizationId = member.organizationId || params.fallback?.organizationId || organizations?.[0]?.id;
183
+ const organization = organizations?.find((org) => org.id === organizationId) || organizations?.[0];
184
+ if (!organizationId || !organization) {
185
+ throw new Error("The credential is valid, but no organization was found.");
186
+ }
187
+ return {
188
+ token: params.token,
189
+ apiUrl,
190
+ userId: user?.id || member.userId || params.fallback?.userId || "",
191
+ userEmail: user?.email || member.email || params.fallback?.userEmail || "unknown",
192
+ userName: user?.name || member.name || params.fallback?.userName,
193
+ organizationId,
194
+ organizationName: organization.name || params.fallback?.organizationName || "Unknown",
195
+ projectId: project?.projectId || params.fallback?.projectId,
196
+ projectName: project?.name || params.fallback?.projectName,
197
+ projectSlug: project?.slug || params.fallback?.projectSlug,
198
+ environmentId: environment?.environmentId || params.fallback?.environmentId || "",
199
+ environmentName: pickEnvironmentName(
200
+ environment,
201
+ params.fallback?.environmentName
202
+ )
203
+ };
204
+ }
205
+ function isCredentialError(error2) {
206
+ const err = error2;
207
+ const code = err?.data?.code || err?.shape?.data?.code || err?.code;
208
+ const message = error2 instanceof Error ? error2.message : String(error2);
209
+ return code === "UNAUTHORIZED" || code === "FORBIDDEN" || /unauthorized|forbidden|invalid api key|invalid token|not authenticated|not logged in/i.test(
210
+ message
211
+ );
212
+ }
213
+
133
214
  // src/commands/account.ts
134
215
  function registerAccountCommands(program2) {
135
216
  const account = program2.command("account").description("Manage your user account");
@@ -268,6 +349,19 @@ function registerAccountCommands(program2) {
268
349
  flag: "--name"
269
350
  });
270
351
  const client = getApiClient();
352
+ const profile = getCurrentProfile();
353
+ let organizationId = profile?.organizationId;
354
+ if (!organizationId) {
355
+ const resolved = await resolveProfileFromCredential({
356
+ apiUrl: getApiUrl(),
357
+ token: getToken() || "",
358
+ fallback: profile
359
+ });
360
+ organizationId = resolved.organizationId;
361
+ }
362
+ if (!organizationId) {
363
+ throw new AuthError();
364
+ }
271
365
  const _spinner = startSpinner("Creating API key...");
272
366
  const result = await client.user.createApiKey.mutate({
273
367
  name,
@@ -277,7 +371,8 @@ function registerAccountCommands(program2) {
277
371
  rateLimitTimeWindow: Number.parseInt(
278
372
  options.rateLimitWindow || "60000"
279
373
  ),
280
- rateLimitMax: Number.parseInt(options.rateLimitMax || "100")
374
+ rateLimitMax: Number.parseInt(options.rateLimitMax || "100"),
375
+ metadata: { organizationId }
281
376
  });
282
377
  succeedSpinner("API key created.");
283
378
  if (isJsonMode()) {
@@ -327,26 +422,6 @@ function registerAccountCommands(program2) {
327
422
  handleError(err);
328
423
  }
329
424
  });
330
- account.command("generate-token").description("Generate a one-time metrics/auth token").action(async () => {
331
- try {
332
- if (!isLoggedIn()) throw new AuthError();
333
- const client = getApiClient();
334
- const _spinner = startSpinner("Generating token...");
335
- const result = await client.user.generateToken.mutate();
336
- succeedSpinner();
337
- if (isJsonMode()) {
338
- outputData(result);
339
- return;
340
- }
341
- const r = result;
342
- log("");
343
- log(`Token: ${colors.cyan(r.token || String(result))}`);
344
- log("");
345
- } catch (err) {
346
- failSpinner();
347
- handleError(err);
348
- }
349
- });
350
425
  account.command("metrics-token").description("Get the organization metrics token").action(async () => {
351
426
  try {
352
427
  if (!isLoggedIn()) throw new AuthError();
@@ -1164,6 +1239,14 @@ function formatDate(date) {
1164
1239
 
1165
1240
  // src/commands/apps.ts
1166
1241
  import open from "open";
1242
+
1243
+ // src/utils/url.ts
1244
+ function formatAppUrl(host) {
1245
+ if (!host) return null;
1246
+ return /^https?:\/\//i.test(host) ? host : `https://${host}`;
1247
+ }
1248
+
1249
+ // src/commands/apps.ts
1167
1250
  function registerAppsCommands(program2) {
1168
1251
  const apps = program2.command("apps").description("Manage applications");
1169
1252
  apps.command("list").alias("ls").description("List all applications").action(async () => {
@@ -1367,7 +1450,7 @@ function registerAppsCommands(program2) {
1367
1450
  log(` ${colors.dim("No custom domain")}`);
1368
1451
  }
1369
1452
  log("");
1370
- const appUrl = app.appSubdomain ? `https://${app.appSubdomain}` : null;
1453
+ const appUrl = formatAppUrl(app.appSubdomain);
1371
1454
  if (appUrl) {
1372
1455
  log(`${colors.bold("URL")}`);
1373
1456
  log(` ${appUrl}`);
@@ -2172,9 +2255,9 @@ function registerAppsCommands(program2) {
2172
2255
  succeedSpinner();
2173
2256
  let url = null;
2174
2257
  if (app.domain && app.domain.length > 0) {
2175
- url = `https://${app.domain[0].host}`;
2258
+ url = formatAppUrl(app.domain[0].host) ?? "";
2176
2259
  } else if (app.appSubdomain) {
2177
- url = `https://${app.appSubdomain}`;
2260
+ url = formatAppUrl(app.appSubdomain) ?? "";
2178
2261
  }
2179
2262
  if (!url) {
2180
2263
  error(
@@ -2777,7 +2860,7 @@ function escapeHtml(value) {
2777
2860
  return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2778
2861
  }
2779
2862
  function startAuthServer(options = {}) {
2780
- return new Promise((resolve2) => {
2863
+ return new Promise((resolve3) => {
2781
2864
  const app = express();
2782
2865
  const expectedState = options.state ?? createAuthState();
2783
2866
  let server;
@@ -2832,10 +2915,19 @@ function startAuthServer(options = {}) {
2832
2915
  callbackRejecter(new Error(String(error2)));
2833
2916
  return;
2834
2917
  }
2835
- if (!token || !userId || !userEmail || !organizationId || !organizationName || !environmentId || !environmentName) {
2836
- res.status(400).send("Missing required parameters");
2918
+ const missing = [];
2919
+ if (!token) missing.push("token");
2920
+ if (!userId) missing.push("userId");
2921
+ if (!userEmail) missing.push("userEmail");
2922
+ if (!organizationId) missing.push("organizationId");
2923
+ if (!organizationName) missing.push("organizationName");
2924
+ if (!environmentId) missing.push("environmentId");
2925
+ if (!environmentName) missing.push("environmentName");
2926
+ if (missing.length > 0) {
2927
+ const list = missing.join(", ");
2928
+ res.status(400).send(`Missing required parameters: ${list}`);
2837
2929
  callbackRejecter(
2838
- new Error("Missing required parameters from auth callback")
2930
+ new Error(`Missing required parameters from auth callback: ${list}`)
2839
2931
  );
2840
2932
  return;
2841
2933
  }
@@ -2892,7 +2984,7 @@ function startAuthServer(options = {}) {
2892
2984
  server = app.listen(0, "127.0.0.1", () => {
2893
2985
  const address = server.address();
2894
2986
  const port = typeof address === "object" && address ? address.port : 0;
2895
- resolve2({
2987
+ resolve3({
2896
2988
  port,
2897
2989
  state: expectedState,
2898
2990
  waitForCallback: () => callbackPromise,
@@ -2902,86 +2994,9 @@ function startAuthServer(options = {}) {
2902
2994
  });
2903
2995
  }
2904
2996
 
2905
- // src/lib/auth-profile.ts
2906
- import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
2907
- import superjson from "superjson";
2908
- function createCredentialClient(apiUrl, token) {
2909
- return createTRPCProxyClient({
2910
- transformer: superjson,
2911
- links: [
2912
- httpBatchLink({
2913
- url: `${apiUrl.replace(/\/+$/, "")}/api/trpc`,
2914
- headers: () => ({ "x-api-key": token }),
2915
- fetch: platformFetch
2916
- })
2917
- ]
2918
- });
2919
- }
2920
- function pickUser(memberOrUser) {
2921
- return memberOrUser?.user ?? memberOrUser;
2922
- }
2923
- function pickEnvironmentName(environment, fallback) {
2924
- return environment?.displayName || environment?.name || environment?.slug || fallback || "production";
2925
- }
2926
- async function queryOrNull(query) {
2927
- try {
2928
- return await query();
2929
- } catch {
2930
- return null;
2931
- }
2932
- }
2933
- async function resolveProfileFromCredential(params) {
2934
- const apiUrl = params.apiUrl.replace(/\/+$/, "");
2935
- const client = createCredentialClient(apiUrl, params.token);
2936
- const member = await client.user.get.query();
2937
- if (!member) {
2938
- throw new Error(
2939
- "The credential is valid, but it is not scoped to an organization."
2940
- );
2941
- }
2942
- const user = pickUser(member);
2943
- const organizations = await client.organization.all.query();
2944
- const [activeProject, activeEnvironment] = await Promise.all([
2945
- queryOrNull(() => client.project.getActive.query()),
2946
- queryOrNull(() => client.environment.getActive.query())
2947
- ]);
2948
- const project = activeProject;
2949
- const environment = activeEnvironment;
2950
- const organizationId = member.organizationId || params.fallback?.organizationId || organizations?.[0]?.id;
2951
- const organization = organizations?.find((org) => org.id === organizationId) || organizations?.[0];
2952
- if (!organizationId || !organization) {
2953
- throw new Error("The credential is valid, but no organization was found.");
2954
- }
2955
- return {
2956
- token: params.token,
2957
- apiUrl,
2958
- userId: user?.id || member.userId || params.fallback?.userId || "",
2959
- userEmail: user?.email || member.email || params.fallback?.userEmail || "unknown",
2960
- userName: user?.name || member.name || params.fallback?.userName,
2961
- organizationId,
2962
- organizationName: organization.name || params.fallback?.organizationName || "Unknown",
2963
- projectId: project?.projectId || params.fallback?.projectId,
2964
- projectName: project?.name || params.fallback?.projectName,
2965
- projectSlug: project?.slug || params.fallback?.projectSlug,
2966
- environmentId: environment?.environmentId || params.fallback?.environmentId || "",
2967
- environmentName: pickEnvironmentName(
2968
- environment,
2969
- params.fallback?.environmentName
2970
- )
2971
- };
2972
- }
2973
- function isCredentialError(error2) {
2974
- const err = error2;
2975
- const code = err?.data?.code || err?.shape?.data?.code || err?.code;
2976
- const message = error2 instanceof Error ? error2.message : String(error2);
2977
- return code === "UNAUTHORIZED" || code === "FORBIDDEN" || /unauthorized|forbidden|invalid api key|invalid token|not authenticated|not logged in/i.test(
2978
- message
2979
- );
2980
- }
2981
-
2982
2997
  // src/commands/auth.ts
2983
2998
  function refuseBrowserAuthForAgent(action) {
2984
- const message = `tarout ${action} requires a browser. Agents should ask the user to run \`tarout token <api-token>\` or set TAROUT_TOKEN \u2014 generate one at https://tarout.sa/dashboard/account/api-keys.`;
2999
+ const message = `tarout ${action} requires a browser. Agents should ask the user to run \`tarout token <api-token>\` or set TAROUT_TOKEN \u2014 generate one at https://tarout.sa/dashboard/settings/profile.`;
2985
3000
  if (isJsonMode()) {
2986
3001
  outputError("AUTH_BOOTSTRAP_REQUIRED", message, {
2987
3002
  action,
@@ -3250,16 +3265,34 @@ function registerAuthCommands(program2) {
3250
3265
  () => input("Token name (e.g., ci-deploy):")
3251
3266
  );
3252
3267
  }
3253
- const { getApiClient: getApiClient2 } = await import("./api-735LN7BA.js");
3268
+ const { getApiClient: getApiClient2 } = await import("./api-QAKANRFX.js");
3254
3269
  const client = getApiClient2();
3255
3270
  const profile = getCurrentProfile();
3271
+ let keyOrg = profile?.organizationId;
3272
+ let keyEnv = profile?.environmentId;
3273
+ let keyProj = profile?.projectId;
3274
+ if (!keyOrg) {
3275
+ const resolved = await resolveProfileFromCredential({
3276
+ apiUrl: getApiUrl(),
3277
+ token: getToken() || "",
3278
+ fallback: profile
3279
+ });
3280
+ keyOrg = resolved.organizationId;
3281
+ keyEnv = resolved.environmentId;
3282
+ keyProj = resolved.projectId;
3283
+ }
3284
+ if (!keyOrg) {
3285
+ throw new Error(
3286
+ "Could not determine your organization. Run `tarout auth login` first, or pass a token scoped to an organization."
3287
+ );
3288
+ }
3256
3289
  const _spinner = startSpinner("Creating API token...");
3257
3290
  const result = await client.user.createApiKey.mutate({
3258
3291
  name: tokenName,
3259
3292
  metadata: {
3260
- organizationId: profile?.organizationId || "",
3261
- environmentId: profile?.environmentId || "",
3262
- projectId: profile?.projectId || ""
3293
+ organizationId: keyOrg,
3294
+ environmentId: keyEnv || "",
3295
+ projectId: keyProj || ""
3263
3296
  }
3264
3297
  });
3265
3298
  succeedSpinner("API token created!");
@@ -3839,7 +3872,7 @@ function getDefaultPort(pkg) {
3839
3872
  return framework?.defaultPort || 3e3;
3840
3873
  }
3841
3874
  function runCommand(command, env, options = {}) {
3842
- return new Promise((resolve2) => {
3875
+ return new Promise((resolve3) => {
3843
3876
  const [cmd, ...args] = parseCommand(command);
3844
3877
  const spawnOptions = {
3845
3878
  cwd: options.cwd || process.cwd(),
@@ -3874,7 +3907,7 @@ function runCommand(command, env, options = {}) {
3874
3907
  child.on("close", (code, signal) => {
3875
3908
  process.off("SIGINT", handleSignal);
3876
3909
  process.off("SIGTERM", handleSignal);
3877
- resolve2({
3910
+ resolve3({
3878
3911
  exitCode: code ?? 1,
3879
3912
  signal
3880
3913
  });
@@ -3883,7 +3916,7 @@ function runCommand(command, env, options = {}) {
3883
3916
  process.off("SIGINT", handleSignal);
3884
3917
  process.off("SIGTERM", handleSignal);
3885
3918
  console.error(`Failed to start process: ${err.message}`);
3886
- resolve2({
3919
+ resolve3({
3887
3920
  exitCode: 1,
3888
3921
  signal: null
3889
3922
  });
@@ -4071,6 +4104,124 @@ function findApp2(apps, identifier) {
4071
4104
  );
4072
4105
  }
4073
4106
 
4107
+ // src/commands/call.ts
4108
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
4109
+ import { homedir } from "os";
4110
+ import { join as join2 } from "path";
4111
+ var MANIFEST_TTL_MS = 5 * 60 * 1e3;
4112
+ function manifestCachePath() {
4113
+ return join2(homedir(), ".tarout", "surface-manifest-cache.json");
4114
+ }
4115
+ async function fetchManifestFresh(client, apiUrl) {
4116
+ const manifest = await client.settings.getSurfaceManifest.query();
4117
+ try {
4118
+ const dir = join2(homedir(), ".tarout");
4119
+ if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
4120
+ let cache = {};
4121
+ try {
4122
+ cache = JSON.parse(readFileSync2(manifestCachePath(), "utf8"));
4123
+ } catch {
4124
+ }
4125
+ cache[apiUrl] = { at: Date.now(), manifest };
4126
+ writeFileSync(manifestCachePath(), JSON.stringify(cache), { mode: 384 });
4127
+ } catch {
4128
+ }
4129
+ return manifest;
4130
+ }
4131
+ async function loadManifest(client, apiUrl) {
4132
+ try {
4133
+ const cache = JSON.parse(readFileSync2(manifestCachePath(), "utf8"));
4134
+ const entry = cache[apiUrl];
4135
+ if (entry && Date.now() - entry.at < MANIFEST_TTL_MS && Array.isArray(entry.manifest)) {
4136
+ return entry.manifest;
4137
+ }
4138
+ } catch {
4139
+ }
4140
+ return fetchManifestFresh(client, apiUrl);
4141
+ }
4142
+ function registerCallCommand(program2) {
4143
+ program2.command("call [procedure]").description(
4144
+ "Call any platform API procedure directly (e.g. application.create). Use --list to discover."
4145
+ ).option("-i, --input <json>", "JSON input for the procedure", "{}").option("--input-file <path>", "Read JSON input from a file").option(
4146
+ "-l, --list [filter]",
4147
+ "List callable procedures (optionally filtered by substring)"
4148
+ ).action(async (procedure, opts) => {
4149
+ try {
4150
+ if (!isLoggedIn()) throw new AuthError();
4151
+ const client = getApiClient();
4152
+ if (opts.list !== void 0 || !procedure) {
4153
+ startSpinner("Loading control surface...");
4154
+ const manifest2 = await fetchManifestFresh(client, getApiUrl());
4155
+ succeedSpinner();
4156
+ const filter = typeof opts.list === "string" ? opts.list : void 0;
4157
+ const matched = manifest2.filter(
4158
+ (m) => !filter || m.path.includes(filter)
4159
+ );
4160
+ if (isJsonMode()) {
4161
+ outputData(matched);
4162
+ return;
4163
+ }
4164
+ if (!procedure && opts.list === void 0) {
4165
+ log(
4166
+ colors.dim(
4167
+ "No procedure given. Available procedures (call one with `tarout call <procedure> --input '{...}'`):"
4168
+ )
4169
+ );
4170
+ log("");
4171
+ }
4172
+ table(
4173
+ ["Procedure", "Type"],
4174
+ matched.map((m) => [m.path, m.type])
4175
+ );
4176
+ log("");
4177
+ log(colors.dim(`${matched.length} procedures`));
4178
+ return;
4179
+ }
4180
+ const apiUrl = getApiUrl();
4181
+ let manifest = await loadManifest(client, apiUrl);
4182
+ let entry = manifest.find((m) => m.path === procedure);
4183
+ if (!entry) {
4184
+ manifest = await fetchManifestFresh(client, apiUrl);
4185
+ entry = manifest.find((m) => m.path === procedure);
4186
+ }
4187
+ if (!entry) {
4188
+ throw new Error(
4189
+ `Unknown or non-exposed procedure: "${procedure}". Run \`tarout call --list\` to see what's available.`
4190
+ );
4191
+ }
4192
+ let input2 = {};
4193
+ const rawInput = opts.inputFile ? readFileSync2(opts.inputFile, "utf8") : opts.input;
4194
+ if (rawInput && rawInput.trim()) {
4195
+ try {
4196
+ input2 = JSON.parse(rawInput);
4197
+ } catch {
4198
+ throw new Error(
4199
+ `--input must be valid JSON. Received: ${rawInput}`
4200
+ );
4201
+ }
4202
+ }
4203
+ const [routerKey, procKey] = procedure.split(".");
4204
+ const node = client[routerKey ?? ""]?.[procKey ?? ""];
4205
+ if (!node) {
4206
+ throw new Error(`Procedure path not found on client: ${procedure}`);
4207
+ }
4208
+ startSpinner(`Calling ${procedure}...`);
4209
+ const result = entry.type === "mutation" ? await node.mutate(input2) : await node.query(input2);
4210
+ succeedSpinner();
4211
+ if (isJsonMode()) {
4212
+ outputData(result);
4213
+ } else {
4214
+ log(
4215
+ typeof result === "string" ? result : JSON.stringify(result, null, 2)
4216
+ );
4217
+ }
4218
+ } catch (err) {
4219
+ failSpinner();
4220
+ handleError(err);
4221
+ }
4222
+ });
4223
+ }
4224
+
4074
4225
  // src/commands/dashboard.ts
4075
4226
  function registerDashboardCommands(program2) {
4076
4227
  const dashboard = program2.command("dashboard").description("View organization dashboard overview");
@@ -4294,16 +4445,18 @@ function registerDbCommands(program2) {
4294
4445
  outputData(database);
4295
4446
  return;
4296
4447
  }
4297
- quietOutput(dbId);
4448
+ if (dbId) quietOutput(dbId);
4298
4449
  box("Database Created", [
4299
- `ID: ${colors.cyan(dbId)}`,
4300
- `Name: ${database.name}`,
4450
+ `ID: ${colors.cyan(dbId ?? "(pending)")}`,
4451
+ `Name: ${database.name ?? dbName}`,
4301
4452
  `Type: ${getTypeLabel(dbType)}`
4302
4453
  ]);
4303
4454
  log("Next steps:");
4304
- log(
4305
- ` View connection info: ${colors.dim(`tarout db info ${dbId.slice(0, 8)}`)}`
4306
- );
4455
+ if (dbId) {
4456
+ log(
4457
+ ` View connection info: ${colors.dim(`tarout db info ${dbId.slice(0, 8)}`)}`
4458
+ );
4459
+ }
4307
4460
  log("");
4308
4461
  } catch (err) {
4309
4462
  handleError(err);
@@ -4566,7 +4719,7 @@ function registerDbCommands(program2) {
4566
4719
  backups.map((b) => [
4567
4720
  colors.cyan((b.backupId || b.id || "").slice(0, 8)),
4568
4721
  b.schedule || colors.dim("-"),
4569
- b.enabled ? colors.green("yes") : colors.dim("no")
4722
+ b.enabled ? colors.success("yes") : colors.dim("no")
4570
4723
  ])
4571
4724
  );
4572
4725
  log("");
@@ -5149,15 +5302,15 @@ function getConnectCommand(type, details) {
5149
5302
  import { execFile } from "child_process";
5150
5303
  import {
5151
5304
  createReadStream,
5152
- existsSync as existsSync2,
5305
+ existsSync as existsSync3,
5153
5306
  mkdtempSync,
5154
5307
  readdirSync,
5155
- readFileSync as readFileSync2,
5308
+ readFileSync as readFileSync3,
5156
5309
  rmSync,
5157
5310
  statSync
5158
5311
  } from "fs";
5159
5312
  import { tmpdir } from "os";
5160
- import { basename, dirname, join as join2 } from "path";
5313
+ import { basename, dirname, join as join3 } from "path";
5161
5314
  import { promisify } from "util";
5162
5315
  import open3 from "open";
5163
5316
 
@@ -5174,13 +5327,13 @@ function streamDeploymentLogs(deploymentId, options) {
5174
5327
  }
5175
5328
  });
5176
5329
  let buffer = "";
5177
- const done = new Promise((resolve2) => {
5330
+ const done = new Promise((resolve3) => {
5178
5331
  ws.on("close", (code, reason) => {
5179
5332
  if (buffer.length > 0) {
5180
5333
  options.onData(buffer);
5181
5334
  }
5182
5335
  options.onClose?.(code, reason.toString());
5183
- resolve2({ code, reason: reason.toString() });
5336
+ resolve3({ code, reason: reason.toString() });
5184
5337
  });
5185
5338
  });
5186
5339
  ws.on("open", () => {
@@ -5226,13 +5379,13 @@ async function ensureAuthenticatedForDeploy(options) {
5226
5379
  {
5227
5380
  field: "token",
5228
5381
  kind: "password",
5229
- question: `Paste a Tarout API token. Generate one at ${apiUrl}/dashboard/account/tokens`,
5382
+ question: `Paste a Tarout API token. Generate one at ${apiUrl}/dashboard/settings/profile`,
5230
5383
  flag: "--token",
5231
5384
  sensitive: true,
5232
5385
  context: {
5233
5386
  step: "auth",
5234
5387
  reason: "not_logged_in",
5235
- generateUrl: `${apiUrl}/dashboard/account/tokens`
5388
+ generateUrl: `${apiUrl}/dashboard/settings/profile`
5236
5389
  }
5237
5390
  },
5238
5391
  // Unreachable: promptOrEmit exits the process in JSON mode.
@@ -5268,13 +5421,13 @@ async function ensureAuthenticatedForDeploy(options) {
5268
5421
  {
5269
5422
  field: "token",
5270
5423
  kind: "password",
5271
- question: `Stored credentials are invalid or expired. Paste a new Tarout API token from ${apiUrl}/dashboard/account/tokens`,
5424
+ question: `Stored credentials are invalid or expired. Paste a new Tarout API token from ${apiUrl}/dashboard/settings/profile`,
5272
5425
  flag: "--token",
5273
5426
  sensitive: true,
5274
5427
  context: {
5275
5428
  step: "auth",
5276
5429
  reason: "expired_credentials",
5277
- generateUrl: `${apiUrl}/dashboard/account/tokens`
5430
+ generateUrl: `${apiUrl}/dashboard/settings/profile`
5278
5431
  }
5279
5432
  },
5280
5433
  async () => {
@@ -5439,7 +5592,7 @@ async function confirmRegion(region) {
5439
5592
  }
5440
5593
  }
5441
5594
  function inspectCurrentProject(cwd = process.cwd()) {
5442
- const packageJson = readJsonFile(join2(cwd, "package.json"));
5595
+ const packageJson = readJsonFile(join3(cwd, "package.json"));
5443
5596
  const dependencies = new Set(
5444
5597
  Object.keys({
5445
5598
  ...packageJson?.dependencies ?? {},
@@ -5487,8 +5640,8 @@ function printProjectInspection(inspection) {
5487
5640
  }
5488
5641
  function readJsonFile(path) {
5489
5642
  try {
5490
- if (!existsSync2(path)) return null;
5491
- return JSON.parse(readFileSync2(path, "utf8"));
5643
+ if (!existsSync3(path)) return null;
5644
+ return JSON.parse(readFileSync3(path, "utf8"));
5492
5645
  } catch {
5493
5646
  return null;
5494
5647
  }
@@ -5497,7 +5650,7 @@ function readTextFile(path, maxBytes) {
5497
5650
  try {
5498
5651
  const stat = statSync(path);
5499
5652
  if (stat.size > maxBytes) return "";
5500
- return readFileSync2(path, "utf8");
5653
+ return readFileSync3(path, "utf8");
5501
5654
  } catch {
5502
5655
  return "";
5503
5656
  }
@@ -5552,7 +5705,7 @@ function collectInspectableFiles(root) {
5552
5705
  }
5553
5706
  for (const entry of entries) {
5554
5707
  if (files.length >= 400) return;
5555
- const fullPath = join2(dir, entry.name);
5708
+ const fullPath = join3(dir, entry.name);
5556
5709
  if (entry.isDirectory()) {
5557
5710
  if (!excludedDirs.has(entry.name)) walk(fullPath, depth + 1);
5558
5711
  continue;
@@ -5604,7 +5757,7 @@ function detectDatabase(dependencies, files, cwd) {
5604
5757
  }
5605
5758
  }
5606
5759
  const prismaSchema = readTextFile(
5607
- join2(cwd, "prisma", "schema.prisma"),
5760
+ join3(cwd, "prisma", "schema.prisma"),
5608
5761
  256 * 1024
5609
5762
  );
5610
5763
  if (/provider\s*=\s*"postgresql"/i.test(prismaSchema)) {
@@ -5685,15 +5838,15 @@ function detectStorage(dependencies, files) {
5685
5838
  return { detected: reasons.length > 0, reasons: uniqueReasons(reasons) };
5686
5839
  }
5687
5840
  function detectGitSource(cwd) {
5688
- const gitPath = join2(cwd, ".git");
5689
- if (!existsSync2(gitPath)) return { hasGit: false };
5690
- let configPath = join2(gitPath, "config");
5841
+ const gitPath = join3(cwd, ".git");
5842
+ if (!existsSync3(gitPath)) return { hasGit: false };
5843
+ let configPath = join3(gitPath, "config");
5691
5844
  try {
5692
5845
  const stat = statSync(gitPath);
5693
5846
  if (stat.isFile()) {
5694
5847
  const content = readTextFile(gitPath, 4096);
5695
5848
  const match = content.match(/gitdir:\s*(.+)/i);
5696
- if (match?.[1]) configPath = join2(cwd, match[1].trim(), "config");
5849
+ if (match?.[1]) configPath = join3(cwd, match[1].trim(), "config");
5697
5850
  }
5698
5851
  } catch {
5699
5852
  }
@@ -5822,7 +5975,7 @@ async function createAppFromCurrentDirectory(client, profile, options = {}) {
5822
5975
  const defaultName = basename(process.cwd()) || "tarout-app";
5823
5976
  const providedName = options.name?.trim();
5824
5977
  let appName = providedName || defaultName;
5825
- if (!providedName && !shouldSkipConfirmation()) {
5978
+ if (!providedName && !shouldSkipConfirmation() && !isJsonMode()) {
5826
5979
  appName = await promptOrEmit(
5827
5980
  {
5828
5981
  field: "name",
@@ -5940,7 +6093,7 @@ function formatPlanPrice(priceHalalas) {
5940
6093
  return `${(priceHalalas / 100).toFixed(2)} SAR/mo`;
5941
6094
  }
5942
6095
  async function runInlineUpgrade(client, planKey) {
5943
- const { pollCheckoutUntilTerminal } = await import("./billing-WOKNOS4N.js");
6096
+ const { pollCheckoutUntilTerminal } = await import("./billing-GUA4S2Y4.js");
5944
6097
  const _changing = startSpinner(`Switching to plan "${planKey}"...`);
5945
6098
  const result = await client.subscription.changePlan.mutate({ planKey });
5946
6099
  if (result?.applied) {
@@ -6229,31 +6382,48 @@ async function configureOptionalResources(client, profile, app, options, inspect
6229
6382
  const database = await resolveDatabaseChoice(options, inspection);
6230
6383
  const createdResources = [];
6231
6384
  if (database !== "none") {
6232
- const plan = await resolveResourcePlan(
6233
- options.databasePlan,
6234
- "Database plan:"
6235
- );
6236
- const databaseName = formatResourceName(
6237
- app.name,
6238
- database === "postgres" ? "Postgres" : "MySQL"
6385
+ const decision = await resolveDatabaseProvisioning(
6386
+ client,
6387
+ profile,
6388
+ app,
6389
+ database,
6390
+ options
6239
6391
  );
6240
- await createAndAttachDatabase(client, profile, app, {
6241
- kind: database,
6242
- name: databaseName,
6243
- plan
6244
- });
6245
- createdResources.push(
6246
- `${database === "postgres" ? "Postgres" : "MySQL"} (${plan})`
6247
- );
6248
- }
6249
- if (await resolveStorageChoice(options, inspection)) {
6250
- const plan = await resolveResourcePlan(options.storagePlan, "Storage plan:");
6251
- const storageName = formatResourceName(app.name, "files", 50);
6252
- await createAndAttachStorage(client, app, {
6253
- name: storageName,
6254
- plan
6255
- });
6256
- createdResources.push(`Storage (${plan})`);
6392
+ if (decision.action === "reuse") {
6393
+ await attachExistingDatabase(client, app, database, decision);
6394
+ createdResources.push(
6395
+ `${formatDatabaseKind(database)} reused \u2014 ${decision.name} (${decision.plan})`
6396
+ );
6397
+ } else {
6398
+ await createAndAttachDatabase(client, profile, app, {
6399
+ kind: database,
6400
+ name: decision.name,
6401
+ plan: decision.plan
6402
+ });
6403
+ createdResources.push(
6404
+ `${formatDatabaseKind(database)} (${decision.plan})`
6405
+ );
6406
+ }
6407
+ }
6408
+ if (await resolveStorageChoice(options, inspection)) {
6409
+ const decision = await resolveStorageProvisioning(
6410
+ client,
6411
+ profile,
6412
+ app,
6413
+ options
6414
+ );
6415
+ if (decision.action === "reuse") {
6416
+ await attachExistingStorage(client, app, decision);
6417
+ createdResources.push(
6418
+ `Storage reused \u2014 ${decision.name} (${decision.plan})`
6419
+ );
6420
+ } else {
6421
+ await createAndAttachStorage(client, app, {
6422
+ name: decision.name,
6423
+ plan: decision.plan
6424
+ });
6425
+ createdResources.push(`Storage (${decision.plan})`);
6426
+ }
6257
6427
  }
6258
6428
  if (createdResources.length > 0 && !isJsonMode()) {
6259
6429
  box("Provisioned Resources", createdResources);
@@ -6313,9 +6483,10 @@ async function createAndAttachDatabase(client, profile, app, input2) {
6313
6483
  const appName = generateResourceSlug(app.name, input2.kind);
6314
6484
  const label = input2.kind === "postgres" ? "Postgres" : "MySQL";
6315
6485
  const _createSpinner = startSpinner(`Creating ${label} database...`);
6486
+ let databaseId;
6316
6487
  try {
6317
6488
  if (input2.kind === "postgres") {
6318
- await client.postgres.create.mutate({
6489
+ const created = await client.postgres.create.mutate({
6319
6490
  name: input2.name,
6320
6491
  appName,
6321
6492
  dockerImage: "postgres:17",
@@ -6324,8 +6495,9 @@ async function createAndAttachDatabase(client, profile, app, input2) {
6324
6495
  plan: input2.plan,
6325
6496
  region: DEFAULT_REGION
6326
6497
  });
6498
+ databaseId = created?.postgresId;
6327
6499
  } else {
6328
- await client.mysql.create.mutate({
6500
+ const created = await client.mysql.create.mutate({
6329
6501
  name: input2.name,
6330
6502
  appName,
6331
6503
  dockerImage: "mysql:9.1",
@@ -6334,13 +6506,16 @@ async function createAndAttachDatabase(client, profile, app, input2) {
6334
6506
  plan: input2.plan,
6335
6507
  region: DEFAULT_REGION
6336
6508
  });
6509
+ databaseId = created?.mysqlId;
6337
6510
  }
6338
6511
  succeedSpinner(`${label} database created.`);
6339
6512
  } catch (err) {
6340
6513
  failSpinner(`Failed to create ${label} database.`);
6341
6514
  throw err;
6342
6515
  }
6343
- const databaseId = await findDatabaseIdByAppName(client, input2.kind, appName);
6516
+ if (!databaseId) {
6517
+ databaseId = await findDatabaseIdByAppName(client, input2.kind, appName);
6518
+ }
6344
6519
  const _attachSpinner = startSpinner(`Attaching ${label} to ${app.name}...`);
6345
6520
  try {
6346
6521
  if (input2.kind === "postgres") {
@@ -6398,6 +6573,442 @@ async function createAndAttachStorage(client, app, input2) {
6398
6573
  throw err;
6399
6574
  }
6400
6575
  }
6576
+ async function resolveDatabaseProvisioning(client, profile, app, kind, options) {
6577
+ const candidates = await listDatabaseCandidates(client, kind, profile);
6578
+ const requestedPlan = normalizeResourcePlan(options.databasePlan);
6579
+ const planExplicit = Boolean(options.databasePlan);
6580
+ if (options.reuseDatabase) {
6581
+ const picked2 = resolveExplicitDatabaseRef(
6582
+ candidates,
6583
+ options.reuseDatabase,
6584
+ kind
6585
+ );
6586
+ return finalizeDatabaseReuse(
6587
+ client,
6588
+ picked2,
6589
+ kind,
6590
+ requestedPlan,
6591
+ planExplicit
6592
+ );
6593
+ }
6594
+ if (candidates.length === 0 || isJsonMode() || shouldSkipConfirmation()) {
6595
+ return buildDatabaseCreateDecision(app, kind, requestedPlan);
6596
+ }
6597
+ const label = formatDatabaseKind(kind);
6598
+ const choices = [
6599
+ { name: `Create a new ${label} database`, value: "__create__" },
6600
+ ...candidates.map((c) => ({
6601
+ name: formatDatabaseCandidateLine(c),
6602
+ value: c.id
6603
+ }))
6604
+ ];
6605
+ log("");
6606
+ log(
6607
+ `Found ${candidates.length} existing ${label} database${candidates.length === 1 ? "" : "s"} in this project.`
6608
+ );
6609
+ const picked = await select(
6610
+ `Reuse an existing ${label} database or create a new one?`,
6611
+ choices,
6612
+ {
6613
+ field: kind === "postgres" ? "reuse_postgres" : "reuse_mysql",
6614
+ flag: kind === "postgres" ? "--reuse-database <id|name|auto>" : "--reuse-database <id|name|auto>",
6615
+ context: { kind }
6616
+ }
6617
+ );
6618
+ if (picked === "__create__") {
6619
+ const plan = requestedPlan ?? await resolveResourcePlan(void 0, "Database plan:");
6620
+ return {
6621
+ action: "create",
6622
+ plan,
6623
+ name: formatResourceName(app.name, label)
6624
+ };
6625
+ }
6626
+ const candidate = candidates.find((c) => c.id === picked);
6627
+ if (!candidate) {
6628
+ throw new Error(
6629
+ `Unable to resolve picked ${label} database (id=${picked}).`
6630
+ );
6631
+ }
6632
+ return finalizeDatabaseReuse(
6633
+ client,
6634
+ candidate,
6635
+ kind,
6636
+ requestedPlan,
6637
+ planExplicit
6638
+ );
6639
+ }
6640
+ async function resolveStorageProvisioning(client, profile, app, options) {
6641
+ const candidates = await listStorageCandidates(client, profile);
6642
+ const requestedPlan = normalizeResourcePlan(options.storagePlan);
6643
+ const planExplicit = Boolean(options.storagePlan);
6644
+ if (options.reuseStorage) {
6645
+ const picked2 = resolveExplicitStorageRef(candidates, options.reuseStorage);
6646
+ return finalizeStorageReuse(client, picked2, requestedPlan, planExplicit);
6647
+ }
6648
+ if (candidates.length === 0 || isJsonMode() || shouldSkipConfirmation()) {
6649
+ return buildStorageCreateDecision(app, requestedPlan);
6650
+ }
6651
+ const choices = [
6652
+ { name: "Create a new storage bucket", value: "__create__" },
6653
+ ...candidates.map((c) => ({
6654
+ name: formatStorageCandidateLine(c),
6655
+ value: c.bucketId
6656
+ }))
6657
+ ];
6658
+ log("");
6659
+ log(
6660
+ `Found ${candidates.length} existing storage bucket${candidates.length === 1 ? "" : "s"} in this project.`
6661
+ );
6662
+ const picked = await select(
6663
+ "Reuse an existing storage bucket or create a new one?",
6664
+ choices,
6665
+ {
6666
+ field: "reuse_storage",
6667
+ flag: "--reuse-storage <id|name|auto>"
6668
+ }
6669
+ );
6670
+ if (picked === "__create__") {
6671
+ const plan = requestedPlan ?? await resolveResourcePlan(void 0, "Storage plan:");
6672
+ return {
6673
+ action: "create",
6674
+ plan,
6675
+ name: formatResourceName(app.name, "files", 50)
6676
+ };
6677
+ }
6678
+ const candidate = candidates.find((c) => c.bucketId === picked);
6679
+ if (!candidate) {
6680
+ throw new Error(`Unable to resolve picked storage bucket (id=${picked}).`);
6681
+ }
6682
+ return finalizeStorageReuse(client, candidate, requestedPlan, planExplicit);
6683
+ }
6684
+ async function listDatabaseCandidates(client, kind, profile) {
6685
+ const rows = kind === "postgres" ? await client.postgres.allByOrganization.query() : await client.mysql.allByOrganization.query();
6686
+ return (rows ?? []).filter((r) => {
6687
+ if (!r?.projectId || !profile.projectId) return false;
6688
+ return r.projectId === profile.projectId;
6689
+ }).map((r) => ({
6690
+ id: kind === "postgres" ? r.postgresId : r.mysqlId,
6691
+ name: r.name,
6692
+ appName: r.appName,
6693
+ plan: r.plan ?? "FREE",
6694
+ region: r.region ?? null,
6695
+ projectId: r.projectId ?? null,
6696
+ environmentId: r.environmentId ?? null,
6697
+ applicationStatus: r.applicationStatus ?? null,
6698
+ isReadOnly: r.isReadOnly ?? null,
6699
+ createdAt: r.createdAt ?? null
6700
+ })).filter((c) => Boolean(c.id));
6701
+ }
6702
+ async function listStorageCandidates(client, profile) {
6703
+ const rows = await client.storage.allByOrganization.query();
6704
+ return (rows ?? []).filter((r) => {
6705
+ if (!r?.projectId || !profile.projectId) return false;
6706
+ return r.projectId === profile.projectId;
6707
+ }).map((r) => ({
6708
+ bucketId: r.bucketId,
6709
+ name: r.name,
6710
+ plan: r.plan ?? "FREE",
6711
+ region: r.region ?? null,
6712
+ projectId: r.projectId ?? null,
6713
+ environmentId: r.environmentId ?? null,
6714
+ applicationStatus: r.applicationStatus ?? null,
6715
+ isUploadBlocked: r.isUploadBlocked ?? null,
6716
+ createdAt: r.createdAt ?? null
6717
+ })).filter((c) => Boolean(c.bucketId));
6718
+ }
6719
+ function resolveExplicitDatabaseRef(candidates, ref, kind) {
6720
+ const label = formatDatabaseKind(kind);
6721
+ const normalized = ref.trim();
6722
+ if (normalized.toLowerCase() === "auto") {
6723
+ if (candidates.length === 0) {
6724
+ throw new InvalidArgumentError(
6725
+ `--reuse-database=auto: no existing ${label} databases in this project.`
6726
+ );
6727
+ }
6728
+ if (candidates.length > 1) {
6729
+ const sample = candidates.slice(0, 5).map((c) => `${c.name} (${c.id})`).join(", ");
6730
+ throw new InvalidArgumentError(
6731
+ `--reuse-database=auto needs exactly one match, found ${candidates.length}. Candidates: ${sample}. Pick one by id or name.`
6732
+ );
6733
+ }
6734
+ return candidates[0];
6735
+ }
6736
+ const lower = normalized.toLowerCase();
6737
+ const match = candidates.find((c) => c.id === normalized) ?? candidates.find((c) => c.name.toLowerCase() === lower) ?? candidates.find((c) => (c.appName ?? "").toLowerCase() === lower);
6738
+ if (!match) {
6739
+ const known = candidates.map((c) => `${c.name} (${c.id.slice(0, 8)})`);
6740
+ const suggestions = findSimilar(
6741
+ normalized,
6742
+ candidates.flatMap((c) => [c.name, c.id])
6743
+ );
6744
+ throw new NotFoundError(
6745
+ `${label} database`,
6746
+ normalized,
6747
+ suggestions.length > 0 ? suggestions : known.length > 0 ? known.slice(0, 5) : [
6748
+ `No ${label} databases found in this project. Drop --reuse-database to create a new one.`
6749
+ ]
6750
+ );
6751
+ }
6752
+ return match;
6753
+ }
6754
+ function resolveExplicitStorageRef(candidates, ref) {
6755
+ const normalized = ref.trim();
6756
+ if (normalized.toLowerCase() === "auto") {
6757
+ if (candidates.length === 0) {
6758
+ throw new InvalidArgumentError(
6759
+ "--reuse-storage=auto: no existing storage buckets in this project."
6760
+ );
6761
+ }
6762
+ if (candidates.length > 1) {
6763
+ const sample = candidates.slice(0, 5).map((c) => `${c.name} (${c.bucketId})`).join(", ");
6764
+ throw new InvalidArgumentError(
6765
+ `--reuse-storage=auto needs exactly one match, found ${candidates.length}. Candidates: ${sample}. Pick one by id or name.`
6766
+ );
6767
+ }
6768
+ return candidates[0];
6769
+ }
6770
+ const lower = normalized.toLowerCase();
6771
+ const match = candidates.find((c) => c.bucketId === normalized) ?? candidates.find((c) => c.name.toLowerCase() === lower);
6772
+ if (!match) {
6773
+ const known = candidates.map(
6774
+ (c) => `${c.name} (${c.bucketId.slice(0, 8)})`
6775
+ );
6776
+ const suggestions = findSimilar(
6777
+ normalized,
6778
+ candidates.flatMap((c) => [c.name, c.bucketId])
6779
+ );
6780
+ throw new NotFoundError(
6781
+ "Storage bucket",
6782
+ normalized,
6783
+ suggestions.length > 0 ? suggestions : known.length > 0 ? known.slice(0, 5) : [
6784
+ "No storage buckets found in this project. Drop --reuse-storage to create a new one."
6785
+ ]
6786
+ );
6787
+ }
6788
+ return match;
6789
+ }
6790
+ async function finalizeDatabaseReuse(client, candidate, kind, requestedPlan, planExplicit) {
6791
+ let plan = candidate.plan;
6792
+ let upgraded = false;
6793
+ if (planExplicit && requestedPlan) {
6794
+ const cmp = compareResourcePlan(requestedPlan, candidate.plan);
6795
+ if (cmp > 0) {
6796
+ const ok = await confirmUpgrade(
6797
+ `${formatDatabaseKind(kind)} ${candidate.name} is on ${candidate.plan}. Upgrade to ${requestedPlan} before attaching?`,
6798
+ true,
6799
+ kind === "postgres" ? "upgrade_postgres" : "upgrade_mysql"
6800
+ );
6801
+ if (ok) {
6802
+ await runDatabaseUpgrade(client, kind, candidate.id, requestedPlan);
6803
+ plan = requestedPlan;
6804
+ upgraded = true;
6805
+ }
6806
+ } else if (cmp < 0 && !isJsonMode()) {
6807
+ log(
6808
+ colors.warn(
6809
+ `Requested ${requestedPlan} is lower than existing ${candidate.plan}; attaching at current plan.`
6810
+ )
6811
+ );
6812
+ }
6813
+ }
6814
+ return {
6815
+ action: "reuse",
6816
+ databaseId: candidate.id,
6817
+ plan,
6818
+ name: candidate.name,
6819
+ upgraded
6820
+ };
6821
+ }
6822
+ async function finalizeStorageReuse(client, candidate, requestedPlan, planExplicit) {
6823
+ let plan = candidate.plan;
6824
+ let upgraded = false;
6825
+ if (planExplicit && requestedPlan) {
6826
+ const cmp = compareResourcePlan(requestedPlan, candidate.plan);
6827
+ if (cmp > 0) {
6828
+ if (candidate.plan !== "FREE") {
6829
+ if (!isJsonMode()) {
6830
+ log(
6831
+ colors.warn(
6832
+ `Storage bucket ${candidate.name} is on ${candidate.plan}. Inline upgrades to ${requestedPlan} are only supported from FREE \u2014 upgrade from the dashboard. Attaching at current plan.`
6833
+ )
6834
+ );
6835
+ }
6836
+ } else if (requestedPlan === "STARTER" || requestedPlan === "STANDARD" || requestedPlan === "PRO") {
6837
+ const ok = await confirmUpgrade(
6838
+ `Storage bucket ${candidate.name} is on FREE. Upgrade to ${requestedPlan} before attaching?`,
6839
+ true,
6840
+ "upgrade_storage"
6841
+ );
6842
+ if (ok) {
6843
+ await runStorageUpgrade(client, candidate.bucketId, requestedPlan);
6844
+ plan = requestedPlan;
6845
+ upgraded = true;
6846
+ }
6847
+ }
6848
+ } else if (cmp < 0 && !isJsonMode()) {
6849
+ log(
6850
+ colors.warn(
6851
+ `Requested ${requestedPlan} is lower than existing ${candidate.plan}; attaching at current plan.`
6852
+ )
6853
+ );
6854
+ }
6855
+ }
6856
+ return {
6857
+ action: "reuse",
6858
+ bucketId: candidate.bucketId,
6859
+ plan,
6860
+ name: candidate.name,
6861
+ upgraded
6862
+ };
6863
+ }
6864
+ async function confirmUpgrade(message, defaultValue, field) {
6865
+ if (isJsonMode() || shouldSkipConfirmation()) {
6866
+ return false;
6867
+ }
6868
+ return confirm(message, defaultValue, {
6869
+ field,
6870
+ flag: "--yes (default attaches at current plan; pass --database-plan/--storage-plan with --yes to force an upgrade)"
6871
+ });
6872
+ }
6873
+ async function runDatabaseUpgrade(client, kind, id, targetPlan) {
6874
+ if (targetPlan === "FREE") return;
6875
+ const label = formatDatabaseKind(kind);
6876
+ const _spinner = startSpinner(`Upgrading ${label} to ${targetPlan}...`);
6877
+ try {
6878
+ if (kind === "postgres") {
6879
+ await client.postgres.upgrade.mutate({
6880
+ postgresId: id,
6881
+ targetPlan
6882
+ });
6883
+ } else {
6884
+ await client.mysql.upgrade.mutate({
6885
+ mysqlId: id,
6886
+ targetPlan
6887
+ });
6888
+ }
6889
+ succeedSpinner(`${label} upgraded to ${targetPlan}.`);
6890
+ } catch (err) {
6891
+ failSpinner(`Failed to upgrade ${label}.`);
6892
+ throw err;
6893
+ }
6894
+ }
6895
+ async function runStorageUpgrade(client, bucketId, targetPlan) {
6896
+ if (targetPlan === "FREE") return;
6897
+ const _spinner = startSpinner(`Upgrading storage to ${targetPlan}...`);
6898
+ try {
6899
+ await client.storage.upgrade.mutate({
6900
+ bucketId,
6901
+ targetPlan
6902
+ });
6903
+ succeedSpinner(`Storage upgraded to ${targetPlan}.`);
6904
+ } catch (err) {
6905
+ failSpinner("Failed to upgrade storage.");
6906
+ throw err;
6907
+ }
6908
+ }
6909
+ async function attachExistingDatabase(client, app, kind, decision) {
6910
+ const label = formatDatabaseKind(kind);
6911
+ const _spinner = startSpinner(`Attaching ${label} to ${app.name}...`);
6912
+ try {
6913
+ if (kind === "postgres") {
6914
+ await client.postgres.attachToApplication.mutate({
6915
+ postgresId: decision.databaseId,
6916
+ applicationId: app.applicationId
6917
+ });
6918
+ } else {
6919
+ await client.mysql.attachToApplication.mutate({
6920
+ mysqlId: decision.databaseId,
6921
+ applicationId: app.applicationId
6922
+ });
6923
+ }
6924
+ succeedSpinner(`${label} credentials attached.`);
6925
+ } catch (err) {
6926
+ failSpinner(`Failed to attach ${label} credentials.`);
6927
+ throw err;
6928
+ }
6929
+ }
6930
+ async function attachExistingStorage(client, app, decision) {
6931
+ const _spinner = startSpinner(`Attaching storage to ${app.name}...`);
6932
+ try {
6933
+ await client.storage.attachToApplication.mutate({
6934
+ bucketId: decision.bucketId,
6935
+ applicationId: app.applicationId
6936
+ });
6937
+ succeedSpinner("Storage credentials attached.");
6938
+ } catch (err) {
6939
+ failSpinner("Failed to attach storage credentials.");
6940
+ throw err;
6941
+ }
6942
+ }
6943
+ async function buildDatabaseCreateDecision(app, kind, requestedPlan) {
6944
+ const plan = requestedPlan ?? await resolveResourcePlan(void 0, "Database plan:");
6945
+ return {
6946
+ action: "create",
6947
+ plan,
6948
+ name: formatResourceName(app.name, formatDatabaseKind(kind))
6949
+ };
6950
+ }
6951
+ async function buildStorageCreateDecision(app, requestedPlan) {
6952
+ const plan = requestedPlan ?? await resolveResourcePlan(void 0, "Storage plan:");
6953
+ return {
6954
+ action: "create",
6955
+ plan,
6956
+ name: formatResourceName(app.name, "files", 50)
6957
+ };
6958
+ }
6959
+ function formatDatabaseCandidateLine(c) {
6960
+ const parts = [c.name];
6961
+ const badges = [c.plan];
6962
+ if (c.region) badges.push(c.region);
6963
+ const ageBadge = describeAge(c.createdAt);
6964
+ if (ageBadge) badges.push(ageBadge);
6965
+ const status = describeApplicationStatus(c.applicationStatus);
6966
+ if (status) badges.push(status);
6967
+ if (c.isReadOnly) badges.push("read-only");
6968
+ parts.push(`(${badges.join(" \xB7 ")})`);
6969
+ parts.push(colors.dim(`[${c.id.slice(0, 8)}]`));
6970
+ return parts.join(" ");
6971
+ }
6972
+ function formatStorageCandidateLine(c) {
6973
+ const parts = [c.name];
6974
+ const badges = [c.plan];
6975
+ if (c.region) badges.push(c.region);
6976
+ const ageBadge = describeAge(c.createdAt);
6977
+ if (ageBadge) badges.push(ageBadge);
6978
+ const status = describeApplicationStatus(c.applicationStatus);
6979
+ if (status) badges.push(status);
6980
+ if (c.isUploadBlocked) badges.push("upload blocked");
6981
+ parts.push(`(${badges.join(" \xB7 ")})`);
6982
+ parts.push(colors.dim(`[${c.bucketId.slice(0, 8)}]`));
6983
+ return parts.join(" ");
6984
+ }
6985
+ function describeAge(value) {
6986
+ if (!value) return null;
6987
+ const date = value instanceof Date ? value : new Date(value);
6988
+ const time = date.getTime();
6989
+ if (!Number.isFinite(time)) return null;
6990
+ const days = Math.max(0, Math.floor((Date.now() - time) / 864e5));
6991
+ if (days === 0) return "today";
6992
+ if (days === 1) return "1d ago";
6993
+ if (days < 30) return `${days}d ago`;
6994
+ if (days < 365) return `${Math.floor(days / 30)}mo ago`;
6995
+ return `${Math.floor(days / 365)}y ago`;
6996
+ }
6997
+ function describeApplicationStatus(status) {
6998
+ if (!status) return null;
6999
+ const normalized = status.toLowerCase();
7000
+ if (normalized === "running" || normalized === "done") return null;
7001
+ return normalized;
7002
+ }
7003
+ var RESOURCE_PLAN_RANK = {
7004
+ FREE: 0,
7005
+ STARTER: 1,
7006
+ STANDARD: 2,
7007
+ PRO: 3
7008
+ };
7009
+ function compareResourcePlan(a, b) {
7010
+ return RESOURCE_PLAN_RANK[a] - RESOURCE_PLAN_RANK[b];
7011
+ }
6401
7012
  function normalizeDatabaseKind(value) {
6402
7013
  if (!value) return void 0;
6403
7014
  const normalized = value.trim().toLowerCase();
@@ -6541,8 +7152,8 @@ async function uploadCurrentDirectorySource(client, applicationId, appName) {
6541
7152
  }
6542
7153
  }
6543
7154
  async function createSourceArchive() {
6544
- const tempDir = mkdtempSync(join2(tmpdir(), "tarout-source-"));
6545
- const archivePath = join2(tempDir, "source.zip");
7155
+ const tempDir = mkdtempSync(join3(tmpdir(), "tarout-source-"));
7156
+ const archivePath = join3(tempDir, "source.zip");
6546
7157
  const excludes = [
6547
7158
  ".git/*",
6548
7159
  ".tarout/*",
@@ -6595,6 +7206,12 @@ function registerDeployCommands(program2) {
6595
7206
  ).option("--storage", "Provision file storage and attach it to the app").option(
6596
7207
  "--storage-plan <plan>",
6597
7208
  "Storage plan: free, starter, standard, or pro"
7209
+ ).option(
7210
+ "--reuse-database <ref>",
7211
+ "Reuse an existing database in this project: <id>, <name>, or 'auto' (exactly one match)"
7212
+ ).option(
7213
+ "--reuse-storage <ref>",
7214
+ "Reuse an existing storage bucket in this project: <id>, <name>, or 'auto' (exactly one match)"
6598
7215
  ).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(
6599
7216
  "--framework-preset <preset>",
6600
7217
  "Framework preset override (e.g. nextjs, vite, astro)"
@@ -6742,7 +7359,7 @@ function registerDeployCommands(program2) {
6742
7359
  applicationId: app.applicationId,
6743
7360
  name: app.name,
6744
7361
  status: app.applicationStatus,
6745
- url: app.appSubdomain ? `https://${app.appSubdomain}` : app.domain?.[0]?.host ? `https://${app.domain[0].host}` : null,
7362
+ url: formatAppUrl(app.appSubdomain) ?? formatAppUrl(app.domain?.[0]?.host),
6746
7363
  cloudStatus
6747
7364
  });
6748
7365
  return;
@@ -6751,7 +7368,7 @@ function registerDeployCommands(program2) {
6751
7368
  log(`${colors.bold(app.name)}`);
6752
7369
  log("");
6753
7370
  log(`Status: ${getStatusBadge(app.applicationStatus)}`);
6754
- const appUrl = app.appSubdomain ? `https://${app.appSubdomain}` : app.domain?.[0]?.host ? `https://${app.domain[0].host}` : null;
7371
+ const appUrl = formatAppUrl(app.appSubdomain) ?? formatAppUrl(app.domain?.[0]?.host);
6755
7372
  if (appUrl) {
6756
7373
  log(`URL: ${colors.cyan(appUrl)}`);
6757
7374
  }
@@ -7180,7 +7797,7 @@ function formatDate5(date) {
7180
7797
  });
7181
7798
  }
7182
7799
  function sleep(ms) {
7183
- return new Promise((resolve2) => setTimeout(resolve2, ms));
7800
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
7184
7801
  }
7185
7802
  async function streamDeploymentWithLogs(client, deploymentId, appName, applicationId) {
7186
7803
  stopSpinner();
@@ -7195,6 +7812,23 @@ async function streamDeploymentWithLogs(client, deploymentId, appName, applicati
7195
7812
  const startTime = Date.now();
7196
7813
  let wsConnected = false;
7197
7814
  let lastStatus = deployment.status;
7815
+ const backfillLogsIfEmpty = async () => {
7816
+ if (logLines.length > 0) return;
7817
+ try {
7818
+ const res = await client.deployment.getDeploymentLogs.query({
7819
+ deploymentId,
7820
+ offset: 0,
7821
+ limit: 5e3
7822
+ });
7823
+ if (Array.isArray(res?.lines)) {
7824
+ for (const line of res.lines) {
7825
+ logLines.push(line);
7826
+ if (isErrorLine(line)) errors.push(line);
7827
+ }
7828
+ }
7829
+ } catch {
7830
+ }
7831
+ };
7198
7832
  if (!isJsonMode()) {
7199
7833
  log("");
7200
7834
  log(
@@ -7238,9 +7872,10 @@ async function streamDeploymentWithLogs(client, deploymentId, appName, applicati
7238
7872
  if (lastStatus === "done") {
7239
7873
  await sleep(500);
7240
7874
  cleanup?.();
7875
+ await backfillLogsIfEmpty();
7241
7876
  const finalApp = await client.application.one.query({ applicationId });
7242
7877
  const duration2 = Math.round((Date.now() - startTime) / 1e3);
7243
- const deployUrl = finalApp.appSubdomain ? `https://${finalApp.appSubdomain}` : finalApp.domain?.[0]?.host ? `https://${finalApp.domain[0].host}` : null;
7878
+ const deployUrl = formatAppUrl(finalApp.appSubdomain) ?? formatAppUrl(finalApp.domain?.[0]?.host);
7244
7879
  if (isJsonMode()) {
7245
7880
  outputData({
7246
7881
  deploymentId,
@@ -7262,6 +7897,7 @@ async function streamDeploymentWithLogs(client, deploymentId, appName, applicati
7262
7897
  }
7263
7898
  if (lastStatus === "error" || lastStatus === "cancelled") {
7264
7899
  cleanup?.();
7900
+ await backfillLogsIfEmpty();
7265
7901
  const errorAnalysis = analyzeDeploymentError(
7266
7902
  logLines,
7267
7903
  updatedDeployment.errorMessage
@@ -7321,6 +7957,7 @@ async function streamDeploymentWithLogs(client, deploymentId, appName, applicati
7321
7957
  }
7322
7958
  }
7323
7959
  cleanup?.();
7960
+ await backfillLogsIfEmpty();
7324
7961
  const duration = Math.round((Date.now() - startTime) / 1e3);
7325
7962
  if (isJsonMode()) {
7326
7963
  outputError("DEPLOYMENT_TIMEOUT", "Deployment timed out", {
@@ -9724,837 +10361,65 @@ function truncate(str, max) {
9724
10361
  return `${str.slice(0, max - 3)}...`;
9725
10362
  }
9726
10363
 
9727
- // src/commands/email.ts
9728
- function registerEmailCommands(program2) {
9729
- const email = program2.command("email").description("Manage email service configuration, templates, and logs");
9730
- const config = email.command("config").description("Manage email configuration");
9731
- config.command("get").description("Show email configuration for an environment").option("-e, --env <environment-id>", "Environment ID").action(async (options) => {
10364
+ // src/commands/env.ts
10365
+ import { chmodSync, existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
10366
+ function registerEnvCommands(program2) {
10367
+ const env = program2.command("env").argument("<app>", "Application ID or name").description("Manage environment variables");
10368
+ env.command("list").alias("ls").description("List all environment variables").option("--reveal", "Show actual values (not masked)").action(async (options, command) => {
9732
10369
  try {
9733
10370
  if (!isLoggedIn()) throw new AuthError();
9734
- const environmentId = options.env || await input("Environment ID:", void 0, {
9735
- field: "environment_id",
9736
- flag: "--env"
9737
- });
10371
+ const appIdentifier = command.parent.args[0];
9738
10372
  const client = getApiClient();
9739
- const _spinner = startSpinner("Fetching email config...");
9740
- const data = await client.emailService.getConfig.query({
9741
- environmentId
10373
+ const _spinner = startSpinner("Fetching environment variables...");
10374
+ const apps = await client.application.allByOrganization.query();
10375
+ const app = findApp6(apps, appIdentifier);
10376
+ if (!app) {
10377
+ failSpinner();
10378
+ const suggestions = findSimilar(
10379
+ appIdentifier,
10380
+ apps.map((a) => a.name)
10381
+ );
10382
+ throw new NotFoundError("Application", appIdentifier, suggestions);
10383
+ }
10384
+ const variables = await client.envVariable.list.query({
10385
+ applicationId: app.applicationId,
10386
+ includeValues: options.reveal || false
9742
10387
  });
9743
10388
  succeedSpinner();
9744
10389
  if (isJsonMode()) {
9745
- outputData(data);
10390
+ outputData(variables);
9746
10391
  return;
9747
10392
  }
9748
- const c = data;
9749
- if (!c) {
9750
- log("No email configuration found.");
10393
+ if (variables.length === 0) {
10394
+ log("");
10395
+ log("No environment variables found.");
10396
+ log("");
10397
+ log(
10398
+ `Set one with: ${colors.dim(`tarout env ${app.name} set KEY=value`)}`
10399
+ );
9751
10400
  return;
9752
10401
  }
9753
10402
  log("");
9754
- log(colors.bold("Email Configuration"));
9755
- log(` Provider: ${c.provider || "-"}`);
9756
- log(` From Name: ${c.fromName || "-"}`);
9757
- log(` From Email: ${c.fromEmail || "-"}`);
9758
- log(` Domain: ${c.fromDomain || "-"}`);
9759
- log(` Reply To: ${c.replyToEmail || "-"}`);
9760
- log(
9761
- ` Verified: ${c.isVerified ? colors.success("yes") : colors.warn("no")}`
10403
+ table(
10404
+ ["KEY", "VALUE", "SECRET", "UPDATED"],
10405
+ variables.map((v) => [
10406
+ colors.cyan(v.key),
10407
+ options.reveal ? v.value || colors.dim("-") : maskValue(v.value),
10408
+ v.isSecret ? colors.warn("Yes") : "No",
10409
+ formatDate7(v.updatedAt)
10410
+ ])
9762
10411
  );
9763
- log(` ID: ${colors.dim(c.id || c.emailServiceId || "-")}`);
9764
10412
  log("");
10413
+ log(
10414
+ colors.dim(
10415
+ `${variables.length} variable${variables.length === 1 ? "" : "s"}`
10416
+ )
10417
+ );
9765
10418
  } catch (err) {
9766
- failSpinner();
9767
10419
  handleError(err);
9768
10420
  }
9769
10421
  });
9770
- config.command("create").description("Create email configuration for an environment").option("-e, --env <environment-id>", "Environment ID").option(
9771
- "-p, --provider <provider>",
9772
- "Email provider (resend, sendgrid, mailgun, ses)"
9773
- ).option("--api-key <key>", "API key").option("--from-domain <domain>", "From domain").option("--from-name <name>", "From name").option("--from-email <email>", "From email address").option("--reply-to <email>", "Reply-to email").action(
9774
- async (options) => {
9775
- try {
9776
- if (!isLoggedIn()) throw new AuthError();
9777
- const environmentId = options.env || await input("Environment ID:", void 0, {
9778
- field: "environment_id",
9779
- flag: "--env"
9780
- });
9781
- const provider = options.provider || await select(
9782
- "Email provider:",
9783
- [
9784
- { name: "Resend", value: "resend" },
9785
- { name: "SendGrid", value: "sendgrid" },
9786
- { name: "Mailgun", value: "mailgun" },
9787
- { name: "Amazon SES", value: "ses" }
9788
- ],
9789
- { field: "email_provider", flag: "--provider" }
9790
- );
9791
- const apiKey = options.apiKey || await input("API Key:", void 0, {
9792
- field: "api_key",
9793
- flag: "--api-key",
9794
- sensitive: true
9795
- });
9796
- const fromDomain = options.fromDomain || await input("From domain (e.g. mail.example.com):", void 0, {
9797
- field: "from_domain",
9798
- flag: "--from-domain"
9799
- });
9800
- const fromName = options.fromName || await input("From name:", void 0, {
9801
- field: "from_name",
9802
- flag: "--from-name"
9803
- });
9804
- const fromEmail = options.fromEmail || await input(
9805
- `From email (e.g. hello@${fromDomain}):`,
9806
- void 0,
9807
- { field: "from_email", flag: "--from-email" }
9808
- );
9809
- const client = getApiClient();
9810
- const _spinner = startSpinner("Creating email config...");
9811
- const result = await client.emailService.createConfig.mutate({
9812
- environmentId,
9813
- provider,
9814
- apiKey,
9815
- fromDomain,
9816
- fromName,
9817
- fromEmail,
9818
- replyToEmail: options.replyTo
9819
- });
9820
- succeedSpinner("Email configuration created.");
9821
- if (isJsonMode()) outputData(result);
9822
- else quietOutput(result.id || environmentId);
9823
- } catch (err) {
9824
- failSpinner();
9825
- handleError(err);
9826
- }
9827
- }
9828
- );
9829
- config.command("update <email-service-id>").description("Update email configuration").option("--api-key <key>", "New API key").option("--from-domain <domain>", "New from domain").option("--from-name <name>", "New from name").option("--from-email <email>", "New from email").option("--reply-to <email>", "New reply-to email").action(
9830
- async (emailServiceId, options) => {
9831
- try {
9832
- if (!isLoggedIn()) throw new AuthError();
9833
- const client = getApiClient();
9834
- const _spinner = startSpinner("Updating email config...");
9835
- await client.emailService.updateConfig.mutate({
9836
- emailServiceId,
9837
- apiKey: options.apiKey,
9838
- fromDomain: options.fromDomain,
9839
- fromName: options.fromName,
9840
- fromEmail: options.fromEmail,
9841
- replyToEmail: options.replyTo
9842
- });
9843
- succeedSpinner("Email configuration updated.");
9844
- if (isJsonMode()) outputData({ updated: true, emailServiceId });
9845
- } catch (err) {
9846
- failSpinner();
9847
- handleError(err);
9848
- }
9849
- }
9850
- );
9851
- config.command("delete <email-service-id>").alias("rm").description("Delete email configuration").action(async (emailServiceId) => {
9852
- try {
9853
- if (!isLoggedIn()) throw new AuthError();
9854
- if (!shouldSkipConfirmation()) {
9855
- const confirmed = await confirm(
9856
- `Delete email configuration "${emailServiceId}"?`,
9857
- false,
9858
- {
9859
- field: "confirm_delete_email_config",
9860
- flag: "--yes",
9861
- context: { emailServiceId }
9862
- }
9863
- );
9864
- if (!confirmed) {
9865
- log("Cancelled.");
9866
- return;
9867
- }
9868
- }
9869
- const client = getApiClient();
9870
- const _spinner = startSpinner("Deleting email config...");
9871
- await client.emailService.deleteConfig.mutate({
9872
- emailServiceId
9873
- });
9874
- succeedSpinner("Email configuration deleted.");
9875
- if (isJsonMode()) outputData({ deleted: true, emailServiceId });
9876
- } catch (err) {
9877
- failSpinner();
9878
- handleError(err);
9879
- }
9880
- });
9881
- const domain = email.command("domain").description("Manage email domain verification");
9882
- domain.command("verify").description("Initiate email domain verification").option("-e, --env <environment-id>", "Environment ID").option("-d, --domain <domain>", "Domain to verify").action(async (options) => {
9883
- try {
9884
- if (!isLoggedIn()) throw new AuthError();
9885
- const environmentId = options.env || await input("Environment ID:", void 0, {
9886
- field: "environment_id",
9887
- flag: "--env"
9888
- });
9889
- const domainName = options.domain || await input("Domain to verify:", void 0, {
9890
- field: "domain",
9891
- flag: "--domain"
9892
- });
9893
- const client = getApiClient();
9894
- const _spinner = startSpinner("Initiating domain verification...");
9895
- const result = await client.emailService.verifyDomain.mutate({
9896
- environmentId,
9897
- domain: domainName
9898
- });
9899
- succeedSpinner("Verification initiated.");
9900
- if (isJsonMode()) outputData(result);
9901
- } catch (err) {
9902
- failSpinner();
9903
- handleError(err);
9904
- }
9905
- });
9906
- domain.command("status").description("Check email domain verification status").option("-e, --env <environment-id>", "Environment ID").option("-d, --domain <domain>", "Domain to check").action(async (options) => {
9907
- try {
9908
- if (!isLoggedIn()) throw new AuthError();
9909
- const environmentId = options.env || await input("Environment ID:", void 0, {
9910
- field: "environment_id",
9911
- flag: "--env"
9912
- });
9913
- const domainName = options.domain || await input("Domain name:", void 0, {
9914
- field: "domain",
9915
- flag: "--domain"
9916
- });
9917
- const client = getApiClient();
9918
- const _spinner = startSpinner("Checking domain status...");
9919
- const data = await client.emailService.getDomainStatus.query({
9920
- environmentId,
9921
- domain: domainName
9922
- });
9923
- succeedSpinner();
9924
- if (isJsonMode()) {
9925
- outputData(data);
9926
- return;
9927
- }
9928
- const s = data;
9929
- log("");
9930
- log(`Domain: ${colors.cyan(domainName)}`);
9931
- log(
9932
- `Status: ${s.verified || s.status === "verified" ? colors.success("verified") : colors.warn(s.status || "pending")}`
9933
- );
9934
- log("");
9935
- } catch (err) {
9936
- failSpinner();
9937
- handleError(err);
9938
- }
9939
- });
9940
- domain.command("dns-records").description("Get DNS records needed for email domain verification").option("-e, --env <environment-id>", "Environment ID").option("-d, --domain <domain>", "Domain name").option("--type <type>", "Domain type (managed/external)").action(
9941
- async (options) => {
9942
- try {
9943
- if (!isLoggedIn()) throw new AuthError();
9944
- const environmentId = options.env || await input("Environment ID:", void 0, {
9945
- field: "environment_id",
9946
- flag: "--env"
9947
- });
9948
- const domainName = options.domain || await input("Domain name:", void 0, {
9949
- field: "domain",
9950
- flag: "--domain"
9951
- });
9952
- const client = getApiClient();
9953
- const _spinner = startSpinner("Fetching DNS records...");
9954
- const data = await client.emailService.getDnsRecords.query({
9955
- environmentId,
9956
- domain: domainName,
9957
- domainType: options.type || "external"
9958
- });
9959
- succeedSpinner();
9960
- if (isJsonMode()) {
9961
- outputData(data);
9962
- return;
9963
- }
9964
- const records = Array.isArray(data) ? data : data?.records || [];
9965
- log("");
9966
- log(colors.bold("DNS Records for Email Verification"));
9967
- log("");
9968
- table(
9969
- ["TYPE", "HOST", "VALUE"],
9970
- records.map((r) => [
9971
- colors.cyan(r.type || "-"),
9972
- r.name || r.host || "-",
9973
- (r.value || r.data || "-").slice(0, 60)
9974
- ])
9975
- );
9976
- log("");
9977
- } catch (err) {
9978
- failSpinner();
9979
- handleError(err);
9980
- }
9981
- }
9982
- );
9983
- const templates = email.command("templates").description("Manage email templates");
9984
- templates.command("list").alias("ls").description("List email templates").option("-e, --env <environment-id>", "Environment ID").option("--active", "Show only active templates").action(async (options) => {
9985
- try {
9986
- if (!isLoggedIn()) throw new AuthError();
9987
- const environmentId = options.env || await input("Environment ID:", void 0, {
9988
- field: "environment_id",
9989
- flag: "--env"
9990
- });
9991
- const client = getApiClient();
9992
- const _spinner = startSpinner("Fetching templates...");
9993
- const list = await client.emailService.listTemplates.query({
9994
- environmentId,
9995
- isActive: options.active
9996
- });
9997
- succeedSpinner();
9998
- if (isJsonMode()) {
9999
- outputData(list);
10000
- return;
10001
- }
10002
- const items = Array.isArray(list) ? list : [];
10003
- if (items.length === 0) {
10004
- log("No templates found.");
10005
- return;
10006
- }
10007
- log("");
10008
- table(
10009
- ["NAME", "SLUG", "SUBJECT", "ACTIVE", "ID"],
10010
- items.map((t) => [
10011
- colors.bold(t.name || "-"),
10012
- t.slug || "-",
10013
- (t.subject || "-").slice(0, 40),
10014
- t.isActive ? colors.success("yes") : colors.dim("no"),
10015
- colors.dim(t.id || t.templateId || "-")
10016
- ])
10017
- );
10018
- log("");
10019
- } catch (err) {
10020
- failSpinner();
10021
- handleError(err);
10022
- }
10023
- });
10024
- templates.command("get-by-slug <slug>").description("Get an email template by its slug").option("-e, --env <environment-id>", "Environment ID").action(async (slug, options) => {
10025
- try {
10026
- if (!isLoggedIn()) throw new AuthError();
10027
- const environmentId = options.env || await input("Environment ID:", void 0, {
10028
- field: "environment_id",
10029
- flag: "--env"
10030
- });
10031
- const client = getApiClient();
10032
- const _spinner = startSpinner("Fetching template...");
10033
- const data = await client.emailService.getTemplateBySlug.query({
10034
- environmentId,
10035
- slug
10036
- });
10037
- succeedSpinner();
10038
- if (isJsonMode()) {
10039
- outputData(data);
10040
- return;
10041
- }
10042
- const t = data;
10043
- if (!t) {
10044
- log(`No template found with slug "${slug}".`);
10045
- return;
10046
- }
10047
- log("");
10048
- log(colors.bold(t.name || slug));
10049
- log(` Slug: ${t.slug || slug}`);
10050
- log(` Subject: ${t.subject || "-"}`);
10051
- log(
10052
- ` Active: ${t.isActive ? colors.success("yes") : colors.dim("no")}`
10053
- );
10054
- log(` ID: ${colors.dim(t.id || t.templateId || "-")}`);
10055
- log("");
10056
- } catch (err) {
10057
- failSpinner();
10058
- handleError(err);
10059
- }
10060
- });
10061
- templates.command("get <template-id>").description("Show email template details").action(async (templateId) => {
10062
- try {
10063
- if (!isLoggedIn()) throw new AuthError();
10064
- const client = getApiClient();
10065
- const _spinner = startSpinner("Fetching template...");
10066
- const data = await client.emailService.getTemplate.query({
10067
- templateId
10068
- });
10069
- succeedSpinner();
10070
- if (isJsonMode()) {
10071
- outputData(data);
10072
- return;
10073
- }
10074
- const t = data;
10075
- log("");
10076
- log(colors.bold(t.name || templateId));
10077
- log(` Slug: ${t.slug || "-"}`);
10078
- log(` Subject: ${t.subject || "-"}`);
10079
- log(
10080
- ` Active: ${t.isActive ? colors.success("yes") : colors.dim("no")}`
10081
- );
10082
- log(` Variables: ${(t.variables || []).join(", ") || "-"}`);
10083
- log(` ID: ${colors.dim(t.id || t.templateId || "-")}`);
10084
- log("");
10085
- } catch (err) {
10086
- failSpinner();
10087
- handleError(err);
10088
- }
10089
- });
10090
- templates.command("create").description("Create an email template").option("-e, --env <environment-id>", "Environment ID").option("-n, --name <name>", "Template name").option("--slug <slug>", "Template slug").option("--subject <subject>", "Email subject").option("--html <html>", "HTML content").option("--description <text>", "Template description").action(
10091
- async (options) => {
10092
- try {
10093
- if (!isLoggedIn()) throw new AuthError();
10094
- const environmentId = options.env || await input("Environment ID:", void 0, {
10095
- field: "environment_id",
10096
- flag: "--env"
10097
- });
10098
- const name = options.name || await input("Template name:", void 0, {
10099
- field: "template_name",
10100
- flag: "--name"
10101
- });
10102
- const subject = options.subject || await input("Email subject:", void 0, {
10103
- field: "email_subject",
10104
- flag: "--subject"
10105
- });
10106
- const htmlContent = options.html || await input("HTML content (or paste):", void 0, {
10107
- field: "html_content",
10108
- flag: "--html"
10109
- });
10110
- const client = getApiClient();
10111
- const _spinner = startSpinner("Creating template...");
10112
- const result = await client.emailService.createTemplate.mutate({
10113
- environmentId,
10114
- name,
10115
- slug: options.slug || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
10116
- subject,
10117
- htmlContent,
10118
- description: options.description,
10119
- isActive: true
10120
- });
10121
- succeedSpinner("Template created.");
10122
- if (isJsonMode()) outputData(result);
10123
- else
10124
- quietOutput(
10125
- result.id || result.templateId || "created"
10126
- );
10127
- } catch (err) {
10128
- failSpinner();
10129
- handleError(err);
10130
- }
10131
- }
10132
- );
10133
- templates.command("update <template-id>").description("Update an email template").option("-n, --name <name>", "New name").option("--slug <slug>", "New slug").option("--subject <subject>", "New subject").option("--html <html>", "New HTML content").option("--activate", "Activate the template").option("--deactivate", "Deactivate the template").action(
10134
- async (templateId, options) => {
10135
- try {
10136
- if (!isLoggedIn()) throw new AuthError();
10137
- const isActive = options.activate ? true : options.deactivate ? false : void 0;
10138
- const client = getApiClient();
10139
- const _spinner = startSpinner("Updating template...");
10140
- await client.emailService.updateTemplate.mutate({
10141
- templateId,
10142
- name: options.name,
10143
- slug: options.slug,
10144
- subject: options.subject,
10145
- htmlContent: options.html,
10146
- isActive
10147
- });
10148
- succeedSpinner("Template updated.");
10149
- if (isJsonMode()) outputData({ updated: true, templateId });
10150
- } catch (err) {
10151
- failSpinner();
10152
- handleError(err);
10153
- }
10154
- }
10155
- );
10156
- templates.command("delete <template-id>").alias("rm").description("Delete an email template").action(async (templateId) => {
10157
- try {
10158
- if (!isLoggedIn()) throw new AuthError();
10159
- if (!shouldSkipConfirmation()) {
10160
- const confirmed = await confirm(
10161
- `Delete template "${templateId}"?`,
10162
- false,
10163
- {
10164
- field: "confirm_delete_template",
10165
- flag: "--yes",
10166
- context: { templateId }
10167
- }
10168
- );
10169
- if (!confirmed) {
10170
- log("Cancelled.");
10171
- return;
10172
- }
10173
- }
10174
- const client = getApiClient();
10175
- const _spinner = startSpinner("Deleting template...");
10176
- await client.emailService.deleteTemplate.mutate({
10177
- templateId
10178
- });
10179
- succeedSpinner("Template deleted.");
10180
- if (isJsonMode()) outputData({ deleted: true, templateId });
10181
- } catch (err) {
10182
- failSpinner();
10183
- handleError(err);
10184
- }
10185
- });
10186
- templates.command("seed").description("Seed pre-built templates for an environment").option("-e, --env <environment-id>", "Environment ID").action(async (options) => {
10187
- try {
10188
- if (!isLoggedIn()) throw new AuthError();
10189
- const environmentId = options.env || await input("Environment ID:", void 0, {
10190
- field: "environment_id",
10191
- flag: "--env"
10192
- });
10193
- const client = getApiClient();
10194
- const _spinner = startSpinner("Seeding templates...");
10195
- const result = await client.emailService.seedPrebuiltTemplates.mutate({
10196
- environmentId
10197
- });
10198
- succeedSpinner("Templates seeded.");
10199
- if (isJsonMode()) outputData(result);
10200
- } catch (err) {
10201
- failSpinner();
10202
- handleError(err);
10203
- }
10204
- });
10205
- email.command("send").description("Send an email").option("-e, --env <environment-id>", "Environment ID").option("--to <email>", "Recipient email").option("--subject <subject>", "Email subject").option("--html <html>", "HTML body").option("--text <text>", "Plain text body").option("--from <email>", "Override from email").action(
10206
- async (options) => {
10207
- try {
10208
- if (!isLoggedIn()) throw new AuthError();
10209
- const environmentId = options.env || await input("Environment ID:", void 0, {
10210
- field: "environment_id",
10211
- flag: "--env"
10212
- });
10213
- const to = options.to || await input("Recipient email:", void 0, {
10214
- field: "recipient_email",
10215
- flag: "--to"
10216
- });
10217
- const subject = options.subject || await input("Subject:", void 0, {
10218
- field: "email_subject",
10219
- flag: "--subject"
10220
- });
10221
- const html = options.html || options.text;
10222
- const client = getApiClient();
10223
- const _spinner = startSpinner("Sending email...");
10224
- const result = await client.emailService.sendEmail.mutate({
10225
- environmentId,
10226
- to,
10227
- subject,
10228
- html,
10229
- text: options.text,
10230
- from: options.from
10231
- });
10232
- succeedSpinner("Email sent.");
10233
- if (isJsonMode()) outputData(result);
10234
- } catch (err) {
10235
- failSpinner();
10236
- handleError(err);
10237
- }
10238
- }
10239
- );
10240
- email.command("send-template").description("Send a templated email").option("-e, --env <environment-id>", "Environment ID").option("--template-id <id>", "Template ID").option("--to <email>", "Recipient email").option("--vars <json>", "Template variables as JSON string").action(
10241
- async (options) => {
10242
- try {
10243
- if (!isLoggedIn()) throw new AuthError();
10244
- const environmentId = options.env || await input("Environment ID:", void 0, {
10245
- field: "environment_id",
10246
- flag: "--env"
10247
- });
10248
- const templateId = options.templateId || await input("Template ID:", void 0, {
10249
- field: "template_id",
10250
- flag: "--template-id"
10251
- });
10252
- const to = options.to || await input("Recipient email:", void 0, {
10253
- field: "recipient_email",
10254
- flag: "--to"
10255
- });
10256
- const variables = options.vars ? JSON.parse(options.vars) : {};
10257
- const client = getApiClient();
10258
- const _spinner = startSpinner("Sending templated email...");
10259
- const result = await client.emailService.sendTemplatedEmail.mutate({
10260
- environmentId,
10261
- templateId,
10262
- to,
10263
- variables
10264
- });
10265
- succeedSpinner("Email sent.");
10266
- if (isJsonMode()) outputData(result);
10267
- } catch (err) {
10268
- failSpinner();
10269
- handleError(err);
10270
- }
10271
- }
10272
- );
10273
- email.command("logs").description("View email sending logs").option("-e, --env <environment-id>", "Environment ID").option("-n, --limit <n>", "Number of logs to show", "50").option("--status <status>", "Filter by status (sent, failed, bounced)").option("--to <email>", "Filter by recipient").action(
10274
- async (options) => {
10275
- try {
10276
- if (!isLoggedIn()) throw new AuthError();
10277
- const environmentId = options.env || await input("Environment ID:", void 0, {
10278
- field: "environment_id",
10279
- flag: "--env"
10280
- });
10281
- const client = getApiClient();
10282
- const _spinner = startSpinner("Fetching logs...");
10283
- const logs = await client.emailService.getEmailLogs.query({
10284
- environmentId,
10285
- status: options.status,
10286
- to: options.to,
10287
- limit: Number.parseInt(options.limit || "50")
10288
- });
10289
- succeedSpinner();
10290
- if (isJsonMode()) {
10291
- outputData(logs);
10292
- return;
10293
- }
10294
- const list = Array.isArray(logs) ? logs : logs?.items || [];
10295
- if (list.length === 0) {
10296
- log("No email logs found.");
10297
- return;
10298
- }
10299
- log("");
10300
- table(
10301
- ["DATE", "TO", "SUBJECT", "STATUS"],
10302
- list.map((l) => [
10303
- l.createdAt ? new Date(l.createdAt).toLocaleString() : "-",
10304
- l.to || "-",
10305
- (l.subject || "-").slice(0, 40),
10306
- l.status === "sent" ? colors.success(l.status) : l.status === "failed" || l.status === "bounced" ? colors.error(l.status) : colors.warn(l.status || "-")
10307
- ])
10308
- );
10309
- log("");
10310
- } catch (err) {
10311
- failSpinner();
10312
- handleError(err);
10313
- }
10314
- }
10315
- );
10316
- email.command("stats").description("Show email sending statistics").option("-e, --env <environment-id>", "Environment ID").option("--start <date>", "Start date (ISO)").option("--end <date>", "End date (ISO)").action(async (options) => {
10317
- try {
10318
- if (!isLoggedIn()) throw new AuthError();
10319
- const environmentId = options.env || await input("Environment ID:", void 0, {
10320
- field: "environment_id",
10321
- flag: "--env"
10322
- });
10323
- const client = getApiClient();
10324
- const _spinner = startSpinner("Fetching stats...");
10325
- const stats = await client.emailService.getEmailStats.query({
10326
- environmentId,
10327
- startDate: options.start,
10328
- endDate: options.end
10329
- });
10330
- succeedSpinner();
10331
- if (isJsonMode()) {
10332
- outputData(stats);
10333
- return;
10334
- }
10335
- const s = stats;
10336
- log("");
10337
- log(colors.bold("Email Statistics"));
10338
- log(` Total Sent: ${colors.cyan(String(s.sent || s.total || 0))}`);
10339
- log(` Delivered: ${colors.success(String(s.delivered || 0))}`);
10340
- log(` Bounced: ${colors.error(String(s.bounced || 0))}`);
10341
- log(` Failed: ${colors.error(String(s.failed || 0))}`);
10342
- log(` Open Rate: ${s.openRate ? `${s.openRate}%` : "-"}`);
10343
- log(` Click Rate: ${s.clickRate ? `${s.clickRate}%` : "-"}`);
10344
- log("");
10345
- } catch (err) {
10346
- failSpinner();
10347
- handleError(err);
10348
- }
10349
- });
10350
- const apiKeys = email.command("api-keys").description("Manage email service API keys");
10351
- apiKeys.command("list").alias("ls").description("List email service API keys").option("-e, --env <environment-id>", "Environment ID").action(async (options) => {
10352
- try {
10353
- if (!isLoggedIn()) throw new AuthError();
10354
- const environmentId = options.env || await input("Environment ID:", void 0, {
10355
- field: "environment_id",
10356
- flag: "--env"
10357
- });
10358
- const client = getApiClient();
10359
- const _spinner = startSpinner("Fetching API keys...");
10360
- const list = await client.emailService.listApiKeys.query({
10361
- environmentId
10362
- });
10363
- succeedSpinner();
10364
- if (isJsonMode()) {
10365
- outputData(list);
10366
- return;
10367
- }
10368
- const items = Array.isArray(list) ? list : [];
10369
- if (items.length === 0) {
10370
- log("No API keys found.");
10371
- return;
10372
- }
10373
- log("");
10374
- table(
10375
- ["NAME", "MODE", "ID", "CREATED"],
10376
- items.map((k) => [
10377
- k.name || "-",
10378
- k.mode || "-",
10379
- colors.dim(k.id || k.apiKeyId || "-"),
10380
- k.createdAt ? new Date(k.createdAt).toLocaleDateString() : "-"
10381
- ])
10382
- );
10383
- log("");
10384
- } catch (err) {
10385
- failSpinner();
10386
- handleError(err);
10387
- }
10388
- });
10389
- apiKeys.command("create").description("Create an email service API key").option("-e, --env <environment-id>", "Environment ID").option("-n, --name <name>", "Key name").option("--mode <mode>", "Key mode (sending/full)", "sending").action(async (options) => {
10390
- try {
10391
- if (!isLoggedIn()) throw new AuthError();
10392
- const environmentId = options.env || await input("Environment ID:", void 0, {
10393
- field: "environment_id",
10394
- flag: "--env"
10395
- });
10396
- const name = options.name || await input("API key name:", void 0, {
10397
- field: "api_key_name",
10398
- flag: "--name"
10399
- });
10400
- const client = getApiClient();
10401
- const _spinner = startSpinner("Creating API key...");
10402
- const result = await client.emailService.createApiKey.mutate({
10403
- environmentId,
10404
- name,
10405
- mode: options.mode || "sending"
10406
- });
10407
- succeedSpinner("API key created.");
10408
- if (isJsonMode()) outputData(result);
10409
- else {
10410
- const r = result;
10411
- log("");
10412
- log(colors.warn("Save this key \u2014 it won't be shown again!"));
10413
- log(`Key: ${colors.cyan(r.key || r.token || "-")}`);
10414
- log("");
10415
- }
10416
- } catch (err) {
10417
- failSpinner();
10418
- handleError(err);
10419
- }
10420
- });
10421
- apiKeys.command("update <api-key-id>").description("Update an email service API key").option("-n, --name <name>", "New name").action(async (apiKeyId, options) => {
10422
- try {
10423
- if (!isLoggedIn()) throw new AuthError();
10424
- const name = options.name || await input("New name:", void 0, {
10425
- field: "new_name",
10426
- flag: "--name"
10427
- });
10428
- const client = getApiClient();
10429
- const _spinner = startSpinner("Updating API key...");
10430
- await client.emailService.updateApiKey.mutate({
10431
- apiKeyId,
10432
- name
10433
- });
10434
- succeedSpinner("API key updated.");
10435
- if (isJsonMode()) outputData({ updated: true, apiKeyId });
10436
- } catch (err) {
10437
- failSpinner();
10438
- handleError(err);
10439
- }
10440
- });
10441
- apiKeys.command("revoke <api-key-id>").description("Revoke an email service API key").action(async (apiKeyId) => {
10442
- try {
10443
- if (!isLoggedIn()) throw new AuthError();
10444
- if (!shouldSkipConfirmation()) {
10445
- const confirmed = await confirm(
10446
- `Revoke API key "${apiKeyId}"?`,
10447
- false,
10448
- {
10449
- field: "confirm_revoke_api_key",
10450
- flag: "--yes",
10451
- context: { apiKeyId }
10452
- }
10453
- );
10454
- if (!confirmed) {
10455
- log("Cancelled.");
10456
- return;
10457
- }
10458
- }
10459
- const client = getApiClient();
10460
- const _spinner = startSpinner("Revoking API key...");
10461
- await client.emailService.revokeApiKey.mutate({ apiKeyId });
10462
- succeedSpinner("API key revoked.");
10463
- if (isJsonMode()) outputData({ revoked: true, apiKeyId });
10464
- } catch (err) {
10465
- failSpinner();
10466
- handleError(err);
10467
- }
10468
- });
10469
- apiKeys.command("delete <api-key-id>").alias("rm").description("Delete an email service API key").action(async (apiKeyId) => {
10470
- try {
10471
- if (!isLoggedIn()) throw new AuthError();
10472
- if (!shouldSkipConfirmation()) {
10473
- const confirmed = await confirm(
10474
- `Delete API key "${apiKeyId}"?`,
10475
- false,
10476
- {
10477
- field: "confirm_delete_api_key",
10478
- flag: "--yes",
10479
- context: { apiKeyId }
10480
- }
10481
- );
10482
- if (!confirmed) {
10483
- log("Cancelled.");
10484
- return;
10485
- }
10486
- }
10487
- const client = getApiClient();
10488
- const _spinner = startSpinner("Deleting API key...");
10489
- await client.emailService.deleteApiKey.mutate({ apiKeyId });
10490
- succeedSpinner("API key deleted.");
10491
- if (isJsonMode()) outputData({ deleted: true, apiKeyId });
10492
- } catch (err) {
10493
- failSpinner();
10494
- handleError(err);
10495
- }
10496
- });
10497
- }
10498
-
10499
- // src/commands/env.ts
10500
- import { chmodSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
10501
- function registerEnvCommands(program2) {
10502
- const env = program2.command("env").argument("<app>", "Application ID or name").description("Manage environment variables");
10503
- env.command("list").alias("ls").description("List all environment variables").option("--reveal", "Show actual values (not masked)").action(async (options, command) => {
10504
- try {
10505
- if (!isLoggedIn()) throw new AuthError();
10506
- const appIdentifier = command.parent.args[0];
10507
- const client = getApiClient();
10508
- const _spinner = startSpinner("Fetching environment variables...");
10509
- const apps = await client.application.allByOrganization.query();
10510
- const app = findApp6(apps, appIdentifier);
10511
- if (!app) {
10512
- failSpinner();
10513
- const suggestions = findSimilar(
10514
- appIdentifier,
10515
- apps.map((a) => a.name)
10516
- );
10517
- throw new NotFoundError("Application", appIdentifier, suggestions);
10518
- }
10519
- const variables = await client.envVariable.list.query({
10520
- applicationId: app.applicationId,
10521
- includeValues: options.reveal || false
10522
- });
10523
- succeedSpinner();
10524
- if (isJsonMode()) {
10525
- outputData(variables);
10526
- return;
10527
- }
10528
- if (variables.length === 0) {
10529
- log("");
10530
- log("No environment variables found.");
10531
- log("");
10532
- log(
10533
- `Set one with: ${colors.dim(`tarout env ${app.name} set KEY=value`)}`
10534
- );
10535
- return;
10536
- }
10537
- log("");
10538
- table(
10539
- ["KEY", "VALUE", "SECRET", "UPDATED"],
10540
- variables.map((v) => [
10541
- colors.cyan(v.key),
10542
- options.reveal ? v.value || colors.dim("-") : maskValue(v.value),
10543
- v.isSecret ? colors.warn("Yes") : "No",
10544
- formatDate7(v.updatedAt)
10545
- ])
10546
- );
10547
- log("");
10548
- log(
10549
- colors.dim(
10550
- `${variables.length} variable${variables.length === 1 ? "" : "s"}`
10551
- )
10552
- );
10553
- } catch (err) {
10554
- handleError(err);
10555
- }
10556
- });
10557
- env.command("set").argument("<key=value>", "Variable to set (KEY=value format)").description("Set an environment variable").option("-s, --secret", "Mark as secret (default)", true).option("--no-secret", "Mark as non-secret").action(async (keyValue, options, command) => {
10422
+ env.command("set").argument("<key=value>", "Variable to set (KEY=value format)").description("Set an environment variable").option("-s, --secret", "Mark as secret (default)", true).option("--no-secret", "Mark as non-secret").action(async (keyValue, options, command) => {
10558
10423
  try {
10559
10424
  if (!isLoggedIn()) throw new AuthError();
10560
10425
  const appIdentifier = command.parent.parent.args[0];
@@ -10657,7 +10522,7 @@ function registerEnvCommands(program2) {
10657
10522
  );
10658
10523
  throw new NotFoundError("Application", appIdentifier, suggestions);
10659
10524
  }
10660
- if (existsSync3(options.output) && !shouldSkipConfirmation()) {
10525
+ if (existsSync4(options.output) && !shouldSkipConfirmation()) {
10661
10526
  succeedSpinner();
10662
10527
  const confirmed = await confirm(
10663
10528
  `File ${options.output} already exists. Overwrite?`,
@@ -10678,7 +10543,7 @@ function registerEnvCommands(program2) {
10678
10543
  format: "dotenv",
10679
10544
  maskSecrets: !options.reveal
10680
10545
  });
10681
- writeFileSync(options.output, result.content, { mode: 384 });
10546
+ writeFileSync2(options.output, result.content, { mode: 384 });
10682
10547
  try {
10683
10548
  chmodSync(options.output, 384);
10684
10549
  } catch {
@@ -10697,10 +10562,10 @@ function registerEnvCommands(program2) {
10697
10562
  try {
10698
10563
  if (!isLoggedIn()) throw new AuthError();
10699
10564
  const appIdentifier = command.parent.parent.args[0];
10700
- if (!existsSync3(options.input)) {
10565
+ if (!existsSync4(options.input)) {
10701
10566
  throw new InvalidArgumentError(`File not found: ${options.input}`);
10702
10567
  }
10703
- const content = readFileSync3(options.input, "utf-8");
10568
+ const content = readFileSync4(options.input, "utf-8");
10704
10569
  const client = getApiClient();
10705
10570
  const _spinner = startSpinner("Uploading environment variables...");
10706
10571
  const apps = await client.application.allByOrganization.query();
@@ -11406,32 +11271,247 @@ function registerInboxCommands(program2) {
11406
11271
  });
11407
11272
  inbox.command("clear").description("Delete all notifications").action(async () => {
11408
11273
  try {
11409
- if (!isLoggedIn()) throw new AuthError();
11410
- if (!shouldSkipConfirmation()) {
11411
- const confirmed = await confirm(
11412
- "Clear all notifications? This cannot be undone.",
11413
- false,
11414
- {
11415
- field: "confirm_clear_notifications",
11416
- flag: "--yes"
11274
+ if (!isLoggedIn()) throw new AuthError();
11275
+ if (!shouldSkipConfirmation()) {
11276
+ const confirmed = await confirm(
11277
+ "Clear all notifications? This cannot be undone.",
11278
+ false,
11279
+ {
11280
+ field: "confirm_clear_notifications",
11281
+ flag: "--yes"
11282
+ }
11283
+ );
11284
+ if (!confirmed) {
11285
+ log("Cancelled.");
11286
+ return;
11287
+ }
11288
+ }
11289
+ const client = getApiClient();
11290
+ const _spinner = startSpinner("Clearing notifications...");
11291
+ await client.appNotification.clearAll.mutate();
11292
+ succeedSpinner("All notifications cleared.");
11293
+ if (isJsonMode()) outputData({ cleared: true });
11294
+ } catch (err) {
11295
+ failSpinner();
11296
+ handleError(err);
11297
+ }
11298
+ });
11299
+ }
11300
+
11301
+ // src/commands/init.ts
11302
+ import { existsSync as existsSync5, writeFileSync as writeFileSync3 } from "fs";
11303
+ import { basename as basename2, join as join4, resolve } from "path";
11304
+ var DEFAULT_REGION2 = "me-central2";
11305
+ var STARTER_INDEX_JS = `import { createServer } from "node:http";
11306
+
11307
+ const port = process.env.PORT || 3000;
11308
+
11309
+ createServer((_req, res) => {
11310
+ res.writeHead(200, { "content-type": "application/json" });
11311
+ res.end(JSON.stringify({ ok: true, message: "Hello from Tarout" }));
11312
+ }).listen(port, () => {
11313
+ console.log(\`Listening on http://localhost:\${port}\`);
11314
+ });
11315
+ `;
11316
+ function toPackageName(name) {
11317
+ return name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "tarout-app";
11318
+ }
11319
+ function scaffoldStarter(cwd) {
11320
+ const written = [];
11321
+ const pkgPath = join4(cwd, "package.json");
11322
+ if (existsSync5(pkgPath)) return written;
11323
+ const pkg = {
11324
+ name: toPackageName(basename2(cwd) || "tarout-app"),
11325
+ version: "0.1.0",
11326
+ private: true,
11327
+ type: "module",
11328
+ scripts: { start: "node index.js", dev: "node index.js" }
11329
+ };
11330
+ writeFileSync3(pkgPath, `${JSON.stringify(pkg, null, 2)}
11331
+ `);
11332
+ written.push("package.json");
11333
+ const indexPath = join4(cwd, "index.js");
11334
+ if (!existsSync5(indexPath)) {
11335
+ writeFileSync3(indexPath, STARTER_INDEX_JS);
11336
+ written.push("index.js");
11337
+ }
11338
+ return written;
11339
+ }
11340
+ function envValueNeedsQuote(value) {
11341
+ return /\s|#|"|'/.test(value);
11342
+ }
11343
+ function writeEnvFile(cwd, vars) {
11344
+ const lines = Object.entries(vars).map(
11345
+ ([k, v]) => `${k}=${envValueNeedsQuote(v) ? JSON.stringify(v) : v}`
11346
+ );
11347
+ const primary = join4(cwd, ".env");
11348
+ const target = existsSync5(primary) ? join4(cwd, ".env.tarout") : primary;
11349
+ writeFileSync3(target, `${lines.join("\n")}
11350
+ `, { mode: 384 });
11351
+ return target;
11352
+ }
11353
+ function registerInitCommand(program2) {
11354
+ program2.command("init").argument("[path]", "Project directory (defaults to current)").description(
11355
+ "Provision a project backend (app, database, storage, env) and link it \u2014 the start of init \u2192 dev \u2192 deploy"
11356
+ ).option(
11357
+ "--api-url <url>",
11358
+ "Custom API URL (defaults to saved profile or https://tarout.sa)"
11359
+ ).option("--token <token>", "API token for this run").option("--name <name>", "Application name (defaults to directory name)").option(
11360
+ "--plan <plan>",
11361
+ "App hosting plan: free, shared, or dedicated",
11362
+ "free"
11363
+ ).option("-r, --region <region>", "Deployment region", DEFAULT_REGION2).option("--description <text>", "Description for the newly created app").option(
11364
+ "--database <type>",
11365
+ "Provision a database: none, postgres, or mysql (defaults to auto-detected)"
11366
+ ).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) => {
11367
+ try {
11368
+ const cwd = cwdArg ? resolve(cwdArg) : process.cwd();
11369
+ if (cwdArg) process.chdir(cwd);
11370
+ if (options.scaffold) {
11371
+ const files = scaffoldStarter(cwd);
11372
+ emitEvent({ event: "scaffold_done", files });
11373
+ }
11374
+ emitEvent({ event: "inspect_started", cwd });
11375
+ const inspection = inspectCurrentProject(cwd);
11376
+ emitEvent({
11377
+ event: "inspect_done",
11378
+ database: inspection.database,
11379
+ storage: inspection.storage,
11380
+ git: inspection.git
11381
+ });
11382
+ emitEvent({ event: "auth_check_started" });
11383
+ const profile = await ensureAuthenticatedForDeploy({
11384
+ apiUrl: options.apiUrl,
11385
+ token: options.token,
11386
+ region: options.region
11387
+ });
11388
+ emitEvent({
11389
+ event: "auth_check_done",
11390
+ userEmail: profile.userEmail,
11391
+ organization: profile.organizationName
11392
+ });
11393
+ const client = getApiClient();
11394
+ let app;
11395
+ let createdApp = false;
11396
+ const linked = getProjectConfig();
11397
+ if (linked) {
11398
+ const apps = await client.application.allByOrganization.query();
11399
+ const existing = findApp3(apps, linked.applicationId) ?? findApp3(apps, linked.name);
11400
+ if (existing) {
11401
+ app = existing;
11402
+ emitEvent({
11403
+ event: "app_reused",
11404
+ applicationId: app.applicationId,
11405
+ name: app.name
11406
+ });
11407
+ }
11408
+ }
11409
+ if (!app) {
11410
+ try {
11411
+ emitEvent({ event: "app_create_started" });
11412
+ app = await createAppFromCurrentDirectory(client, profile, {
11413
+ name: options.name,
11414
+ plan: options.plan,
11415
+ region: options.region,
11416
+ description: options.description
11417
+ });
11418
+ createdApp = true;
11419
+ emitEvent({
11420
+ event: "app_create_done",
11421
+ applicationId: app.applicationId,
11422
+ name: app.name,
11423
+ plan: app.plan ?? options.plan ?? "free"
11424
+ });
11425
+ } catch (err) {
11426
+ if (isEntitlementError(err)) {
11427
+ const message = err instanceof Error ? err.message : "Plan upgrade required";
11428
+ outputError("NEEDS_UPGRADE", message, {
11429
+ suggestedPlan: inferSuggestedPlan(options.plan),
11430
+ failedEntitlementKey: extractEntitlementKeyFromError(err),
11431
+ hint: "Run `tarout billing upgrade <plan> --wait` to add slots, then retry `tarout init`."
11432
+ });
11433
+ exit(ExitCode.PERMISSION_DENIED);
11417
11434
  }
11435
+ throw err;
11436
+ }
11437
+ }
11438
+ if (!app) {
11439
+ throw new AuthError();
11440
+ }
11441
+ if (createdApp) {
11442
+ emitEvent({
11443
+ event: "provision_started",
11444
+ database: inspection.database,
11445
+ storage: inspection.storage
11446
+ });
11447
+ await configureOptionalResources(
11448
+ client,
11449
+ profile,
11450
+ app,
11451
+ {
11452
+ database: options.database,
11453
+ databasePlan: options.databasePlan,
11454
+ storage: options.storage,
11455
+ storagePlan: options.storagePlan
11456
+ },
11457
+ inspection
11418
11458
  );
11419
- if (!confirmed) {
11420
- log("Cancelled.");
11421
- return;
11459
+ emitEvent({ event: "provision_done" });
11460
+ }
11461
+ let envFile = null;
11462
+ let envCount = 0;
11463
+ if (options.envWrite !== false) {
11464
+ try {
11465
+ const variables = await client.envVariable.list.query({
11466
+ applicationId: app.applicationId,
11467
+ includeValues: true
11468
+ });
11469
+ const vars = envVarsToObject(variables);
11470
+ envCount = Object.keys(vars).length;
11471
+ if (envCount > 0) {
11472
+ envFile = writeEnvFile(cwd, vars);
11473
+ emitEvent({ event: "env_written", file: envFile, count: envCount });
11474
+ }
11475
+ } catch (err) {
11476
+ emitEvent({
11477
+ event: "env_write_skipped",
11478
+ reason: err instanceof Error ? err.message : "unknown"
11479
+ });
11422
11480
  }
11423
11481
  }
11424
- const client = getApiClient();
11425
- const _spinner = startSpinner("Clearing notifications...");
11426
- await client.appNotification.clearAll.mutate();
11427
- succeedSpinner("All notifications cleared.");
11428
- if (isJsonMode()) outputData({ cleared: true });
11482
+ const url = formatAppUrl(app.appSubdomain);
11483
+ if (isJsonMode()) {
11484
+ outputJsonLine({
11485
+ type: "result",
11486
+ ok: true,
11487
+ applicationId: app.applicationId,
11488
+ name: app.name,
11489
+ url,
11490
+ envFile,
11491
+ envCount,
11492
+ nextSteps: ["tarout dev", "tarout deploy --wait"]
11493
+ });
11494
+ return;
11495
+ }
11496
+ box("Project initialized", [
11497
+ `Application: ${colors.cyan(app.name)}`,
11498
+ `ID: ${colors.dim(app.applicationId)}`,
11499
+ ...envFile ? [`Env file: ${colors.dim(envFile)} (${envCount} vars)`] : []
11500
+ ]);
11501
+ log("Next steps:");
11502
+ log(` ${colors.dim("tarout dev")} - Run locally with cloud env vars`);
11503
+ log(` ${colors.dim("tarout deploy --wait")} - Ship to production`);
11504
+ log("");
11429
11505
  } catch (err) {
11430
- failSpinner();
11431
11506
  handleError(err);
11432
11507
  }
11433
11508
  });
11434
11509
  }
11510
+ function emitEvent(payload) {
11511
+ if (isJsonMode()) {
11512
+ outputJsonLine({ type: "event", ...payload });
11513
+ }
11514
+ }
11435
11515
 
11436
11516
  // src/commands/keys.ts
11437
11517
  function registerKeysCommands(program2) {
@@ -11485,8 +11565,8 @@ function registerKeysCommands(program2) {
11485
11565
  });
11486
11566
  }
11487
11567
  if (!publicKey && options.file) {
11488
- const { readFileSync: readFileSync4 } = await import("fs");
11489
- publicKey = readFileSync4(options.file, "utf-8").trim();
11568
+ const { readFileSync: readFileSync5 } = await import("fs");
11569
+ publicKey = readFileSync5(options.file, "utf-8").trim();
11490
11570
  }
11491
11571
  if (!publicKey) {
11492
11572
  log(
@@ -11686,7 +11766,7 @@ function formatDate8(date) {
11686
11766
  }
11687
11767
 
11688
11768
  // src/commands/link.ts
11689
- import { basename as basename2 } from "path";
11769
+ import { basename as basename3 } from "path";
11690
11770
  function registerLinkCommands(program2) {
11691
11771
  program2.command("link").argument(
11692
11772
  "[app]",
@@ -11698,7 +11778,7 @@ function registerLinkCommands(program2) {
11698
11778
  if (!profile) throw new AuthError();
11699
11779
  const client = getApiClient();
11700
11780
  const cwd = process.cwd();
11701
- const dirName = basename2(cwd);
11781
+ const dirName = basename3(cwd);
11702
11782
  if (isProjectLinked()) {
11703
11783
  const existingConfig = getProjectConfig();
11704
11784
  if (existingConfig && !shouldSkipConfirmation() && !options.yes) {
@@ -11879,7 +11959,7 @@ function registerLinkCommands(program2) {
11879
11959
  applicationId: config.applicationId,
11880
11960
  name: config.name,
11881
11961
  status: app.applicationStatus,
11882
- url: app.appSubdomain ? `https://${app.appSubdomain}` : null,
11962
+ url: formatAppUrl(app.appSubdomain),
11883
11963
  linkedAt: config.linkedAt
11884
11964
  });
11885
11965
  return;
@@ -11891,7 +11971,7 @@ function registerLinkCommands(program2) {
11891
11971
  log(`ID: ${colors.dim(config.applicationId)}`);
11892
11972
  log(`Status: ${getStatusIndicator(app.applicationStatus)}`);
11893
11973
  if (app.appSubdomain) {
11894
- log(`URL: ${colors.cyan(`https://${app.appSubdomain}`)}`);
11974
+ log(`URL: ${colors.cyan(formatAppUrl(app.appSubdomain) ?? "")}`);
11895
11975
  }
11896
11976
  log(`Linked: ${new Date(config.linkedAt).toLocaleString()}`);
11897
11977
  log("");
@@ -16394,294 +16474,76 @@ function registerSettingsCommands(program2) {
16394
16474
  log(
16395
16475
  ` Database: ${h.db ? colors.success("ok") : colors.error("error")}`
16396
16476
  );
16397
- if (h.redis !== void 0)
16398
- log(
16399
- ` Redis: ${h.redis ? colors.success("ok") : colors.error("error")}`
16400
- );
16401
- if (h.version) log(` Version: ${colors.dim(h.version)}`);
16402
- log("");
16403
- } catch (err) {
16404
- failSpinner();
16405
- handleError(err);
16406
- }
16407
- });
16408
- settings.command("is-cloud").description("Check if running in Tarout Cloud mode").action(async () => {
16409
- try {
16410
- const client = getApiClient();
16411
- const _spinner = startSpinner("Checking deployment mode...");
16412
- const data = await client.settings.isCloud.query();
16413
- succeedSpinner();
16414
- if (isJsonMode()) {
16415
- outputData(data);
16416
- return;
16417
- }
16418
- const isCloud = data?.isCloud ?? data;
16419
- log(
16420
- `Cloud mode: ${isCloud ? colors.success("yes") : colors.dim("no")}`
16421
- );
16422
- } catch (err) {
16423
- failSpinner();
16424
- handleError(err);
16425
- }
16426
- });
16427
- settings.command("subscription-status").description("Check current subscription status").action(async () => {
16428
- try {
16429
- if (!isLoggedIn()) throw new AuthError();
16430
- const client = getApiClient();
16431
- const _spinner = startSpinner("Checking subscription...");
16432
- const data = await client.settings.isUserSubscribed.query();
16433
- succeedSpinner();
16434
- if (isJsonMode()) {
16435
- outputData(data);
16436
- return;
16437
- }
16438
- const sub = data;
16439
- log("");
16440
- log(colors.bold("Subscription Status"));
16441
- const isSubscribed = sub?.isSubscribed ?? sub;
16442
- log(
16443
- ` Subscribed: ${isSubscribed ? colors.success("yes") : colors.dim("no (free plan)")}`
16444
- );
16445
- if (sub?.planKey) log(` Plan: ${colors.cyan(sub.planKey)}`);
16446
- if (sub?.expiresAt)
16447
- log(` Expires: ${new Date(sub.expiresAt).toLocaleDateString()}`);
16448
- log("");
16449
- } catch (err) {
16450
- failSpinner();
16451
- handleError(err);
16452
- }
16453
- });
16454
- settings.command("openapi").description("Output the OpenAPI specification document").action(async () => {
16455
- try {
16456
- if (!isLoggedIn()) throw new AuthError();
16457
- const client = getApiClient();
16458
- const _spinner = startSpinner("Fetching OpenAPI spec...");
16459
- const data = await client.settings.getOpenApiDocument.query();
16460
- succeedSpinner();
16461
- outputData(data);
16462
- } catch (err) {
16463
- failSpinner();
16464
- handleError(err);
16465
- }
16466
- });
16467
- }
16468
-
16469
- // src/commands/sms.ts
16470
- function registerSmsCommands(program2) {
16471
- const sms = program2.command("sms").description("Manage SMS messaging configuration and logs");
16472
- sms.command("config").description("Show SMS configuration for an environment").option("-e, --env <environment-id>", "Environment ID").action(async (options) => {
16473
- try {
16474
- if (!isLoggedIn()) throw new AuthError();
16475
- const environmentId = options.env || await input("Environment ID:", void 0, {
16476
- field: "environment_id",
16477
- flag: "--env"
16478
- });
16479
- const client = getApiClient();
16480
- const _spinner = startSpinner("Fetching SMS config...");
16481
- const data = await client.sms.getConfig.query({ environmentId });
16482
- succeedSpinner();
16483
- if (isJsonMode()) {
16484
- outputData(data);
16485
- return;
16486
- }
16487
- const c = data;
16488
- log("");
16489
- log(colors.bold("SMS Configuration"));
16490
- log(` Provider: ${c.provider || "-"}`);
16491
- log(` From Number: ${c.fromNumber || "-"}`);
16492
- log(
16493
- ` Enabled: ${c.isEnabled ? colors.success("yes") : colors.dim("no")}`
16494
- );
16495
- log(` Env ID: ${colors.dim(environmentId)}`);
16496
- log("");
16497
- } catch (err) {
16498
- failSpinner();
16499
- handleError(err);
16500
- }
16501
- });
16502
- sms.command("setup").description("Create SMS configuration for an environment").option("-e, --env <environment-id>", "Environment ID").option("-p, --provider <provider>", "SMS provider (twilio, vonage, sinch)").option("--api-key <key>", "API key / Account SID").option("--api-secret <secret>", "API secret / Auth token").option("--from <number>", "From phone number").option("--enabled", "Enable SMS sending", true).action(
16503
- async (options) => {
16504
- try {
16505
- if (!isLoggedIn()) throw new AuthError();
16506
- const environmentId = options.env || await input("Environment ID:", void 0, {
16507
- field: "environment_id",
16508
- flag: "--env"
16509
- });
16510
- const provider = options.provider || await select(
16511
- "SMS provider:",
16512
- [
16513
- { name: "Twilio", value: "twilio" },
16514
- { name: "Vonage", value: "vonage" },
16515
- { name: "Sinch", value: "sinch" }
16516
- ],
16517
- { field: "sms_provider", flag: "--provider" }
16518
- );
16519
- const apiKey = options.apiKey || await input("API Key / Account SID:", void 0, {
16520
- field: "api_key",
16521
- flag: "--api-key",
16522
- sensitive: true
16523
- });
16524
- const apiSecret = options.apiSecret || await input("API Secret / Auth Token:", void 0, {
16525
- field: "api_secret",
16526
- flag: "--api-secret",
16527
- sensitive: true
16528
- });
16529
- const fromNumber = options.from || await input(
16530
- "From phone number (e.g. +1234567890):",
16531
- void 0,
16532
- { field: "from_number", flag: "--from" }
16533
- );
16534
- const client = getApiClient();
16535
- const _spinner = startSpinner("Creating SMS config...");
16536
- const result = await client.sms.createConfig.mutate({
16537
- environmentId,
16538
- provider,
16539
- apiKey,
16540
- apiSecret,
16541
- fromNumber,
16542
- isEnabled: options.enabled ?? true
16543
- });
16544
- succeedSpinner("SMS configuration created.");
16545
- if (isJsonMode()) outputData(result);
16546
- else quietOutput(environmentId);
16547
- } catch (err) {
16548
- failSpinner();
16549
- handleError(err);
16550
- }
16551
- }
16552
- );
16553
- sms.command("update").description("Update SMS configuration").option("-e, --env <environment-id>", "Environment ID").option("-p, --provider <provider>", "New provider").option("--api-key <key>", "New API key").option("--api-secret <secret>", "New API secret").option("--from <number>", "New from number").option("--enable", "Enable SMS").option("--disable", "Disable SMS").action(
16554
- async (options) => {
16555
- try {
16556
- if (!isLoggedIn()) throw new AuthError();
16557
- const environmentId = options.env || await input("Environment ID:", void 0, {
16558
- field: "environment_id",
16559
- flag: "--env"
16560
- });
16561
- const isEnabled = options.enable ? true : options.disable ? false : void 0;
16562
- const client = getApiClient();
16563
- const _spinner = startSpinner("Updating SMS config...");
16564
- await client.sms.updateConfig.mutate({
16565
- environmentId,
16566
- provider: options.provider,
16567
- apiKey: options.apiKey,
16568
- apiSecret: options.apiSecret,
16569
- fromNumber: options.from,
16570
- isEnabled
16571
- });
16572
- succeedSpinner("SMS configuration updated.");
16573
- if (isJsonMode()) outputData({ updated: true, environmentId });
16574
- } catch (err) {
16575
- failSpinner();
16576
- handleError(err);
16577
- }
16578
- }
16579
- );
16580
- sms.command("send").description("Send an SMS message").option("-e, --env <environment-id>", "Environment ID").option("--to <number>", "Recipient phone number").option("-m, --message <text>", "Message body").action(
16581
- async (options) => {
16582
- try {
16583
- if (!isLoggedIn()) throw new AuthError();
16584
- const environmentId = options.env || await input("Environment ID:", void 0, {
16585
- field: "environment_id",
16586
- flag: "--env"
16587
- });
16588
- const to = options.to || await input("Recipient phone number:", void 0, {
16589
- field: "recipient_phone",
16590
- flag: "--to"
16591
- });
16592
- const body = options.message || await input("Message:", void 0, {
16593
- field: "message_body",
16594
- flag: "--message"
16595
- });
16596
- const client = getApiClient();
16597
- const _spinner = startSpinner("Sending SMS...");
16598
- const result = await client.sms.send.mutate({
16599
- environmentId,
16600
- to,
16601
- body
16602
- });
16603
- succeedSpinner("SMS sent.");
16604
- if (isJsonMode()) outputData(result);
16605
- } catch (err) {
16606
- failSpinner();
16607
- handleError(err);
16608
- }
16609
- }
16610
- );
16611
- sms.command("logs").description("View SMS message logs").option("-e, --env <environment-id>", "Environment ID").option("-n, --limit <n>", "Number of logs to show", "50").option("--status <status>", "Filter by status (sent, failed, pending)").action(
16612
- async (options) => {
16613
- try {
16614
- if (!isLoggedIn()) throw new AuthError();
16615
- const environmentId = options.env || await input("Environment ID:", void 0, {
16616
- field: "environment_id",
16617
- flag: "--env"
16618
- });
16619
- const client = getApiClient();
16620
- const _spinner = startSpinner("Fetching logs...");
16621
- const logs = await client.sms.getLogs.query({
16622
- environmentId,
16623
- status: options.status,
16624
- limit: Number.parseInt(options.limit || "50")
16625
- });
16626
- succeedSpinner();
16627
- if (isJsonMode()) {
16628
- outputData(logs);
16629
- return;
16630
- }
16631
- const list = Array.isArray(logs) ? logs : logs?.items || [];
16632
- if (list.length === 0) {
16633
- log("No SMS logs found.");
16634
- return;
16635
- }
16636
- log("");
16637
- table(
16638
- ["DATE", "TO", "STATUS", "MESSAGE"],
16639
- list.map((l) => [
16640
- l.createdAt ? new Date(l.createdAt).toLocaleString() : "-",
16641
- l.to || "-",
16642
- l.status === "sent" ? colors.success(l.status) : l.status === "failed" ? colors.error(l.status) : colors.warn(l.status || "-"),
16643
- (l.body || l.message || "-").slice(0, 50)
16644
- ])
16645
- );
16646
- log("");
16647
- } catch (err) {
16648
- failSpinner();
16649
- handleError(err);
16477
+ if (h.redis !== void 0)
16478
+ log(
16479
+ ` Redis: ${h.redis ? colors.success("ok") : colors.error("error")}`
16480
+ );
16481
+ if (h.version) log(` Version: ${colors.dim(h.version)}`);
16482
+ log("");
16483
+ } catch (err) {
16484
+ failSpinner();
16485
+ handleError(err);
16486
+ }
16487
+ });
16488
+ settings.command("is-cloud").description("Check if running in Tarout Cloud mode").action(async () => {
16489
+ try {
16490
+ const client = getApiClient();
16491
+ const _spinner = startSpinner("Checking deployment mode...");
16492
+ const data = await client.settings.isCloud.query();
16493
+ succeedSpinner();
16494
+ if (isJsonMode()) {
16495
+ outputData(data);
16496
+ return;
16650
16497
  }
16498
+ const isCloud = data?.isCloud ?? data;
16499
+ log(
16500
+ `Cloud mode: ${isCloud ? colors.success("yes") : colors.dim("no")}`
16501
+ );
16502
+ } catch (err) {
16503
+ failSpinner();
16504
+ handleError(err);
16651
16505
  }
16652
- );
16653
- sms.command("stats").description("Show SMS sending statistics").option("-e, --env <environment-id>", "Environment ID").option("--start <date>", "Start date (ISO format)").option("--end <date>", "End date (ISO format)").action(async (options) => {
16506
+ });
16507
+ settings.command("subscription-status").description("Check current subscription status").action(async () => {
16654
16508
  try {
16655
16509
  if (!isLoggedIn()) throw new AuthError();
16656
- const environmentId = options.env || await input("Environment ID:", void 0, {
16657
- field: "environment_id",
16658
- flag: "--env"
16659
- });
16660
16510
  const client = getApiClient();
16661
- const _spinner = startSpinner("Fetching stats...");
16662
- const stats = await client.sms.getStats.query({
16663
- environmentId,
16664
- startDate: options.start,
16665
- endDate: options.end
16666
- });
16511
+ const _spinner = startSpinner("Checking subscription...");
16512
+ const data = await client.settings.isUserSubscribed.query();
16667
16513
  succeedSpinner();
16668
16514
  if (isJsonMode()) {
16669
- outputData(stats);
16515
+ outputData(data);
16670
16516
  return;
16671
16517
  }
16672
- const s = stats;
16518
+ const sub = data;
16673
16519
  log("");
16674
- log(colors.bold("SMS Statistics"));
16675
- log(` Total Sent: ${colors.cyan(String(s.total || s.sent || 0))}`);
16676
- log(` Delivered: ${colors.success(String(s.delivered || 0))}`);
16677
- log(` Failed: ${colors.error(String(s.failed || 0))}`);
16678
- log(` Pending: ${colors.warn(String(s.pending || 0))}`);
16520
+ log(colors.bold("Subscription Status"));
16521
+ const isSubscribed = sub?.isSubscribed ?? sub;
16522
+ log(
16523
+ ` Subscribed: ${isSubscribed ? colors.success("yes") : colors.dim("no (free plan)")}`
16524
+ );
16525
+ if (sub?.planKey) log(` Plan: ${colors.cyan(sub.planKey)}`);
16526
+ if (sub?.expiresAt)
16527
+ log(` Expires: ${new Date(sub.expiresAt).toLocaleDateString()}`);
16679
16528
  log("");
16680
16529
  } catch (err) {
16681
16530
  failSpinner();
16682
16531
  handleError(err);
16683
16532
  }
16684
16533
  });
16534
+ settings.command("openapi").description("Output the OpenAPI specification document").action(async () => {
16535
+ try {
16536
+ if (!isLoggedIn()) throw new AuthError();
16537
+ const client = getApiClient();
16538
+ const _spinner = startSpinner("Fetching OpenAPI spec...");
16539
+ const data = await client.settings.getOpenApiDocument.query();
16540
+ succeedSpinner();
16541
+ outputData(data);
16542
+ } catch (err) {
16543
+ failSpinner();
16544
+ handleError(err);
16545
+ }
16546
+ });
16685
16547
  }
16686
16548
 
16687
16549
  // src/commands/storage.ts
@@ -17862,8 +17724,8 @@ function truncate3(str, max) {
17862
17724
  }
17863
17725
 
17864
17726
  // src/commands/up.ts
17865
- import { resolve } from "path";
17866
- var DEFAULT_REGION2 = "me-central2";
17727
+ import { resolve as resolve2 } from "path";
17728
+ var DEFAULT_REGION3 = "me-central2";
17867
17729
  function normalizeSource(value) {
17868
17730
  if (!value) return "upload";
17869
17731
  const v = value.trim().toLowerCase();
@@ -17892,7 +17754,19 @@ function registerUpCommand(program2) {
17892
17754
  ).option(
17893
17755
  "--api-url <url>",
17894
17756
  "Custom API URL (defaults to saved profile or https://tarout.sa)"
17895
- ).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("--repo <owner/repo>", "GitHub repository (when --source github)").option("--branch <branch>", "GitHub branch (with --source github)", "main").option("-r, --region <region>", "Deployment region", DEFAULT_REGION2).option("--description <text>", "Description for the newly created app").option(
17757
+ ).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(
17758
+ "--app <ref>",
17759
+ "Deploy to an existing app by id or name (skips create-or-pick prompt; 'auto' picks the lone match)"
17760
+ ).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(
17761
+ "--database <type>",
17762
+ "Provision and attach a database: none, postgres, or mysql (defaults to auto-detected)"
17763
+ ).option("--database-plan <plan>", "Database plan (e.g. free, starter)").option("--storage", "Provision and attach file storage").option("--storage-plan <plan>", "Storage plan (e.g. free, starter)").option(
17764
+ "--reuse-database <ref>",
17765
+ "Reuse an existing database in this project: <id>, <name>, or 'auto'"
17766
+ ).option(
17767
+ "--reuse-storage <ref>",
17768
+ "Reuse an existing storage bucket in this project: <id>, <name>, or 'auto'"
17769
+ ).option("--description <text>", "Description for the newly created app").option(
17896
17770
  "--framework-preset <preset>",
17897
17771
  "Framework preset override (e.g. nextjs, vite, astro)"
17898
17772
  ).option("--root-directory <path>", "Project root directory inside the source").option("--install-command <cmd>", "Custom install command").option("--build-command <cmd>", "Custom build command").option(
@@ -17903,91 +17777,203 @@ function registerUpCommand(program2) {
17903
17777
  "Idempotency key for safe retries (Phase 2; logged only in v1)"
17904
17778
  ).action(async (cwdArg, options) => {
17905
17779
  try {
17906
- const cwd = cwdArg ? resolve(cwdArg) : process.cwd();
17780
+ const cwd = cwdArg ? resolve2(cwdArg) : process.cwd();
17907
17781
  if (cwdArg) process.chdir(cwd);
17908
17782
  const source = normalizeSource(options.source);
17909
17783
  const idempotencyKey = options.idempotencyKey?.trim();
17910
17784
  if (idempotencyKey) {
17911
- emitEvent({
17785
+ emitEvent2({
17912
17786
  event: "idempotency_key_received",
17913
17787
  idempotencyKey,
17914
17788
  note: "Phase 2 will use this to dedupe. Today it is logged only."
17915
17789
  });
17916
17790
  }
17917
- emitEvent({ event: "inspect_started", cwd });
17791
+ emitEvent2({ event: "inspect_started", cwd });
17918
17792
  const inspection = inspectCurrentProject(cwd);
17919
- emitEvent({
17793
+ emitEvent2({
17920
17794
  event: "inspect_done",
17921
17795
  database: inspection.database,
17922
17796
  storage: inspection.storage,
17923
17797
  git: inspection.git
17924
17798
  });
17925
- emitEvent({ event: "auth_check_started" });
17799
+ emitEvent2({ event: "auth_check_started" });
17926
17800
  const profile = await ensureAuthenticatedForDeploy({
17927
17801
  apiUrl: options.apiUrl,
17928
17802
  token: options.token,
17929
17803
  region: options.region
17930
17804
  });
17931
- emitEvent({
17805
+ emitEvent2({
17932
17806
  event: "auth_check_done",
17933
17807
  userEmail: profile.userEmail,
17934
17808
  organization: profile.organizationName
17935
17809
  });
17936
17810
  const client = getApiClient();
17937
17811
  let app;
17938
- try {
17939
- emitEvent({ event: "app_create_started" });
17940
- app = await createAppFromCurrentDirectory(client, profile, {
17941
- name: options.name,
17942
- plan: options.plan,
17943
- region: options.region,
17944
- description: options.description,
17945
- frameworkPreset: options.frameworkPreset,
17946
- rootDirectory: options.rootDirectory,
17947
- installCommand: options.installCommand,
17948
- buildCommand: options.buildCommand,
17949
- outputDirectory: options.outputDirectory,
17950
- startCommand: options.startCommand
17812
+ let reused = false;
17813
+ const linked = getProjectConfig();
17814
+ let allApps;
17815
+ const loadApps = async () => {
17816
+ if (!allApps) {
17817
+ allApps = await client.application.allByOrganization.query();
17818
+ }
17819
+ return allApps;
17820
+ };
17821
+ if (options.app) {
17822
+ const apps = await loadApps();
17823
+ const picked = resolveAppRef(apps, options.app);
17824
+ app = picked;
17825
+ reused = true;
17826
+ setProjectConfig({
17827
+ applicationId: picked.applicationId,
17828
+ name: picked.name,
17829
+ organizationId: profile.organizationId,
17830
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
17951
17831
  });
17952
- emitEvent({
17953
- event: "app_create_done",
17954
- applicationId: app.applicationId,
17955
- name: app.name,
17956
- plan: app.plan ?? options.plan ?? "free"
17832
+ emitEvent2({
17833
+ event: "app_reused",
17834
+ applicationId: picked.applicationId,
17835
+ name: picked.name,
17836
+ via: "--app"
17957
17837
  });
17958
- } catch (err) {
17959
- if (isEntitlementError(err)) {
17960
- const message = err instanceof Error ? err.message : "Plan upgrade required";
17961
- if (isJsonMode() || shouldSkipConfirmation()) {
17962
- outputError("NEEDS_UPGRADE", message, {
17963
- suggestedPlan: inferSuggestedPlan(options.plan),
17964
- failedEntitlementKey: extractEntitlementKeyFromError(err),
17965
- hint: "Run `tarout billing upgrade <plan> --wait` to add slots, then retry `tarout up`."
17966
- });
17967
- exit(ExitCode.PERMISSION_DENIED);
17968
- }
17838
+ } else if (linked) {
17839
+ const apps = await loadApps();
17840
+ const existing = findApp3(apps, linked.applicationId) ?? findApp3(apps, linked.name);
17841
+ if (existing) {
17842
+ app = existing;
17843
+ reused = true;
17844
+ emitEvent2({
17845
+ event: "app_reused",
17846
+ applicationId: app.applicationId,
17847
+ name: app.name,
17848
+ via: "linked"
17849
+ });
17850
+ }
17851
+ }
17852
+ if (!app && !isJsonMode() && !shouldSkipConfirmation()) {
17853
+ const apps = await loadApps();
17854
+ if (apps.length > 0) {
17855
+ const createValue = "__create__";
17969
17856
  log("");
17970
- log(colors.warn(message));
17971
- const upgraded = await promptUpgradeFromEntitlementError(
17972
- client,
17973
- err,
17974
- options.plan
17857
+ log(
17858
+ `Found ${apps.length} existing app${apps.length === 1 ? "" : "s"} in this organization.`
17975
17859
  );
17976
- if (!upgraded) {
17977
- outputError("NEEDS_UPGRADE", message, {
17978
- suggestedPlan: inferSuggestedPlan(options.plan),
17979
- failedEntitlementKey: extractEntitlementKeyFromError(err),
17980
- hint: "Run `tarout billing upgrade <plan> --wait`, then retry `tarout up`."
17860
+ const selected = await select(
17861
+ "Deploy to an existing app or create a new one?",
17862
+ [
17863
+ {
17864
+ name: `Create a new app${options.name ? ` named "${options.name}"` : ""}`,
17865
+ value: createValue
17866
+ },
17867
+ ...apps.map((existing) => ({
17868
+ name: `${existing.name} ${colors.dim(`(${existing.applicationId.slice(0, 8)})`)}`,
17869
+ value: existing.applicationId
17870
+ }))
17871
+ ],
17872
+ {
17873
+ field: "deploy_target_app",
17874
+ flag: "--app <id|name|auto>"
17875
+ }
17876
+ );
17877
+ if (selected !== createValue) {
17878
+ const picked = findApp3(apps, selected);
17879
+ if (!picked) {
17880
+ throw new NotFoundError("Application", selected);
17881
+ }
17882
+ app = picked;
17883
+ reused = true;
17884
+ setProjectConfig({
17885
+ applicationId: picked.applicationId,
17886
+ name: picked.name,
17887
+ organizationId: profile.organizationId,
17888
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
17889
+ });
17890
+ emitEvent2({
17891
+ event: "app_reused",
17892
+ applicationId: picked.applicationId,
17893
+ name: picked.name,
17894
+ via: "interactive"
17981
17895
  });
17982
- exit(ExitCode.PERMISSION_DENIED);
17983
17896
  }
17984
- box("Upgrade complete", [
17985
- colors.success("Subscription updated."),
17986
- `Run ${colors.cyan("tarout up")} again to deploy on the new plan.`
17987
- ]);
17988
- return;
17989
17897
  }
17990
- throw err;
17898
+ }
17899
+ if (!reused) {
17900
+ try {
17901
+ emitEvent2({ event: "app_create_started" });
17902
+ app = await createAppFromCurrentDirectory(client, profile, {
17903
+ name: options.name,
17904
+ plan: options.plan,
17905
+ region: options.region,
17906
+ description: options.description,
17907
+ frameworkPreset: options.frameworkPreset,
17908
+ rootDirectory: options.rootDirectory,
17909
+ installCommand: options.installCommand,
17910
+ buildCommand: options.buildCommand,
17911
+ outputDirectory: options.outputDirectory,
17912
+ startCommand: options.startCommand
17913
+ });
17914
+ emitEvent2({
17915
+ event: "app_create_done",
17916
+ applicationId: app.applicationId,
17917
+ name: app.name,
17918
+ plan: app.plan ?? options.plan ?? "free"
17919
+ });
17920
+ } catch (err) {
17921
+ if (isEntitlementError(err)) {
17922
+ const message = err instanceof Error ? err.message : "Plan upgrade required";
17923
+ if (isJsonMode() || shouldSkipConfirmation()) {
17924
+ outputError("NEEDS_UPGRADE", message, {
17925
+ suggestedPlan: inferSuggestedPlan(options.plan),
17926
+ failedEntitlementKey: extractEntitlementKeyFromError(err),
17927
+ hint: "Run `tarout billing upgrade <plan> --wait` to add slots, then retry `tarout up`."
17928
+ });
17929
+ exit(ExitCode.PERMISSION_DENIED);
17930
+ }
17931
+ log("");
17932
+ log(colors.warn(message));
17933
+ const upgraded = await promptUpgradeFromEntitlementError(
17934
+ client,
17935
+ err,
17936
+ options.plan
17937
+ );
17938
+ if (!upgraded) {
17939
+ outputError("NEEDS_UPGRADE", message, {
17940
+ suggestedPlan: inferSuggestedPlan(options.plan),
17941
+ failedEntitlementKey: extractEntitlementKeyFromError(err),
17942
+ hint: "Run `tarout billing upgrade <plan> --wait`, then retry `tarout up`."
17943
+ });
17944
+ exit(ExitCode.PERMISSION_DENIED);
17945
+ }
17946
+ box("Upgrade complete", [
17947
+ colors.success("Subscription updated."),
17948
+ `Run ${colors.cyan("tarout up")} again to deploy on the new plan.`
17949
+ ]);
17950
+ return;
17951
+ }
17952
+ throw err;
17953
+ }
17954
+ emitEvent2({
17955
+ event: "provision_started",
17956
+ database: inspection.database,
17957
+ storage: inspection.storage
17958
+ });
17959
+ await configureOptionalResources(
17960
+ client,
17961
+ profile,
17962
+ app,
17963
+ {
17964
+ database: options.database,
17965
+ databasePlan: options.databasePlan,
17966
+ storage: options.storage,
17967
+ storagePlan: options.storagePlan,
17968
+ reuseDatabase: options.reuseDatabase,
17969
+ reuseStorage: options.reuseStorage
17970
+ },
17971
+ inspection
17972
+ );
17973
+ emitEvent2({ event: "provision_done" });
17974
+ }
17975
+ if (!app) {
17976
+ throw new Error("Failed to resolve the target application.");
17991
17977
  }
17992
17978
  if (source === "github") {
17993
17979
  if (!options.repo) {
@@ -17996,7 +17982,7 @@ function registerUpCommand(program2) {
17996
17982
  );
17997
17983
  }
17998
17984
  const { owner, repository } = parseRepo(options.repo);
17999
- emitEvent({
17985
+ emitEvent2({
18000
17986
  event: "github_connect_started",
18001
17987
  owner,
18002
17988
  repository,
@@ -18026,24 +18012,24 @@ function registerUpCommand(program2) {
18026
18012
  watchPaths: null,
18027
18013
  enableSubmodules: false
18028
18014
  });
18029
- emitEvent({
18015
+ emitEvent2({
18030
18016
  event: "github_connect_done",
18031
18017
  repository: `${owner}/${repository}`
18032
18018
  });
18033
18019
  } else {
18034
- emitEvent({ event: "upload_started" });
18020
+ emitEvent2({ event: "upload_started" });
18035
18021
  await uploadCurrentDirectorySource(
18036
18022
  client,
18037
18023
  app.applicationId,
18038
18024
  app.name
18039
18025
  );
18040
- emitEvent({ event: "upload_done" });
18026
+ emitEvent2({ event: "upload_done" });
18041
18027
  }
18042
- emitEvent({ event: "deploy_started", applicationId: app.applicationId });
18028
+ emitEvent2({ event: "deploy_started", applicationId: app.applicationId });
18043
18029
  const result = await client.application.deployToCloud.mutate({
18044
18030
  applicationId: app.applicationId
18045
18031
  });
18046
- emitEvent({
18032
+ emitEvent2({
18047
18033
  event: "deploy_enqueued",
18048
18034
  deploymentId: result.deploymentId
18049
18035
  });
@@ -18068,11 +18054,41 @@ function registerUpCommand(program2) {
18068
18054
  }
18069
18055
  });
18070
18056
  }
18071
- function emitEvent(payload) {
18057
+ function emitEvent2(payload) {
18072
18058
  if (isJsonMode()) {
18073
18059
  outputJsonLine({ type: "event", ...payload });
18074
18060
  }
18075
18061
  }
18062
+ function resolveAppRef(apps, ref) {
18063
+ const normalized = ref.trim();
18064
+ if (normalized.toLowerCase() === "auto") {
18065
+ if (apps.length === 0) {
18066
+ throw new InvalidArgumentError(
18067
+ "--app=auto: no existing apps in this organization. Drop --app to create a new one."
18068
+ );
18069
+ }
18070
+ if (apps.length > 1) {
18071
+ const sample = apps.slice(0, 5).map((a) => `${a.name} (${a.applicationId.slice(0, 8)})`).join(", ");
18072
+ throw new InvalidArgumentError(
18073
+ `--app=auto needs exactly one match, found ${apps.length}. Candidates: ${sample}. Pick one by id or name.`
18074
+ );
18075
+ }
18076
+ return apps[0];
18077
+ }
18078
+ const match = findApp3(apps, normalized);
18079
+ if (match) return match;
18080
+ const suggestions = findSimilar(
18081
+ normalized,
18082
+ apps.flatMap((a) => [a.name, a.applicationId])
18083
+ );
18084
+ throw new NotFoundError(
18085
+ "Application",
18086
+ normalized,
18087
+ suggestions.length > 0 ? suggestions : apps.length > 0 ? apps.slice(0, 5).map((a) => `${a.name} (${a.applicationId.slice(0, 8)})`) : [
18088
+ "No apps exist in this organization yet. Drop --app to create a new one."
18089
+ ]
18090
+ );
18091
+ }
18076
18092
 
18077
18093
  // src/commands/wallet.ts
18078
18094
  function registerWalletCommands(program2) {
@@ -18245,214 +18261,6 @@ function truncate4(str, max) {
18245
18261
  return `${str.slice(0, max - 3)}...`;
18246
18262
  }
18247
18263
 
18248
- // src/commands/whatsapp.ts
18249
- function registerWhatsappCommands(program2) {
18250
- const wa = program2.command("whatsapp").description("Manage WhatsApp messaging configuration and logs");
18251
- wa.command("config").description("Show WhatsApp configuration for an environment").option("-e, --env <environment-id>", "Environment ID").action(async (options) => {
18252
- try {
18253
- if (!isLoggedIn()) throw new AuthError();
18254
- const environmentId = options.env || await input("Environment ID:", void 0, {
18255
- field: "environment_id",
18256
- flag: "--env"
18257
- });
18258
- const client = getApiClient();
18259
- const _spinner = startSpinner("Fetching WhatsApp config...");
18260
- const data = await client.whatsapp.getConfig.query({ environmentId });
18261
- succeedSpinner();
18262
- if (isJsonMode()) {
18263
- outputData(data);
18264
- return;
18265
- }
18266
- const c = data;
18267
- log("");
18268
- log(colors.bold("WhatsApp Configuration"));
18269
- log(` Provider: ${c.provider || "-"}`);
18270
- log(` Number: ${c.whatsappNumber || "-"}`);
18271
- log(
18272
- ` Enabled: ${c.isEnabled ? colors.success("yes") : colors.dim("no")}`
18273
- );
18274
- log("");
18275
- } catch (err) {
18276
- failSpinner();
18277
- handleError(err);
18278
- }
18279
- });
18280
- wa.command("setup").description("Create WhatsApp configuration").option("-e, --env <environment-id>", "Environment ID").option("-p, --provider <provider>", "Provider (twilio, meta)").option("--api-key <key>", "API key / token").option("--number <number>", "WhatsApp business number").option("--enabled", "Enable WhatsApp", true).action(
18281
- async (options) => {
18282
- try {
18283
- if (!isLoggedIn()) throw new AuthError();
18284
- const environmentId = options.env || await input("Environment ID:", void 0, {
18285
- field: "environment_id",
18286
- flag: "--env"
18287
- });
18288
- const provider = options.provider || await select(
18289
- "WhatsApp provider:",
18290
- [
18291
- { name: "Meta (official)", value: "meta" },
18292
- { name: "Twilio", value: "twilio" }
18293
- ],
18294
- { field: "whatsapp_provider", flag: "--provider" }
18295
- );
18296
- const apiKey = options.apiKey || await input("API Key / Token:", void 0, {
18297
- field: "api_key",
18298
- flag: "--api-key",
18299
- sensitive: true
18300
- });
18301
- const whatsappNumber = options.number || await input(
18302
- "WhatsApp business number (e.g. +1234567890):",
18303
- void 0,
18304
- { field: "whatsapp_number", flag: "--number" }
18305
- );
18306
- const client = getApiClient();
18307
- const _spinner = startSpinner("Creating WhatsApp config...");
18308
- const result = await client.whatsapp.createConfig.mutate({
18309
- environmentId,
18310
- provider,
18311
- apiKey,
18312
- whatsappNumber,
18313
- isEnabled: options.enabled ?? true
18314
- });
18315
- succeedSpinner("WhatsApp configuration created.");
18316
- if (isJsonMode()) outputData(result);
18317
- else quietOutput(environmentId);
18318
- } catch (err) {
18319
- failSpinner();
18320
- handleError(err);
18321
- }
18322
- }
18323
- );
18324
- wa.command("update").description("Update WhatsApp configuration").option("-e, --env <environment-id>", "Environment ID").option("-p, --provider <provider>", "New provider").option("--api-key <key>", "New API key").option("--number <number>", "New WhatsApp number").option("--enable", "Enable WhatsApp").option("--disable", "Disable WhatsApp").action(
18325
- async (options) => {
18326
- try {
18327
- if (!isLoggedIn()) throw new AuthError();
18328
- const environmentId = options.env || await input("Environment ID:", void 0, {
18329
- field: "environment_id",
18330
- flag: "--env"
18331
- });
18332
- const isEnabled = options.enable ? true : options.disable ? false : void 0;
18333
- const client = getApiClient();
18334
- const _spinner = startSpinner("Updating WhatsApp config...");
18335
- await client.whatsapp.updateConfig.mutate({
18336
- environmentId,
18337
- provider: options.provider,
18338
- apiKey: options.apiKey,
18339
- whatsappNumber: options.number,
18340
- isEnabled
18341
- });
18342
- succeedSpinner("WhatsApp configuration updated.");
18343
- if (isJsonMode()) outputData({ updated: true, environmentId });
18344
- } catch (err) {
18345
- failSpinner();
18346
- handleError(err);
18347
- }
18348
- }
18349
- );
18350
- wa.command("send").description("Send a WhatsApp message").option("-e, --env <environment-id>", "Environment ID").option("--to <number>", "Recipient phone number").option("-m, --message <text>", "Message body").option("--template <name>", "Template name").action(
18351
- async (options) => {
18352
- try {
18353
- if (!isLoggedIn()) throw new AuthError();
18354
- const environmentId = options.env || await input("Environment ID:", void 0, {
18355
- field: "environment_id",
18356
- flag: "--env"
18357
- });
18358
- const to = options.to || await input("Recipient phone number:", void 0, {
18359
- field: "recipient_phone",
18360
- flag: "--to"
18361
- });
18362
- const client = getApiClient();
18363
- const _spinner = startSpinner("Sending WhatsApp message...");
18364
- const result = await client.whatsapp.send.mutate({
18365
- environmentId,
18366
- to,
18367
- body: options.message,
18368
- templateName: options.template,
18369
- messageType: options.template ? "template" : "text"
18370
- });
18371
- succeedSpinner("Message sent.");
18372
- if (isJsonMode()) outputData(result);
18373
- } catch (err) {
18374
- failSpinner();
18375
- handleError(err);
18376
- }
18377
- }
18378
- );
18379
- wa.command("logs").description("View WhatsApp message logs").option("-e, --env <environment-id>", "Environment ID").option("-n, --limit <n>", "Number of logs to show", "50").option("--status <status>", "Filter by status").action(
18380
- async (options) => {
18381
- try {
18382
- if (!isLoggedIn()) throw new AuthError();
18383
- const environmentId = options.env || await input("Environment ID:", void 0, {
18384
- field: "environment_id",
18385
- flag: "--env"
18386
- });
18387
- const client = getApiClient();
18388
- const _spinner = startSpinner("Fetching logs...");
18389
- const logs = await client.whatsapp.getLogs.query({
18390
- environmentId,
18391
- status: options.status,
18392
- limit: Number.parseInt(options.limit || "50")
18393
- });
18394
- succeedSpinner();
18395
- if (isJsonMode()) {
18396
- outputData(logs);
18397
- return;
18398
- }
18399
- const list = Array.isArray(logs) ? logs : logs?.items || [];
18400
- if (list.length === 0) {
18401
- log("No WhatsApp logs found.");
18402
- return;
18403
- }
18404
- log("");
18405
- table(
18406
- ["DATE", "TO", "TYPE", "STATUS", "MESSAGE"],
18407
- list.map((l) => [
18408
- l.createdAt ? new Date(l.createdAt).toLocaleString() : "-",
18409
- l.to || "-",
18410
- l.messageType || "text",
18411
- l.status === "sent" ? colors.success(l.status) : l.status === "failed" ? colors.error(l.status) : colors.warn(l.status || "-"),
18412
- (l.body || l.message || "-").slice(0, 40)
18413
- ])
18414
- );
18415
- log("");
18416
- } catch (err) {
18417
- failSpinner();
18418
- handleError(err);
18419
- }
18420
- }
18421
- );
18422
- wa.command("stats").description("Show WhatsApp messaging statistics").option("-e, --env <environment-id>", "Environment ID").option("--start <date>", "Start date (ISO)").option("--end <date>", "End date (ISO)").action(async (options) => {
18423
- try {
18424
- if (!isLoggedIn()) throw new AuthError();
18425
- const environmentId = options.env || await input("Environment ID:", void 0, {
18426
- field: "environment_id",
18427
- flag: "--env"
18428
- });
18429
- const client = getApiClient();
18430
- const _spinner = startSpinner("Fetching stats...");
18431
- const stats = await client.whatsapp.getStats.query({
18432
- environmentId,
18433
- startDate: options.start,
18434
- endDate: options.end
18435
- });
18436
- succeedSpinner();
18437
- if (isJsonMode()) {
18438
- outputData(stats);
18439
- return;
18440
- }
18441
- const s = stats;
18442
- log("");
18443
- log(colors.bold("WhatsApp Statistics"));
18444
- log(` Total Sent: ${colors.cyan(String(s.total || s.sent || 0))}`);
18445
- log(` Delivered: ${colors.success(String(s.delivered || 0))}`);
18446
- log(` Failed: ${colors.error(String(s.failed || 0))}`);
18447
- log(` Read: ${colors.dim(String(s.read || 0))}`);
18448
- log("");
18449
- } catch (err) {
18450
- failSpinner();
18451
- handleError(err);
18452
- }
18453
- });
18454
- }
18455
-
18456
18264
  // src/index.ts
18457
18265
  var program = new Command();
18458
18266
  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(
@@ -18472,6 +18280,7 @@ program.name("tarout").description("Tarout PaaS Command Line Interface").version
18472
18280
  registerAuthCommands(program);
18473
18281
  registerAppsCommands(program);
18474
18282
  registerDeployCommands(program);
18283
+ registerInitCommand(program);
18475
18284
  registerUpCommand(program);
18476
18285
  registerLogsCommand(program);
18477
18286
  registerEnvCommands(program);
@@ -18496,14 +18305,12 @@ registerBackupsCommands(program);
18496
18305
  registerAccountCommands(program);
18497
18306
  registerDashboardCommands(program);
18498
18307
  registerDestinationsCommands(program);
18499
- registerEmailCommands(program);
18500
18308
  registerInboxCommands(program);
18501
18309
  registerProvidersCommands(program);
18502
18310
  registerSettingsCommands(program);
18503
- registerSmsCommands(program);
18504
- registerWhatsappCommands(program);
18505
18311
  registerFirewallCommands(program);
18506
18312
  registerQueuesCommands(program);
18313
+ registerCallCommand(program);
18507
18314
  var argErrorPatterns = [
18508
18315
  /invalid/i,
18509
18316
  /missing required/i,