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 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
- if (!apiKey) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openhome-cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "CLI for managing OpenHome voice AI abilities",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 { getApiKey, getConfig, getTrackedAbilities } from "../config/store.js";
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
- if (!apiKey) {
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
+ }