openhome-cli 0.1.0
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/README.md +470 -0
- package/bin/openhome.js +2 -0
- package/dist/chunk-Q4UKUXDB.js +164 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3184 -0
- package/dist/store-DR7EKQ5T.js +16 -0
- package/package.json +44 -0
- package/src/api/client.ts +231 -0
- package/src/api/contracts.ts +103 -0
- package/src/api/endpoints.ts +19 -0
- package/src/api/mock-client.ts +145 -0
- package/src/cli.ts +339 -0
- package/src/commands/agents.ts +88 -0
- package/src/commands/assign.ts +123 -0
- package/src/commands/chat.ts +265 -0
- package/src/commands/config-edit.ts +163 -0
- package/src/commands/delete.ts +107 -0
- package/src/commands/deploy.ts +430 -0
- package/src/commands/init.ts +895 -0
- package/src/commands/list.ts +78 -0
- package/src/commands/login.ts +54 -0
- package/src/commands/logout.ts +14 -0
- package/src/commands/logs.ts +174 -0
- package/src/commands/status.ts +174 -0
- package/src/commands/toggle.ts +118 -0
- package/src/commands/trigger.ts +193 -0
- package/src/commands/validate.ts +53 -0
- package/src/commands/whoami.ts +54 -0
- package/src/config/keychain.ts +62 -0
- package/src/config/store.ts +137 -0
- package/src/ui/format.ts +95 -0
- package/src/util/zip.ts +74 -0
- package/src/validation/rules.ts +71 -0
- package/src/validation/validator.ts +204 -0
- package/tasks/feature-request-sdk-api.md +246 -0
- package/tasks/prd-openhome-cli.md +605 -0
- package/templates/api/README.md.tmpl +11 -0
- package/templates/api/__init__.py.tmpl +0 -0
- package/templates/api/config.json.tmpl +4 -0
- package/templates/api/main.py.tmpl +30 -0
- package/templates/basic/README.md.tmpl +7 -0
- package/templates/basic/__init__.py.tmpl +0 -0
- package/templates/basic/config.json.tmpl +4 -0
- package/templates/basic/main.py.tmpl +22 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
|
+
import { MockApiClient } from "../api/mock-client.js";
|
|
3
|
+
import { getApiKey, getConfig } from "../config/store.js";
|
|
4
|
+
import { error, warn, info, table, p } from "../ui/format.js";
|
|
5
|
+
import type { TableRow } from "../ui/format.js";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
|
|
8
|
+
function statusColor(status: string): string {
|
|
9
|
+
switch (status) {
|
|
10
|
+
case "active":
|
|
11
|
+
return chalk.green(status);
|
|
12
|
+
case "processing":
|
|
13
|
+
return chalk.yellow(status);
|
|
14
|
+
case "failed":
|
|
15
|
+
return chalk.red(status);
|
|
16
|
+
case "disabled":
|
|
17
|
+
return chalk.gray(status);
|
|
18
|
+
default:
|
|
19
|
+
return status;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function listCommand(
|
|
24
|
+
opts: { mock?: boolean } = {},
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
p.intro("📋 Your abilities");
|
|
27
|
+
|
|
28
|
+
let client: ApiClient | MockApiClient;
|
|
29
|
+
|
|
30
|
+
if (opts.mock) {
|
|
31
|
+
client = new MockApiClient();
|
|
32
|
+
} else {
|
|
33
|
+
const apiKey = getApiKey();
|
|
34
|
+
if (!apiKey) {
|
|
35
|
+
error("Not authenticated. Run: openhome login");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
client = new ApiClient(apiKey, getConfig().api_base_url);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const s = p.spinner();
|
|
42
|
+
s.start("Fetching abilities...");
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const { abilities } = await client.listAbilities();
|
|
46
|
+
s.stop(`Found ${abilities.length} ability(s).`);
|
|
47
|
+
|
|
48
|
+
if (abilities.length === 0) {
|
|
49
|
+
info("No abilities found. Run: openhome init");
|
|
50
|
+
p.outro("Deploy your first ability with: openhome deploy");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rows: TableRow[] = abilities.map((a) => ({
|
|
55
|
+
Name: a.unique_name,
|
|
56
|
+
Display: a.display_name,
|
|
57
|
+
Version: a.version,
|
|
58
|
+
Status: statusColor(a.status),
|
|
59
|
+
Updated: new Date(a.updated_at).toLocaleDateString(),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
console.log("");
|
|
63
|
+
table(rows);
|
|
64
|
+
p.outro(`${abilities.length} ability(s) total.`);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
s.stop("Failed.");
|
|
67
|
+
|
|
68
|
+
if (err instanceof NotImplementedError) {
|
|
69
|
+
p.note("Use --mock to see example output.", "API Not Available Yet");
|
|
70
|
+
p.outro("List endpoint not yet implemented.");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
error(
|
|
74
|
+
`Failed to list abilities: ${err instanceof Error ? err.message : String(err)}`,
|
|
75
|
+
);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ApiClient } from "../api/client.js";
|
|
2
|
+
import type { Personality } from "../api/contracts.js";
|
|
3
|
+
import { saveApiKey } from "../config/store.js";
|
|
4
|
+
import { success, error, info, p, handleCancel } from "../ui/format.js";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
|
|
7
|
+
export async function loginCommand(): Promise<void> {
|
|
8
|
+
p.intro("🔑 OpenHome Login");
|
|
9
|
+
|
|
10
|
+
const apiKey = await p.password({
|
|
11
|
+
message: "Enter your OpenHome API key",
|
|
12
|
+
validate: (val) => {
|
|
13
|
+
if (!val || !val.trim()) return "API key is required";
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
handleCancel(apiKey);
|
|
17
|
+
|
|
18
|
+
const s = p.spinner();
|
|
19
|
+
s.start("Verifying API key...");
|
|
20
|
+
|
|
21
|
+
let agents: Personality[];
|
|
22
|
+
try {
|
|
23
|
+
const client = new ApiClient(apiKey as string);
|
|
24
|
+
const verification = await client.verifyApiKey(apiKey as string);
|
|
25
|
+
if (!verification.valid) {
|
|
26
|
+
s.stop("Verification failed.");
|
|
27
|
+
error(verification.message ?? "Invalid API key.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
agents = await client.getPersonalities();
|
|
31
|
+
s.stop("API key verified.");
|
|
32
|
+
} catch (err) {
|
|
33
|
+
s.stop("Verification failed.");
|
|
34
|
+
error(err instanceof Error ? err.message : String(err));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
saveApiKey(apiKey as string);
|
|
39
|
+
success("API key saved.");
|
|
40
|
+
|
|
41
|
+
// Show agents on this account
|
|
42
|
+
if (agents.length > 0) {
|
|
43
|
+
p.note(
|
|
44
|
+
agents
|
|
45
|
+
.map((a) => `${chalk.bold(a.name)} ${chalk.gray(a.id)}`)
|
|
46
|
+
.join("\n"),
|
|
47
|
+
`${agents.length} agent(s) on this account`,
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
info("No agents found. Create one at https://app.openhome.com");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
p.outro("Logged in! You're ready to go.");
|
|
54
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getConfig, saveConfig } from "../config/store.js";
|
|
2
|
+
import { keychainDelete } from "../config/keychain.js";
|
|
3
|
+
import { success } from "../ui/format.js";
|
|
4
|
+
|
|
5
|
+
export async function logoutCommand(): Promise<void> {
|
|
6
|
+
keychainDelete();
|
|
7
|
+
|
|
8
|
+
const config = getConfig();
|
|
9
|
+
delete config.api_key;
|
|
10
|
+
delete config.default_personality_id;
|
|
11
|
+
saveConfig(config);
|
|
12
|
+
|
|
13
|
+
success("Logged out. API key and default agent cleared.");
|
|
14
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { getApiKey, getConfig } from "../config/store.js";
|
|
3
|
+
import { ApiClient } from "../api/client.js";
|
|
4
|
+
import { WS_BASE, ENDPOINTS } from "../api/endpoints.js";
|
|
5
|
+
import { error, success, info, p, handleCancel } from "../ui/format.js";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
|
|
8
|
+
const PING_INTERVAL = 30_000;
|
|
9
|
+
|
|
10
|
+
export async function logsCommand(
|
|
11
|
+
opts: { agent?: string } = {},
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
p.intro("📡 Stream agent logs");
|
|
14
|
+
|
|
15
|
+
const apiKey = getApiKey();
|
|
16
|
+
if (!apiKey) {
|
|
17
|
+
error("Not authenticated. Run: openhome login");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let agentId = opts.agent ?? getConfig().default_personality_id;
|
|
22
|
+
|
|
23
|
+
if (!agentId) {
|
|
24
|
+
const s = p.spinner();
|
|
25
|
+
s.start("Fetching agents...");
|
|
26
|
+
try {
|
|
27
|
+
const client = new ApiClient(apiKey);
|
|
28
|
+
const agents = await client.getPersonalities();
|
|
29
|
+
s.stop(`Found ${agents.length} agent(s).`);
|
|
30
|
+
|
|
31
|
+
if (agents.length === 0) {
|
|
32
|
+
error("No agents found.");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const selected = await p.select({
|
|
37
|
+
message: "Which agent?",
|
|
38
|
+
options: agents.map((a) => ({
|
|
39
|
+
value: a.id,
|
|
40
|
+
label: a.name,
|
|
41
|
+
hint: a.id,
|
|
42
|
+
})),
|
|
43
|
+
});
|
|
44
|
+
handleCancel(selected);
|
|
45
|
+
agentId = selected as string;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
s.stop("Failed.");
|
|
48
|
+
error(
|
|
49
|
+
`Could not fetch agents: ${err instanceof Error ? err.message : String(err)}`,
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const wsUrl = `${WS_BASE}${ENDPOINTS.voiceStream(apiKey, agentId)}`;
|
|
56
|
+
info(`Streaming logs from agent ${chalk.bold(agentId)}...`);
|
|
57
|
+
info(`Press ${chalk.bold("Ctrl+C")} to stop.\n`);
|
|
58
|
+
|
|
59
|
+
await new Promise<void>((resolve) => {
|
|
60
|
+
const ws = new WebSocket(wsUrl, {
|
|
61
|
+
perMessageDeflate: false,
|
|
62
|
+
headers: {
|
|
63
|
+
Origin: "https://app.openhome.com",
|
|
64
|
+
"User-Agent":
|
|
65
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
let pingInterval: ReturnType<typeof setInterval> | null = null;
|
|
70
|
+
|
|
71
|
+
ws.on("open", () => {
|
|
72
|
+
success("Connected. Waiting for messages...\n");
|
|
73
|
+
|
|
74
|
+
pingInterval = setInterval(() => {
|
|
75
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
76
|
+
ws.send(JSON.stringify({ type: "ping" }));
|
|
77
|
+
}
|
|
78
|
+
}, PING_INTERVAL);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
ws.on("message", (raw: WebSocket.Data) => {
|
|
82
|
+
try {
|
|
83
|
+
const msg = JSON.parse(raw.toString()) as {
|
|
84
|
+
type: string;
|
|
85
|
+
data: unknown;
|
|
86
|
+
};
|
|
87
|
+
const ts = chalk.gray(new Date().toLocaleTimeString());
|
|
88
|
+
|
|
89
|
+
switch (msg.type) {
|
|
90
|
+
case "log":
|
|
91
|
+
console.log(
|
|
92
|
+
`${ts} ${chalk.blue("[LOG]")} ${JSON.stringify(msg.data)}`,
|
|
93
|
+
);
|
|
94
|
+
break;
|
|
95
|
+
case "action":
|
|
96
|
+
console.log(
|
|
97
|
+
`${ts} ${chalk.magenta("[ACTION]")} ${JSON.stringify(msg.data)}`,
|
|
98
|
+
);
|
|
99
|
+
break;
|
|
100
|
+
case "progress":
|
|
101
|
+
console.log(
|
|
102
|
+
`${ts} ${chalk.yellow("[PROGRESS]")} ${JSON.stringify(msg.data)}`,
|
|
103
|
+
);
|
|
104
|
+
break;
|
|
105
|
+
case "question":
|
|
106
|
+
console.log(
|
|
107
|
+
`${ts} ${chalk.cyan("[QUESTION]")} ${JSON.stringify(msg.data)}`,
|
|
108
|
+
);
|
|
109
|
+
break;
|
|
110
|
+
case "message": {
|
|
111
|
+
const data = msg.data as {
|
|
112
|
+
content?: string;
|
|
113
|
+
role?: string;
|
|
114
|
+
live?: boolean;
|
|
115
|
+
};
|
|
116
|
+
if (data.content && !data.live) {
|
|
117
|
+
const role =
|
|
118
|
+
data.role === "assistant"
|
|
119
|
+
? chalk.cyan("AGENT")
|
|
120
|
+
: chalk.green("USER");
|
|
121
|
+
console.log(`${ts} ${chalk.white(`[${role}]`)} ${data.content}`);
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "text": {
|
|
126
|
+
const textData = msg.data as string;
|
|
127
|
+
if (textData === "audio-init") {
|
|
128
|
+
ws.send(JSON.stringify({ type: "text", data: "bot-speaking" }));
|
|
129
|
+
} else if (textData === "audio-end") {
|
|
130
|
+
ws.send(JSON.stringify({ type: "text", data: "bot-speak-end" }));
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "audio":
|
|
135
|
+
ws.send(JSON.stringify({ type: "ack", data: "audio-received" }));
|
|
136
|
+
break;
|
|
137
|
+
case "error-event": {
|
|
138
|
+
const errData = msg.data as { message?: string; title?: string };
|
|
139
|
+
console.log(
|
|
140
|
+
`${ts} ${chalk.red("[ERROR]")} ${errData?.message || errData?.title || JSON.stringify(msg.data)}`,
|
|
141
|
+
);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
default:
|
|
145
|
+
console.log(
|
|
146
|
+
`${ts} ${chalk.gray(`[${msg.type}]`)} ${JSON.stringify(msg.data)}`,
|
|
147
|
+
);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
// ignore non-JSON
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
ws.on("error", (err: Error) => {
|
|
156
|
+
error(`WebSocket error: ${err.message}`);
|
|
157
|
+
resolve();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
ws.on("close", (code: number) => {
|
|
161
|
+
if (pingInterval) clearInterval(pingInterval);
|
|
162
|
+
console.log("");
|
|
163
|
+
info(`Connection closed (code: ${code})`);
|
|
164
|
+
resolve();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Handle Ctrl+C
|
|
168
|
+
process.on("SIGINT", () => {
|
|
169
|
+
console.log("");
|
|
170
|
+
info("Stopping log stream...");
|
|
171
|
+
ws.close(1000);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
5
|
+
import { MockApiClient } from "../api/mock-client.js";
|
|
6
|
+
import { getApiKey, getConfig, getTrackedAbilities } from "../config/store.js";
|
|
7
|
+
import { error, warn, info, p, handleCancel } from "../ui/format.js";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
|
|
10
|
+
function statusBadge(status: string): string {
|
|
11
|
+
switch (status) {
|
|
12
|
+
case "active":
|
|
13
|
+
return chalk.bgGreen.black(` ${status.toUpperCase()} `);
|
|
14
|
+
case "processing":
|
|
15
|
+
return chalk.bgYellow.black(` ${status.toUpperCase()} `);
|
|
16
|
+
case "failed":
|
|
17
|
+
return chalk.bgRed.white(` ${status.toUpperCase()} `);
|
|
18
|
+
case "disabled":
|
|
19
|
+
return chalk.bgGray.white(` ${status.toUpperCase()} `);
|
|
20
|
+
default:
|
|
21
|
+
return chalk.bgWhite.black(` ${status.toUpperCase()} `);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Read unique_name from a config.json in a directory. */
|
|
26
|
+
function readAbilityName(dir: string): string | null {
|
|
27
|
+
const configPath = join(dir, "config.json");
|
|
28
|
+
if (!existsSync(configPath)) return null;
|
|
29
|
+
try {
|
|
30
|
+
const cfg = JSON.parse(readFileSync(configPath, "utf8")) as {
|
|
31
|
+
unique_name?: string;
|
|
32
|
+
};
|
|
33
|
+
return cfg.unique_name ?? null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Resolve ability name: check cwd, then pick from tracked abilities. */
|
|
40
|
+
async function resolveAbilityName(): Promise<string | undefined> {
|
|
41
|
+
// Check current directory first
|
|
42
|
+
const cwdName = readAbilityName(resolve("."));
|
|
43
|
+
if (cwdName) {
|
|
44
|
+
info(`Detected ability: ${cwdName}`);
|
|
45
|
+
return cwdName;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Build list from tracked abilities
|
|
49
|
+
const tracked = getTrackedAbilities();
|
|
50
|
+
const options: { value: string; label: string; hint?: string }[] = [];
|
|
51
|
+
const home = homedir();
|
|
52
|
+
|
|
53
|
+
for (const a of tracked) {
|
|
54
|
+
const name = readAbilityName(a.path);
|
|
55
|
+
if (name) {
|
|
56
|
+
options.push({
|
|
57
|
+
value: name,
|
|
58
|
+
label: a.name,
|
|
59
|
+
hint: a.path.startsWith(home)
|
|
60
|
+
? `~${a.path.slice(home.length)}`
|
|
61
|
+
: a.path,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (options.length === 1) {
|
|
67
|
+
info(`Using ability: ${options[0].label}`);
|
|
68
|
+
return options[0].value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (options.length > 0) {
|
|
72
|
+
const selected = await p.select({
|
|
73
|
+
message: "Which ability do you want to check?",
|
|
74
|
+
options,
|
|
75
|
+
});
|
|
76
|
+
handleCancel(selected);
|
|
77
|
+
return selected as string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function statusCommand(
|
|
84
|
+
abilityArg?: string,
|
|
85
|
+
opts: { mock?: boolean } = {},
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
let abilityId = abilityArg;
|
|
88
|
+
|
|
89
|
+
// If no arg, try to resolve from cwd or tracked abilities
|
|
90
|
+
if (!abilityId) {
|
|
91
|
+
abilityId = await resolveAbilityName();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!abilityId) {
|
|
95
|
+
error(
|
|
96
|
+
"No ability found. Pass a name, run from an ability directory, or create one with: openhome init",
|
|
97
|
+
);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
p.intro(`🔍 Status: ${abilityId}`);
|
|
102
|
+
|
|
103
|
+
let client: ApiClient | MockApiClient;
|
|
104
|
+
|
|
105
|
+
if (opts.mock) {
|
|
106
|
+
client = new MockApiClient();
|
|
107
|
+
} else {
|
|
108
|
+
const apiKey = getApiKey();
|
|
109
|
+
if (!apiKey) {
|
|
110
|
+
error("Not authenticated. Run: openhome login");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
client = new ApiClient(apiKey, getConfig().api_base_url);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const s = p.spinner();
|
|
117
|
+
s.start("Fetching status...");
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const ability = await client.getAbility(abilityId);
|
|
121
|
+
s.stop("Status loaded.");
|
|
122
|
+
|
|
123
|
+
// Main info
|
|
124
|
+
p.note(
|
|
125
|
+
[
|
|
126
|
+
`Name: ${ability.unique_name}`,
|
|
127
|
+
`Display: ${ability.display_name}`,
|
|
128
|
+
`Status: ${statusBadge(ability.status)}`,
|
|
129
|
+
`Version: v${ability.version}`,
|
|
130
|
+
`Updated: ${new Date(ability.updated_at).toLocaleString()}`,
|
|
131
|
+
`Created: ${new Date(ability.created_at).toLocaleString()}`,
|
|
132
|
+
ability.personality_ids.length > 0
|
|
133
|
+
? `Linked to: ${ability.personality_ids.join(", ")}`
|
|
134
|
+
: null,
|
|
135
|
+
]
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
.join("\n"),
|
|
138
|
+
"Ability Details",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Validation errors
|
|
142
|
+
if (ability.validation_errors.length > 0) {
|
|
143
|
+
p.note(
|
|
144
|
+
ability.validation_errors.map((e) => chalk.red(`✗ ${e}`)).join("\n"),
|
|
145
|
+
"Validation Errors",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Deploy history
|
|
150
|
+
if (ability.deploy_history.length > 0) {
|
|
151
|
+
const historyLines = ability.deploy_history.map((event) => {
|
|
152
|
+
const icon =
|
|
153
|
+
event.status === "success" ? chalk.green("✓") : chalk.red("✗");
|
|
154
|
+
return `${icon} v${event.version} ${event.message} ${chalk.gray(new Date(event.timestamp).toLocaleString())}`;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
p.note(historyLines.join("\n"), "Deploy History");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
p.outro("Done.");
|
|
161
|
+
} catch (err) {
|
|
162
|
+
s.stop("Failed.");
|
|
163
|
+
|
|
164
|
+
if (err instanceof NotImplementedError) {
|
|
165
|
+
p.note("Use --mock to see example output.", "API Not Available Yet");
|
|
166
|
+
p.outro("Status endpoint not yet implemented.");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
error(
|
|
170
|
+
`Failed to get status: ${err instanceof Error ? err.message : String(err)}`,
|
|
171
|
+
);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ApiClient, NotImplementedError } from "../api/client.js";
|
|
2
|
+
import { MockApiClient } from "../api/mock-client.js";
|
|
3
|
+
import { getApiKey, getConfig } from "../config/store.js";
|
|
4
|
+
import { error, success, p, handleCancel } from "../ui/format.js";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
|
|
7
|
+
export async function toggleCommand(
|
|
8
|
+
abilityArg?: string,
|
|
9
|
+
opts: { mock?: boolean; enable?: boolean; disable?: boolean } = {},
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
p.intro("⚡ Enable / Disable ability");
|
|
12
|
+
|
|
13
|
+
let client: ApiClient | MockApiClient;
|
|
14
|
+
|
|
15
|
+
if (opts.mock) {
|
|
16
|
+
client = new MockApiClient();
|
|
17
|
+
} else {
|
|
18
|
+
const apiKey = getApiKey();
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
error("Not authenticated. Run: openhome login");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
client = new ApiClient(apiKey, getConfig().api_base_url);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Fetch abilities
|
|
27
|
+
const s = p.spinner();
|
|
28
|
+
s.start("Fetching abilities...");
|
|
29
|
+
|
|
30
|
+
let abilities: Awaited<ReturnType<typeof client.listAbilities>>["abilities"];
|
|
31
|
+
try {
|
|
32
|
+
const result = await client.listAbilities();
|
|
33
|
+
abilities = result.abilities;
|
|
34
|
+
s.stop(`Found ${abilities.length} ability(s).`);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
s.stop("Failed to fetch abilities.");
|
|
37
|
+
error(err instanceof Error ? err.message : String(err));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (abilities.length === 0) {
|
|
42
|
+
p.outro("No abilities found.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Resolve target
|
|
47
|
+
let targetId: string;
|
|
48
|
+
let targetName: string;
|
|
49
|
+
|
|
50
|
+
if (abilityArg) {
|
|
51
|
+
const match = abilities.find(
|
|
52
|
+
(a) =>
|
|
53
|
+
a.unique_name === abilityArg ||
|
|
54
|
+
a.display_name === abilityArg ||
|
|
55
|
+
a.ability_id === abilityArg,
|
|
56
|
+
);
|
|
57
|
+
if (!match) {
|
|
58
|
+
error(`No ability found matching "${abilityArg}".`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
targetId = match.ability_id;
|
|
62
|
+
targetName = match.unique_name;
|
|
63
|
+
} else {
|
|
64
|
+
const selected = await p.select({
|
|
65
|
+
message: "Which ability do you want to toggle?",
|
|
66
|
+
options: abilities.map((a) => ({
|
|
67
|
+
value: a.ability_id,
|
|
68
|
+
label: a.unique_name,
|
|
69
|
+
hint: `${a.status === "disabled" ? chalk.gray("disabled") : chalk.green("enabled")} v${a.version}`,
|
|
70
|
+
})),
|
|
71
|
+
});
|
|
72
|
+
handleCancel(selected);
|
|
73
|
+
targetId = selected as string;
|
|
74
|
+
targetName =
|
|
75
|
+
abilities.find((a) => a.ability_id === targetId)?.unique_name ?? targetId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Resolve enable/disable
|
|
79
|
+
let enabled: boolean;
|
|
80
|
+
|
|
81
|
+
if (opts.enable) {
|
|
82
|
+
enabled = true;
|
|
83
|
+
} else if (opts.disable) {
|
|
84
|
+
enabled = false;
|
|
85
|
+
} else {
|
|
86
|
+
const current = abilities.find((a) => a.ability_id === targetId);
|
|
87
|
+
const action = await p.select({
|
|
88
|
+
message: `"${targetName}" is currently ${current?.status ?? "unknown"}. What do you want to do?`,
|
|
89
|
+
options: [
|
|
90
|
+
{ value: "enable", label: "Enable" },
|
|
91
|
+
{ value: "disable", label: "Disable" },
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
handleCancel(action);
|
|
95
|
+
enabled = action === "enable";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
s.start(`${enabled ? "Enabling" : "Disabling"} "${targetName}"...`);
|
|
99
|
+
try {
|
|
100
|
+
const result = await client.toggleCapability(targetId, enabled);
|
|
101
|
+
s.stop("Done.");
|
|
102
|
+
success(
|
|
103
|
+
result.message ??
|
|
104
|
+
`"${targetName}" ${enabled ? "enabled" : "disabled"} successfully.`,
|
|
105
|
+
);
|
|
106
|
+
p.outro("Done.");
|
|
107
|
+
} catch (err) {
|
|
108
|
+
s.stop("Failed.");
|
|
109
|
+
|
|
110
|
+
if (err instanceof NotImplementedError) {
|
|
111
|
+
p.note("Toggle endpoint not yet implemented.", "API Not Available Yet");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
error(`Toggle failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|