@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/README.md +22 -0
- package/bin/tarout-mcp +3 -0
- package/dist/{api-735LN7BA.js → api-QAKANRFX.js} +2 -1
- package/dist/{billing-WOKNOS4N.js → billing-GUA4S2Y4.js} +3 -2
- package/dist/chunk-5DAFGMBH.js +137 -0
- package/dist/{chunk-5XBVQICT.js → chunk-BS6DFVSU.js} +4 -2
- package/dist/{chunk-7YS2WBLB.js → chunk-NHNK5ZQ5.js} +5 -135
- package/dist/index.js +1378 -1571
- package/dist/mcp/stdio.d.ts +2 -0
- package/dist/mcp/stdio.js +55 -0
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
stopSpinner,
|
|
7
7
|
succeedSpinner,
|
|
8
8
|
updateSpinner
|
|
9
|
-
} from "./chunk-
|
|
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-
|
|
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.
|
|
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
|
|
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 =
|
|
2258
|
+
url = formatAppUrl(app.domain[0].host) ?? "";
|
|
2176
2259
|
} else if (app.appSubdomain) {
|
|
2177
|
-
url =
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2778
2861
|
}
|
|
2779
2862
|
function startAuthServer(options = {}) {
|
|
2780
|
-
return new Promise((
|
|
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
|
-
|
|
2836
|
-
|
|
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(
|
|
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
|
-
|
|
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/
|
|
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-
|
|
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:
|
|
3261
|
-
environmentId:
|
|
3262
|
-
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4305
|
-
|
|
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.
|
|
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
|
|
5305
|
+
existsSync as existsSync3,
|
|
5153
5306
|
mkdtempSync,
|
|
5154
5307
|
readdirSync,
|
|
5155
|
-
readFileSync as
|
|
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
|
|
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((
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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(
|
|
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 (!
|
|
5491
|
-
return JSON.parse(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
5689
|
-
if (!
|
|
5690
|
-
let configPath =
|
|
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 =
|
|
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-
|
|
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
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
database === "postgres" ? "Postgres" : "MySQL"
|
|
6385
|
+
const decision = await resolveDatabaseProvisioning(
|
|
6386
|
+
client,
|
|
6387
|
+
profile,
|
|
6388
|
+
app,
|
|
6389
|
+
database,
|
|
6390
|
+
options
|
|
6239
6391
|
);
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
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
|
-
|
|
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(
|
|
6545
|
-
const archivePath =
|
|
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
|
|
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
|
|
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((
|
|
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
|
|
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/
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
const
|
|
9731
|
-
|
|
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
|
|
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
|
|
9740
|
-
const
|
|
9741
|
-
|
|
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(
|
|
10390
|
+
outputData(variables);
|
|
9746
10391
|
return;
|
|
9747
10392
|
}
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
log("No
|
|
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
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (!
|
|
10565
|
+
if (!existsSync4(options.input)) {
|
|
10701
10566
|
throw new InvalidArgumentError(`File not found: ${options.input}`);
|
|
10702
10567
|
}
|
|
10703
|
-
const content =
|
|
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
|
-
|
|
11420
|
-
|
|
11421
|
-
|
|
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
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
|
|
11428
|
-
|
|
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:
|
|
11489
|
-
publicKey =
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
-
|
|
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("
|
|
16662
|
-
const
|
|
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(
|
|
16515
|
+
outputData(data);
|
|
16670
16516
|
return;
|
|
16671
16517
|
}
|
|
16672
|
-
const
|
|
16518
|
+
const sub = data;
|
|
16673
16519
|
log("");
|
|
16674
|
-
log(colors.bold("
|
|
16675
|
-
|
|
16676
|
-
log(
|
|
16677
|
-
|
|
16678
|
-
|
|
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
|
|
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(
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
17791
|
+
emitEvent2({ event: "inspect_started", cwd });
|
|
17918
17792
|
const inspection = inspectCurrentProject(cwd);
|
|
17919
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
|
|
17942
|
-
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
|
|
17946
|
-
|
|
17947
|
-
|
|
17948
|
-
|
|
17949
|
-
|
|
17950
|
-
|
|
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
|
-
|
|
17953
|
-
event: "
|
|
17954
|
-
applicationId:
|
|
17955
|
-
name:
|
|
17956
|
-
|
|
17832
|
+
emitEvent2({
|
|
17833
|
+
event: "app_reused",
|
|
17834
|
+
applicationId: picked.applicationId,
|
|
17835
|
+
name: picked.name,
|
|
17836
|
+
via: "--app"
|
|
17957
17837
|
});
|
|
17958
|
-
}
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
|
|
17963
|
-
|
|
17964
|
-
|
|
17965
|
-
|
|
17966
|
-
|
|
17967
|
-
|
|
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(
|
|
17971
|
-
|
|
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
|
-
|
|
17977
|
-
|
|
17978
|
-
|
|
17979
|
-
|
|
17980
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18015
|
+
emitEvent2({
|
|
18030
18016
|
event: "github_connect_done",
|
|
18031
18017
|
repository: `${owner}/${repository}`
|
|
18032
18018
|
});
|
|
18033
18019
|
} else {
|
|
18034
|
-
|
|
18020
|
+
emitEvent2({ event: "upload_started" });
|
|
18035
18021
|
await uploadCurrentDirectorySource(
|
|
18036
18022
|
client,
|
|
18037
18023
|
app.applicationId,
|
|
18038
18024
|
app.name
|
|
18039
18025
|
);
|
|
18040
|
-
|
|
18026
|
+
emitEvent2({ event: "upload_done" });
|
|
18041
18027
|
}
|
|
18042
|
-
|
|
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
|
-
|
|
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
|
|
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,
|