openhome-cli 0.1.12 → 0.1.13
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 +121 -3
- package/package.json +1 -1
- package/src/commands/deploy.ts +153 -4
package/dist/cli.js
CHANGED
|
@@ -856,6 +856,10 @@ async function resolveAbilityDir(pathArg) {
|
|
|
856
856
|
}
|
|
857
857
|
async function deployCommand(pathArg, opts = {}) {
|
|
858
858
|
p.intro("\u{1F680} Deploy ability");
|
|
859
|
+
if (pathArg && pathArg.endsWith(".zip") && existsSync2(resolve(pathArg))) {
|
|
860
|
+
await deployZip(resolve(pathArg), opts);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
859
863
|
const targetDir = await resolveAbilityDir(pathArg);
|
|
860
864
|
const s = p.spinner();
|
|
861
865
|
s.start("Validating ability...");
|
|
@@ -1059,8 +1063,9 @@ async function deployCommand(pathArg, opts = {}) {
|
|
|
1059
1063
|
p.outro("Mock deploy complete.");
|
|
1060
1064
|
return;
|
|
1061
1065
|
}
|
|
1062
|
-
const apiKey = getApiKey();
|
|
1063
|
-
|
|
1066
|
+
const apiKey = getApiKey() ?? "";
|
|
1067
|
+
const jwt = getJwt() ?? void 0;
|
|
1068
|
+
if (!apiKey && !jwt) {
|
|
1064
1069
|
error("Not authenticated. Run: openhome login");
|
|
1065
1070
|
process.exit(1);
|
|
1066
1071
|
}
|
|
@@ -1074,7 +1079,7 @@ async function deployCommand(pathArg, opts = {}) {
|
|
|
1074
1079
|
}
|
|
1075
1080
|
s.start("Uploading ability...");
|
|
1076
1081
|
try {
|
|
1077
|
-
const client = new ApiClient(apiKey, getConfig().api_base_url);
|
|
1082
|
+
const client = new ApiClient(apiKey, getConfig().api_base_url, jwt);
|
|
1078
1083
|
const result = await client.uploadAbility(
|
|
1079
1084
|
zipBuffer,
|
|
1080
1085
|
imageBuffer,
|
|
@@ -1125,6 +1130,119 @@ Or rename it in config.json and redeploy.`
|
|
|
1125
1130
|
process.exit(1);
|
|
1126
1131
|
}
|
|
1127
1132
|
}
|
|
1133
|
+
async function deployZip(zipPath, opts = {}) {
|
|
1134
|
+
const s = p.spinner();
|
|
1135
|
+
const zipName = basename(zipPath, ".zip");
|
|
1136
|
+
const nameInput = await p.text({
|
|
1137
|
+
message: "Ability name (unique, lowercase, hyphens only)",
|
|
1138
|
+
placeholder: zipName.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
1139
|
+
validate: (val) => {
|
|
1140
|
+
if (!val || !val.trim()) return "Name is required";
|
|
1141
|
+
if (!/^[a-z0-9-]+$/.test(val.trim()))
|
|
1142
|
+
return "Lowercase letters, numbers, and hyphens only";
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
handleCancel(nameInput);
|
|
1146
|
+
const descInput = await p.text({
|
|
1147
|
+
message: "Description",
|
|
1148
|
+
placeholder: "What does this ability do?",
|
|
1149
|
+
validate: (val) => {
|
|
1150
|
+
if (!val || !val.trim()) return "Description is required";
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
handleCancel(descInput);
|
|
1154
|
+
const catChoice = await p.select({
|
|
1155
|
+
message: "Category",
|
|
1156
|
+
options: [
|
|
1157
|
+
{ value: "skill", label: "Skill", hint: "User-triggered" },
|
|
1158
|
+
{ value: "brain", label: "Brain Skill", hint: "Auto-triggered" },
|
|
1159
|
+
{
|
|
1160
|
+
value: "daemon",
|
|
1161
|
+
label: "Background Daemon",
|
|
1162
|
+
hint: "Runs continuously"
|
|
1163
|
+
}
|
|
1164
|
+
]
|
|
1165
|
+
});
|
|
1166
|
+
handleCancel(catChoice);
|
|
1167
|
+
const hotwordsInput = await p.text({
|
|
1168
|
+
message: "Trigger words (comma-separated)",
|
|
1169
|
+
placeholder: "hey openhome, start ability",
|
|
1170
|
+
validate: (val) => {
|
|
1171
|
+
if (!val || !val.trim()) return "At least one trigger word is required";
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
handleCancel(hotwordsInput);
|
|
1175
|
+
const name = nameInput.trim();
|
|
1176
|
+
const description = descInput.trim();
|
|
1177
|
+
const category = catChoice;
|
|
1178
|
+
const hotwords = hotwordsInput.split(",").map((w) => w.trim()).filter(Boolean);
|
|
1179
|
+
const personalityId = opts.personality ?? getConfig().default_personality_id;
|
|
1180
|
+
const metadata = {
|
|
1181
|
+
name,
|
|
1182
|
+
description,
|
|
1183
|
+
category,
|
|
1184
|
+
matching_hotwords: hotwords,
|
|
1185
|
+
personality_id: personalityId
|
|
1186
|
+
};
|
|
1187
|
+
const zipBuffer = readFileSync2(zipPath);
|
|
1188
|
+
if (opts.dryRun) {
|
|
1189
|
+
p.note(
|
|
1190
|
+
[
|
|
1191
|
+
`Zip: ${zipPath}`,
|
|
1192
|
+
`Name: ${name}`,
|
|
1193
|
+
`Description: ${description}`,
|
|
1194
|
+
`Category: ${category}`,
|
|
1195
|
+
`Hotwords: ${hotwords.join(", ")}`,
|
|
1196
|
+
`Agent: ${personalityId ?? "(none set)"}`
|
|
1197
|
+
].join("\n"),
|
|
1198
|
+
"Dry Run \u2014 would deploy"
|
|
1199
|
+
);
|
|
1200
|
+
p.outro("No changes made.");
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
const confirmed = await p.confirm({
|
|
1204
|
+
message: `Deploy "${name}" to OpenHome?`
|
|
1205
|
+
});
|
|
1206
|
+
handleCancel(confirmed);
|
|
1207
|
+
if (!confirmed) {
|
|
1208
|
+
p.cancel("Aborted.");
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (opts.mock) {
|
|
1212
|
+
s.start("Uploading (mock)...");
|
|
1213
|
+
const mockClient = new MockApiClient();
|
|
1214
|
+
await mockClient.uploadAbility(zipBuffer, null, null, metadata);
|
|
1215
|
+
s.stop("Mock upload complete.");
|
|
1216
|
+
p.outro("Mock deploy complete.");
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const apiKey = getApiKey() ?? "";
|
|
1220
|
+
const jwt = getJwt() ?? void 0;
|
|
1221
|
+
if (!apiKey && !jwt) {
|
|
1222
|
+
error("Not authenticated. Run: openhome login");
|
|
1223
|
+
process.exit(1);
|
|
1224
|
+
}
|
|
1225
|
+
s.start("Uploading ability...");
|
|
1226
|
+
try {
|
|
1227
|
+
const client = new ApiClient(apiKey, getConfig().api_base_url, jwt);
|
|
1228
|
+
const result = await client.uploadAbility(zipBuffer, null, null, metadata);
|
|
1229
|
+
s.stop("Upload complete.");
|
|
1230
|
+
p.note(
|
|
1231
|
+
[
|
|
1232
|
+
`Ability ID: ${result.ability_id}`,
|
|
1233
|
+
`Version: ${result.version}`,
|
|
1234
|
+
`Status: ${result.status}`,
|
|
1235
|
+
result.message ? `Message: ${result.message}` : ""
|
|
1236
|
+
].filter(Boolean).join("\n"),
|
|
1237
|
+
"Deploy Result"
|
|
1238
|
+
);
|
|
1239
|
+
p.outro("Deployed successfully! \u{1F389}");
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
s.stop("Upload failed.");
|
|
1242
|
+
error(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1243
|
+
process.exit(1);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1128
1246
|
|
|
1129
1247
|
// src/commands/init.ts
|
|
1130
1248
|
var DAEMON_TEMPLATES = /* @__PURE__ */ new Set(["background", "alarm"]);
|
package/package.json
CHANGED
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,
|
|
@@ -116,6 +121,13 @@ export async function deployCommand(
|
|
|
116
121
|
opts: { dryRun?: boolean; mock?: boolean; personality?: string } = {},
|
|
117
122
|
): Promise<void> {
|
|
118
123
|
p.intro("🚀 Deploy ability");
|
|
124
|
+
|
|
125
|
+
// Direct zip upload path
|
|
126
|
+
if (pathArg && pathArg.endsWith(".zip") && existsSync(resolve(pathArg))) {
|
|
127
|
+
await deployZip(resolve(pathArg), opts);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
const targetDir = await resolveAbilityDir(pathArg);
|
|
120
132
|
|
|
121
133
|
// Step 1: Validate
|
|
@@ -352,8 +364,9 @@ export async function deployCommand(
|
|
|
352
364
|
return;
|
|
353
365
|
}
|
|
354
366
|
|
|
355
|
-
const apiKey = getApiKey();
|
|
356
|
-
|
|
367
|
+
const apiKey = getApiKey() ?? "";
|
|
368
|
+
const jwt = getJwt() ?? undefined;
|
|
369
|
+
if (!apiKey && !jwt) {
|
|
357
370
|
error("Not authenticated. Run: openhome login");
|
|
358
371
|
process.exit(1);
|
|
359
372
|
}
|
|
@@ -371,7 +384,7 @@ export async function deployCommand(
|
|
|
371
384
|
|
|
372
385
|
s.start("Uploading ability...");
|
|
373
386
|
try {
|
|
374
|
-
const client = new ApiClient(apiKey, getConfig().api_base_url);
|
|
387
|
+
const client = new ApiClient(apiKey, getConfig().api_base_url, jwt);
|
|
375
388
|
const result = await client.uploadAbility(
|
|
376
389
|
zipBuffer,
|
|
377
390
|
imageBuffer,
|
|
@@ -428,3 +441,139 @@ export async function deployCommand(
|
|
|
428
441
|
process.exit(1);
|
|
429
442
|
}
|
|
430
443
|
}
|
|
444
|
+
|
|
445
|
+
async function deployZip(
|
|
446
|
+
zipPath: string,
|
|
447
|
+
opts: { dryRun?: boolean; mock?: boolean; personality?: string } = {},
|
|
448
|
+
): Promise<void> {
|
|
449
|
+
const s = p.spinner();
|
|
450
|
+
const zipName = basename(zipPath, ".zip");
|
|
451
|
+
|
|
452
|
+
// Prompt for required metadata
|
|
453
|
+
const nameInput = await p.text({
|
|
454
|
+
message: "Ability name (unique, lowercase, hyphens only)",
|
|
455
|
+
placeholder: zipName.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
456
|
+
validate: (val) => {
|
|
457
|
+
if (!val || !val.trim()) return "Name is required";
|
|
458
|
+
if (!/^[a-z0-9-]+$/.test(val.trim()))
|
|
459
|
+
return "Lowercase letters, numbers, and hyphens only";
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
handleCancel(nameInput);
|
|
463
|
+
|
|
464
|
+
const descInput = await p.text({
|
|
465
|
+
message: "Description",
|
|
466
|
+
placeholder: "What does this ability do?",
|
|
467
|
+
validate: (val) => {
|
|
468
|
+
if (!val || !val.trim()) return "Description is required";
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
handleCancel(descInput);
|
|
472
|
+
|
|
473
|
+
const catChoice = await p.select({
|
|
474
|
+
message: "Category",
|
|
475
|
+
options: [
|
|
476
|
+
{ value: "skill", label: "Skill", hint: "User-triggered" },
|
|
477
|
+
{ value: "brain", label: "Brain Skill", hint: "Auto-triggered" },
|
|
478
|
+
{
|
|
479
|
+
value: "daemon",
|
|
480
|
+
label: "Background Daemon",
|
|
481
|
+
hint: "Runs continuously",
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
});
|
|
485
|
+
handleCancel(catChoice);
|
|
486
|
+
|
|
487
|
+
const hotwordsInput = await p.text({
|
|
488
|
+
message: "Trigger words (comma-separated)",
|
|
489
|
+
placeholder: "hey openhome, start ability",
|
|
490
|
+
validate: (val) => {
|
|
491
|
+
if (!val || !val.trim()) return "At least one trigger word is required";
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
handleCancel(hotwordsInput);
|
|
495
|
+
|
|
496
|
+
const name = (nameInput as string).trim();
|
|
497
|
+
const description = (descInput as string).trim();
|
|
498
|
+
const category = catChoice as AbilityCategory;
|
|
499
|
+
const hotwords = (hotwordsInput as string)
|
|
500
|
+
.split(",")
|
|
501
|
+
.map((w) => w.trim())
|
|
502
|
+
.filter(Boolean);
|
|
503
|
+
|
|
504
|
+
const personalityId = opts.personality ?? getConfig().default_personality_id;
|
|
505
|
+
|
|
506
|
+
const metadata: UploadAbilityMetadata = {
|
|
507
|
+
name,
|
|
508
|
+
description,
|
|
509
|
+
category,
|
|
510
|
+
matching_hotwords: hotwords,
|
|
511
|
+
personality_id: personalityId,
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const zipBuffer = readFileSync(zipPath);
|
|
515
|
+
|
|
516
|
+
if (opts.dryRun) {
|
|
517
|
+
p.note(
|
|
518
|
+
[
|
|
519
|
+
`Zip: ${zipPath}`,
|
|
520
|
+
`Name: ${name}`,
|
|
521
|
+
`Description: ${description}`,
|
|
522
|
+
`Category: ${category}`,
|
|
523
|
+
`Hotwords: ${hotwords.join(", ")}`,
|
|
524
|
+
`Agent: ${personalityId ?? "(none set)"}`,
|
|
525
|
+
].join("\n"),
|
|
526
|
+
"Dry Run — would deploy",
|
|
527
|
+
);
|
|
528
|
+
p.outro("No changes made.");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const confirmed = await p.confirm({
|
|
533
|
+
message: `Deploy "${name}" to OpenHome?`,
|
|
534
|
+
});
|
|
535
|
+
handleCancel(confirmed);
|
|
536
|
+
if (!confirmed) {
|
|
537
|
+
p.cancel("Aborted.");
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (opts.mock) {
|
|
542
|
+
s.start("Uploading (mock)...");
|
|
543
|
+
const mockClient = new MockApiClient();
|
|
544
|
+
await mockClient.uploadAbility(zipBuffer, null, null, metadata);
|
|
545
|
+
s.stop("Mock upload complete.");
|
|
546
|
+
p.outro("Mock deploy complete.");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const apiKey = getApiKey() ?? "";
|
|
551
|
+
const jwt = getJwt() ?? undefined;
|
|
552
|
+
if (!apiKey && !jwt) {
|
|
553
|
+
error("Not authenticated. Run: openhome login");
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
s.start("Uploading ability...");
|
|
558
|
+
try {
|
|
559
|
+
const client = new ApiClient(apiKey, getConfig().api_base_url, jwt);
|
|
560
|
+
const result = await client.uploadAbility(zipBuffer, null, null, metadata);
|
|
561
|
+
s.stop("Upload complete.");
|
|
562
|
+
p.note(
|
|
563
|
+
[
|
|
564
|
+
`Ability ID: ${result.ability_id}`,
|
|
565
|
+
`Version: ${result.version}`,
|
|
566
|
+
`Status: ${result.status}`,
|
|
567
|
+
result.message ? `Message: ${result.message}` : "",
|
|
568
|
+
]
|
|
569
|
+
.filter(Boolean)
|
|
570
|
+
.join("\n"),
|
|
571
|
+
"Deploy Result",
|
|
572
|
+
);
|
|
573
|
+
p.outro("Deployed successfully! 🎉");
|
|
574
|
+
} catch (err) {
|
|
575
|
+
s.stop("Upload failed.");
|
|
576
|
+
error(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
}
|