openhome-cli 0.1.17 → 0.1.20
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 +120 -82
- package/package.json +1 -1
- package/src/api/client.ts +20 -4
- package/src/commands/assign.ts +2 -0
- package/src/commands/delete.ts +2 -0
- package/src/commands/deploy.ts +19 -9
- package/src/commands/handle-session-expired.ts +23 -0
- package/src/commands/list.ts +2 -0
- package/src/commands/toggle.ts +2 -0
package/dist/cli.js
CHANGED
|
@@ -49,6 +49,12 @@ var ApiError = class extends Error {
|
|
|
49
49
|
this.name = "ApiError";
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
|
+
var SessionExpiredError = class extends Error {
|
|
53
|
+
constructor() {
|
|
54
|
+
super("Session token expired or invalid");
|
|
55
|
+
this.name = "SessionExpiredError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
52
58
|
var ApiClient = class {
|
|
53
59
|
constructor(apiKey, baseUrl, jwt) {
|
|
54
60
|
this.apiKey = apiKey;
|
|
@@ -82,6 +88,9 @@ var ApiClient = class {
|
|
|
82
88
|
throw new NotImplementedError(path);
|
|
83
89
|
}
|
|
84
90
|
const message = body?.detail ?? body?.error?.message ?? response.statusText;
|
|
91
|
+
if (useJwt && (response.status === 401 || message.toLowerCase().includes("token not valid") || message.toLowerCase().includes("token is invalid") || message.toLowerCase().includes("not valid for any token"))) {
|
|
92
|
+
throw new SessionExpiredError();
|
|
93
|
+
}
|
|
85
94
|
throw new ApiError(String(response.status), message);
|
|
86
95
|
}
|
|
87
96
|
return response.json();
|
|
@@ -101,9 +110,7 @@ var ApiClient = class {
|
|
|
101
110
|
const form = new FormData();
|
|
102
111
|
form.append(
|
|
103
112
|
"zip_file",
|
|
104
|
-
new Blob([zipBuffer], {
|
|
105
|
-
type: "application/zip"
|
|
106
|
-
}),
|
|
113
|
+
new Blob([new Uint8Array(zipBuffer)], { type: "application/zip" }),
|
|
107
114
|
"ability.zip"
|
|
108
115
|
);
|
|
109
116
|
if (imageBuffer && imageName) {
|
|
@@ -111,7 +118,7 @@ var ApiClient = class {
|
|
|
111
118
|
const imageMime = imageExt === "jpg" || imageExt === "jpeg" ? "image/jpeg" : "image/png";
|
|
112
119
|
form.append(
|
|
113
120
|
"image_file",
|
|
114
|
-
new Blob([imageBuffer], { type: imageMime }),
|
|
121
|
+
new Blob([new Uint8Array(imageBuffer)], { type: imageMime }),
|
|
115
122
|
imageName
|
|
116
123
|
);
|
|
117
124
|
}
|
|
@@ -684,6 +691,25 @@ async function createAbilityZip(dirPath) {
|
|
|
684
691
|
});
|
|
685
692
|
}
|
|
686
693
|
|
|
694
|
+
// src/commands/handle-session-expired.ts
|
|
695
|
+
import chalk3 from "chalk";
|
|
696
|
+
async function handleIfSessionExpired(err) {
|
|
697
|
+
if (!(err instanceof SessionExpiredError)) return false;
|
|
698
|
+
console.log("");
|
|
699
|
+
p.note(
|
|
700
|
+
[
|
|
701
|
+
"Your session token has expired or been invalidated.",
|
|
702
|
+
"This happens when you log into the OpenHome website again.",
|
|
703
|
+
"",
|
|
704
|
+
`You need to grab a fresh token \u2014 it only takes 30 seconds.`
|
|
705
|
+
].join("\n"),
|
|
706
|
+
chalk3.yellow("Session expired")
|
|
707
|
+
);
|
|
708
|
+
await setupJwt();
|
|
709
|
+
p.note("Token updated. Run the command again to continue.", "Ready");
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
|
|
687
713
|
// src/api/mock-client.ts
|
|
688
714
|
var MOCK_PERSONALITIES = [
|
|
689
715
|
{ id: "pers_alice", name: "Alice", description: "Friendly assistant" },
|
|
@@ -791,6 +817,12 @@ var MockApiClient = class {
|
|
|
791
817
|
};
|
|
792
818
|
|
|
793
819
|
// src/commands/deploy.ts
|
|
820
|
+
function expandPath(p2) {
|
|
821
|
+
if (p2.startsWith("~/") || p2 === "~") {
|
|
822
|
+
return join2(homedir(), p2.slice(2));
|
|
823
|
+
}
|
|
824
|
+
return resolve(p2);
|
|
825
|
+
}
|
|
794
826
|
var IMAGE_EXTENSIONS = ["png", "jpg", "jpeg"];
|
|
795
827
|
var ICON_NAMES = IMAGE_EXTENSIONS.flatMap((ext) => [
|
|
796
828
|
`icon.${ext}`,
|
|
@@ -854,7 +886,7 @@ async function resolveAbilityDir(pathArg) {
|
|
|
854
886
|
}
|
|
855
887
|
});
|
|
856
888
|
handleCancel(pathInput);
|
|
857
|
-
return
|
|
889
|
+
return expandPath(pathInput.trim());
|
|
858
890
|
}
|
|
859
891
|
async function deployCommand(pathArg, opts = {}) {
|
|
860
892
|
p.intro("\u{1F680} Upload Ability");
|
|
@@ -934,13 +966,13 @@ async function deployCommand(pathArg, opts = {}) {
|
|
|
934
966
|
placeholder: "~/path/to/ability.zip",
|
|
935
967
|
validate: (val) => {
|
|
936
968
|
if (!val || !val.trim()) return "Path is required";
|
|
937
|
-
if (!existsSync2(
|
|
969
|
+
if (!existsSync2(expandPath(val.trim())))
|
|
938
970
|
return `File not found: ${val.trim()}`;
|
|
939
971
|
if (!val.trim().endsWith(".zip")) return "Must be a .zip file";
|
|
940
972
|
}
|
|
941
973
|
});
|
|
942
974
|
handleCancel(zipInput);
|
|
943
|
-
zipPath =
|
|
975
|
+
zipPath = expandPath(zipInput.trim());
|
|
944
976
|
} else {
|
|
945
977
|
zipPath = selected;
|
|
946
978
|
}
|
|
@@ -950,13 +982,13 @@ async function deployCommand(pathArg, opts = {}) {
|
|
|
950
982
|
placeholder: "~/Downloads/my-ability.zip",
|
|
951
983
|
validate: (val) => {
|
|
952
984
|
if (!val || !val.trim()) return "Path is required";
|
|
953
|
-
if (!existsSync2(
|
|
985
|
+
if (!existsSync2(expandPath(val.trim())))
|
|
954
986
|
return `File not found: ${val.trim()}`;
|
|
955
987
|
if (!val.trim().endsWith(".zip")) return "Must be a .zip file";
|
|
956
988
|
}
|
|
957
989
|
});
|
|
958
990
|
handleCancel(zipInput);
|
|
959
|
-
zipPath =
|
|
991
|
+
zipPath = expandPath(zipInput.trim());
|
|
960
992
|
}
|
|
961
993
|
await deployZip(zipPath, opts);
|
|
962
994
|
return;
|
|
@@ -1079,7 +1111,7 @@ async function deployCommand(pathArg, opts = {}) {
|
|
|
1079
1111
|
placeholder: "./icon.png",
|
|
1080
1112
|
validate: (val) => {
|
|
1081
1113
|
if (!val || !val.trim()) return void 0;
|
|
1082
|
-
const resolved =
|
|
1114
|
+
const resolved = expandPath(val.trim());
|
|
1083
1115
|
if (!existsSync2(resolved)) return `File not found: ${val.trim()}`;
|
|
1084
1116
|
if (!IMAGE_EXTS.has(extname(resolved).toLowerCase()))
|
|
1085
1117
|
return "Image must be PNG or JPG";
|
|
@@ -1087,7 +1119,7 @@ async function deployCommand(pathArg, opts = {}) {
|
|
|
1087
1119
|
});
|
|
1088
1120
|
handleCancel(imgInput);
|
|
1089
1121
|
const trimmed = imgInput.trim();
|
|
1090
|
-
if (trimmed) imagePath =
|
|
1122
|
+
if (trimmed) imagePath = expandPath(trimmed);
|
|
1091
1123
|
} else if (selected !== "__skip__") {
|
|
1092
1124
|
imagePath = selected;
|
|
1093
1125
|
}
|
|
@@ -1097,7 +1129,7 @@ async function deployCommand(pathArg, opts = {}) {
|
|
|
1097
1129
|
placeholder: "./icon.png",
|
|
1098
1130
|
validate: (val) => {
|
|
1099
1131
|
if (!val || !val.trim()) return void 0;
|
|
1100
|
-
const resolved =
|
|
1132
|
+
const resolved = expandPath(val.trim());
|
|
1101
1133
|
if (!existsSync2(resolved)) return `File not found: ${val.trim()}`;
|
|
1102
1134
|
if (!IMAGE_EXTS.has(extname(resolved).toLowerCase()))
|
|
1103
1135
|
return "Image must be PNG or JPG";
|
|
@@ -1105,7 +1137,7 @@ async function deployCommand(pathArg, opts = {}) {
|
|
|
1105
1137
|
});
|
|
1106
1138
|
handleCancel(imgInput);
|
|
1107
1139
|
const trimmed = imgInput.trim();
|
|
1108
|
-
if (trimmed) imagePath =
|
|
1140
|
+
if (trimmed) imagePath = expandPath(trimmed);
|
|
1109
1141
|
}
|
|
1110
1142
|
}
|
|
1111
1143
|
const imageBuffer = imagePath ? readFileSync2(imagePath) : null;
|
|
@@ -1219,6 +1251,7 @@ async function deployCommand(pathArg, opts = {}) {
|
|
|
1219
1251
|
p.outro("Zip ready for manual upload.");
|
|
1220
1252
|
return;
|
|
1221
1253
|
}
|
|
1254
|
+
if (await handleIfSessionExpired(err)) return;
|
|
1222
1255
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1223
1256
|
if (msg.toLowerCase().includes("same name")) {
|
|
1224
1257
|
error(`An ability named "${uniqueName}" already exists.`);
|
|
@@ -1341,6 +1374,7 @@ async function deployZip(zipPath, opts = {}) {
|
|
|
1341
1374
|
p.outro("Deployed successfully! \u{1F389}");
|
|
1342
1375
|
} catch (err) {
|
|
1343
1376
|
s.stop("Upload failed.");
|
|
1377
|
+
if (await handleIfSessionExpired(err)) return;
|
|
1344
1378
|
error(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1345
1379
|
process.exit(1);
|
|
1346
1380
|
}
|
|
@@ -2139,7 +2173,7 @@ async function initCommand(nameArg) {
|
|
|
2139
2173
|
}
|
|
2140
2174
|
|
|
2141
2175
|
// src/commands/delete.ts
|
|
2142
|
-
import
|
|
2176
|
+
import chalk4 from "chalk";
|
|
2143
2177
|
async function deleteCommand(abilityArg, opts = {}) {
|
|
2144
2178
|
p.intro("\u{1F5D1}\uFE0F Delete ability");
|
|
2145
2179
|
let client;
|
|
@@ -2194,7 +2228,7 @@ async function deleteCommand(abilityArg, opts = {}) {
|
|
|
2194
2228
|
options: abilities.map((a) => ({
|
|
2195
2229
|
value: a.ability_id,
|
|
2196
2230
|
label: a.unique_name,
|
|
2197
|
-
hint: `${
|
|
2231
|
+
hint: `${chalk4.gray(a.status)} v${a.version}`
|
|
2198
2232
|
}))
|
|
2199
2233
|
});
|
|
2200
2234
|
handleCancel(selected);
|
|
@@ -2222,13 +2256,14 @@ async function deleteCommand(abilityArg, opts = {}) {
|
|
|
2222
2256
|
p.note("API Not Available Yet", "Delete endpoint not yet implemented.");
|
|
2223
2257
|
return;
|
|
2224
2258
|
}
|
|
2259
|
+
if (await handleIfSessionExpired(err)) return;
|
|
2225
2260
|
error(`Delete failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2226
2261
|
process.exit(1);
|
|
2227
2262
|
}
|
|
2228
2263
|
}
|
|
2229
2264
|
|
|
2230
2265
|
// src/commands/toggle.ts
|
|
2231
|
-
import
|
|
2266
|
+
import chalk5 from "chalk";
|
|
2232
2267
|
async function toggleCommand(abilityArg, opts = {}) {
|
|
2233
2268
|
p.intro("\u26A1 Enable / Disable ability");
|
|
2234
2269
|
let client;
|
|
@@ -2283,7 +2318,7 @@ async function toggleCommand(abilityArg, opts = {}) {
|
|
|
2283
2318
|
options: abilities.map((a) => ({
|
|
2284
2319
|
value: a.ability_id,
|
|
2285
2320
|
label: a.unique_name,
|
|
2286
|
-
hint: `${a.status === "disabled" ?
|
|
2321
|
+
hint: `${a.status === "disabled" ? chalk5.gray("disabled") : chalk5.green("enabled")} v${a.version}`
|
|
2287
2322
|
}))
|
|
2288
2323
|
});
|
|
2289
2324
|
handleCancel(selected);
|
|
@@ -2321,13 +2356,14 @@ async function toggleCommand(abilityArg, opts = {}) {
|
|
|
2321
2356
|
p.note("Toggle endpoint not yet implemented.", "API Not Available Yet");
|
|
2322
2357
|
return;
|
|
2323
2358
|
}
|
|
2359
|
+
if (await handleIfSessionExpired(err)) return;
|
|
2324
2360
|
error(`Toggle failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2325
2361
|
process.exit(1);
|
|
2326
2362
|
}
|
|
2327
2363
|
}
|
|
2328
2364
|
|
|
2329
2365
|
// src/commands/assign.ts
|
|
2330
|
-
import
|
|
2366
|
+
import chalk6 from "chalk";
|
|
2331
2367
|
async function assignCommand(opts = {}) {
|
|
2332
2368
|
p.intro("\u{1F517} Assign abilities to agent");
|
|
2333
2369
|
let client;
|
|
@@ -2378,7 +2414,7 @@ async function assignCommand(opts = {}) {
|
|
|
2378
2414
|
options: personalities.map((pers) => ({
|
|
2379
2415
|
value: pers.id,
|
|
2380
2416
|
label: pers.name,
|
|
2381
|
-
hint:
|
|
2417
|
+
hint: chalk6.gray(pers.id)
|
|
2382
2418
|
}))
|
|
2383
2419
|
});
|
|
2384
2420
|
handleCancel(agentId);
|
|
@@ -2416,23 +2452,24 @@ async function assignCommand(opts = {}) {
|
|
|
2416
2452
|
p.note("Assign endpoint not yet implemented.", "API Not Available Yet");
|
|
2417
2453
|
return;
|
|
2418
2454
|
}
|
|
2455
|
+
if (await handleIfSessionExpired(err)) return;
|
|
2419
2456
|
error(`Assign failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2420
2457
|
process.exit(1);
|
|
2421
2458
|
}
|
|
2422
2459
|
}
|
|
2423
2460
|
|
|
2424
2461
|
// src/commands/list.ts
|
|
2425
|
-
import
|
|
2462
|
+
import chalk7 from "chalk";
|
|
2426
2463
|
function statusColor(status) {
|
|
2427
2464
|
switch (status) {
|
|
2428
2465
|
case "active":
|
|
2429
|
-
return
|
|
2466
|
+
return chalk7.green(status);
|
|
2430
2467
|
case "processing":
|
|
2431
|
-
return
|
|
2468
|
+
return chalk7.yellow(status);
|
|
2432
2469
|
case "failed":
|
|
2433
|
-
return
|
|
2470
|
+
return chalk7.red(status);
|
|
2434
2471
|
case "disabled":
|
|
2435
|
-
return
|
|
2472
|
+
return chalk7.gray(status);
|
|
2436
2473
|
default:
|
|
2437
2474
|
return status;
|
|
2438
2475
|
}
|
|
@@ -2484,6 +2521,7 @@ async function listCommand(opts = {}) {
|
|
|
2484
2521
|
p.outro("List endpoint not yet implemented.");
|
|
2485
2522
|
return;
|
|
2486
2523
|
}
|
|
2524
|
+
if (await handleIfSessionExpired(err)) return;
|
|
2487
2525
|
error(
|
|
2488
2526
|
`Failed to list abilities: ${err instanceof Error ? err.message : String(err)}`
|
|
2489
2527
|
);
|
|
@@ -2495,19 +2533,19 @@ async function listCommand(opts = {}) {
|
|
|
2495
2533
|
import { join as join4, resolve as resolve3 } from "path";
|
|
2496
2534
|
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
2497
2535
|
import { homedir as homedir3 } from "os";
|
|
2498
|
-
import
|
|
2536
|
+
import chalk8 from "chalk";
|
|
2499
2537
|
function statusBadge(status) {
|
|
2500
2538
|
switch (status) {
|
|
2501
2539
|
case "active":
|
|
2502
|
-
return
|
|
2540
|
+
return chalk8.bgGreen.black(` ${status.toUpperCase()} `);
|
|
2503
2541
|
case "processing":
|
|
2504
|
-
return
|
|
2542
|
+
return chalk8.bgYellow.black(` ${status.toUpperCase()} `);
|
|
2505
2543
|
case "failed":
|
|
2506
|
-
return
|
|
2544
|
+
return chalk8.bgRed.white(` ${status.toUpperCase()} `);
|
|
2507
2545
|
case "disabled":
|
|
2508
|
-
return
|
|
2546
|
+
return chalk8.bgGray.white(` ${status.toUpperCase()} `);
|
|
2509
2547
|
default:
|
|
2510
|
-
return
|
|
2548
|
+
return chalk8.bgWhite.black(` ${status.toUpperCase()} `);
|
|
2511
2549
|
}
|
|
2512
2550
|
}
|
|
2513
2551
|
function readAbilityName(dir) {
|
|
@@ -2596,14 +2634,14 @@ async function statusCommand(abilityArg, opts = {}) {
|
|
|
2596
2634
|
);
|
|
2597
2635
|
if (ability.validation_errors.length > 0) {
|
|
2598
2636
|
p.note(
|
|
2599
|
-
ability.validation_errors.map((e) =>
|
|
2637
|
+
ability.validation_errors.map((e) => chalk8.red(`\u2717 ${e}`)).join("\n"),
|
|
2600
2638
|
"Validation Errors"
|
|
2601
2639
|
);
|
|
2602
2640
|
}
|
|
2603
2641
|
if (ability.deploy_history.length > 0) {
|
|
2604
2642
|
const historyLines = ability.deploy_history.map((event) => {
|
|
2605
|
-
const icon = event.status === "success" ?
|
|
2606
|
-
return `${icon} v${event.version} ${event.message} ${
|
|
2643
|
+
const icon = event.status === "success" ? chalk8.green("\u2713") : chalk8.red("\u2717");
|
|
2644
|
+
return `${icon} v${event.version} ${event.message} ${chalk8.gray(new Date(event.timestamp).toLocaleString())}`;
|
|
2607
2645
|
});
|
|
2608
2646
|
p.note(historyLines.join("\n"), "Deploy History");
|
|
2609
2647
|
}
|
|
@@ -2623,7 +2661,7 @@ async function statusCommand(abilityArg, opts = {}) {
|
|
|
2623
2661
|
}
|
|
2624
2662
|
|
|
2625
2663
|
// src/commands/agents.ts
|
|
2626
|
-
import
|
|
2664
|
+
import chalk9 from "chalk";
|
|
2627
2665
|
async function agentsCommand(opts = {}) {
|
|
2628
2666
|
p.intro("\u{1F916} Your Agents");
|
|
2629
2667
|
let client;
|
|
@@ -2648,7 +2686,7 @@ async function agentsCommand(opts = {}) {
|
|
|
2648
2686
|
return;
|
|
2649
2687
|
}
|
|
2650
2688
|
p.note(
|
|
2651
|
-
personalities.map((pers) => `${
|
|
2689
|
+
personalities.map((pers) => `${chalk9.bold(pers.name)} ${chalk9.gray(pers.id)}`).join("\n"),
|
|
2652
2690
|
"Agents"
|
|
2653
2691
|
);
|
|
2654
2692
|
const config = getConfig();
|
|
@@ -2702,7 +2740,7 @@ async function logoutCommand() {
|
|
|
2702
2740
|
|
|
2703
2741
|
// src/commands/chat.ts
|
|
2704
2742
|
import WebSocket from "ws";
|
|
2705
|
-
import
|
|
2743
|
+
import chalk10 from "chalk";
|
|
2706
2744
|
import * as readline from "readline";
|
|
2707
2745
|
var PING_INTERVAL = 3e4;
|
|
2708
2746
|
async function chatCommand(agentArg, opts = {}) {
|
|
@@ -2743,7 +2781,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2743
2781
|
}
|
|
2744
2782
|
}
|
|
2745
2783
|
const wsUrl = `${WS_BASE}${ENDPOINTS.voiceStream(apiKey, agentId)}`;
|
|
2746
|
-
info(`Connecting to agent ${
|
|
2784
|
+
info(`Connecting to agent ${chalk10.bold(agentId)}...`);
|
|
2747
2785
|
await new Promise((resolve6) => {
|
|
2748
2786
|
const ws = new WebSocket(wsUrl, {
|
|
2749
2787
|
perMessageDeflate: false,
|
|
@@ -2759,7 +2797,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2759
2797
|
output: process.stdout
|
|
2760
2798
|
});
|
|
2761
2799
|
function promptUser() {
|
|
2762
|
-
rl.question(
|
|
2800
|
+
rl.question(chalk10.green("You: "), (input) => {
|
|
2763
2801
|
const trimmed = input.trim();
|
|
2764
2802
|
if (!trimmed) {
|
|
2765
2803
|
promptUser();
|
|
@@ -2795,7 +2833,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2795
2833
|
}, PING_INTERVAL);
|
|
2796
2834
|
success("Connected! Type a message and press Enter. Type /quit to exit.");
|
|
2797
2835
|
console.log(
|
|
2798
|
-
|
|
2836
|
+
chalk10.gray(
|
|
2799
2837
|
" Tip: Send trigger words to activate abilities (e.g. 'play aquaprime')"
|
|
2800
2838
|
)
|
|
2801
2839
|
);
|
|
@@ -2810,7 +2848,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2810
2848
|
const data = msg.data;
|
|
2811
2849
|
if (data.content && data.role === "assistant") {
|
|
2812
2850
|
if (data.live && !data.final) {
|
|
2813
|
-
const prefix = `${
|
|
2851
|
+
const prefix = `${chalk10.cyan("Agent:")} `;
|
|
2814
2852
|
readline.clearLine(process.stdout, 0);
|
|
2815
2853
|
readline.cursorTo(process.stdout, 0);
|
|
2816
2854
|
process.stdout.write(`${prefix}${data.content}`);
|
|
@@ -2819,7 +2857,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2819
2857
|
if (currentResponse !== "") {
|
|
2820
2858
|
console.log("");
|
|
2821
2859
|
} else {
|
|
2822
|
-
console.log(`${
|
|
2860
|
+
console.log(`${chalk10.cyan("Agent:")} ${data.content}`);
|
|
2823
2861
|
}
|
|
2824
2862
|
currentResponse = "";
|
|
2825
2863
|
console.log("");
|
|
@@ -2835,7 +2873,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2835
2873
|
ws.send(JSON.stringify({ type: "text", data: "bot-speak-end" }));
|
|
2836
2874
|
if (currentResponse === "") {
|
|
2837
2875
|
console.log(
|
|
2838
|
-
|
|
2876
|
+
chalk10.gray(" (Agent sent audio \u2014 text-only mode)")
|
|
2839
2877
|
);
|
|
2840
2878
|
console.log("");
|
|
2841
2879
|
}
|
|
@@ -2895,7 +2933,7 @@ async function chatCommand(agentArg, opts = {}) {
|
|
|
2895
2933
|
|
|
2896
2934
|
// src/commands/trigger.ts
|
|
2897
2935
|
import WebSocket2 from "ws";
|
|
2898
|
-
import
|
|
2936
|
+
import chalk11 from "chalk";
|
|
2899
2937
|
var PING_INTERVAL2 = 3e4;
|
|
2900
2938
|
var RESPONSE_TIMEOUT = 3e4;
|
|
2901
2939
|
async function triggerCommand(phraseArg, opts = {}) {
|
|
@@ -2947,7 +2985,7 @@ async function triggerCommand(phraseArg, opts = {}) {
|
|
|
2947
2985
|
}
|
|
2948
2986
|
}
|
|
2949
2987
|
const wsUrl = `${WS_BASE}${ENDPOINTS.voiceStream(apiKey, agentId)}`;
|
|
2950
|
-
info(`Sending "${
|
|
2988
|
+
info(`Sending "${chalk11.bold(phrase)}" to agent ${chalk11.bold(agentId)}...`);
|
|
2951
2989
|
const s = p.spinner();
|
|
2952
2990
|
s.start("Waiting for response...");
|
|
2953
2991
|
await new Promise((resolve6) => {
|
|
@@ -2977,7 +3015,7 @@ async function triggerCommand(phraseArg, opts = {}) {
|
|
|
2977
3015
|
s.stop("Timed out waiting for response.");
|
|
2978
3016
|
if (fullResponse) {
|
|
2979
3017
|
console.log(`
|
|
2980
|
-
${
|
|
3018
|
+
${chalk11.cyan("Agent:")} ${fullResponse}`);
|
|
2981
3019
|
}
|
|
2982
3020
|
cleanup();
|
|
2983
3021
|
resolve6();
|
|
@@ -2994,7 +3032,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}`);
|
|
|
2994
3032
|
if (!data.live || data.final) {
|
|
2995
3033
|
s.stop("Response received.");
|
|
2996
3034
|
console.log(`
|
|
2997
|
-
${
|
|
3035
|
+
${chalk11.cyan("Agent:")} ${fullResponse}
|
|
2998
3036
|
`);
|
|
2999
3037
|
cleanup();
|
|
3000
3038
|
resolve6();
|
|
@@ -3011,7 +3049,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}
|
|
|
3011
3049
|
if (fullResponse) {
|
|
3012
3050
|
s.stop("Response received.");
|
|
3013
3051
|
console.log(`
|
|
3014
|
-
${
|
|
3052
|
+
${chalk11.cyan("Agent:")} ${fullResponse}
|
|
3015
3053
|
`);
|
|
3016
3054
|
cleanup();
|
|
3017
3055
|
resolve6();
|
|
@@ -3050,7 +3088,7 @@ ${chalk10.cyan("Agent:")} ${fullResponse}
|
|
|
3050
3088
|
}
|
|
3051
3089
|
|
|
3052
3090
|
// src/commands/whoami.ts
|
|
3053
|
-
import
|
|
3091
|
+
import chalk12 from "chalk";
|
|
3054
3092
|
import { homedir as homedir4 } from "os";
|
|
3055
3093
|
async function whoamiCommand() {
|
|
3056
3094
|
p.intro("\u{1F464} OpenHome CLI Status");
|
|
@@ -3060,17 +3098,17 @@ async function whoamiCommand() {
|
|
|
3060
3098
|
const home = homedir4();
|
|
3061
3099
|
if (apiKey) {
|
|
3062
3100
|
const masked = apiKey.slice(0, 6) + "..." + apiKey.slice(-4);
|
|
3063
|
-
info(`Authenticated: ${
|
|
3101
|
+
info(`Authenticated: ${chalk12.green("yes")} (key: ${chalk12.gray(masked)})`);
|
|
3064
3102
|
} else {
|
|
3065
3103
|
info(
|
|
3066
|
-
`Authenticated: ${
|
|
3104
|
+
`Authenticated: ${chalk12.red("no")} \u2014 run ${chalk12.bold("openhome login")}`
|
|
3067
3105
|
);
|
|
3068
3106
|
}
|
|
3069
3107
|
if (config.default_personality_id) {
|
|
3070
|
-
info(`Default agent: ${
|
|
3108
|
+
info(`Default agent: ${chalk12.bold(config.default_personality_id)}`);
|
|
3071
3109
|
} else {
|
|
3072
3110
|
info(
|
|
3073
|
-
`Default agent: ${
|
|
3111
|
+
`Default agent: ${chalk12.gray("not set")} \u2014 run ${chalk12.bold("openhome agents")}`
|
|
3074
3112
|
);
|
|
3075
3113
|
}
|
|
3076
3114
|
if (config.api_base_url) {
|
|
@@ -3079,12 +3117,12 @@ async function whoamiCommand() {
|
|
|
3079
3117
|
if (tracked.length > 0) {
|
|
3080
3118
|
const lines = tracked.map((a) => {
|
|
3081
3119
|
const shortPath = a.path.startsWith(home) ? `~${a.path.slice(home.length)}` : a.path;
|
|
3082
|
-
return ` ${
|
|
3120
|
+
return ` ${chalk12.bold(a.name)} ${chalk12.gray(shortPath)}`;
|
|
3083
3121
|
});
|
|
3084
3122
|
p.note(lines.join("\n"), `${tracked.length} tracked ability(s)`);
|
|
3085
3123
|
} else {
|
|
3086
3124
|
info(
|
|
3087
|
-
`Tracked abilities: ${
|
|
3125
|
+
`Tracked abilities: ${chalk12.gray("none")} \u2014 run ${chalk12.bold("openhome init")}`
|
|
3088
3126
|
);
|
|
3089
3127
|
}
|
|
3090
3128
|
p.outro("Done.");
|
|
@@ -3226,7 +3264,7 @@ async function configEditCommand(pathArg) {
|
|
|
3226
3264
|
|
|
3227
3265
|
// src/commands/logs.ts
|
|
3228
3266
|
import WebSocket3 from "ws";
|
|
3229
|
-
import
|
|
3267
|
+
import chalk13 from "chalk";
|
|
3230
3268
|
var PING_INTERVAL3 = 3e4;
|
|
3231
3269
|
async function logsCommand(opts = {}) {
|
|
3232
3270
|
p.intro("\u{1F4E1} Stream agent logs");
|
|
@@ -3266,8 +3304,8 @@ async function logsCommand(opts = {}) {
|
|
|
3266
3304
|
}
|
|
3267
3305
|
}
|
|
3268
3306
|
const wsUrl = `${WS_BASE}${ENDPOINTS.voiceStream(apiKey, agentId)}`;
|
|
3269
|
-
info(`Streaming logs from agent ${
|
|
3270
|
-
info(`Press ${
|
|
3307
|
+
info(`Streaming logs from agent ${chalk13.bold(agentId)}...`);
|
|
3308
|
+
info(`Press ${chalk13.bold("Ctrl+C")} to stop.
|
|
3271
3309
|
`);
|
|
3272
3310
|
await new Promise((resolve6) => {
|
|
3273
3311
|
const ws = new WebSocket3(wsUrl, {
|
|
@@ -3289,33 +3327,33 @@ async function logsCommand(opts = {}) {
|
|
|
3289
3327
|
ws.on("message", (raw) => {
|
|
3290
3328
|
try {
|
|
3291
3329
|
const msg = JSON.parse(raw.toString());
|
|
3292
|
-
const ts =
|
|
3330
|
+
const ts = chalk13.gray((/* @__PURE__ */ new Date()).toLocaleTimeString());
|
|
3293
3331
|
switch (msg.type) {
|
|
3294
3332
|
case "log":
|
|
3295
3333
|
console.log(
|
|
3296
|
-
`${ts} ${
|
|
3334
|
+
`${ts} ${chalk13.blue("[LOG]")} ${JSON.stringify(msg.data)}`
|
|
3297
3335
|
);
|
|
3298
3336
|
break;
|
|
3299
3337
|
case "action":
|
|
3300
3338
|
console.log(
|
|
3301
|
-
`${ts} ${
|
|
3339
|
+
`${ts} ${chalk13.magenta("[ACTION]")} ${JSON.stringify(msg.data)}`
|
|
3302
3340
|
);
|
|
3303
3341
|
break;
|
|
3304
3342
|
case "progress":
|
|
3305
3343
|
console.log(
|
|
3306
|
-
`${ts} ${
|
|
3344
|
+
`${ts} ${chalk13.yellow("[PROGRESS]")} ${JSON.stringify(msg.data)}`
|
|
3307
3345
|
);
|
|
3308
3346
|
break;
|
|
3309
3347
|
case "question":
|
|
3310
3348
|
console.log(
|
|
3311
|
-
`${ts} ${
|
|
3349
|
+
`${ts} ${chalk13.cyan("[QUESTION]")} ${JSON.stringify(msg.data)}`
|
|
3312
3350
|
);
|
|
3313
3351
|
break;
|
|
3314
3352
|
case "message": {
|
|
3315
3353
|
const data = msg.data;
|
|
3316
3354
|
if (data.content && !data.live) {
|
|
3317
|
-
const role = data.role === "assistant" ?
|
|
3318
|
-
console.log(`${ts} ${
|
|
3355
|
+
const role = data.role === "assistant" ? chalk13.cyan("AGENT") : chalk13.green("USER");
|
|
3356
|
+
console.log(`${ts} ${chalk13.white(`[${role}]`)} ${data.content}`);
|
|
3319
3357
|
}
|
|
3320
3358
|
break;
|
|
3321
3359
|
}
|
|
@@ -3334,13 +3372,13 @@ async function logsCommand(opts = {}) {
|
|
|
3334
3372
|
case "error-event": {
|
|
3335
3373
|
const errData = msg.data;
|
|
3336
3374
|
console.log(
|
|
3337
|
-
`${ts} ${
|
|
3375
|
+
`${ts} ${chalk13.red("[ERROR]")} ${errData?.message || errData?.title || JSON.stringify(msg.data)}`
|
|
3338
3376
|
);
|
|
3339
3377
|
break;
|
|
3340
3378
|
}
|
|
3341
3379
|
default:
|
|
3342
3380
|
console.log(
|
|
3343
|
-
`${ts} ${
|
|
3381
|
+
`${ts} ${chalk13.gray(`[${msg.type}]`)} ${JSON.stringify(msg.data)}`
|
|
3344
3382
|
);
|
|
3345
3383
|
break;
|
|
3346
3384
|
}
|
|
@@ -3366,7 +3404,7 @@ async function logsCommand(opts = {}) {
|
|
|
3366
3404
|
}
|
|
3367
3405
|
|
|
3368
3406
|
// src/commands/set-jwt.ts
|
|
3369
|
-
import
|
|
3407
|
+
import chalk14 from "chalk";
|
|
3370
3408
|
async function setJwtCommand(token) {
|
|
3371
3409
|
p.intro("\u{1F511} Enable Management Features");
|
|
3372
3410
|
if (token) {
|
|
@@ -3388,21 +3426,21 @@ async function setJwtCommand(token) {
|
|
|
3388
3426
|
[
|
|
3389
3427
|
"Here's what you'll do:",
|
|
3390
3428
|
"",
|
|
3391
|
-
`${
|
|
3429
|
+
`${chalk14.bold("1.")} We'll open ${chalk14.bold("app.openhome.com")} \u2014 make sure you're logged in`,
|
|
3392
3430
|
"",
|
|
3393
|
-
`${
|
|
3394
|
-
` Mac \u2192 ${
|
|
3395
|
-
` Windows / Linux \u2192 ${
|
|
3431
|
+
`${chalk14.bold("2.")} Open the browser console:`,
|
|
3432
|
+
` Mac \u2192 ${chalk14.cyan("Cmd + Option + J")}`,
|
|
3433
|
+
` Windows / Linux \u2192 ${chalk14.cyan("F12")} then click ${chalk14.cyan("Console")}`,
|
|
3396
3434
|
"",
|
|
3397
|
-
`${
|
|
3398
|
-
` ${
|
|
3399
|
-
` Type ${
|
|
3435
|
+
`${chalk14.bold("3.")} Chrome may show this warning \u2014 it's expected:`,
|
|
3436
|
+
` ${chalk14.yellow(`"Don't paste code you don't understand..."`)}`,
|
|
3437
|
+
` Type ${chalk14.cyan("allow pasting")} and press Enter to dismiss it.`,
|
|
3400
3438
|
"",
|
|
3401
|
-
`${
|
|
3439
|
+
`${chalk14.bold("4.")} Paste this command and press Enter:`,
|
|
3402
3440
|
"",
|
|
3403
|
-
` ${
|
|
3441
|
+
` ${chalk14.green("copy(localStorage.getItem('access_token')), '\u2713 Token copied to clipboard!'")}`,
|
|
3404
3442
|
"",
|
|
3405
|
-
`${
|
|
3443
|
+
`${chalk14.bold("5.")} Your token is copied to clipboard \u2014 paste it back here.`
|
|
3406
3444
|
].join("\n"),
|
|
3407
3445
|
"Enable management features (one-time setup)"
|
|
3408
3446
|
);
|
|
@@ -3423,7 +3461,7 @@ async function setJwtCommand(token) {
|
|
|
3423
3461
|
|
|
3424
3462
|
// src/commands/validate.ts
|
|
3425
3463
|
import { resolve as resolve5 } from "path";
|
|
3426
|
-
import
|
|
3464
|
+
import chalk15 from "chalk";
|
|
3427
3465
|
async function validateCommand(pathArg = ".") {
|
|
3428
3466
|
const targetDir = resolve5(pathArg);
|
|
3429
3467
|
p.intro(`\u{1F50E} Validate ability`);
|
|
@@ -3439,7 +3477,7 @@ async function validateCommand(pathArg = ".") {
|
|
|
3439
3477
|
if (result.errors.length > 0) {
|
|
3440
3478
|
p.note(
|
|
3441
3479
|
result.errors.map(
|
|
3442
|
-
(issue) => `${
|
|
3480
|
+
(issue) => `${chalk15.red("\u2717")} ${issue.file ? chalk15.bold(`[${issue.file}]`) + " " : ""}${issue.message}`
|
|
3443
3481
|
).join("\n"),
|
|
3444
3482
|
`${result.errors.length} Error(s)`
|
|
3445
3483
|
);
|
|
@@ -3447,7 +3485,7 @@ async function validateCommand(pathArg = ".") {
|
|
|
3447
3485
|
if (result.warnings.length > 0) {
|
|
3448
3486
|
p.note(
|
|
3449
3487
|
result.warnings.map(
|
|
3450
|
-
(w) => `${
|
|
3488
|
+
(w) => `${chalk15.yellow("\u26A0")} ${w.file ? chalk15.bold(`[${w.file}]`) + " " : ""}${w.message}`
|
|
3451
3489
|
).join("\n"),
|
|
3452
3490
|
`${result.warnings.length} Warning(s)`
|
|
3453
3491
|
);
|
|
@@ -3493,9 +3531,9 @@ async function checkForUpdates() {
|
|
|
3493
3531
|
);
|
|
3494
3532
|
process.exit(0);
|
|
3495
3533
|
} else {
|
|
3496
|
-
const { default:
|
|
3534
|
+
const { default: chalk16 } = await import("chalk");
|
|
3497
3535
|
console.log(
|
|
3498
|
-
|
|
3536
|
+
chalk16.yellow(
|
|
3499
3537
|
` Update available: v${version} \u2192 v${latest} Run: npm install -g openhome-cli@latest
|
|
3500
3538
|
`
|
|
3501
3539
|
)
|
package/package.json
CHANGED
package/src/api/client.ts
CHANGED
|
@@ -33,6 +33,13 @@ export class ApiError extends Error {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export class SessionExpiredError extends Error {
|
|
37
|
+
constructor() {
|
|
38
|
+
super("Session token expired or invalid");
|
|
39
|
+
this.name = "SessionExpiredError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
export interface IApiClient {
|
|
37
44
|
getPersonalities(): Promise<Personality[]>;
|
|
38
45
|
verifyApiKey(apiKey: string): Promise<VerifyApiKeyResponse>;
|
|
@@ -107,6 +114,17 @@ export class ApiClient implements IApiClient {
|
|
|
107
114
|
(body as ApiErrorResponse | null)?.error?.message ??
|
|
108
115
|
response.statusText;
|
|
109
116
|
|
|
117
|
+
// Detect expired/invalid JWT
|
|
118
|
+
if (
|
|
119
|
+
useJwt &&
|
|
120
|
+
(response.status === 401 ||
|
|
121
|
+
message.toLowerCase().includes("token not valid") ||
|
|
122
|
+
message.toLowerCase().includes("token is invalid") ||
|
|
123
|
+
message.toLowerCase().includes("not valid for any token"))
|
|
124
|
+
) {
|
|
125
|
+
throw new SessionExpiredError();
|
|
126
|
+
}
|
|
127
|
+
|
|
110
128
|
throw new ApiError(String(response.status), message);
|
|
111
129
|
}
|
|
112
130
|
|
|
@@ -134,9 +152,7 @@ export class ApiClient implements IApiClient {
|
|
|
134
152
|
const form = new FormData();
|
|
135
153
|
form.append(
|
|
136
154
|
"zip_file",
|
|
137
|
-
new Blob([zipBuffer
|
|
138
|
-
type: "application/zip",
|
|
139
|
-
}),
|
|
155
|
+
new Blob([new Uint8Array(zipBuffer)], { type: "application/zip" }),
|
|
140
156
|
"ability.zip",
|
|
141
157
|
);
|
|
142
158
|
|
|
@@ -146,7 +162,7 @@ export class ApiClient implements IApiClient {
|
|
|
146
162
|
imageExt === "jpg" || imageExt === "jpeg" ? "image/jpeg" : "image/png";
|
|
147
163
|
form.append(
|
|
148
164
|
"image_file",
|
|
149
|
-
new Blob([imageBuffer
|
|
165
|
+
new Blob([new Uint8Array(imageBuffer)], { type: imageMime }),
|
|
150
166
|
imageName,
|
|
151
167
|
);
|
|
152
168
|
}
|
package/src/commands/assign.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
|
+
import { handleIfSessionExpired } from "./handle-session-expired.js";
|
|
2
3
|
import { MockApiClient } from "../api/mock-client.js";
|
|
3
4
|
import { getApiKey, getConfig, getJwt } from "../config/store.js";
|
|
4
5
|
import { error, success, info, p, handleCancel } from "../ui/format.js";
|
|
@@ -124,6 +125,7 @@ export async function assignCommand(
|
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
if (await handleIfSessionExpired(err)) return;
|
|
127
129
|
error(`Assign failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
128
130
|
process.exit(1);
|
|
129
131
|
}
|
package/src/commands/delete.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
|
+
import { handleIfSessionExpired } from "./handle-session-expired.js";
|
|
2
3
|
import { MockApiClient } from "../api/mock-client.js";
|
|
3
4
|
import { getApiKey, getConfig, getJwt } from "../config/store.js";
|
|
4
5
|
import { error, success, p, handleCancel } from "../ui/format.js";
|
|
@@ -108,6 +109,7 @@ export async function deleteCommand(
|
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
if (await handleIfSessionExpired(err)) return;
|
|
111
113
|
error(`Delete failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
112
114
|
process.exit(1);
|
|
113
115
|
}
|
package/src/commands/deploy.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { homedir } from "node:os";
|
|
|
10
10
|
import { validateAbility } from "../validation/validator.js";
|
|
11
11
|
import { createAbilityZip } from "../util/zip.js";
|
|
12
12
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
13
|
+
import { handleIfSessionExpired } from "./handle-session-expired.js";
|
|
13
14
|
import { MockApiClient } from "../api/mock-client.js";
|
|
14
15
|
import {
|
|
15
16
|
getApiKey,
|
|
@@ -31,6 +32,13 @@ interface AbilityConfig {
|
|
|
31
32
|
[key: string]: unknown;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function expandPath(p: string): string {
|
|
36
|
+
if (p.startsWith("~/") || p === "~") {
|
|
37
|
+
return join(homedir(), p.slice(2));
|
|
38
|
+
}
|
|
39
|
+
return resolve(p);
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg"];
|
|
35
43
|
const ICON_NAMES = IMAGE_EXTENSIONS.flatMap((ext) => [
|
|
36
44
|
`icon.${ext}`,
|
|
@@ -113,7 +121,7 @@ async function resolveAbilityDir(pathArg?: string): Promise<string> {
|
|
|
113
121
|
},
|
|
114
122
|
});
|
|
115
123
|
handleCancel(pathInput);
|
|
116
|
-
return
|
|
124
|
+
return expandPath((pathInput as string).trim());
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
export async function deployCommand(
|
|
@@ -218,13 +226,13 @@ export async function deployCommand(
|
|
|
218
226
|
placeholder: "~/path/to/ability.zip",
|
|
219
227
|
validate: (val) => {
|
|
220
228
|
if (!val || !val.trim()) return "Path is required";
|
|
221
|
-
if (!existsSync(
|
|
229
|
+
if (!existsSync(expandPath(val.trim())))
|
|
222
230
|
return `File not found: ${val.trim()}`;
|
|
223
231
|
if (!val.trim().endsWith(".zip")) return "Must be a .zip file";
|
|
224
232
|
},
|
|
225
233
|
});
|
|
226
234
|
handleCancel(zipInput);
|
|
227
|
-
zipPath =
|
|
235
|
+
zipPath = expandPath((zipInput as string).trim());
|
|
228
236
|
} else {
|
|
229
237
|
zipPath = selected as string;
|
|
230
238
|
}
|
|
@@ -234,13 +242,13 @@ export async function deployCommand(
|
|
|
234
242
|
placeholder: "~/Downloads/my-ability.zip",
|
|
235
243
|
validate: (val) => {
|
|
236
244
|
if (!val || !val.trim()) return "Path is required";
|
|
237
|
-
if (!existsSync(
|
|
245
|
+
if (!existsSync(expandPath(val.trim())))
|
|
238
246
|
return `File not found: ${val.trim()}`;
|
|
239
247
|
if (!val.trim().endsWith(".zip")) return "Must be a .zip file";
|
|
240
248
|
},
|
|
241
249
|
});
|
|
242
250
|
handleCancel(zipInput);
|
|
243
|
-
zipPath =
|
|
251
|
+
zipPath = expandPath((zipInput as string).trim());
|
|
244
252
|
}
|
|
245
253
|
|
|
246
254
|
await deployZip(zipPath, opts);
|
|
@@ -386,7 +394,7 @@ export async function deployCommand(
|
|
|
386
394
|
placeholder: "./icon.png",
|
|
387
395
|
validate: (val) => {
|
|
388
396
|
if (!val || !val.trim()) return undefined;
|
|
389
|
-
const resolved =
|
|
397
|
+
const resolved = expandPath(val.trim());
|
|
390
398
|
if (!existsSync(resolved)) return `File not found: ${val.trim()}`;
|
|
391
399
|
if (!IMAGE_EXTS.has(extname(resolved).toLowerCase()))
|
|
392
400
|
return "Image must be PNG or JPG";
|
|
@@ -394,7 +402,7 @@ export async function deployCommand(
|
|
|
394
402
|
});
|
|
395
403
|
handleCancel(imgInput);
|
|
396
404
|
const trimmed = (imgInput as string).trim();
|
|
397
|
-
if (trimmed) imagePath =
|
|
405
|
+
if (trimmed) imagePath = expandPath(trimmed);
|
|
398
406
|
} else if (selected !== "__skip__") {
|
|
399
407
|
imagePath = selected as string;
|
|
400
408
|
}
|
|
@@ -405,7 +413,7 @@ export async function deployCommand(
|
|
|
405
413
|
placeholder: "./icon.png",
|
|
406
414
|
validate: (val) => {
|
|
407
415
|
if (!val || !val.trim()) return undefined;
|
|
408
|
-
const resolved =
|
|
416
|
+
const resolved = expandPath(val.trim());
|
|
409
417
|
if (!existsSync(resolved)) return `File not found: ${val.trim()}`;
|
|
410
418
|
if (!IMAGE_EXTS.has(extname(resolved).toLowerCase()))
|
|
411
419
|
return "Image must be PNG or JPG";
|
|
@@ -413,7 +421,7 @@ export async function deployCommand(
|
|
|
413
421
|
});
|
|
414
422
|
handleCancel(imgInput);
|
|
415
423
|
const trimmed = (imgInput as string).trim();
|
|
416
|
-
if (trimmed) imagePath =
|
|
424
|
+
if (trimmed) imagePath = expandPath(trimmed);
|
|
417
425
|
}
|
|
418
426
|
}
|
|
419
427
|
|
|
@@ -549,6 +557,7 @@ export async function deployCommand(
|
|
|
549
557
|
return;
|
|
550
558
|
}
|
|
551
559
|
|
|
560
|
+
if (await handleIfSessionExpired(err)) return;
|
|
552
561
|
const msg = err instanceof Error ? err.message : String(err);
|
|
553
562
|
if (msg.toLowerCase().includes("same name")) {
|
|
554
563
|
error(`An ability named "${uniqueName}" already exists.`);
|
|
@@ -693,6 +702,7 @@ async function deployZip(
|
|
|
693
702
|
p.outro("Deployed successfully! 🎉");
|
|
694
703
|
} catch (err) {
|
|
695
704
|
s.stop("Upload failed.");
|
|
705
|
+
if (await handleIfSessionExpired(err)) return;
|
|
696
706
|
error(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
697
707
|
process.exit(1);
|
|
698
708
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SessionExpiredError } from "../api/client.js";
|
|
2
|
+
import { setupJwt } from "./login.js";
|
|
3
|
+
import { error, p } from "../ui/format.js";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
|
|
6
|
+
export async function handleIfSessionExpired(err: unknown): Promise<boolean> {
|
|
7
|
+
if (!(err instanceof SessionExpiredError)) return false;
|
|
8
|
+
|
|
9
|
+
console.log("");
|
|
10
|
+
p.note(
|
|
11
|
+
[
|
|
12
|
+
"Your session token has expired or been invalidated.",
|
|
13
|
+
"This happens when you log into the OpenHome website again.",
|
|
14
|
+
"",
|
|
15
|
+
`You need to grab a fresh token — it only takes 30 seconds.`,
|
|
16
|
+
].join("\n"),
|
|
17
|
+
chalk.yellow("Session expired"),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
await setupJwt();
|
|
21
|
+
p.note("Token updated. Run the command again to continue.", "Ready");
|
|
22
|
+
return true;
|
|
23
|
+
}
|
package/src/commands/list.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
|
+
import { handleIfSessionExpired } from "./handle-session-expired.js";
|
|
2
3
|
import { MockApiClient } from "../api/mock-client.js";
|
|
3
4
|
import { getApiKey, getConfig, getJwt } from "../config/store.js";
|
|
4
5
|
import { error, warn, info, table, p } from "../ui/format.js";
|
|
@@ -77,6 +78,7 @@ export async function listCommand(
|
|
|
77
78
|
p.outro("List endpoint not yet implemented.");
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
81
|
+
if (await handleIfSessionExpired(err)) return;
|
|
80
82
|
error(
|
|
81
83
|
`Failed to list abilities: ${err instanceof Error ? err.message : String(err)}`,
|
|
82
84
|
);
|
package/src/commands/toggle.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
|
+
import { handleIfSessionExpired } from "./handle-session-expired.js";
|
|
2
3
|
import { MockApiClient } from "../api/mock-client.js";
|
|
3
4
|
import { getApiKey, getConfig, getJwt } from "../config/store.js";
|
|
4
5
|
import { error, success, p, handleCancel } from "../ui/format.js";
|
|
@@ -119,6 +120,7 @@ export async function toggleCommand(
|
|
|
119
120
|
return;
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
if (await handleIfSessionExpired(err)) return;
|
|
122
124
|
error(`Toggle failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
123
125
|
process.exit(1);
|
|
124
126
|
}
|