openhome-cli 0.1.11 → 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
@@ -367,7 +367,7 @@ async function loginCommand() {
367
367
  "",
368
368
  `${chalk2.bold("4.")} Paste this command and press Enter:`,
369
369
  "",
370
- ` ${chalk2.green("copy(localStorage.getItem('access_token'))")}`,
370
+ ` ${chalk2.green("copy(localStorage.getItem('access_token')), '\u2713 Token copied to clipboard!'")}`,
371
371
  "",
372
372
  `${chalk2.bold("5.")} Your token is copied to clipboard \u2014 paste it back here.`
373
373
  ].join("\n"),
@@ -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"]);
@@ -3180,7 +3298,7 @@ async function setJwtCommand(token) {
3180
3298
  "",
3181
3299
  `${chalk13.bold("4.")} Paste this command and press Enter:`,
3182
3300
  "",
3183
- ` ${chalk13.green("copy(localStorage.getItem('access_token'))")}`,
3301
+ ` ${chalk13.green("copy(localStorage.getItem('access_token')), '\u2713 Token copied to clipboard!'")}`,
3184
3302
  "",
3185
3303
  `${chalk13.bold("5.")} Your token is copied to clipboard \u2014 paste it back here.`
3186
3304
  ].join("\n"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openhome-cli",
3
- "version": "0.1.11",
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
+ }
@@ -115,7 +115,7 @@ export async function loginCommand(): Promise<void> {
115
115
  "",
116
116
  `${chalk.bold("4.")} Paste this command and press Enter:`,
117
117
  "",
118
- ` ${chalk.green("copy(localStorage.getItem('access_token'))")}`,
118
+ ` ${chalk.green("copy(localStorage.getItem('access_token')), '✓ Token copied to clipboard!'")}`,
119
119
  "",
120
120
  `${chalk.bold("5.")} Your token is copied to clipboard — paste it back here.`,
121
121
  ].join("\n"),
@@ -42,7 +42,7 @@ export async function setJwtCommand(token?: string): Promise<void> {
42
42
  "",
43
43
  `${chalk.bold("4.")} Paste this command and press Enter:`,
44
44
  "",
45
- ` ${chalk.green("copy(localStorage.getItem('access_token'))")}`,
45
+ ` ${chalk.green("copy(localStorage.getItem('access_token')), '✓ Token copied to clipboard!'")}`,
46
46
  "",
47
47
  `${chalk.bold("5.")} Your token is copied to clipboard — paste it back here.`,
48
48
  ].join("\n"),