@townco/cli 0.1.55 → 0.1.56

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 CHANGED
@@ -64,3 +64,5 @@ npx @townco/cli delete my-agent
64
64
 
65
65
  - `todo_write` - Task management
66
66
  - `web_search` - Web search (requires EXA_API_KEY)
67
+ - `generate_image` - Image generation (requires GEMINI_API_KEY)
68
+ - `browser` - Browser automation (requires KERNEL_API_KEY)
@@ -48,6 +48,16 @@ const AVAILABLE_TOOLS = [
48
48
  value: "filesystem",
49
49
  description: "Read, write, and search files in the project directory",
50
50
  },
51
+ {
52
+ label: "Generate Image",
53
+ value: "generate_image",
54
+ description: "Generate images using Google's Gemini model (requires GEMINI_API_KEY)",
55
+ },
56
+ {
57
+ label: "Browser",
58
+ value: "browser",
59
+ description: "Cloud browser automation (requires KERNEL_API_KEY)",
60
+ },
51
61
  ];
52
62
  function NameInput({ nameInput, setNameInput, onSubmit }) {
53
63
  useInput((_input, key) => {
@@ -1,5 +1,5 @@
1
+ import { clearAuthCredentials, getAuthFilePath, getShedUrl, loadAuthCredentials, saveAuthCredentials, } from "@townco/core/auth";
1
2
  import inquirer from "inquirer";
2
- import { clearAuthCredentials, getAuthFilePath, getShedUrl, loadAuthCredentials, saveAuthCredentials, } from "../lib/auth-storage.js";
3
3
  // ============================================================================
4
4
  // Command Implementation
5
5
  // ============================================================================
@@ -159,27 +159,54 @@ export async function runCommand(options) {
159
159
  }
160
160
  // Load environment variables from project .env
161
161
  const configEnvVars = await loadEnvVars(projectRoot);
162
+ // Resolve agent path to load agent definition
163
+ const agentPath = join(projectRoot, "agents", name);
164
+ // Load agent definition to get displayName by parsing the file
165
+ let agentDisplayName = name; // Fallback to agent directory name
166
+ try {
167
+ const agentIndexPath = join(agentPath, "index.ts");
168
+ const content = await readFile(agentIndexPath, "utf-8");
169
+ // Match displayName in the agent definition object
170
+ // Looking for patterns like: displayName: "Researcher" or displayName: 'Researcher'
171
+ const match = content.match(/displayName:\s*["']([^"']+)["']/);
172
+ if (match?.[1]) {
173
+ agentDisplayName = match[1];
174
+ }
175
+ }
176
+ catch (error) {
177
+ // If we can't read the agent definition, just use the directory name
178
+ // Silently fail - the directory name is a reasonable fallback
179
+ }
162
180
  // Start the debugger server as subprocess (OTLP collector + UI)
163
181
  const debuggerPkgPath = require.resolve("@townco/debugger/package.json");
164
182
  const debuggerDir = dirname(debuggerPkgPath);
165
183
  const debuggerProcess = spawn("bun", ["src/index.ts"], {
166
184
  cwd: debuggerDir,
167
185
  stdio: "inherit",
186
+ detached: true,
168
187
  env: {
169
188
  ...process.env,
170
189
  DB_PATH: join(projectRoot, ".traces.db"),
190
+ AGENT_NAME: agentDisplayName,
171
191
  },
172
192
  });
173
193
  console.log(`Debugger UI: http://localhost:4000`);
174
194
  // Cleanup debugger process on exit
195
+ let isDebuggerCleaningUp = false;
175
196
  const cleanupDebugger = () => {
176
- debuggerProcess.kill();
197
+ if (isDebuggerCleaningUp)
198
+ return;
199
+ isDebuggerCleaningUp = true;
200
+ if (debuggerProcess.pid) {
201
+ try {
202
+ process.kill(-debuggerProcess.pid, "SIGKILL");
203
+ }
204
+ catch (e) {
205
+ // Process may already be dead
206
+ }
207
+ }
177
208
  };
178
209
  process.on("exit", cleanupDebugger);
179
- process.on("SIGINT", cleanupDebugger);
180
- process.on("SIGTERM", cleanupDebugger);
181
- // Resolve agent path within the project
182
- const agentPath = join(projectRoot, "agents", name);
183
210
  // Check if agent exists
184
211
  try {
185
212
  const { stat } = await import("node:fs/promises");
@@ -260,11 +287,15 @@ export async function runCommand(options) {
260
287
  },
261
288
  });
262
289
  // Setup cleanup handlers for agent and GUI processes
290
+ let isCleaningUp = false;
263
291
  const cleanupProcesses = () => {
292
+ if (isCleaningUp)
293
+ return;
294
+ isCleaningUp = true;
264
295
  // Kill entire process group by using negative PID
265
296
  if (agentProcess.pid) {
266
297
  try {
267
- process.kill(-agentProcess.pid, "SIGTERM");
298
+ process.kill(-agentProcess.pid, "SIGKILL");
268
299
  }
269
300
  catch (e) {
270
301
  // Process may already be dead
@@ -272,35 +303,26 @@ export async function runCommand(options) {
272
303
  }
273
304
  if (guiProcess.pid) {
274
305
  try {
275
- process.kill(-guiProcess.pid, "SIGTERM");
306
+ process.kill(-guiProcess.pid, "SIGKILL");
276
307
  }
277
308
  catch (e) {
278
309
  // Process may already be dead
279
310
  }
280
311
  }
312
+ // Also cleanup debugger
313
+ cleanupDebugger();
281
314
  };
282
315
  process.on("exit", cleanupProcesses);
283
- process.on("SIGINT", cleanupProcesses);
284
- process.on("SIGTERM", cleanupProcesses);
316
+ // Handle SIGINT (Control-C) explicitly and exit
317
+ const handleSigint = () => {
318
+ cleanupProcesses();
319
+ process.exit(0);
320
+ };
321
+ process.on("SIGINT", handleSigint);
322
+ process.on("SIGTERM", handleSigint);
285
323
  // Render the tabbed UI with dynamic port detection
286
324
  const { waitUntilExit } = render(_jsx(GuiRunner, { agentProcess: agentProcess, guiProcess: guiProcess, agentPort: availablePort, agentPath: agentPath, logger: logger, onExit: () => {
287
- // Kill entire process group by using negative PID
288
- if (agentProcess.pid) {
289
- try {
290
- process.kill(-agentProcess.pid, "SIGTERM");
291
- }
292
- catch (e) {
293
- // Process may already be dead
294
- }
295
- }
296
- if (guiProcess.pid) {
297
- try {
298
- process.kill(-guiProcess.pid, "SIGTERM");
299
- }
300
- catch (e) {
301
- // Process may already be dead
302
- }
303
- }
325
+ cleanupProcesses();
304
326
  } }));
305
327
  await waitUntilExit();
306
328
  process.exit(0);
@@ -336,19 +358,30 @@ export async function runCommand(options) {
336
358
  },
337
359
  });
338
360
  // Setup cleanup handler for agent process
361
+ let isCleaningUp = false;
339
362
  const cleanupAgentProcess = () => {
363
+ if (isCleaningUp)
364
+ return;
365
+ isCleaningUp = true;
340
366
  if (agentProcess.pid) {
341
367
  try {
342
- process.kill(-agentProcess.pid, "SIGTERM");
368
+ process.kill(-agentProcess.pid, "SIGKILL");
343
369
  }
344
370
  catch (e) {
345
371
  // Process may already be dead
346
372
  }
347
373
  }
374
+ // Also cleanup debugger
375
+ cleanupDebugger();
348
376
  };
349
377
  process.on("exit", cleanupAgentProcess);
350
- process.on("SIGINT", cleanupAgentProcess);
351
- process.on("SIGTERM", cleanupAgentProcess);
378
+ // Handle SIGINT (Control-C) explicitly and exit
379
+ const handleSigint = () => {
380
+ cleanupAgentProcess();
381
+ process.exit(0);
382
+ };
383
+ process.on("SIGINT", handleSigint);
384
+ process.on("SIGTERM", handleSigint);
352
385
  agentProcess.on("error", (error) => {
353
386
  logger.error("Failed to start agent", { error: error.message });
354
387
  console.error(`Failed to start agent: ${error.message}`);
@@ -373,9 +406,17 @@ export async function runCommand(options) {
373
406
  if (process.stdin.isTTY) {
374
407
  process.stdin.setRawMode(true);
375
408
  }
409
+ // Setup signal handlers for TUI mode
410
+ const handleTuiSigint = () => {
411
+ cleanupDebugger();
412
+ process.exit(0);
413
+ };
414
+ process.on("SIGINT", handleTuiSigint);
415
+ process.on("SIGTERM", handleTuiSigint);
376
416
  // Render the tabbed UI with Chat and Logs
377
417
  const { waitUntilExit } = render(_jsx(TuiRunner, { agentPath: binPath, workingDir: agentPath, noSession: noSession, onExit: () => {
378
418
  // Cleanup is handled by the ACP client disconnect
419
+ cleanupDebugger();
379
420
  } }));
380
421
  await waitUntilExit();
381
422
  process.exit(0);
@@ -1,5 +1,5 @@
1
+ import { loadAuthCredentials } from "@townco/core/auth";
1
2
  import { authGet } from "../lib/auth-fetch.js";
2
- import { loadAuthCredentials } from "../lib/auth-storage.js";
3
3
  export async function whoamiCommand() {
4
4
  const credentials = loadAuthCredentials();
5
5
  if (!credentials) {
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bun
2
+ import fs from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import { argument, command, constant, flag, multiple, object, option, optional, or, } from "@optique/core";
4
5
  import { message } from "@optique/core/message";
@@ -6,8 +7,16 @@ import { integer, string } from "@optique/core/valueparser";
6
7
  import { run } from "@optique/run";
7
8
  import { initForClaudeCode } from "@townco/agent/scaffold";
8
9
  import { isInsideTownProject } from "@townco/agent/storage";
9
- import { createSecret, deleteSecret, genenv, listSecrets, } from "@townco/secret";
10
+ import { router } from "@townco/api";
11
+ import { findRoot } from "@townco/core";
12
+ import { updateSchema as updateEnvSchema } from "@townco/env/update-schema";
13
+ import { createSecret, deleteSecret, genenv, listSecrets, updateSecret, } from "@townco/secret";
14
+ import { createTRPCClient, httpLink, httpSubscriptionLink, splitLink, } from "@trpc/client";
15
+ import archiver from "archiver";
16
+ import { EventSource } from "eventsource";
17
+ import walk from "ignore-walk";
10
18
  import inquirer from "inquirer";
19
+ import superjson from "superjson";
11
20
  import { match } from "ts-pattern";
12
21
  import { configureCommand } from "./commands/configure.js";
13
22
  import { createCommand } from "./commands/create.js";
@@ -19,7 +28,7 @@ import { whoamiCommand } from "./commands/whoami.js";
19
28
  /**
20
29
  * Securely prompt for a secret value without echoing to the terminal
21
30
  */
22
- async function promptSecret(secretName) {
31
+ const promptSecret = async (secretName) => {
23
32
  const answers = await inquirer.prompt([
24
33
  {
25
34
  type: "password",
@@ -29,8 +38,11 @@ async function promptSecret(secretName) {
29
38
  },
30
39
  ]);
31
40
  return answers.value;
32
- }
33
- const parser = or(command("deploy", constant("deploy"), { brief: message `Deploy a Town.` }), command("configure", constant("configure"), {
41
+ };
42
+ const parser = or(command("deploy", object({ command: constant("deploy") }), {
43
+ brief: message `Deploy agents.`,
44
+ description: message `Deploy agents to the Town cloud.`,
45
+ }), command("configure", constant("configure"), {
34
46
  brief: message `Configure environment variables.`,
35
47
  }), command("login", constant("login"), {
36
48
  brief: message `Log in to Town Shed.`,
@@ -57,14 +69,23 @@ const parser = or(command("deploy", constant("deploy"), { brief: message `Deploy
57
69
  noSession: optional(flag("--no-session")),
58
70
  }), { brief: message `Run an agent.` }), command("secret", object({
59
71
  command: constant("secret"),
60
- subcommand: or(command("list", constant("list"), { brief: message `List secrets.` }), command("add", object({
72
+ subcommand: or(command("list", object({ action: constant("list") }), {
73
+ brief: message `List secrets.`,
74
+ }), command("add", object({
61
75
  action: constant("add"),
62
76
  name: argument(string({ metavar: "NAME" })),
63
77
  value: optional(argument(string({ metavar: "VALUE" }))),
64
- }), { brief: message `Add a secret.` }), command("remove", object({
78
+ }), { brief: message `Add a secret.` }), command("update", object({
79
+ action: constant("update"),
80
+ name: optional(argument(string({ metavar: "NAME" }))),
81
+ value: optional(argument(string({ metavar: "VALUE" }))),
82
+ genenv: optional(flag("-g", "--genenv", {
83
+ description: message `Regenerate .env file.`,
84
+ })),
85
+ }), { brief: message `Update a secret.` }), command("remove", object({
65
86
  action: constant("remove"),
66
87
  name: argument(string({ metavar: "NAME" })),
67
- }), { brief: message `Remove a secret.` }), command("genenv", constant("genenv"), {
88
+ }), { brief: message `Remove a secret.` }), command("genenv", object({ action: constant("genenv") }), {
68
89
  brief: message `Generate .env file.`,
69
90
  })),
70
91
  }), { brief: message `Secrets management.` }));
@@ -76,136 +97,213 @@ const meta = {
76
97
  brief: message `Your one-stop shop for all things Town in the terminal\n`,
77
98
  description: message `Town CLI is a first-class command-line experience for working with Town Agents.`,
78
99
  };
79
- async function main(parser, meta) {
80
- const result = run(parser, meta);
81
- await match(result)
82
- // TODO
83
- .with("deploy", async () => { })
84
- .with("configure", async () => {
85
- await configureCommand();
86
- })
87
- .with("login", async () => {
88
- await loginCommand();
89
- })
90
- .with("whoami", async () => {
91
- await whoamiCommand();
92
- })
93
- .with("upgrade", async () => {
94
- await upgradeCommand();
95
- })
96
- .with({ command: "create" }, async ({ name, model, tools, systemPrompt, init, claude }) => {
97
- // Handle --claude flag (initialize .claude in existing project)
98
- if (claude === true) {
99
- if (init !== null && init !== undefined) {
100
- console.error("Error: --claude flag is redundant with --init (projects include .claude by default)");
101
- process.exit(1);
102
- }
103
- // Check if we're in a Town project
104
- const projectRoot = await isInsideTownProject();
105
- if (projectRoot === null) {
106
- console.error("Error: Not inside a Town project. Use 'town create --init <path>' to create a new project.");
107
- process.exit(1);
108
- }
109
- // Initialize .claude directory only
110
- await initForClaudeCode(projectRoot);
111
- console.log("\n✓ Claude Code workspace initialized successfully!");
100
+ const main = async (parser, meta) => await match(run(parser, meta))
101
+ .with("login", loginCommand)
102
+ .with("whoami", whoamiCommand)
103
+ .with({ command: "deploy" }, async () => {
104
+ const url = "http://localhost:3000/api/trpc";
105
+ const client = createTRPCClient({
106
+ links: [
107
+ splitLink({
108
+ condition: (op) => op.type === "subscription",
109
+ true: httpSubscriptionLink({
110
+ url,
111
+ transformer: superjson,
112
+ EventSource,
113
+ }),
114
+ false: splitLink({
115
+ condition: (op) => op.path === router.uploadArchive.name,
116
+ true: httpLink({
117
+ url,
118
+ transformer: { serialize: (o) => o, deserialize: (o) => o },
119
+ }),
120
+ false: httpLink({ url, transformer: superjson }),
121
+ }),
122
+ }),
123
+ ],
124
+ });
125
+ console.log("Creating archive...");
126
+ const root = await findRoot();
127
+ const arc = archiver("tar", { gzip: true });
128
+ (await walk({ path: root, ignoreFiles: [".gitignore"] }))
129
+ .filter((path) => path.split("/")[0] !== ".git")
130
+ .forEach((path) => {
131
+ if (!fs.statSync(path).isFile())
112
132
  return;
113
- }
114
- // Check if --init flag is present for project scaffolding
133
+ arc.append(path, { name: path });
134
+ });
135
+ arc.finalize();
136
+ console.log("Uploading archive...");
137
+ const { sha256, cached } = await client.uploadArchive.mutate(Buffer.from(await Array.fromAsync(arc)));
138
+ console.log(`Archive uploaded: ${sha256} (${cached ? "cached" : "new"})`);
139
+ console.log("Deploying...");
140
+ client.deploy.subscribe({ sha256 }, {
141
+ onData: ({ status }) => console.log(` ${status}`),
142
+ onError: (err) => console.error(err),
143
+ onComplete: () => console.log("\n✓ Deployment complete!"),
144
+ });
145
+ })
146
+ .with("configure", configureCommand)
147
+ .with("upgrade", upgradeCommand)
148
+ .with({ command: "create" }, async ({ name, model, tools, systemPrompt, init, claude }) => {
149
+ // Handle --claude flag (initialize .claude in existing project)
150
+ if (claude === true) {
115
151
  if (init !== null && init !== undefined) {
116
- // Project mode - scaffold a standalone project
117
- await createProjectCommand({
118
- path: init,
119
- });
152
+ console.error("Error: --claude flag is redundant with --init (projects include .claude by default)");
153
+ process.exit(1);
120
154
  }
121
- else {
122
- // Check if we're inside a Town project
123
- const projectRoot = await isInsideTownProject();
124
- if (projectRoot === null) {
125
- // Not in a project - prompt user to initialize
126
- const answer = await inquirer.prompt([
127
- {
128
- type: "confirm",
129
- name: "initProject",
130
- message: "Not inside a Town project. Initialize project in current directory?",
131
- default: true,
132
- },
133
- ]);
134
- if (answer.initProject) {
135
- // Initialize project first
136
- await createProjectCommand({ path: process.cwd() });
137
- // Then create agent
138
- await createCommand({
139
- ...(name !== undefined && { name }),
140
- ...(model !== undefined && { model }),
141
- ...(tools.length > 0 && { tools }),
142
- ...(systemPrompt !== undefined && { systemPrompt }),
143
- agentsDir: join(process.cwd(), "agents"),
144
- });
145
- }
146
- else {
147
- // User declined
148
- console.log("\nPlease run 'town create' inside a project directory, or run:\n" +
149
- " town create --init <path>\n" +
150
- "to create a project.");
151
- process.exit(1);
152
- }
153
- }
154
- else {
155
- // Agent mode - create agent in existing project
156
- // Create command starts a long-running Ink session
155
+ // Check if we're in a Town project
156
+ const projectRoot = await isInsideTownProject();
157
+ if (projectRoot === null) {
158
+ console.error("Error: Not inside a Town project. Use 'town create --init <path>' to create a new project.");
159
+ process.exit(1);
160
+ }
161
+ // Initialize .claude directory only
162
+ await initForClaudeCode(projectRoot);
163
+ console.log("\n✓ Claude Code workspace initialized successfully!");
164
+ return;
165
+ }
166
+ // Check if --init flag is present for project scaffolding
167
+ if (init !== null && init !== undefined) {
168
+ // Project mode - scaffold a standalone project
169
+ await createProjectCommand({
170
+ path: init,
171
+ });
172
+ }
173
+ else {
174
+ // Check if we're inside a Town project
175
+ const projectRoot = await isInsideTownProject();
176
+ if (projectRoot === null) {
177
+ // Not in a project - prompt user to initialize
178
+ const answer = await inquirer.prompt([
179
+ {
180
+ type: "confirm",
181
+ name: "initProject",
182
+ message: "Not inside a Town project. Initialize project in current directory?",
183
+ default: true,
184
+ },
185
+ ]);
186
+ if (answer.initProject) {
187
+ // Initialize project first
188
+ await createProjectCommand({ path: process.cwd() });
189
+ // Then create agent
157
190
  await createCommand({
158
191
  ...(name !== undefined && { name }),
159
192
  ...(model !== undefined && { model }),
160
193
  ...(tools.length > 0 && { tools }),
161
194
  ...(systemPrompt !== undefined && { systemPrompt }),
162
- agentsDir: join(projectRoot, "agents"),
195
+ agentsDir: join(process.cwd(), "agents"),
163
196
  });
164
197
  }
198
+ else {
199
+ // User declined
200
+ console.log("\nPlease run 'town create' inside a project directory, or run:\n" +
201
+ " town create --init <path>\n" +
202
+ "to create a project.");
203
+ process.exit(1);
204
+ }
165
205
  }
166
- })
167
- .with({ command: "run" }, async ({ name, http, gui, port, noSession }) => {
168
- const options = {
169
- name,
170
- http: http === true,
171
- gui: gui === true,
172
- noSession: noSession === true,
206
+ else {
207
+ // Agent mode - create agent in existing project
208
+ // Create command starts a long-running Ink session
209
+ await createCommand({
210
+ ...(name !== undefined && { name }),
211
+ ...(model !== undefined && { model }),
212
+ ...(tools.length > 0 && { tools }),
213
+ ...(systemPrompt !== undefined && { systemPrompt }),
214
+ agentsDir: join(projectRoot, "agents"),
215
+ });
216
+ }
217
+ }
218
+ })
219
+ .with({ command: "run" }, async ({ name, http, gui, port, noSession }) => {
220
+ const options = {
221
+ name,
222
+ http: http === true,
223
+ gui: gui === true,
224
+ noSession: noSession === true,
225
+ };
226
+ if (port !== null && port !== undefined) {
227
+ options.port = port;
228
+ }
229
+ await runCommand(options);
230
+ })
231
+ .with({ command: "secret" }, async ({ subcommand }) => {
232
+ await match(subcommand)
233
+ .with({ action: "list" }, async () => {
234
+ const truncate = (str, maxLength = 50) => {
235
+ if (str.length <= maxLength)
236
+ return str;
237
+ return `${str.slice(0, maxLength - 3)}...`;
173
238
  };
174
- if (port !== null && port !== undefined) {
175
- options.port = port;
239
+ console.table((await listSecrets()).map((secret) => ({
240
+ Key: secret.key,
241
+ Valid: secret.valid ? "✓" : "✗",
242
+ Error: truncate(secret.error ?? ""),
243
+ })));
244
+ })
245
+ .with({ action: "add" }, async ({ name, value }) => {
246
+ // If value is not provided, prompt securely
247
+ const secretValue = value ?? (await promptSecret(name));
248
+ if (!secretValue) {
249
+ console.error("Error: Secret value cannot be empty");
250
+ process.exit(1);
176
251
  }
177
- await runCommand(options);
252
+ await createSecret(name, secretValue);
253
+ await updateEnvSchema();
254
+ console.log(`Secret '${name}' added successfully (& @packages/env schema updated).`);
178
255
  })
179
- .with({ command: "secret" }, async ({ subcommand }) => {
180
- await match(subcommand)
181
- .with("list", async () => {
256
+ .with({ action: "update" }, async ({ name, value, genenv: regen }) => {
257
+ let secretName = name;
258
+ // If name is not provided, show a list prompt to select from existing secrets
259
+ if (!secretName) {
182
260
  const secrets = await listSecrets();
183
- for (const secret of secrets) {
184
- console.log(`${secret.key}\t${secret.value}\t${secret.valid ? "✓" : "✗"}\t${secret.error ?? ""}`);
185
- }
186
- })
187
- .with({ action: "add" }, async ({ name, value }) => {
188
- // If value is not provided, prompt securely
189
- const secretValue = value ?? (await promptSecret(name));
190
- if (!secretValue) {
191
- console.error("Error: Secret value cannot be empty");
261
+ if (secrets.length === 0) {
262
+ console.error("No secrets found to update.");
192
263
  process.exit(1);
193
264
  }
194
- await createSecret(name, secretValue);
195
- console.log(`Secret '${name}' added successfully.`);
196
- })
197
- .with({ action: "remove" }, async ({ name }) => {
198
- await deleteSecret(name);
199
- console.log(`Secret '${name}' removed successfully.`);
200
- })
201
- .with("genenv", async () => {
265
+ const answer = await inquirer.prompt([
266
+ {
267
+ type: "list",
268
+ name: "selectedSecret",
269
+ message: "Select a secret to update:",
270
+ choices: secrets.map((s) => ({
271
+ name: `${s.key} ${s.valid ? "✓" : "✗"}`,
272
+ value: s.key,
273
+ })),
274
+ },
275
+ ]);
276
+ secretName = answer.selectedSecret;
277
+ }
278
+ // If value is not provided, prompt securely
279
+ if (!secretName) {
280
+ console.error("Error: Secret name is required");
281
+ process.exit(1);
282
+ }
283
+ const secretValue = value ?? (await promptSecret(secretName));
284
+ if (!secretValue) {
285
+ console.error("Error: Secret value cannot be empty");
286
+ process.exit(1);
287
+ }
288
+ await updateSecret(secretName, secretValue);
289
+ console.log(`Secret '${secretName}' updated successfully.`);
290
+ if (regen) {
202
291
  await genenv();
203
292
  console.log(".env file generated successfully.");
204
- })
205
- .exhaustive();
293
+ }
294
+ })
295
+ .with({ action: "remove" }, async ({ name }) => {
296
+ await deleteSecret(name);
297
+ await updateEnvSchema();
298
+ console.log(`Secret '${name}' removed successfully (& @packages/env schema updated).`);
299
+ })
300
+ .with({ action: "genenv" }, async () => {
301
+ await genenv();
302
+ console.log(".env file generated successfully.");
206
303
  })
207
304
  .exhaustive();
208
- }
305
+ })
306
+ .exhaustive();
209
307
  main(parser, meta).catch((error) => {
210
308
  console.error("Error:", error);
211
309
  process.exit(1);
@@ -1,4 +1,4 @@
1
- import { type AuthCredentials } from "./auth-storage.js";
1
+ import { type AuthCredentials } from "@townco/core/auth";
2
2
  /**
3
3
  * Get valid credentials, refreshing if necessary
4
4
  * Throws if not logged in or refresh fails
@@ -1,4 +1,4 @@
1
- import { isTokenExpired, loadAuthCredentials, saveAuthCredentials, } from "./auth-storage.js";
1
+ import { isTokenExpired, loadAuthCredentials, saveAuthCredentials, } from "@townco/core/auth";
2
2
  // ============================================================================
3
3
  // Token Refresh
4
4
  // ============================================================================