openhome-cli 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +165 -6
- package/package.json +1 -1
- package/src/cli.ts +10 -2
- package/src/commands/deploy.ts +190 -5
package/dist/cli.js
CHANGED
|
@@ -855,7 +855,44 @@ async function resolveAbilityDir(pathArg) {
|
|
|
855
855
|
return resolve(pathInput.trim());
|
|
856
856
|
}
|
|
857
857
|
async function deployCommand(pathArg, opts = {}) {
|
|
858
|
-
p.intro("\u{1F680}
|
|
858
|
+
p.intro("\u{1F680} Upload Ability");
|
|
859
|
+
if (pathArg && pathArg.endsWith(".zip") && existsSync2(resolve(pathArg))) {
|
|
860
|
+
await deployZip(resolve(pathArg), opts);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (!pathArg) {
|
|
864
|
+
const mode = await p.select({
|
|
865
|
+
message: "What do you want to upload?",
|
|
866
|
+
options: [
|
|
867
|
+
{
|
|
868
|
+
value: "zip",
|
|
869
|
+
label: "\u{1F4E6} Upload a zip file",
|
|
870
|
+
hint: "I already have a .zip ready"
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
value: "folder",
|
|
874
|
+
label: "\u{1F4C1} Upload from a folder",
|
|
875
|
+
hint: "Point me to an ability directory"
|
|
876
|
+
}
|
|
877
|
+
]
|
|
878
|
+
});
|
|
879
|
+
handleCancel(mode);
|
|
880
|
+
if (mode === "zip") {
|
|
881
|
+
const zipInput = await p.text({
|
|
882
|
+
message: "Path to your zip file",
|
|
883
|
+
placeholder: "./my-ability.zip",
|
|
884
|
+
validate: (val) => {
|
|
885
|
+
if (!val || !val.trim()) return "Path is required";
|
|
886
|
+
const resolved = resolve(val.trim());
|
|
887
|
+
if (!existsSync2(resolved)) return `File not found: ${val.trim()}`;
|
|
888
|
+
if (!resolved.endsWith(".zip")) return "Must be a .zip file";
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
handleCancel(zipInput);
|
|
892
|
+
await deployZip(resolve(zipInput.trim()), opts);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
859
896
|
const targetDir = await resolveAbilityDir(pathArg);
|
|
860
897
|
const s = p.spinner();
|
|
861
898
|
s.start("Validating ability...");
|
|
@@ -1059,8 +1096,9 @@ async function deployCommand(pathArg, opts = {}) {
|
|
|
1059
1096
|
p.outro("Mock deploy complete.");
|
|
1060
1097
|
return;
|
|
1061
1098
|
}
|
|
1062
|
-
const apiKey = getApiKey();
|
|
1063
|
-
|
|
1099
|
+
const apiKey = getApiKey() ?? "";
|
|
1100
|
+
const jwt = getJwt() ?? void 0;
|
|
1101
|
+
if (!apiKey && !jwt) {
|
|
1064
1102
|
error("Not authenticated. Run: openhome login");
|
|
1065
1103
|
process.exit(1);
|
|
1066
1104
|
}
|
|
@@ -1074,7 +1112,7 @@ async function deployCommand(pathArg, opts = {}) {
|
|
|
1074
1112
|
}
|
|
1075
1113
|
s.start("Uploading ability...");
|
|
1076
1114
|
try {
|
|
1077
|
-
const client = new ApiClient(apiKey, getConfig().api_base_url);
|
|
1115
|
+
const client = new ApiClient(apiKey, getConfig().api_base_url, jwt);
|
|
1078
1116
|
const result = await client.uploadAbility(
|
|
1079
1117
|
zipBuffer,
|
|
1080
1118
|
imageBuffer,
|
|
@@ -1125,6 +1163,119 @@ Or rename it in config.json and redeploy.`
|
|
|
1125
1163
|
process.exit(1);
|
|
1126
1164
|
}
|
|
1127
1165
|
}
|
|
1166
|
+
async function deployZip(zipPath, opts = {}) {
|
|
1167
|
+
const s = p.spinner();
|
|
1168
|
+
const zipName = basename(zipPath, ".zip");
|
|
1169
|
+
const nameInput = await p.text({
|
|
1170
|
+
message: "Ability name (unique, lowercase, hyphens only)",
|
|
1171
|
+
placeholder: zipName.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
1172
|
+
validate: (val) => {
|
|
1173
|
+
if (!val || !val.trim()) return "Name is required";
|
|
1174
|
+
if (!/^[a-z0-9-]+$/.test(val.trim()))
|
|
1175
|
+
return "Lowercase letters, numbers, and hyphens only";
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
handleCancel(nameInput);
|
|
1179
|
+
const descInput = await p.text({
|
|
1180
|
+
message: "Description",
|
|
1181
|
+
placeholder: "What does this ability do?",
|
|
1182
|
+
validate: (val) => {
|
|
1183
|
+
if (!val || !val.trim()) return "Description is required";
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
handleCancel(descInput);
|
|
1187
|
+
const catChoice = await p.select({
|
|
1188
|
+
message: "Category",
|
|
1189
|
+
options: [
|
|
1190
|
+
{ value: "skill", label: "Skill", hint: "User-triggered" },
|
|
1191
|
+
{ value: "brain", label: "Brain Skill", hint: "Auto-triggered" },
|
|
1192
|
+
{
|
|
1193
|
+
value: "daemon",
|
|
1194
|
+
label: "Background Daemon",
|
|
1195
|
+
hint: "Runs continuously"
|
|
1196
|
+
}
|
|
1197
|
+
]
|
|
1198
|
+
});
|
|
1199
|
+
handleCancel(catChoice);
|
|
1200
|
+
const hotwordsInput = await p.text({
|
|
1201
|
+
message: "Trigger words (comma-separated)",
|
|
1202
|
+
placeholder: "hey openhome, start ability",
|
|
1203
|
+
validate: (val) => {
|
|
1204
|
+
if (!val || !val.trim()) return "At least one trigger word is required";
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
handleCancel(hotwordsInput);
|
|
1208
|
+
const name = nameInput.trim();
|
|
1209
|
+
const description = descInput.trim();
|
|
1210
|
+
const category = catChoice;
|
|
1211
|
+
const hotwords = hotwordsInput.split(",").map((w) => w.trim()).filter(Boolean);
|
|
1212
|
+
const personalityId = opts.personality ?? getConfig().default_personality_id;
|
|
1213
|
+
const metadata = {
|
|
1214
|
+
name,
|
|
1215
|
+
description,
|
|
1216
|
+
category,
|
|
1217
|
+
matching_hotwords: hotwords,
|
|
1218
|
+
personality_id: personalityId
|
|
1219
|
+
};
|
|
1220
|
+
const zipBuffer = readFileSync2(zipPath);
|
|
1221
|
+
if (opts.dryRun) {
|
|
1222
|
+
p.note(
|
|
1223
|
+
[
|
|
1224
|
+
`Zip: ${zipPath}`,
|
|
1225
|
+
`Name: ${name}`,
|
|
1226
|
+
`Description: ${description}`,
|
|
1227
|
+
`Category: ${category}`,
|
|
1228
|
+
`Hotwords: ${hotwords.join(", ")}`,
|
|
1229
|
+
`Agent: ${personalityId ?? "(none set)"}`
|
|
1230
|
+
].join("\n"),
|
|
1231
|
+
"Dry Run \u2014 would deploy"
|
|
1232
|
+
);
|
|
1233
|
+
p.outro("No changes made.");
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
const confirmed = await p.confirm({
|
|
1237
|
+
message: `Deploy "${name}" to OpenHome?`
|
|
1238
|
+
});
|
|
1239
|
+
handleCancel(confirmed);
|
|
1240
|
+
if (!confirmed) {
|
|
1241
|
+
p.cancel("Aborted.");
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (opts.mock) {
|
|
1245
|
+
s.start("Uploading (mock)...");
|
|
1246
|
+
const mockClient = new MockApiClient();
|
|
1247
|
+
await mockClient.uploadAbility(zipBuffer, null, null, metadata);
|
|
1248
|
+
s.stop("Mock upload complete.");
|
|
1249
|
+
p.outro("Mock deploy complete.");
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const apiKey = getApiKey() ?? "";
|
|
1253
|
+
const jwt = getJwt() ?? void 0;
|
|
1254
|
+
if (!apiKey && !jwt) {
|
|
1255
|
+
error("Not authenticated. Run: openhome login");
|
|
1256
|
+
process.exit(1);
|
|
1257
|
+
}
|
|
1258
|
+
s.start("Uploading ability...");
|
|
1259
|
+
try {
|
|
1260
|
+
const client = new ApiClient(apiKey, getConfig().api_base_url, jwt);
|
|
1261
|
+
const result = await client.uploadAbility(zipBuffer, null, null, metadata);
|
|
1262
|
+
s.stop("Upload complete.");
|
|
1263
|
+
p.note(
|
|
1264
|
+
[
|
|
1265
|
+
`Ability ID: ${result.ability_id}`,
|
|
1266
|
+
`Version: ${result.version}`,
|
|
1267
|
+
`Status: ${result.status}`,
|
|
1268
|
+
result.message ? `Message: ${result.message}` : ""
|
|
1269
|
+
].filter(Boolean).join("\n"),
|
|
1270
|
+
"Deploy Result"
|
|
1271
|
+
);
|
|
1272
|
+
p.outro("Deployed successfully! \u{1F389}");
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
s.stop("Upload failed.");
|
|
1275
|
+
error(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1276
|
+
process.exit(1);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1128
1279
|
|
|
1129
1280
|
// src/commands/init.ts
|
|
1130
1281
|
var DAEMON_TEMPLATES = /* @__PURE__ */ new Set(["background", "alarm"]);
|
|
@@ -3300,10 +3451,15 @@ async function interactiveMenu() {
|
|
|
3300
3451
|
const choice = await p.select({
|
|
3301
3452
|
message: "What would you like to do?",
|
|
3302
3453
|
options: [
|
|
3454
|
+
{
|
|
3455
|
+
value: "deploy",
|
|
3456
|
+
label: "\u2B06\uFE0F Upload Ability",
|
|
3457
|
+
hint: "Upload a zip file to OpenHome"
|
|
3458
|
+
},
|
|
3303
3459
|
{
|
|
3304
3460
|
value: "init",
|
|
3305
|
-
label: "\u2728
|
|
3306
|
-
hint: "
|
|
3461
|
+
label: "\u2728 Scaffold Ability",
|
|
3462
|
+
hint: "Generate a new ability from a template"
|
|
3307
3463
|
},
|
|
3308
3464
|
{
|
|
3309
3465
|
value: "list",
|
|
@@ -3350,6 +3506,9 @@ async function interactiveMenu() {
|
|
|
3350
3506
|
});
|
|
3351
3507
|
handleCancel(choice);
|
|
3352
3508
|
switch (choice) {
|
|
3509
|
+
case "deploy":
|
|
3510
|
+
await deployCommand();
|
|
3511
|
+
break;
|
|
3353
3512
|
case "init":
|
|
3354
3513
|
await initCommand();
|
|
3355
3514
|
break;
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -108,10 +108,15 @@ async function interactiveMenu(): Promise<void> {
|
|
|
108
108
|
const choice = await p.select({
|
|
109
109
|
message: "What would you like to do?",
|
|
110
110
|
options: [
|
|
111
|
+
{
|
|
112
|
+
value: "deploy",
|
|
113
|
+
label: "⬆️ Upload Ability",
|
|
114
|
+
hint: "Upload a zip file to OpenHome",
|
|
115
|
+
},
|
|
111
116
|
{
|
|
112
117
|
value: "init",
|
|
113
|
-
label: "✨
|
|
114
|
-
hint: "
|
|
118
|
+
label: "✨ Scaffold Ability",
|
|
119
|
+
hint: "Generate a new ability from a template",
|
|
115
120
|
},
|
|
116
121
|
{
|
|
117
122
|
value: "list",
|
|
@@ -159,6 +164,9 @@ async function interactiveMenu(): Promise<void> {
|
|
|
159
164
|
handleCancel(choice);
|
|
160
165
|
|
|
161
166
|
switch (choice) {
|
|
167
|
+
case "deploy":
|
|
168
|
+
await deployCommand();
|
|
169
|
+
break;
|
|
162
170
|
case "init":
|
|
163
171
|
await initCommand();
|
|
164
172
|
break;
|
package/src/commands/deploy.ts
CHANGED
|
@@ -11,7 +11,12 @@ import { validateAbility } from "../validation/validator.js";
|
|
|
11
11
|
import { createAbilityZip } from "../util/zip.js";
|
|
12
12
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
13
13
|
import { MockApiClient } from "../api/mock-client.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getApiKey,
|
|
16
|
+
getConfig,
|
|
17
|
+
getJwt,
|
|
18
|
+
getTrackedAbilities,
|
|
19
|
+
} from "../config/store.js";
|
|
15
20
|
import type {
|
|
16
21
|
AbilityCategory,
|
|
17
22
|
UploadAbilityMetadata,
|
|
@@ -115,7 +120,50 @@ export async function deployCommand(
|
|
|
115
120
|
pathArg?: string,
|
|
116
121
|
opts: { dryRun?: boolean; mock?: boolean; personality?: string } = {},
|
|
117
122
|
): Promise<void> {
|
|
118
|
-
p.intro("🚀
|
|
123
|
+
p.intro("🚀 Upload Ability");
|
|
124
|
+
|
|
125
|
+
// Explicit zip file passed
|
|
126
|
+
if (pathArg && pathArg.endsWith(".zip") && existsSync(resolve(pathArg))) {
|
|
127
|
+
await deployZip(resolve(pathArg), opts);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// No arg — ask whether they have a zip or a folder
|
|
132
|
+
if (!pathArg) {
|
|
133
|
+
const mode = await p.select({
|
|
134
|
+
message: "What do you want to upload?",
|
|
135
|
+
options: [
|
|
136
|
+
{
|
|
137
|
+
value: "zip",
|
|
138
|
+
label: "📦 Upload a zip file",
|
|
139
|
+
hint: "I already have a .zip ready",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
value: "folder",
|
|
143
|
+
label: "📁 Upload from a folder",
|
|
144
|
+
hint: "Point me to an ability directory",
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
handleCancel(mode);
|
|
149
|
+
|
|
150
|
+
if (mode === "zip") {
|
|
151
|
+
const zipInput = await p.text({
|
|
152
|
+
message: "Path to your zip file",
|
|
153
|
+
placeholder: "./my-ability.zip",
|
|
154
|
+
validate: (val) => {
|
|
155
|
+
if (!val || !val.trim()) return "Path is required";
|
|
156
|
+
const resolved = resolve(val.trim());
|
|
157
|
+
if (!existsSync(resolved)) return `File not found: ${val.trim()}`;
|
|
158
|
+
if (!resolved.endsWith(".zip")) return "Must be a .zip file";
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
handleCancel(zipInput);
|
|
162
|
+
await deployZip(resolve((zipInput as string).trim()), opts);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
119
167
|
const targetDir = await resolveAbilityDir(pathArg);
|
|
120
168
|
|
|
121
169
|
// Step 1: Validate
|
|
@@ -352,8 +400,9 @@ export async function deployCommand(
|
|
|
352
400
|
return;
|
|
353
401
|
}
|
|
354
402
|
|
|
355
|
-
const apiKey = getApiKey();
|
|
356
|
-
|
|
403
|
+
const apiKey = getApiKey() ?? "";
|
|
404
|
+
const jwt = getJwt() ?? undefined;
|
|
405
|
+
if (!apiKey && !jwt) {
|
|
357
406
|
error("Not authenticated. Run: openhome login");
|
|
358
407
|
process.exit(1);
|
|
359
408
|
}
|
|
@@ -371,7 +420,7 @@ export async function deployCommand(
|
|
|
371
420
|
|
|
372
421
|
s.start("Uploading ability...");
|
|
373
422
|
try {
|
|
374
|
-
const client = new ApiClient(apiKey, getConfig().api_base_url);
|
|
423
|
+
const client = new ApiClient(apiKey, getConfig().api_base_url, jwt);
|
|
375
424
|
const result = await client.uploadAbility(
|
|
376
425
|
zipBuffer,
|
|
377
426
|
imageBuffer,
|
|
@@ -428,3 +477,139 @@ export async function deployCommand(
|
|
|
428
477
|
process.exit(1);
|
|
429
478
|
}
|
|
430
479
|
}
|
|
480
|
+
|
|
481
|
+
async function deployZip(
|
|
482
|
+
zipPath: string,
|
|
483
|
+
opts: { dryRun?: boolean; mock?: boolean; personality?: string } = {},
|
|
484
|
+
): Promise<void> {
|
|
485
|
+
const s = p.spinner();
|
|
486
|
+
const zipName = basename(zipPath, ".zip");
|
|
487
|
+
|
|
488
|
+
// Prompt for required metadata
|
|
489
|
+
const nameInput = await p.text({
|
|
490
|
+
message: "Ability name (unique, lowercase, hyphens only)",
|
|
491
|
+
placeholder: zipName.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
492
|
+
validate: (val) => {
|
|
493
|
+
if (!val || !val.trim()) return "Name is required";
|
|
494
|
+
if (!/^[a-z0-9-]+$/.test(val.trim()))
|
|
495
|
+
return "Lowercase letters, numbers, and hyphens only";
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
handleCancel(nameInput);
|
|
499
|
+
|
|
500
|
+
const descInput = await p.text({
|
|
501
|
+
message: "Description",
|
|
502
|
+
placeholder: "What does this ability do?",
|
|
503
|
+
validate: (val) => {
|
|
504
|
+
if (!val || !val.trim()) return "Description is required";
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
handleCancel(descInput);
|
|
508
|
+
|
|
509
|
+
const catChoice = await p.select({
|
|
510
|
+
message: "Category",
|
|
511
|
+
options: [
|
|
512
|
+
{ value: "skill", label: "Skill", hint: "User-triggered" },
|
|
513
|
+
{ value: "brain", label: "Brain Skill", hint: "Auto-triggered" },
|
|
514
|
+
{
|
|
515
|
+
value: "daemon",
|
|
516
|
+
label: "Background Daemon",
|
|
517
|
+
hint: "Runs continuously",
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
});
|
|
521
|
+
handleCancel(catChoice);
|
|
522
|
+
|
|
523
|
+
const hotwordsInput = await p.text({
|
|
524
|
+
message: "Trigger words (comma-separated)",
|
|
525
|
+
placeholder: "hey openhome, start ability",
|
|
526
|
+
validate: (val) => {
|
|
527
|
+
if (!val || !val.trim()) return "At least one trigger word is required";
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
handleCancel(hotwordsInput);
|
|
531
|
+
|
|
532
|
+
const name = (nameInput as string).trim();
|
|
533
|
+
const description = (descInput as string).trim();
|
|
534
|
+
const category = catChoice as AbilityCategory;
|
|
535
|
+
const hotwords = (hotwordsInput as string)
|
|
536
|
+
.split(",")
|
|
537
|
+
.map((w) => w.trim())
|
|
538
|
+
.filter(Boolean);
|
|
539
|
+
|
|
540
|
+
const personalityId = opts.personality ?? getConfig().default_personality_id;
|
|
541
|
+
|
|
542
|
+
const metadata: UploadAbilityMetadata = {
|
|
543
|
+
name,
|
|
544
|
+
description,
|
|
545
|
+
category,
|
|
546
|
+
matching_hotwords: hotwords,
|
|
547
|
+
personality_id: personalityId,
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const zipBuffer = readFileSync(zipPath);
|
|
551
|
+
|
|
552
|
+
if (opts.dryRun) {
|
|
553
|
+
p.note(
|
|
554
|
+
[
|
|
555
|
+
`Zip: ${zipPath}`,
|
|
556
|
+
`Name: ${name}`,
|
|
557
|
+
`Description: ${description}`,
|
|
558
|
+
`Category: ${category}`,
|
|
559
|
+
`Hotwords: ${hotwords.join(", ")}`,
|
|
560
|
+
`Agent: ${personalityId ?? "(none set)"}`,
|
|
561
|
+
].join("\n"),
|
|
562
|
+
"Dry Run — would deploy",
|
|
563
|
+
);
|
|
564
|
+
p.outro("No changes made.");
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const confirmed = await p.confirm({
|
|
569
|
+
message: `Deploy "${name}" to OpenHome?`,
|
|
570
|
+
});
|
|
571
|
+
handleCancel(confirmed);
|
|
572
|
+
if (!confirmed) {
|
|
573
|
+
p.cancel("Aborted.");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (opts.mock) {
|
|
578
|
+
s.start("Uploading (mock)...");
|
|
579
|
+
const mockClient = new MockApiClient();
|
|
580
|
+
await mockClient.uploadAbility(zipBuffer, null, null, metadata);
|
|
581
|
+
s.stop("Mock upload complete.");
|
|
582
|
+
p.outro("Mock deploy complete.");
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const apiKey = getApiKey() ?? "";
|
|
587
|
+
const jwt = getJwt() ?? undefined;
|
|
588
|
+
if (!apiKey && !jwt) {
|
|
589
|
+
error("Not authenticated. Run: openhome login");
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
s.start("Uploading ability...");
|
|
594
|
+
try {
|
|
595
|
+
const client = new ApiClient(apiKey, getConfig().api_base_url, jwt);
|
|
596
|
+
const result = await client.uploadAbility(zipBuffer, null, null, metadata);
|
|
597
|
+
s.stop("Upload complete.");
|
|
598
|
+
p.note(
|
|
599
|
+
[
|
|
600
|
+
`Ability ID: ${result.ability_id}`,
|
|
601
|
+
`Version: ${result.version}`,
|
|
602
|
+
`Status: ${result.status}`,
|
|
603
|
+
result.message ? `Message: ${result.message}` : "",
|
|
604
|
+
]
|
|
605
|
+
.filter(Boolean)
|
|
606
|
+
.join("\n"),
|
|
607
|
+
"Deploy Result",
|
|
608
|
+
);
|
|
609
|
+
p.outro("Deployed successfully! 🎉");
|
|
610
|
+
} catch (err) {
|
|
611
|
+
s.stop("Upload failed.");
|
|
612
|
+
error(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
}
|