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 +123 -5
- package/package.json +1 -1
- package/src/commands/deploy.ts +153 -4
- package/src/commands/login.ts +1 -1
- package/src/commands/set-jwt.ts +1 -1
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
|
-
|
|
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
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
|
+
}
|
package/src/commands/login.ts
CHANGED
|
@@ -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"),
|
package/src/commands/set-jwt.ts
CHANGED
|
@@ -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"),
|