@townco/cli 0.1.82 → 0.1.84

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/index.js CHANGED
@@ -1,111 +1,24 @@
1
1
  #!/usr/bin/env bun
2
- import fs from "node:fs";
3
- import afs from "node:fs/promises";
4
- import { join } from "node:path";
5
- import { argument, command, constant, flag, multiple, object, option, optional, or, } from "@optique/core";
2
+ import { command, constant, or } from "@optique/core";
6
3
  import { message } from "@optique/core/message";
7
- import { integer, string } from "@optique/core/valueparser";
8
4
  import { run } from "@optique/run";
9
- import { initForClaudeCode } from "@townco/agent/scaffold";
10
- import { isInsideTownProject } from "@townco/agent/storage";
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";
18
- import inquirer from "inquirer";
19
- import superjson from "superjson";
20
5
  import { match } from "ts-pattern";
21
- import { batchCommand } from "./commands/batch.js";
22
- import { configureCommand } from "./commands/configure.js";
23
- import { createCommand } from "./commands/create.js";
24
- import { createProjectCommand } from "./commands/create-project.js";
25
- import { loginCommand } from "./commands/login.js";
26
- import { runCommand } from "./commands/run.js";
27
- import { upgradeCommand } from "./commands/upgrade.js";
28
- import { whoamiCommand } from "./commands/whoami.js";
29
- import { getValidCredentials } from "./lib/auth-fetch";
30
- /**
31
- * Securely prompt for a secret value without echoing to the terminal
32
- */
33
- const promptSecret = async (secretName) => {
34
- const answers = await inquirer.prompt([
35
- {
36
- type: "password",
37
- name: "value",
38
- message: `Enter value for secret '${secretName}':`,
39
- mask: "*",
40
- },
41
- ]);
42
- return answers.value;
43
- };
44
- const parser = or(command("deploy", object({
45
- command: constant("deploy"),
46
- agent: option("-a", "--agent", string()),
47
- }), {
48
- brief: message `Deploy agents.`,
49
- description: message `Deploy agents to the Town cloud.`,
50
- }), command("configure", constant("configure"), {
6
+ import batchCmd from "@/commands/batch-wrapper";
7
+ import { configureCommand } from "@/commands/configure";
8
+ import createCmd from "@/commands/create-wrapper";
9
+ import deployCmd from "@/commands/deploy";
10
+ import { loginCommand } from "@/commands/login";
11
+ import runCmd from "@/commands/run-wrapper";
12
+ import secretCmd from "@/commands/secret";
13
+ import { upgradeCommand } from "@/commands/upgrade";
14
+ import { whoamiCommand } from "@/commands/whoami";
15
+ const parser = or(batchCmd.def, command("configure", constant("configure"), {
51
16
  brief: message `Configure environment variables.`,
52
- }), command("login", constant("login"), {
53
- brief: message `Log in to Town Shed.`,
17
+ }), command("login", constant("login"), { brief: message `Log in to Town Shed.` }), command("upgrade", constant("upgrade"), {
18
+ brief: message `Upgrade dependencies by cleaning and reinstalling.`,
54
19
  }), command("whoami", constant("whoami"), {
55
20
  brief: message `Show current login status.`,
56
- }), command("upgrade", constant("upgrade"), {
57
- brief: message `Upgrade dependencies by cleaning and reinstalling.`,
58
- }), command("create", object({
59
- command: constant("create"),
60
- name: optional(option("-n", "--name", string())),
61
- model: optional(option("-m", "--model", string())),
62
- tools: multiple(option("-t", "--tool", string())),
63
- systemPrompt: optional(option("-p", "--prompt", string())),
64
- init: optional(option("--init", string())),
65
- claude: optional(flag("--claude")),
66
- }), {
67
- brief: message `Create a new agent or project (with --init <path>). Use --claude to add Claude Code integration.`,
68
- }), command("run", object({
69
- command: constant("run"),
70
- name: argument(string({ metavar: "NAME" })),
71
- http: optional(flag("--http")),
72
- gui: optional(flag("--gui")),
73
- cli: optional(flag("--cli")),
74
- prompt: optional(argument(string({ metavar: "PROMPT" }))),
75
- port: optional(option("-p", "--port", integer())),
76
- noSession: optional(flag("--no-session")),
77
- }), { brief: message `Run an agent.` }), command("batch", object({
78
- command: constant("batch"),
79
- name: argument(string({ metavar: "NAME" })),
80
- queries: multiple(argument(string({ metavar: "QUERY" }))),
81
- file: optional(option("-f", "--file", string())),
82
- concurrency: optional(option("-c", "--concurrency", integer())),
83
- port: optional(option("-p", "--port", integer())),
84
- }), {
85
- brief: message `Run multiple queries in parallel against an agent.`,
86
- description: message `Run multiple queries in parallel, each in its own session. Queries can be provided as arguments or loaded from a file.`,
87
- }), command("secret", object({
88
- command: constant("secret"),
89
- subcommand: or(command("list", object({ action: constant("list") }), {
90
- brief: message `List secrets.`,
91
- }), command("add", object({
92
- action: constant("add"),
93
- name: argument(string({ metavar: "NAME" })),
94
- value: optional(argument(string({ metavar: "VALUE" }))),
95
- }), { brief: message `Add a secret.` }), command("update", object({
96
- action: constant("update"),
97
- name: optional(argument(string({ metavar: "NAME" }))),
98
- value: optional(argument(string({ metavar: "VALUE" }))),
99
- genenv: optional(flag("-g", "--genenv", {
100
- description: message `Regenerate .env file.`,
101
- })),
102
- }), { brief: message `Update a secret.` }), command("remove", object({
103
- action: constant("remove"),
104
- name: argument(string({ metavar: "NAME" })),
105
- }), { brief: message `Remove a secret.` }), command("genenv", object({ action: constant("genenv") }), {
106
- brief: message `Generate .env file.`,
107
- })),
108
- }), { brief: message `Secrets management.` }));
21
+ }), createCmd.def, deployCmd.def, runCmd.def, secretCmd.def);
109
22
  const meta = {
110
23
  programName: "town",
111
24
  help: "both",
@@ -115,245 +28,15 @@ const meta = {
115
28
  description: message `Town CLI is a first-class command-line experience for working with Town Agents.`,
116
29
  };
117
30
  const main = async (parser, meta) => await match(run(parser, meta))
118
- .with("login", loginCommand)
119
- .with("whoami", whoamiCommand)
120
- .with({ command: "deploy" }, async ({ agent }) => {
121
- const projectRoot = await isInsideTownProject();
122
- if (!projectRoot)
123
- throw new Error("Not inside a Town project");
124
- if (!(await afs.exists(join(projectRoot, "agents", agent))))
125
- throw new Error(`Agent ${agent} not found`);
126
- const { accessToken, shedUrl } = await getValidCredentials();
127
- const authHeader = { Authorization: `Bearer ${accessToken}` };
128
- const url = `${shedUrl}/api/trpc`;
129
- const baseLinkOpts = { url, transformer: superjson };
130
- const client = createTRPCClient({
131
- links: [
132
- splitLink({
133
- condition: (op) => op.type === "subscription",
134
- true: httpSubscriptionLink({
135
- ...baseLinkOpts,
136
- EventSource,
137
- eventSourceOptions: {
138
- fetch: async (url, init) => fetch(url, {
139
- ...init,
140
- headers: { ...init.headers, ...authHeader },
141
- }),
142
- },
143
- }),
144
- false: httpLink({ ...baseLinkOpts, headers: authHeader }),
145
- }),
146
- ],
147
- });
148
- console.log("Creating archive...");
149
- const root = await findRoot({ rootMarker: "package.json" });
150
- const arc = archiver("tar", { gzip: true });
151
- const chunks = [];
152
- const done = new Promise((ok, err) => {
153
- arc.on("data", (chunk) => chunks.push(chunk));
154
- arc.on("end", () => ok(Buffer.concat(chunks)));
155
- arc.on("error", err);
156
- });
157
- (await walk({ path: root, ignoreFiles: [".gitignore"] }))
158
- .filter((path) => path.split("/")[0] !== ".git")
159
- .forEach((path) => {
160
- if (!fs.statSync(path).isFile())
161
- return;
162
- arc.append(fs.createReadStream(path), { name: path });
163
- });
164
- arc.finalize();
165
- console.log("Uploading archive...");
166
- const { sha256, cached } = await client.uploadArchive.mutate(await done);
167
- console.log(`Archive uploaded: ${sha256} (${cached ? "cached" : "new"})`);
168
- console.log("Deploying...");
169
- client.deploy.subscribe({ sha256, agent, shedUrl }, {
170
- onData: ({ status, error }) => console.log(status ? status : error),
171
- onError: (err) => console.error(err),
172
- onComplete: () => console.log("\n✓ Deployment complete!"),
173
- });
174
- })
175
31
  .with("configure", configureCommand)
32
+ .with("login", loginCommand)
176
33
  .with("upgrade", upgradeCommand)
177
- .with({ command: "create" }, async ({ name, model, tools, systemPrompt, init, claude }) => {
178
- // Handle --claude flag (initialize .claude in existing project)
179
- if (claude === true) {
180
- if (init !== null && init !== undefined) {
181
- console.error("Error: --claude flag is redundant with --init (projects include .claude by default)");
182
- process.exit(1);
183
- }
184
- // Check if we're in a Town project
185
- const projectRoot = await isInsideTownProject();
186
- if (projectRoot === null) {
187
- console.error("Error: Not inside a Town project. Use 'town create --init <path>' to create a new project.");
188
- process.exit(1);
189
- }
190
- // Initialize .claude directory only
191
- await initForClaudeCode(projectRoot);
192
- console.log("\n✓ Claude Code workspace initialized successfully!");
193
- return;
194
- }
195
- // Check if --init flag is present for project scaffolding
196
- if (init !== null && init !== undefined) {
197
- // Project mode - scaffold a standalone project
198
- await createProjectCommand({
199
- path: init,
200
- });
201
- }
202
- else {
203
- // Check if we're inside a Town project
204
- const projectRoot = await isInsideTownProject();
205
- if (projectRoot === null) {
206
- // Not in a project - prompt user to initialize
207
- const answer = await inquirer.prompt([
208
- {
209
- type: "confirm",
210
- name: "initProject",
211
- message: "Not inside a Town project. Initialize project in current directory?",
212
- default: true,
213
- },
214
- ]);
215
- if (answer.initProject) {
216
- // Initialize project first
217
- await createProjectCommand({ path: process.cwd() });
218
- // Then create agent
219
- await createCommand({
220
- ...(name !== undefined && { name }),
221
- ...(model !== undefined && { model }),
222
- ...(tools.length > 0 && { tools }),
223
- ...(systemPrompt !== undefined && { systemPrompt }),
224
- agentsDir: join(process.cwd(), "agents"),
225
- });
226
- }
227
- else {
228
- // User declined
229
- console.log("\nPlease run 'town create' inside a project directory, or run:\n" +
230
- " town create --init <path>\n" +
231
- "to create a project.");
232
- process.exit(1);
233
- }
234
- }
235
- else {
236
- // Agent mode - create agent in existing project
237
- // Create command starts a long-running Ink session
238
- await createCommand({
239
- ...(name !== undefined && { name }),
240
- ...(model !== undefined && { model }),
241
- ...(tools.length > 0 && { tools }),
242
- ...(systemPrompt !== undefined && { systemPrompt }),
243
- agentsDir: join(projectRoot, "agents"),
244
- });
245
- }
246
- }
247
- })
248
- .with({ command: "run" }, async ({ name, http, gui, cli, prompt, port, noSession }) => {
249
- const options = {
250
- name,
251
- http: http === true,
252
- gui: gui === true,
253
- cli: cli === true,
254
- noSession: noSession === true,
255
- };
256
- if (prompt !== null && prompt !== undefined) {
257
- options.prompt = prompt;
258
- }
259
- if (port !== null && port !== undefined) {
260
- options.port = port;
261
- }
262
- await runCommand(options);
263
- })
264
- .with({ command: "batch" }, async ({ name, queries, file, concurrency, port }) => {
265
- const options = {
266
- name,
267
- };
268
- if (queries.length > 0) {
269
- options.queries = [...queries]; // Convert readonly array to mutable
270
- }
271
- if (file !== null && file !== undefined) {
272
- options.file = file;
273
- }
274
- if (concurrency !== null && concurrency !== undefined) {
275
- options.concurrency = concurrency;
276
- }
277
- if (port !== null && port !== undefined) {
278
- options.port = port;
279
- }
280
- await batchCommand(options);
281
- })
282
- .with({ command: "secret" }, async ({ subcommand }) => {
283
- await match(subcommand)
284
- .with({ action: "list" }, async () => {
285
- const truncate = (str, maxLength = 50) => {
286
- if (str.length <= maxLength)
287
- return str;
288
- return `${str.slice(0, maxLength - 3)}...`;
289
- };
290
- console.table((await listSecrets()).map((secret) => ({
291
- Key: secret.key,
292
- Valid: secret.valid ? "✓" : "✗",
293
- Error: truncate(secret.error ?? ""),
294
- })));
295
- })
296
- .with({ action: "add" }, async ({ name, value }) => {
297
- // If value is not provided, prompt securely
298
- const secretValue = value ?? (await promptSecret(name));
299
- if (!secretValue) {
300
- console.error("Error: Secret value cannot be empty");
301
- process.exit(1);
302
- }
303
- await createSecret(name, secretValue);
304
- await updateEnvSchema();
305
- console.log(`Secret '${name}' added successfully (& @packages/env schema updated).`);
306
- })
307
- .with({ action: "update" }, async ({ name, value, genenv: regen }) => {
308
- let secretName = name;
309
- // If name is not provided, show a list prompt to select from existing secrets
310
- if (!secretName) {
311
- const secrets = await listSecrets();
312
- if (secrets.length === 0) {
313
- console.error("No secrets found to update.");
314
- process.exit(1);
315
- }
316
- const answer = await inquirer.prompt([
317
- {
318
- type: "list",
319
- name: "selectedSecret",
320
- message: "Select a secret to update:",
321
- choices: secrets.map((s) => ({
322
- name: `${s.key} ${s.valid ? "✓" : "✗"}`,
323
- value: s.key,
324
- })),
325
- },
326
- ]);
327
- secretName = answer.selectedSecret;
328
- }
329
- // If value is not provided, prompt securely
330
- if (!secretName) {
331
- console.error("Error: Secret name is required");
332
- process.exit(1);
333
- }
334
- const secretValue = value ?? (await promptSecret(secretName));
335
- if (!secretValue) {
336
- console.error("Error: Secret value cannot be empty");
337
- process.exit(1);
338
- }
339
- await updateSecret(secretName, secretValue);
340
- console.log(`Secret '${secretName}' updated successfully.`);
341
- if (regen) {
342
- await genenv();
343
- console.log(".env file generated successfully.");
344
- }
345
- })
346
- .with({ action: "remove" }, async ({ name }) => {
347
- await deleteSecret(name);
348
- await updateEnvSchema();
349
- console.log(`Secret '${name}' removed successfully (& @packages/env schema updated).`);
350
- })
351
- .with({ action: "genenv" }, async () => {
352
- await genenv();
353
- console.log(".env file generated successfully.");
354
- })
355
- .exhaustive();
356
- })
34
+ .with("whoami", whoamiCommand)
35
+ .with({ command: "batch" }, batchCmd.impl)
36
+ .with({ command: "create" }, createCmd.impl)
37
+ .with({ command: "deploy" }, deployCmd.impl)
38
+ .with({ command: "run" }, runCmd.impl)
39
+ .with({ command: "secret" }, secretCmd.impl)
357
40
  .exhaustive();
358
41
  main(parser, meta).catch((error) => {
359
42
  console.error("Error:", error);
@@ -0,0 +1,8 @@
1
+ import type { InferValue } from "@optique/core";
2
+ export declare const createCommand: <T extends import("@optique/core").Parser<unknown, ["matched", string] | ["parsing", unknown] | undefined>>({ def, impl, }: {
3
+ def: T;
4
+ impl: (def: InferValue<T>) => unknown;
5
+ }) => {
6
+ def: T;
7
+ impl: (def: InferValue<T>) => unknown;
8
+ };
@@ -0,0 +1 @@
1
+ export const createCommand = ({ def, impl, }) => ({ def, impl });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/cli",
3
- "version": "0.1.82",
3
+ "version": "0.1.84",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "town": "./dist/index.js"
@@ -11,11 +11,11 @@
11
11
  ],
12
12
  "repository": "github:townco/town",
13
13
  "scripts": {
14
- "check": "tsc --noEmit",
15
- "build": "tsc"
14
+ "check": "tsgo --noEmit",
15
+ "build": "tsgo"
16
16
  },
17
17
  "devDependencies": {
18
- "@townco/tsconfig": "0.1.74",
18
+ "@townco/tsconfig": "0.1.76",
19
19
  "@types/archiver": "^7.0.0",
20
20
  "@types/bun": "^1.3.1",
21
21
  "@types/ignore-walk": "^4.0.3",
@@ -25,26 +25,22 @@
25
25
  "dependencies": {
26
26
  "@optique/core": "^0.6.2",
27
27
  "@optique/run": "^0.6.2",
28
- "@townco/agent": "0.1.82",
29
- "@townco/core": "0.0.55",
30
- "@townco/debugger": "0.1.32",
31
- "@townco/env": "0.1.27",
32
- "@townco/secret": "0.1.77",
33
- "@townco/ui": "0.1.77",
28
+ "@townco/agent": "0.1.84",
29
+ "@townco/core": "0.0.57",
30
+ "@townco/debugger": "0.1.34",
31
+ "@townco/env": "0.1.29",
32
+ "@townco/secret": "0.1.79",
33
+ "@townco/ui": "0.1.79",
34
34
  "@trpc/client": "^11.7.2",
35
- "@types/inquirer": "^9.0.9",
36
- "@types/ws": "^8.5.13",
37
35
  "archiver": "^7.0.1",
38
36
  "eventsource": "^4.1.0",
39
- "ignore": "^7.0.5",
40
37
  "ignore-walk": "^8.0.0",
41
38
  "ink": "^6.4.0",
42
39
  "ink-text-input": "^6.0.0",
43
40
  "inquirer": "^12.10.0",
44
41
  "open": "^10.2.0",
45
42
  "superjson": "^2.2.5",
46
- "ts-pattern": "^5.9.0",
47
- "ws": "^8.18.0"
43
+ "ts-pattern": "^5.9.0"
48
44
  },
49
45
  "peerDependencies": {
50
46
  "react": ">=19.0.0"