@townco/cli 0.1.3 ā 0.1.4
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/commands/configure.d.ts +1 -0
- package/dist/commands/configure.js +146 -0
- package/dist/commands/run.js +33 -0
- package/dist/index.js +7 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function configureCommand(): Promise<void>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
const ENV_KEYS = [
|
|
7
|
+
{
|
|
8
|
+
key: "ANTHROPIC_API_KEY",
|
|
9
|
+
description: "Anthropic API key for Claude models",
|
|
10
|
+
required: true,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
key: "EXA_API_KEY",
|
|
14
|
+
description: "Exa API key for web search tool",
|
|
15
|
+
required: false,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: "OPENAI_API_KEY",
|
|
19
|
+
description: "OpenAI API key (optional, for OpenAI models)",
|
|
20
|
+
required: false,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
function getConfigDir() {
|
|
24
|
+
return join(homedir(), ".config", "town");
|
|
25
|
+
}
|
|
26
|
+
function getEnvFilePath() {
|
|
27
|
+
return join(getConfigDir(), ".env");
|
|
28
|
+
}
|
|
29
|
+
async function loadExistingEnv() {
|
|
30
|
+
const envPath = getEnvFilePath();
|
|
31
|
+
if (!existsSync(envPath)) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
const content = await readFile(envPath, "utf-8");
|
|
35
|
+
const config = {};
|
|
36
|
+
for (const line of content.split("\n")) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
39
|
+
continue;
|
|
40
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
41
|
+
const value = valueParts.join("=").trim();
|
|
42
|
+
if (key && value) {
|
|
43
|
+
config[key.trim()] = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
async function saveEnv(config) {
|
|
49
|
+
const configDir = getConfigDir();
|
|
50
|
+
await mkdir(configDir, { recursive: true });
|
|
51
|
+
const lines = [
|
|
52
|
+
"# Town CLI Configuration",
|
|
53
|
+
"# Environment variables for Town agents",
|
|
54
|
+
"",
|
|
55
|
+
];
|
|
56
|
+
for (const { key, description } of ENV_KEYS) {
|
|
57
|
+
const value = config[key];
|
|
58
|
+
if (value) {
|
|
59
|
+
lines.push(`# ${description}`);
|
|
60
|
+
lines.push(`${key}=${value}`);
|
|
61
|
+
lines.push("");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await writeFile(getEnvFilePath(), lines.join("\n"), "utf-8");
|
|
65
|
+
}
|
|
66
|
+
export async function configureCommand() {
|
|
67
|
+
console.log("š§ Town Configuration\n");
|
|
68
|
+
const existingConfig = await loadExistingEnv();
|
|
69
|
+
const hasExisting = Object.keys(existingConfig).length > 0;
|
|
70
|
+
if (hasExisting) {
|
|
71
|
+
console.log("Found existing configuration:\n");
|
|
72
|
+
for (const { key, description } of ENV_KEYS) {
|
|
73
|
+
const value = existingConfig[key];
|
|
74
|
+
if (value) {
|
|
75
|
+
const masked = value.slice(0, 4) + "..." + value.slice(-4);
|
|
76
|
+
console.log(` ${key}: ${masked}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
console.log("");
|
|
80
|
+
const { action } = await inquirer.prompt([
|
|
81
|
+
{
|
|
82
|
+
type: "list",
|
|
83
|
+
name: "action",
|
|
84
|
+
message: "What would you like to do?",
|
|
85
|
+
choices: [
|
|
86
|
+
{ name: "Update existing keys", value: "update" },
|
|
87
|
+
{ name: "Add new keys", value: "add" },
|
|
88
|
+
{ name: "Reconfigure all keys", value: "reconfigure" },
|
|
89
|
+
{ name: "Cancel", value: "cancel" },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
if (action === "cancel") {
|
|
94
|
+
console.log("Configuration cancelled.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (action === "reconfigure") {
|
|
98
|
+
// Clear existing config for fresh start
|
|
99
|
+
Object.keys(existingConfig).forEach((key) => {
|
|
100
|
+
delete existingConfig[key];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const newConfig = { ...existingConfig };
|
|
105
|
+
// Prompt for each key
|
|
106
|
+
for (const { key, description, required } of ENV_KEYS) {
|
|
107
|
+
const hasValue = !!existingConfig[key];
|
|
108
|
+
if (hasValue && !hasExisting) {
|
|
109
|
+
// Skip if we're only adding new keys and this one exists
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const { shouldSet } = await inquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: "confirm",
|
|
115
|
+
name: "shouldSet",
|
|
116
|
+
message: hasValue
|
|
117
|
+
? `Update ${key}? (${description})`
|
|
118
|
+
: `Set ${key}? (${description})`,
|
|
119
|
+
default: required || !hasValue,
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
if (shouldSet) {
|
|
123
|
+
const { value } = await inquirer.prompt([
|
|
124
|
+
{
|
|
125
|
+
type: "password",
|
|
126
|
+
name: "value",
|
|
127
|
+
message: `Enter ${key}:`,
|
|
128
|
+
mask: "*",
|
|
129
|
+
validate: (input) => {
|
|
130
|
+
if (required && !input.trim()) {
|
|
131
|
+
return "This key is required";
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
if (value.trim()) {
|
|
138
|
+
newConfig[key] = value.trim();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Save configuration
|
|
143
|
+
await saveEnv(newConfig);
|
|
144
|
+
console.log(`\nā
Configuration saved to ${getEnvFilePath()}`);
|
|
145
|
+
console.log("\nThese environment variables will be automatically loaded when running agents.");
|
|
146
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
2
5
|
import { join } from "node:path";
|
|
3
6
|
import { agentExists, getAgentPath } from "@townco/agent/storage";
|
|
7
|
+
async function loadEnvVars() {
|
|
8
|
+
const envPath = join(homedir(), ".config", "town", ".env");
|
|
9
|
+
const envVars = {};
|
|
10
|
+
if (!existsSync(envPath)) {
|
|
11
|
+
return envVars;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const content = await readFile(envPath, "utf-8");
|
|
15
|
+
for (const line of content.split("\n")) {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
18
|
+
continue;
|
|
19
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
20
|
+
const value = valueParts.join("=").trim();
|
|
21
|
+
if (key && value) {
|
|
22
|
+
envVars[key.trim()] = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.warn(`Warning: Could not load environment variables from ${envPath}`);
|
|
28
|
+
}
|
|
29
|
+
return envVars;
|
|
30
|
+
}
|
|
4
31
|
export async function runCommand(options) {
|
|
5
32
|
const { name, http = false, gui = false, port = 3100 } = options;
|
|
33
|
+
// Load environment variables from ~/.config/town/.env
|
|
34
|
+
const configEnvVars = await loadEnvVars();
|
|
6
35
|
// Check if agent exists
|
|
7
36
|
const exists = await agentExists(name);
|
|
8
37
|
if (!exists) {
|
|
@@ -35,6 +64,7 @@ export async function runCommand(options) {
|
|
|
35
64
|
stdio: "pipe",
|
|
36
65
|
env: {
|
|
37
66
|
...process.env,
|
|
67
|
+
...configEnvVars,
|
|
38
68
|
NODE_ENV: process.env.NODE_ENV || "production",
|
|
39
69
|
PORT: port.toString(),
|
|
40
70
|
},
|
|
@@ -51,6 +81,7 @@ export async function runCommand(options) {
|
|
|
51
81
|
stdio: "inherit",
|
|
52
82
|
env: {
|
|
53
83
|
...process.env,
|
|
84
|
+
...configEnvVars,
|
|
54
85
|
VITE_AGENT_URL: `http://localhost:${port}`,
|
|
55
86
|
},
|
|
56
87
|
});
|
|
@@ -87,6 +118,7 @@ export async function runCommand(options) {
|
|
|
87
118
|
stdio: "inherit",
|
|
88
119
|
env: {
|
|
89
120
|
...process.env,
|
|
121
|
+
...configEnvVars,
|
|
90
122
|
NODE_ENV: process.env.NODE_ENV || "production",
|
|
91
123
|
PORT: port.toString(),
|
|
92
124
|
},
|
|
@@ -117,6 +149,7 @@ export async function runCommand(options) {
|
|
|
117
149
|
stdio: "inherit",
|
|
118
150
|
env: {
|
|
119
151
|
...process.env,
|
|
152
|
+
...configEnvVars,
|
|
120
153
|
NODE_ENV: process.env.NODE_ENV || "production",
|
|
121
154
|
},
|
|
122
155
|
});
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { run } from "@optique/run";
|
|
|
6
6
|
import { createSecret, deleteSecret, genenv, listSecrets } from "@townco/secret";
|
|
7
7
|
import inquirer from "inquirer";
|
|
8
8
|
import { match } from "ts-pattern";
|
|
9
|
+
import { configureCommand } from "./commands/configure.js";
|
|
9
10
|
import { createCommand } from "./commands/create.js";
|
|
10
11
|
import { deleteCommand } from "./commands/delete.js";
|
|
11
12
|
import { editCommand } from "./commands/edit.js";
|
|
@@ -25,7 +26,9 @@ async function promptSecret(secretName) {
|
|
|
25
26
|
]);
|
|
26
27
|
return answers.value;
|
|
27
28
|
}
|
|
28
|
-
const parser = or(command("deploy", constant("deploy"), { brief: message `Deploy a Town.` }), command("
|
|
29
|
+
const parser = or(command("deploy", constant("deploy"), { brief: message `Deploy a Town.` }), command("configure", constant("configure"), {
|
|
30
|
+
brief: message `Configure environment variables.`,
|
|
31
|
+
}), command("create", object({
|
|
29
32
|
command: constant("create"),
|
|
30
33
|
name: optional(option("-n", "--name", string())),
|
|
31
34
|
model: optional(option("-m", "--model", string())),
|
|
@@ -69,6 +72,9 @@ async function main(parser, meta) {
|
|
|
69
72
|
await match(result)
|
|
70
73
|
// TODO
|
|
71
74
|
.with("deploy", async () => { })
|
|
75
|
+
.with("configure", async () => {
|
|
76
|
+
await configureCommand();
|
|
77
|
+
})
|
|
72
78
|
.with({ command: "create" }, async ({ name, model, tools, systemPrompt }) => {
|
|
73
79
|
// Create command starts a long-running Ink session
|
|
74
80
|
// Only pass defined properties to satisfy exactOptionalPropertyTypes
|