@chvor/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/bin/chvor.js +2 -0
- package/dist/cli.js +226 -0
- package/dist/commands/auth.js +50 -0
- package/dist/commands/docker.js +17 -0
- package/dist/commands/init.js +221 -0
- package/dist/commands/instances.js +126 -0
- package/dist/commands/onboard.js +84 -0
- package/dist/commands/skill.js +340 -0
- package/dist/commands/start.js +19 -0
- package/dist/commands/stop.js +4 -0
- package/dist/commands/update.js +23 -0
- package/dist/lib/config.js +26 -0
- package/dist/lib/download.js +145 -0
- package/dist/lib/paths.js +45 -0
- package/dist/lib/platform.js +28 -0
- package/dist/lib/process.js +220 -0
- package/dist/lib/registry-auth.js +130 -0
- package/dist/lib/template-loader.js +190 -0
- package/dist/lib/template-provisioner.js +120 -0
- package/dist/lib/validate.js +7 -0
- package/package.json +44 -0
package/bin/chvor.js
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
|
|
7
|
+
const program = new Command()
|
|
8
|
+
.name("chvor")
|
|
9
|
+
.description("Your own AI — install and run chvor.")
|
|
10
|
+
.version(pkg.version);
|
|
11
|
+
program
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const { isOnboarded } = await import("./lib/config.js");
|
|
14
|
+
if (!isOnboarded()) {
|
|
15
|
+
const { onboard } = await import("./commands/onboard.js");
|
|
16
|
+
await onboard();
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const { start } = await import("./commands/start.js");
|
|
20
|
+
await start({});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
program
|
|
24
|
+
.command("start")
|
|
25
|
+
.description("Start the chvor server")
|
|
26
|
+
.option("-p, --port <port>", "Server port")
|
|
27
|
+
.option("--foreground", "Run in foreground (no detach)")
|
|
28
|
+
.option("-i, --instance <name>", "Named instance to start")
|
|
29
|
+
.action(async (opts) => {
|
|
30
|
+
if (opts.instance) {
|
|
31
|
+
const { setInstance } = await import("./lib/paths.js");
|
|
32
|
+
setInstance(opts.instance);
|
|
33
|
+
}
|
|
34
|
+
if (opts.port) {
|
|
35
|
+
const { validatePort } = await import("./lib/validate.js");
|
|
36
|
+
validatePort(opts.port);
|
|
37
|
+
}
|
|
38
|
+
const { start } = await import("./commands/start.js");
|
|
39
|
+
await start(opts);
|
|
40
|
+
});
|
|
41
|
+
program
|
|
42
|
+
.command("stop")
|
|
43
|
+
.description("Stop the running chvor server")
|
|
44
|
+
.option("-i, --instance <name>", "Named instance to stop")
|
|
45
|
+
.action(async (opts) => {
|
|
46
|
+
if (opts.instance) {
|
|
47
|
+
const { setInstance } = await import("./lib/paths.js");
|
|
48
|
+
setInstance(opts.instance);
|
|
49
|
+
}
|
|
50
|
+
const { stop } = await import("./commands/stop.js");
|
|
51
|
+
await stop();
|
|
52
|
+
});
|
|
53
|
+
program
|
|
54
|
+
.command("init")
|
|
55
|
+
.description("Set up a new agent from a template")
|
|
56
|
+
.option("-t, --template <name>", "Template name or ID")
|
|
57
|
+
.option("-n, --name <instance>", "Instance name for multi-instance support")
|
|
58
|
+
.option("--from <path>", "Local template directory path")
|
|
59
|
+
.action(async (opts) => {
|
|
60
|
+
const { init } = await import("./commands/init.js");
|
|
61
|
+
await init(opts);
|
|
62
|
+
});
|
|
63
|
+
const instancesCmd = program
|
|
64
|
+
.command("instances")
|
|
65
|
+
.description("Manage chvor instances");
|
|
66
|
+
instancesCmd
|
|
67
|
+
.action(async () => {
|
|
68
|
+
const { listInstances } = await import("./commands/instances.js");
|
|
69
|
+
await listInstances();
|
|
70
|
+
});
|
|
71
|
+
instancesCmd
|
|
72
|
+
.command("start <name>")
|
|
73
|
+
.description("Start a named instance")
|
|
74
|
+
.action(async (name) => {
|
|
75
|
+
const { startInstance } = await import("./commands/instances.js");
|
|
76
|
+
await startInstance(name);
|
|
77
|
+
});
|
|
78
|
+
instancesCmd
|
|
79
|
+
.command("stop <name>")
|
|
80
|
+
.description("Stop a named instance")
|
|
81
|
+
.action(async (name) => {
|
|
82
|
+
const { stopInstance } = await import("./commands/instances.js");
|
|
83
|
+
await stopInstance(name);
|
|
84
|
+
});
|
|
85
|
+
program
|
|
86
|
+
.command("onboard")
|
|
87
|
+
.description("Interactive first-time setup wizard")
|
|
88
|
+
.action(async () => {
|
|
89
|
+
const { onboard } = await import("./commands/onboard.js");
|
|
90
|
+
await onboard();
|
|
91
|
+
});
|
|
92
|
+
program
|
|
93
|
+
.command("update")
|
|
94
|
+
.description("Update to the latest chvor release")
|
|
95
|
+
.action(async () => {
|
|
96
|
+
const { update } = await import("./commands/update.js");
|
|
97
|
+
await update();
|
|
98
|
+
});
|
|
99
|
+
program
|
|
100
|
+
.command("docker")
|
|
101
|
+
.description("Pull and run chvor as a Docker container")
|
|
102
|
+
.option("-p, --port <port>", "Host port", "3001")
|
|
103
|
+
.action(async (opts) => {
|
|
104
|
+
const { docker } = await import("./commands/docker.js");
|
|
105
|
+
await docker(opts);
|
|
106
|
+
});
|
|
107
|
+
const skillCmd = program
|
|
108
|
+
.command("skill")
|
|
109
|
+
.description("Manage skills from the community registry");
|
|
110
|
+
skillCmd
|
|
111
|
+
.command("search <query>")
|
|
112
|
+
.description("Search the skill registry")
|
|
113
|
+
.action(async (query) => {
|
|
114
|
+
const { skillSearch } = await import("./commands/skill.js");
|
|
115
|
+
await skillSearch(query);
|
|
116
|
+
});
|
|
117
|
+
skillCmd
|
|
118
|
+
.command("install <name>")
|
|
119
|
+
.description("Install a skill from the registry")
|
|
120
|
+
.action(async (name) => {
|
|
121
|
+
const { skillInstall } = await import("./commands/skill.js");
|
|
122
|
+
await skillInstall(name);
|
|
123
|
+
});
|
|
124
|
+
skillCmd
|
|
125
|
+
.command("uninstall <name>")
|
|
126
|
+
.description("Uninstall a registry skill")
|
|
127
|
+
.action(async (name) => {
|
|
128
|
+
const { skillUninstall } = await import("./commands/skill.js");
|
|
129
|
+
await skillUninstall(name);
|
|
130
|
+
});
|
|
131
|
+
skillCmd
|
|
132
|
+
.command("update [name]")
|
|
133
|
+
.description("Update one or all registry skills")
|
|
134
|
+
.action(async (name) => {
|
|
135
|
+
const { skillUpdate } = await import("./commands/skill.js");
|
|
136
|
+
await skillUpdate(name);
|
|
137
|
+
});
|
|
138
|
+
skillCmd
|
|
139
|
+
.command("list")
|
|
140
|
+
.description("List all installed skills")
|
|
141
|
+
.action(async () => {
|
|
142
|
+
const { skillList } = await import("./commands/skill.js");
|
|
143
|
+
await skillList();
|
|
144
|
+
});
|
|
145
|
+
skillCmd
|
|
146
|
+
.command("info <name>")
|
|
147
|
+
.description("Show details for a registry skill")
|
|
148
|
+
.action(async (name) => {
|
|
149
|
+
const { skillInfo } = await import("./commands/skill.js");
|
|
150
|
+
await skillInfo(name);
|
|
151
|
+
});
|
|
152
|
+
skillCmd
|
|
153
|
+
.command("publish <path>")
|
|
154
|
+
.description("Validate a skill file for publishing")
|
|
155
|
+
.action(async (path) => {
|
|
156
|
+
const { skillPublish } = await import("./commands/skill.js");
|
|
157
|
+
await skillPublish(path);
|
|
158
|
+
});
|
|
159
|
+
const toolCmd = program
|
|
160
|
+
.command("tool")
|
|
161
|
+
.description("Manage tools from the community registry");
|
|
162
|
+
toolCmd
|
|
163
|
+
.command("search <query>")
|
|
164
|
+
.description("Search the tool registry")
|
|
165
|
+
.action(async (query) => {
|
|
166
|
+
const { toolSearch } = await import("./commands/skill.js");
|
|
167
|
+
await toolSearch(query);
|
|
168
|
+
});
|
|
169
|
+
toolCmd
|
|
170
|
+
.command("install <name>")
|
|
171
|
+
.description("Install a tool from the registry")
|
|
172
|
+
.action(async (name) => {
|
|
173
|
+
const { toolInstall } = await import("./commands/skill.js");
|
|
174
|
+
await toolInstall(name);
|
|
175
|
+
});
|
|
176
|
+
toolCmd
|
|
177
|
+
.command("uninstall <name>")
|
|
178
|
+
.description("Uninstall a registry tool")
|
|
179
|
+
.action(async (name) => {
|
|
180
|
+
const { toolUninstall } = await import("./commands/skill.js");
|
|
181
|
+
await toolUninstall(name);
|
|
182
|
+
});
|
|
183
|
+
toolCmd
|
|
184
|
+
.command("update [name]")
|
|
185
|
+
.description("Update one or all registry tools")
|
|
186
|
+
.action(async (name) => {
|
|
187
|
+
const { toolUpdate } = await import("./commands/skill.js");
|
|
188
|
+
await toolUpdate(name);
|
|
189
|
+
});
|
|
190
|
+
toolCmd
|
|
191
|
+
.command("list")
|
|
192
|
+
.description("List all installed tools")
|
|
193
|
+
.action(async () => {
|
|
194
|
+
const { toolList } = await import("./commands/skill.js");
|
|
195
|
+
await toolList();
|
|
196
|
+
});
|
|
197
|
+
toolCmd
|
|
198
|
+
.command("info <name>")
|
|
199
|
+
.description("Show details for a registry tool")
|
|
200
|
+
.action(async (name) => {
|
|
201
|
+
const { toolInfo } = await import("./commands/skill.js");
|
|
202
|
+
await toolInfo(name);
|
|
203
|
+
});
|
|
204
|
+
toolCmd
|
|
205
|
+
.command("publish <path>")
|
|
206
|
+
.description("Validate and publish a tool file")
|
|
207
|
+
.action(async (path) => {
|
|
208
|
+
const { toolPublish } = await import("./commands/skill.js");
|
|
209
|
+
await toolPublish(path);
|
|
210
|
+
});
|
|
211
|
+
const authCmd = program
|
|
212
|
+
.command("auth")
|
|
213
|
+
.description("Manage authentication");
|
|
214
|
+
authCmd
|
|
215
|
+
.command("reset")
|
|
216
|
+
.description("Reset authentication credentials (requires access to data directory)")
|
|
217
|
+
.option("-i, --instance <name>", "Named instance to reset")
|
|
218
|
+
.action(async (opts) => {
|
|
219
|
+
if (opts.instance) {
|
|
220
|
+
const { setInstance } = await import("./lib/paths.js");
|
|
221
|
+
setInstance(opts.instance);
|
|
222
|
+
}
|
|
223
|
+
const { authReset } = await import("./commands/auth.js");
|
|
224
|
+
await authReset();
|
|
225
|
+
});
|
|
226
|
+
await program.parseAsync(process.argv);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { confirm } from "@inquirer/prompts";
|
|
4
|
+
import { getDataDir } from "../lib/paths.js";
|
|
5
|
+
export async function authReset() {
|
|
6
|
+
const dataDir = getDataDir();
|
|
7
|
+
const dbPath = join(dataDir, "chvor.db");
|
|
8
|
+
if (!existsSync(dbPath)) {
|
|
9
|
+
console.error("No database found at", dbPath);
|
|
10
|
+
console.error("Nothing to reset.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const confirmed = await confirm({
|
|
14
|
+
message: "This will remove all authentication data (login credentials, sessions, API keys).\nYou will need to set up new credentials on next login.\n\nContinue?",
|
|
15
|
+
default: false,
|
|
16
|
+
});
|
|
17
|
+
if (!confirmed) {
|
|
18
|
+
console.log("Cancelled.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Dynamic import — better-sqlite3 is an optional peer dep for the CLI
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
let Database;
|
|
24
|
+
try {
|
|
25
|
+
const mod = await import("better-sqlite3");
|
|
26
|
+
Database = mod.default;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
console.error("better-sqlite3 is not installed. Install it to use auth reset:\n npm install better-sqlite3");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const db = new Database(dbPath);
|
|
33
|
+
try {
|
|
34
|
+
// Check if auth tables exist
|
|
35
|
+
const hasAuthConfig = db
|
|
36
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='auth_config'")
|
|
37
|
+
.get();
|
|
38
|
+
if (!hasAuthConfig) {
|
|
39
|
+
console.log("Auth tables not found — nothing to reset.");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
db.exec("DELETE FROM auth_config");
|
|
43
|
+
db.exec("DELETE FROM auth_sessions");
|
|
44
|
+
db.exec("DELETE FROM api_keys");
|
|
45
|
+
console.log("\nAuthentication reset successfully.\nYou will be prompted to set up new credentials on next login.");
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
db.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { validatePort } from "../lib/validate.js";
|
|
4
|
+
export async function docker(opts) {
|
|
5
|
+
const port = validatePort(opts.port ?? "3001");
|
|
6
|
+
const image = "ghcr.io/luka-zivkovic/chvor:latest";
|
|
7
|
+
console.log("Pulling latest chvor Docker image...");
|
|
8
|
+
execFileSync("docker", ["pull", image], { stdio: "inherit" });
|
|
9
|
+
console.log("Starting chvor container...");
|
|
10
|
+
execFileSync("docker", [
|
|
11
|
+
"run", "-d", "--name", "chvor",
|
|
12
|
+
"-p", `${port}:3001`,
|
|
13
|
+
"-v", `${homedir()}/.chvor:/home/node/.chvor`,
|
|
14
|
+
image,
|
|
15
|
+
], { stdio: "inherit" });
|
|
16
|
+
console.log(`chvor is running at http://localhost:${port}`);
|
|
17
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { input, select, password, confirm } from "@inquirer/prompts";
|
|
6
|
+
import { writeConfig } from "../lib/config.js";
|
|
7
|
+
import { validatePort } from "../lib/validate.js";
|
|
8
|
+
import { getChvorHome, getDataDir, setInstance, ensureDir, } from "../lib/paths.js";
|
|
9
|
+
import { downloadRelease, isInstalled } from "../lib/download.js";
|
|
10
|
+
import { spawnServer, pollHealth } from "../lib/process.js";
|
|
11
|
+
import { resolveTemplate, resolveRegistryTemplate, listBundledTemplates, } from "../lib/template-loader.js";
|
|
12
|
+
import { provision } from "../lib/template-provisioner.js";
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf8"));
|
|
15
|
+
async function pickTemplate() {
|
|
16
|
+
const bundled = listBundledTemplates();
|
|
17
|
+
if (bundled.length === 0) {
|
|
18
|
+
throw new Error("No bundled templates found. Use --template <path> or --from <path> to specify a template directory.");
|
|
19
|
+
}
|
|
20
|
+
const choice = await select({
|
|
21
|
+
message: "Choose a template:",
|
|
22
|
+
choices: bundled.map((t) => ({
|
|
23
|
+
name: `${t.name} — ${t.description}`,
|
|
24
|
+
value: t.id,
|
|
25
|
+
})),
|
|
26
|
+
});
|
|
27
|
+
return resolveTemplate(choice);
|
|
28
|
+
}
|
|
29
|
+
async function collectCredentials(credentialDefs, port, token) {
|
|
30
|
+
if (!credentialDefs.length)
|
|
31
|
+
return;
|
|
32
|
+
console.log("\n This template requires the following credentials:\n");
|
|
33
|
+
for (const cred of credentialDefs) {
|
|
34
|
+
console.log(` ${cred.name}: ${cred.description}`);
|
|
35
|
+
const data = {};
|
|
36
|
+
for (const field of cred.fields) {
|
|
37
|
+
if (field.secret) {
|
|
38
|
+
data[field.name] = await password({ message: ` ${field.label}:` });
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
data[field.name] = await input({ message: ` ${field.label}:` });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const res = await fetch(`http://localhost:${port}/api/credentials`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
Authorization: `Bearer ${token}`,
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
name: cred.name,
|
|
52
|
+
type: cred.type,
|
|
53
|
+
data,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
console.warn(` Warning: failed to save ${cred.name} credential (${res.status}).`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log(` ${cred.name} credential saved.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function init(opts) {
|
|
65
|
+
console.log("\n chvor init — set up a new agent from a template.\n");
|
|
66
|
+
// 1. Resolve template
|
|
67
|
+
let templatePath;
|
|
68
|
+
let manifest;
|
|
69
|
+
let isRegistryTemplate = false;
|
|
70
|
+
const source = opts.from || opts.template;
|
|
71
|
+
if (source?.startsWith("registry:")) {
|
|
72
|
+
const id = source.slice("registry:".length);
|
|
73
|
+
console.log(` Fetching template "${id}" from registry...`);
|
|
74
|
+
const result = await resolveRegistryTemplate(id);
|
|
75
|
+
templatePath = result.path;
|
|
76
|
+
manifest = result.manifest;
|
|
77
|
+
isRegistryTemplate = true;
|
|
78
|
+
}
|
|
79
|
+
else if (source) {
|
|
80
|
+
const result = resolveTemplate(source);
|
|
81
|
+
templatePath = result.path;
|
|
82
|
+
manifest = result.manifest;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const result = await pickTemplate();
|
|
86
|
+
templatePath = result.path;
|
|
87
|
+
manifest = result.manifest;
|
|
88
|
+
}
|
|
89
|
+
console.log(` Template: ${manifest.name} (v${manifest.version})`);
|
|
90
|
+
if (manifest.description) {
|
|
91
|
+
console.log(` ${manifest.description}\n`);
|
|
92
|
+
}
|
|
93
|
+
// 2. Instance name
|
|
94
|
+
const instanceName = opts.name ?? await input({
|
|
95
|
+
message: "Instance name (leave blank for default):",
|
|
96
|
+
default: "",
|
|
97
|
+
});
|
|
98
|
+
if (instanceName) {
|
|
99
|
+
// Validate instance name: alphanumeric, hyphens, underscores
|
|
100
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(instanceName)) {
|
|
101
|
+
throw new Error("Instance name must contain only letters, numbers, hyphens, and underscores.");
|
|
102
|
+
}
|
|
103
|
+
setInstance(instanceName);
|
|
104
|
+
}
|
|
105
|
+
const home = getChvorHome();
|
|
106
|
+
if (existsSync(join(home, "config.json"))) {
|
|
107
|
+
const overwrite = await confirm({
|
|
108
|
+
message: `Instance directory ${home} already exists. Overwrite?`,
|
|
109
|
+
default: false,
|
|
110
|
+
});
|
|
111
|
+
if (!overwrite) {
|
|
112
|
+
console.log(" Aborted.");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 3. Basic onboarding prompts
|
|
117
|
+
const userName = await input({ message: "What's your name?" });
|
|
118
|
+
const detectedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
119
|
+
const timezone = await input({
|
|
120
|
+
message: "Your timezone?",
|
|
121
|
+
default: detectedTimezone,
|
|
122
|
+
});
|
|
123
|
+
const provider = await select({
|
|
124
|
+
message: "LLM provider?",
|
|
125
|
+
choices: [
|
|
126
|
+
{ name: "Anthropic (Claude)", value: "anthropic" },
|
|
127
|
+
{ name: "OpenAI (GPT)", value: "openai" },
|
|
128
|
+
{ name: "Google (Gemini)", value: "google-ai" },
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
const apiKey = await password({ message: "API key:" });
|
|
132
|
+
const defaultPort = instanceName ? "3002" : "3001";
|
|
133
|
+
const port = validatePort(await input({ message: "Port?", default: defaultPort }));
|
|
134
|
+
const token = randomBytes(32).toString("hex");
|
|
135
|
+
// 4. Write config
|
|
136
|
+
writeConfig({
|
|
137
|
+
port,
|
|
138
|
+
token,
|
|
139
|
+
onboarded: true,
|
|
140
|
+
llmProvider: provider,
|
|
141
|
+
instanceName: instanceName || undefined,
|
|
142
|
+
templateName: manifest.name,
|
|
143
|
+
});
|
|
144
|
+
ensureDir(getDataDir());
|
|
145
|
+
// 5. Download release if needed
|
|
146
|
+
const version = pkg.version;
|
|
147
|
+
if (!isInstalled(version)) {
|
|
148
|
+
await downloadRelease(version);
|
|
149
|
+
}
|
|
150
|
+
// 6. Start server
|
|
151
|
+
await spawnServer({ port });
|
|
152
|
+
const healthy = await pollHealth(port, token);
|
|
153
|
+
if (!healthy) {
|
|
154
|
+
console.warn(" Server started but health check did not pass within timeout.");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// 7. Save LLM provider credential
|
|
158
|
+
const providerNames = {
|
|
159
|
+
anthropic: "Anthropic",
|
|
160
|
+
openai: "OpenAI",
|
|
161
|
+
"google-ai": "Google AI",
|
|
162
|
+
};
|
|
163
|
+
await fetch(`http://localhost:${port}/api/credentials`, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: {
|
|
166
|
+
"Content-Type": "application/json",
|
|
167
|
+
Authorization: `Bearer ${token}`,
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
name: providerNames[provider],
|
|
171
|
+
type: provider,
|
|
172
|
+
data: { apiKey },
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
// 8. Save persona (user name + timezone + template persona)
|
|
176
|
+
await fetch(`http://localhost:${port}/api/persona`, {
|
|
177
|
+
method: "PATCH",
|
|
178
|
+
headers: {
|
|
179
|
+
"Content-Type": "application/json",
|
|
180
|
+
Authorization: `Bearer ${token}`,
|
|
181
|
+
},
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
name: userName,
|
|
184
|
+
timezone,
|
|
185
|
+
onboarded: true,
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
// 9. Collect template-specific credentials
|
|
189
|
+
const templateCredentials = (manifest.credentials ?? []).filter((c) => c.type !== provider // Skip if already collected as LLM provider
|
|
190
|
+
);
|
|
191
|
+
await collectCredentials(templateCredentials, port, token);
|
|
192
|
+
// 10. Provision template
|
|
193
|
+
await provision({
|
|
194
|
+
port,
|
|
195
|
+
token,
|
|
196
|
+
templatePath,
|
|
197
|
+
manifest,
|
|
198
|
+
});
|
|
199
|
+
// Clean up temp directory from registry template
|
|
200
|
+
if (isRegistryTemplate) {
|
|
201
|
+
try {
|
|
202
|
+
rmSync(templatePath, { recursive: true, force: true });
|
|
203
|
+
}
|
|
204
|
+
catch { /* non-critical */ }
|
|
205
|
+
}
|
|
206
|
+
// 11. Done
|
|
207
|
+
const label = instanceName ? ` (instance: ${instanceName})` : "";
|
|
208
|
+
console.log(`\n chvor is running at http://localhost:${port}${label}`);
|
|
209
|
+
console.log(" Open this URL in your browser to get started.\n");
|
|
210
|
+
console.log(" Useful commands:");
|
|
211
|
+
if (instanceName) {
|
|
212
|
+
console.log(` chvor stop --instance ${instanceName} Stop this instance`);
|
|
213
|
+
console.log(` chvor start --instance ${instanceName} Start this instance`);
|
|
214
|
+
console.log(" chvor instances List all instances");
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
console.log(" chvor stop Stop the server");
|
|
218
|
+
console.log(" chvor start Start the server");
|
|
219
|
+
}
|
|
220
|
+
console.log(" chvor update Update to latest version\n");
|
|
221
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readdirSync, existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { setInstance } from "../lib/paths.js";
|
|
5
|
+
import { readConfig } from "../lib/config.js";
|
|
6
|
+
import { stopServer } from "../lib/process.js";
|
|
7
|
+
function discoverInstances() {
|
|
8
|
+
const home = homedir();
|
|
9
|
+
const instances = [];
|
|
10
|
+
// Check default instance
|
|
11
|
+
const defaultConfig = join(home, ".chvor", "config.json");
|
|
12
|
+
if (existsSync(defaultConfig)) {
|
|
13
|
+
try {
|
|
14
|
+
const config = JSON.parse(readFileSync(defaultConfig, "utf-8"));
|
|
15
|
+
// Check if running
|
|
16
|
+
const pidPath = join(home, ".chvor", "chvor.pid");
|
|
17
|
+
let running = false;
|
|
18
|
+
let pid;
|
|
19
|
+
if (existsSync(pidPath)) {
|
|
20
|
+
const rawPid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
21
|
+
if (!isNaN(rawPid)) {
|
|
22
|
+
try {
|
|
23
|
+
process.kill(rawPid, 0);
|
|
24
|
+
running = true;
|
|
25
|
+
pid = rawPid;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// not running
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
instances.push({
|
|
33
|
+
name: "(default)",
|
|
34
|
+
port: config.port || "3001",
|
|
35
|
+
running,
|
|
36
|
+
pid,
|
|
37
|
+
template: config.templateName,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.warn(` Warning: could not read default instance config: ${err instanceof Error ? err.message : err}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Scan for named instances (~/.chvor-*/config.json)
|
|
45
|
+
const entries = readdirSync(home, { withFileTypes: true });
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (!entry.isDirectory())
|
|
48
|
+
continue;
|
|
49
|
+
const match = entry.name.match(/^\.chvor-(.+)$/);
|
|
50
|
+
if (!match)
|
|
51
|
+
continue;
|
|
52
|
+
const instanceName = match[1];
|
|
53
|
+
const configPath = join(home, entry.name, "config.json");
|
|
54
|
+
if (!existsSync(configPath))
|
|
55
|
+
continue;
|
|
56
|
+
try {
|
|
57
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
58
|
+
const pidPath = join(home, entry.name, "chvor.pid");
|
|
59
|
+
let running = false;
|
|
60
|
+
let pid;
|
|
61
|
+
if (existsSync(pidPath)) {
|
|
62
|
+
const rawPid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
63
|
+
if (!isNaN(rawPid)) {
|
|
64
|
+
try {
|
|
65
|
+
process.kill(rawPid, 0);
|
|
66
|
+
running = true;
|
|
67
|
+
pid = rawPid;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// not running
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
instances.push({
|
|
75
|
+
name: instanceName,
|
|
76
|
+
port: config.port || "?",
|
|
77
|
+
running,
|
|
78
|
+
pid,
|
|
79
|
+
template: config.templateName,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
console.warn(` Warning: could not read instance "${instanceName}" config: ${err instanceof Error ? err.message : err}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return instances;
|
|
87
|
+
}
|
|
88
|
+
export async function listInstances() {
|
|
89
|
+
const instances = discoverInstances();
|
|
90
|
+
if (instances.length === 0) {
|
|
91
|
+
console.log(" No chvor instances found. Run 'chvor init' to create one.");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
console.log("\n Chvor instances:\n");
|
|
95
|
+
console.log(" " +
|
|
96
|
+
"NAME".padEnd(20) +
|
|
97
|
+
"PORT".padEnd(8) +
|
|
98
|
+
"STATUS".padEnd(12) +
|
|
99
|
+
"TEMPLATE");
|
|
100
|
+
console.log(" " + "-".repeat(60));
|
|
101
|
+
for (const inst of instances) {
|
|
102
|
+
const status = inst.running
|
|
103
|
+
? `running (${inst.pid})`
|
|
104
|
+
: "stopped";
|
|
105
|
+
console.log(" " +
|
|
106
|
+
inst.name.padEnd(20) +
|
|
107
|
+
inst.port.padEnd(8) +
|
|
108
|
+
status.padEnd(12) +
|
|
109
|
+
(inst.template || "-"));
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
}
|
|
113
|
+
export async function startInstance(name) {
|
|
114
|
+
setInstance(name);
|
|
115
|
+
const config = readConfig();
|
|
116
|
+
if (!config.onboarded) {
|
|
117
|
+
console.log(`Instance "${name}" has not been set up. Run 'chvor init --name ${name}' first.`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const { start } = await import("./start.js");
|
|
121
|
+
await start({});
|
|
122
|
+
}
|
|
123
|
+
export async function stopInstance(name) {
|
|
124
|
+
setInstance(name);
|
|
125
|
+
await stopServer();
|
|
126
|
+
}
|