@townco/cli 0.1.82 → 0.1.83
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/batch-wrapper.d.ts +26 -0
- package/dist/commands/batch-wrapper.js +35 -0
- package/dist/commands/batch.js +2 -2
- package/dist/commands/create-wrapper.d.ts +29 -0
- package/dist/commands/create-wrapper.js +92 -0
- package/dist/commands/create.js +38 -12
- package/dist/commands/deploy.d.ts +14 -0
- package/dist/commands/deploy.js +88 -0
- package/dist/commands/login.js +1 -1
- package/dist/commands/run-wrapper.d.ts +32 -0
- package/dist/commands/run-wrapper.js +32 -0
- package/dist/commands/run.js +23 -16
- package/dist/commands/secret.d.ts +62 -0
- package/dist/commands/secret.js +119 -0
- package/dist/components/LogsPane.d.ts +1 -1
- package/dist/components/LogsPane.js +12 -3
- package/dist/components/ProcessPane.d.ts +1 -1
- package/dist/components/ProcessPane.js +9 -1
- package/dist/components/StatusLine.js +4 -1
- package/dist/components/TabbedOutput.d.ts +1 -1
- package/dist/components/TabbedOutput.js +2 -1
- package/dist/index.js +21 -338
- package/dist/lib/command.d.ts +8 -0
- package/dist/lib/command.js +1 -0
- package/package.json +11 -15
- package/dist/commands/delete.d.ts +0 -1
- package/dist/commands/delete.js +0 -60
- package/dist/commands/edit.d.ts +0 -1
- package/dist/commands/edit.js +0 -92
- package/dist/commands/list.d.ts +0 -1
- package/dist/commands/list.js +0 -55
- package/dist/commands/mcp-add.d.ts +0 -14
- package/dist/commands/mcp-add.js +0 -494
- package/dist/commands/mcp-list.d.ts +0 -3
- package/dist/commands/mcp-list.js +0 -63
- package/dist/commands/mcp-remove.d.ts +0 -3
- package/dist/commands/mcp-remove.js +0 -120
- package/dist/commands/tool-add.d.ts +0 -6
- package/dist/commands/tool-add.js +0 -349
- package/dist/commands/tool-list.d.ts +0 -3
- package/dist/commands/tool-list.js +0 -61
- package/dist/commands/tool-register.d.ts +0 -7
- package/dist/commands/tool-register.js +0 -291
- package/dist/commands/tool-remove.d.ts +0 -3
- package/dist/commands/tool-remove.js +0 -202
- package/dist/components/MergedLogsPane.d.ts +0 -11
- package/dist/components/MergedLogsPane.js +0 -205
- package/dist/lib/auth-storage.d.ts +0 -38
- package/dist/lib/auth-storage.js +0 -89
- package/dist/lib/mcp-storage.d.ts +0 -32
- package/dist/lib/mcp-storage.js +0 -111
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
def: import("@optique/core").Parser<{
|
|
3
|
+
readonly command: "batch";
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly queries: readonly string[];
|
|
6
|
+
readonly file: string | undefined;
|
|
7
|
+
readonly concurrency: number | undefined;
|
|
8
|
+
readonly port: number | undefined;
|
|
9
|
+
}, ["matched", string] | ["parsing", {
|
|
10
|
+
readonly command: "batch";
|
|
11
|
+
readonly name: import("@optique/core").ValueParserResult<string> | undefined;
|
|
12
|
+
readonly queries: readonly (import("@optique/core").ValueParserResult<string> | undefined)[];
|
|
13
|
+
readonly file: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
|
|
14
|
+
readonly concurrency: [import("@optique/core").ValueParserResult<number> | undefined] | undefined;
|
|
15
|
+
readonly port: [import("@optique/core").ValueParserResult<number> | undefined] | undefined;
|
|
16
|
+
}] | undefined>;
|
|
17
|
+
impl: (def: {
|
|
18
|
+
readonly command: "batch";
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly queries: readonly string[];
|
|
21
|
+
readonly file: string | undefined;
|
|
22
|
+
readonly concurrency: number | undefined;
|
|
23
|
+
readonly port: number | undefined;
|
|
24
|
+
}) => unknown;
|
|
25
|
+
};
|
|
26
|
+
export default _default;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { argument, command, constant, message, multiple, object, option, optional, string, } from "@optique/core";
|
|
2
|
+
import { integer } from "@optique/core/valueparser";
|
|
3
|
+
import { createCommand } from "@/lib/command";
|
|
4
|
+
import { batchCommand } from "./batch";
|
|
5
|
+
export default createCommand({
|
|
6
|
+
def: command("batch", object({
|
|
7
|
+
command: constant("batch"),
|
|
8
|
+
name: argument(string({ metavar: "NAME" })),
|
|
9
|
+
queries: multiple(argument(string({ metavar: "QUERY" }))),
|
|
10
|
+
file: optional(option("-f", "--file", string())),
|
|
11
|
+
concurrency: optional(option("-c", "--concurrency", integer())),
|
|
12
|
+
port: optional(option("-p", "--port", integer())),
|
|
13
|
+
}), {
|
|
14
|
+
brief: message `Run multiple queries in parallel against an agent.`,
|
|
15
|
+
description: message `Run multiple queries in parallel, each in its own session. Queries can be provided as arguments or loaded from a file.`,
|
|
16
|
+
}),
|
|
17
|
+
impl: async ({ name, queries, file, concurrency, port }) => {
|
|
18
|
+
const options = {
|
|
19
|
+
name,
|
|
20
|
+
};
|
|
21
|
+
if (queries.length > 0) {
|
|
22
|
+
options.queries = [...queries]; // Convert readonly array to mutable
|
|
23
|
+
}
|
|
24
|
+
if (file !== null && file !== undefined) {
|
|
25
|
+
options.file = file;
|
|
26
|
+
}
|
|
27
|
+
if (concurrency !== null && concurrency !== undefined) {
|
|
28
|
+
options.concurrency = concurrency;
|
|
29
|
+
}
|
|
30
|
+
if (port !== null && port !== undefined) {
|
|
31
|
+
options.port = port;
|
|
32
|
+
}
|
|
33
|
+
await batchCommand(options);
|
|
34
|
+
},
|
|
35
|
+
});
|
package/dist/commands/batch.js
CHANGED
|
@@ -183,7 +183,7 @@ export async function batchCommand(options) {
|
|
|
183
183
|
try {
|
|
184
184
|
process.kill(-agentProcess.pid, "SIGINT");
|
|
185
185
|
}
|
|
186
|
-
catch (
|
|
186
|
+
catch (_e) {
|
|
187
187
|
// Process may already be dead
|
|
188
188
|
}
|
|
189
189
|
}
|
|
@@ -191,7 +191,7 @@ export async function batchCommand(options) {
|
|
|
191
191
|
try {
|
|
192
192
|
process.kill(-debuggerProcess.pid, "SIGKILL");
|
|
193
193
|
}
|
|
194
|
-
catch (
|
|
194
|
+
catch (_e) {
|
|
195
195
|
// Process may already be dead
|
|
196
196
|
}
|
|
197
197
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
def: import("@optique/core").Parser<{
|
|
3
|
+
readonly command: "create";
|
|
4
|
+
readonly name: string | undefined;
|
|
5
|
+
readonly model: string | undefined;
|
|
6
|
+
readonly tools: readonly string[];
|
|
7
|
+
readonly systemPrompt: string | undefined;
|
|
8
|
+
readonly init: string | undefined;
|
|
9
|
+
readonly claude: true | undefined;
|
|
10
|
+
}, ["matched", string] | ["parsing", {
|
|
11
|
+
readonly command: "create";
|
|
12
|
+
readonly name: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
|
|
13
|
+
readonly model: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
|
|
14
|
+
readonly tools: readonly (import("@optique/core").ValueParserResult<string> | undefined)[];
|
|
15
|
+
readonly systemPrompt: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
|
|
16
|
+
readonly init: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
|
|
17
|
+
readonly claude: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
|
|
18
|
+
}] | undefined>;
|
|
19
|
+
impl: (def: {
|
|
20
|
+
readonly command: "create";
|
|
21
|
+
readonly name: string | undefined;
|
|
22
|
+
readonly model: string | undefined;
|
|
23
|
+
readonly tools: readonly string[];
|
|
24
|
+
readonly systemPrompt: string | undefined;
|
|
25
|
+
readonly init: string | undefined;
|
|
26
|
+
readonly claude: true | undefined;
|
|
27
|
+
}) => unknown;
|
|
28
|
+
};
|
|
29
|
+
export default _default;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { command, constant, flag, message, multiple, object, option, optional, string, } from "@optique/core";
|
|
3
|
+
import { initForClaudeCode } from "@townco/agent/scaffold";
|
|
4
|
+
import { isInsideTownProject } from "@townco/agent/storage";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
import { createCommand } from "@/lib/command";
|
|
7
|
+
import { createCommand as createAgentCommand } from "./create";
|
|
8
|
+
import { createProjectCommand } from "./create-project";
|
|
9
|
+
export default createCommand({
|
|
10
|
+
def: command("create", object({
|
|
11
|
+
command: constant("create"),
|
|
12
|
+
name: optional(option("-n", "--name", string())),
|
|
13
|
+
model: optional(option("-m", "--model", string())),
|
|
14
|
+
tools: multiple(option("-t", "--tool", string())),
|
|
15
|
+
systemPrompt: optional(option("-p", "--prompt", string())),
|
|
16
|
+
init: optional(option("--init", string())),
|
|
17
|
+
claude: optional(flag("--claude")),
|
|
18
|
+
}), {
|
|
19
|
+
brief: message `Create a new agent or project (with --init <path>). Use --claude to add Claude Code integration.`,
|
|
20
|
+
}),
|
|
21
|
+
impl: async ({ name, model, tools, systemPrompt, init, claude }) => {
|
|
22
|
+
// Handle --claude flag (initialize .claude in existing project)
|
|
23
|
+
if (claude === true) {
|
|
24
|
+
if (init !== null && init !== undefined) {
|
|
25
|
+
console.error("Error: --claude flag is redundant with --init (projects include .claude by default)");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
// Check if we're in a Town project
|
|
29
|
+
const projectRoot = await isInsideTownProject();
|
|
30
|
+
if (projectRoot === null) {
|
|
31
|
+
console.error("Error: Not inside a Town project. Use 'town create --init <path>' to create a new project.");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
// Initialize .claude directory only
|
|
35
|
+
await initForClaudeCode(projectRoot);
|
|
36
|
+
console.log("\n✓ Claude Code workspace initialized successfully!");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Check if --init flag is present for project scaffolding
|
|
40
|
+
if (init !== null && init !== undefined) {
|
|
41
|
+
// Project mode - scaffold a standalone project
|
|
42
|
+
await createProjectCommand({
|
|
43
|
+
path: init,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// Check if we're inside a Town project
|
|
48
|
+
const projectRoot = await isInsideTownProject();
|
|
49
|
+
if (projectRoot === null) {
|
|
50
|
+
// Not in a project - prompt user to initialize
|
|
51
|
+
const answer = await inquirer.prompt([
|
|
52
|
+
{
|
|
53
|
+
type: "confirm",
|
|
54
|
+
name: "initProject",
|
|
55
|
+
message: "Not inside a Town project. Initialize project in current directory?",
|
|
56
|
+
default: true,
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
if (answer.initProject) {
|
|
60
|
+
// Initialize project first
|
|
61
|
+
await createProjectCommand({ path: process.cwd() });
|
|
62
|
+
// Then create agent
|
|
63
|
+
await createAgentCommand({
|
|
64
|
+
...(name !== undefined && { name }),
|
|
65
|
+
...(model !== undefined && { model }),
|
|
66
|
+
...(tools.length > 0 && { tools }),
|
|
67
|
+
...(systemPrompt !== undefined && { systemPrompt }),
|
|
68
|
+
agentsDir: join(process.cwd(), "agents"),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// User declined
|
|
73
|
+
console.log("\nPlease run 'town create' inside a project directory, or run:\n" +
|
|
74
|
+
" town create --init <path>\n" +
|
|
75
|
+
"to create a project.");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Agent mode - create agent in existing project
|
|
81
|
+
// Create command starts a long-running Ink session
|
|
82
|
+
await createAgentCommand({
|
|
83
|
+
...(name !== undefined && { name }),
|
|
84
|
+
...(model !== undefined && { model }),
|
|
85
|
+
...(tools.length > 0 && { tools }),
|
|
86
|
+
...(systemPrompt !== undefined && { systemPrompt }),
|
|
87
|
+
agentsDir: join(projectRoot, "agents"),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
package/dist/commands/create.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { scaffoldAgent } from "@townco/agent/scaffold";
|
|
3
|
-
import {
|
|
3
|
+
import { MultiSelect, SingleSelect } from "@townco/ui/tui";
|
|
4
4
|
import { Box, render, Text, useInput } from "ink";
|
|
5
5
|
import TextInput from "ink-text-input";
|
|
6
6
|
import { useEffect, useState } from "react";
|
|
7
|
-
import { openInEditor } from "../lib/editor-utils";
|
|
8
7
|
const AVAILABLE_MODELS = [
|
|
9
8
|
{
|
|
10
9
|
label: "Claude Sonnet 4.5",
|
|
@@ -65,7 +64,11 @@ function NameInput({ nameInput, setNameInput, onSubmit }) {
|
|
|
65
64
|
process.exit(0);
|
|
66
65
|
}
|
|
67
66
|
});
|
|
68
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
67
|
+
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
68
|
+
_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Enter agent name:" }) }), _jsxs(Box, { children: [
|
|
69
|
+
_jsxs(Text, { children: [">", " "] }), _jsx(TextInput, { value: nameInput, onChange: setNameInput, onSubmit: onSubmit })
|
|
70
|
+
] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: Continue \u2022 Esc: Cancel" }) })
|
|
71
|
+
] }));
|
|
69
72
|
}
|
|
70
73
|
function CreateApp({ name: initialName, model: initialModel, tools: initialTools, systemPrompt: initialSystemPrompt, overwrite = false, agentsDir, }) {
|
|
71
74
|
// Determine the starting stage based on what's provided
|
|
@@ -164,7 +167,8 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
|
|
|
164
167
|
}
|
|
165
168
|
// Model selection stage
|
|
166
169
|
if (stage === "model") {
|
|
167
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
170
|
+
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
171
|
+
_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ["Select model for agent: ", agentDef.name] }) }), _jsx(SingleSelect, { options: AVAILABLE_MODELS, selected: agentDef.model || null, onChange: (model) => setAgentDef({ ...agentDef, model }), onSubmit: (model) => {
|
|
168
172
|
setAgentDef({ ...agentDef, model });
|
|
169
173
|
if (isEditingFromReview) {
|
|
170
174
|
setIsEditingFromReview(false);
|
|
@@ -180,24 +184,33 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
|
|
|
180
184
|
else {
|
|
181
185
|
setStage("name");
|
|
182
186
|
}
|
|
183
|
-
} })
|
|
187
|
+
} })
|
|
188
|
+
] }));
|
|
184
189
|
}
|
|
185
190
|
// Tools selection stage
|
|
186
191
|
if (stage === "tools") {
|
|
187
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
192
|
+
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
193
|
+
_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ["Select tools for agent: ", agentDef.name] }) }), _jsx(MultiSelect, { options: AVAILABLE_TOOLS, selected: agentDef.tools || [], onChange: (tools) => setAgentDef({ ...agentDef, tools }), onSubmit: () => {
|
|
188
194
|
// If editing from review, just go back to review
|
|
189
195
|
if (isEditingFromReview) {
|
|
190
196
|
setIsEditingFromReview(false);
|
|
191
197
|
}
|
|
192
198
|
// Go directly to review stage
|
|
193
199
|
setStage("review");
|
|
194
|
-
}, onCancel: () => setStage("model") })
|
|
200
|
+
}, onCancel: () => setStage("model") })
|
|
201
|
+
] }));
|
|
195
202
|
}
|
|
196
203
|
// Review stage
|
|
197
204
|
if (stage === "review") {
|
|
198
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
205
|
+
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
206
|
+
_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Review Agent Configuration:" }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
207
|
+
_jsxs(Text, { children: [
|
|
208
|
+
_jsx(Text, { bold: true, children: "Name: " }), agentDef.name] }), _jsxs(Text, { children: [
|
|
209
|
+
_jsx(Text, { bold: true, children: "Model: " }), agentDef.model] }), _jsxs(Text, { children: [
|
|
210
|
+
_jsx(Text, { bold: true, children: "Tools: " }), agentDef.tools && agentDef.tools.length > 0
|
|
199
211
|
? agentDef.tools.join(", ")
|
|
200
|
-
: "none"] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: "System Prompt:" }) }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: agentDef.systemPrompt?.split("\n").map((line) => (_jsx(Text, { dimColor: true, children: line || " " }, line))) })
|
|
212
|
+
: "none"] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: "System Prompt:" }) }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: agentDef.systemPrompt?.split("\n").map((line) => (_jsx(Text, { dimColor: true, children: line || " " }, line))) })
|
|
213
|
+
] }), _jsx(SingleSelect, { options: [
|
|
201
214
|
{
|
|
202
215
|
label: "Looks good, continue",
|
|
203
216
|
value: "continue",
|
|
@@ -233,7 +246,8 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
|
|
|
233
246
|
setIsEditingFromReview(true);
|
|
234
247
|
setStage(value);
|
|
235
248
|
}
|
|
236
|
-
}, onCancel: () => setStage("tools") })
|
|
249
|
+
}, onCancel: () => setStage("tools") })
|
|
250
|
+
] }));
|
|
237
251
|
}
|
|
238
252
|
// Done stage
|
|
239
253
|
if (stage === "done") {
|
|
@@ -241,11 +255,23 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
|
|
|
241
255
|
return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { children: ["\u23F3 Creating agent ", agentDef.name, "..."] }) }));
|
|
242
256
|
}
|
|
243
257
|
if (scaffoldStatus === "error") {
|
|
244
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
258
|
+
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
259
|
+
_jsx(Text, { color: "red", children: "\u2717 Error creating agent package" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: scaffoldError }) })
|
|
260
|
+
] }));
|
|
245
261
|
}
|
|
246
262
|
if (scaffoldStatus === "done") {
|
|
247
263
|
const modelLabel = agentDef.model?.replace("claude-", "") || "";
|
|
248
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
264
|
+
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
265
|
+
_jsx(Text, { bold: true, children: "Agent created successfully!" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
266
|
+
_jsxs(Text, { children: [
|
|
267
|
+
_jsx(Text, { color: "green", children: "\u25CF" }),
|
|
268
|
+
" ",
|
|
269
|
+
_jsx(Text, { bold: true, children: agentDef.name })
|
|
270
|
+
] }), _jsxs(Text, { dimColor: true, children: [" Model: ", modelLabel] }), agentDef.tools && agentDef.tools.length > 0 && (_jsxs(Text, { dimColor: true, children: [" Tools: ", agentDef.tools.join(", ")] })), _jsxs(Text, { dimColor: true, children: [" Path: ", agentPath] })
|
|
271
|
+
] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
272
|
+
_jsxs(Text, { dimColor: true, children: ["Run agent: town run ", agentDef.name] }), _jsxs(Text, { dimColor: true, children: ["With GUI: town run ", agentDef.name, " --gui"] })
|
|
273
|
+
] })
|
|
274
|
+
] }));
|
|
249
275
|
}
|
|
250
276
|
}
|
|
251
277
|
return null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
def: import("@optique/core").Parser<{
|
|
3
|
+
readonly command: "deploy";
|
|
4
|
+
readonly agent: string;
|
|
5
|
+
}, ["matched", string] | ["parsing", {
|
|
6
|
+
readonly command: "deploy";
|
|
7
|
+
readonly agent: import("@optique/core").ValueParserResult<string> | undefined;
|
|
8
|
+
}] | undefined>;
|
|
9
|
+
impl: (def: {
|
|
10
|
+
readonly command: "deploy";
|
|
11
|
+
readonly agent: string;
|
|
12
|
+
}) => unknown;
|
|
13
|
+
};
|
|
14
|
+
export default _default;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import afs from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { command, constant, message, object, option, string, } from "@optique/core";
|
|
5
|
+
import { isInsideTownProject } from "@townco/agent/storage";
|
|
6
|
+
import { findRoot } from "@townco/core/path";
|
|
7
|
+
import { createTRPCClient, httpLink, httpSubscriptionLink, splitLink, } from "@trpc/client";
|
|
8
|
+
import archiver from "archiver";
|
|
9
|
+
import { EventSource } from "eventsource";
|
|
10
|
+
import walk from "ignore-walk";
|
|
11
|
+
import superjson from "superjson";
|
|
12
|
+
import { getValidCredentials } from "@/lib/auth-fetch";
|
|
13
|
+
import { createCommand } from "@/lib/command";
|
|
14
|
+
const MAX_ARCHIVE_SIZE = 4.5 * 1024 * 1024; // 4.5MB
|
|
15
|
+
export default createCommand({
|
|
16
|
+
def: command("deploy", object({
|
|
17
|
+
command: constant("deploy"),
|
|
18
|
+
agent: option("-a", "--agent", string()),
|
|
19
|
+
}), {
|
|
20
|
+
brief: message `Deploy agents.`,
|
|
21
|
+
description: message `Deploy agents to the Town cloud.`,
|
|
22
|
+
}),
|
|
23
|
+
impl: async ({ agent }) => {
|
|
24
|
+
const projectRoot = await isInsideTownProject();
|
|
25
|
+
if (!projectRoot)
|
|
26
|
+
throw new Error("Not inside a Town project");
|
|
27
|
+
if (!(await afs.exists(join(projectRoot, "agents", agent))))
|
|
28
|
+
throw new Error(`Agent ${agent} not found`);
|
|
29
|
+
const { accessToken, shedUrl } = await getValidCredentials();
|
|
30
|
+
const authHeader = { Authorization: `Bearer ${accessToken}` };
|
|
31
|
+
const url = `${shedUrl}/api/trpc`;
|
|
32
|
+
const baseLinkOpts = { url, transformer: superjson };
|
|
33
|
+
const client = createTRPCClient({
|
|
34
|
+
links: [
|
|
35
|
+
splitLink({
|
|
36
|
+
condition: (op) => op.type === "subscription",
|
|
37
|
+
true: httpSubscriptionLink({
|
|
38
|
+
...baseLinkOpts,
|
|
39
|
+
EventSource,
|
|
40
|
+
eventSourceOptions: {
|
|
41
|
+
fetch: async (url, init) => fetch(url, {
|
|
42
|
+
...init,
|
|
43
|
+
headers: { ...init.headers, ...authHeader },
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
false: httpLink({ ...baseLinkOpts, headers: authHeader }),
|
|
48
|
+
}),
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
console.log("Creating archive...");
|
|
52
|
+
const root = await findRoot({ rootMarker: "package.json" });
|
|
53
|
+
const chunks = [];
|
|
54
|
+
const arc = archiver("tar", { gzip: true });
|
|
55
|
+
const done = new Promise((ok, err) => {
|
|
56
|
+
arc.on("data", (chunk) => chunks.push(chunk));
|
|
57
|
+
arc.on("end", () => {
|
|
58
|
+
console.log("Archive created");
|
|
59
|
+
ok(Buffer.concat(chunks));
|
|
60
|
+
});
|
|
61
|
+
arc.on("error", err);
|
|
62
|
+
});
|
|
63
|
+
(await walk({ path: root, ignoreFiles: [".gitignore"] }))
|
|
64
|
+
.filter((path) => path.split("/")[0] !== ".git")
|
|
65
|
+
.reduce((totalSize, path) => {
|
|
66
|
+
const stats = fs.statSync(path);
|
|
67
|
+
if (!stats.isFile())
|
|
68
|
+
return totalSize;
|
|
69
|
+
const fileSize = fs.statSync(path).size;
|
|
70
|
+
const newTotal = totalSize + fileSize;
|
|
71
|
+
if (newTotal > MAX_ARCHIVE_SIZE) {
|
|
72
|
+
throw new Error(`Archive size limit exceeded: ${(newTotal / 1024 / 1024).toFixed(2)}MB > 4.5MB`);
|
|
73
|
+
}
|
|
74
|
+
arc.append(fs.createReadStream(path), { name: path });
|
|
75
|
+
return newTotal;
|
|
76
|
+
}, 0);
|
|
77
|
+
arc.finalize();
|
|
78
|
+
console.log("Uploading archive...");
|
|
79
|
+
const { sha256, cached } = await client.uploadArchive.mutate(await done);
|
|
80
|
+
console.log(`Archive uploaded: ${sha256} (${cached ? "cached" : "new"})`);
|
|
81
|
+
console.log("Deploying...");
|
|
82
|
+
client.deploy.subscribe({ sha256, agent, shedUrl }, {
|
|
83
|
+
onData: ({ status, error }) => console.log(status ? status : error),
|
|
84
|
+
onError: (err) => console.error(err),
|
|
85
|
+
onComplete: () => console.log("\n✓ Deployment complete!"),
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
});
|
package/dist/commands/login.js
CHANGED
|
@@ -80,7 +80,7 @@ export async function loginCommand() {
|
|
|
80
80
|
console.error(`\nLogin failed: ${errorData.error || "Unknown error"}`);
|
|
81
81
|
if (response.status === 401) {
|
|
82
82
|
console.log("\nPlease check your email and password and try again.");
|
|
83
|
-
console.log(
|
|
83
|
+
console.log(`If you don't have an account, sign up at ${shedUrl}`);
|
|
84
84
|
}
|
|
85
85
|
process.exit(1);
|
|
86
86
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
def: import("@optique/core").Parser<{
|
|
3
|
+
readonly command: "run";
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly http: true | undefined;
|
|
6
|
+
readonly gui: true | undefined;
|
|
7
|
+
readonly cli: true | undefined;
|
|
8
|
+
readonly prompt: string | undefined;
|
|
9
|
+
readonly port: number | undefined;
|
|
10
|
+
readonly noSession: true | undefined;
|
|
11
|
+
}, ["matched", string] | ["parsing", {
|
|
12
|
+
readonly command: "run";
|
|
13
|
+
readonly name: import("@optique/core").ValueParserResult<string> | undefined;
|
|
14
|
+
readonly http: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
|
|
15
|
+
readonly gui: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
|
|
16
|
+
readonly cli: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
|
|
17
|
+
readonly prompt: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
|
|
18
|
+
readonly port: [import("@optique/core").ValueParserResult<number> | undefined] | undefined;
|
|
19
|
+
readonly noSession: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
|
|
20
|
+
}] | undefined>;
|
|
21
|
+
impl: (def: {
|
|
22
|
+
readonly command: "run";
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly http: true | undefined;
|
|
25
|
+
readonly gui: true | undefined;
|
|
26
|
+
readonly cli: true | undefined;
|
|
27
|
+
readonly prompt: string | undefined;
|
|
28
|
+
readonly port: number | undefined;
|
|
29
|
+
readonly noSession: true | undefined;
|
|
30
|
+
}) => unknown;
|
|
31
|
+
};
|
|
32
|
+
export default _default;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { argument, command, constant, flag, message, object, option, optional, string, } from "@optique/core";
|
|
2
|
+
import { integer } from "@optique/core/valueparser";
|
|
3
|
+
import { createCommand } from "@/lib/command";
|
|
4
|
+
import { runCommand } from "./run.js";
|
|
5
|
+
export default createCommand({
|
|
6
|
+
def: command("run", object({
|
|
7
|
+
command: constant("run"),
|
|
8
|
+
name: argument(string({ metavar: "NAME" })),
|
|
9
|
+
http: optional(flag("--http")),
|
|
10
|
+
gui: optional(flag("--gui")),
|
|
11
|
+
cli: optional(flag("--cli")),
|
|
12
|
+
prompt: optional(argument(string({ metavar: "PROMPT" }))),
|
|
13
|
+
port: optional(option("-p", "--port", integer())),
|
|
14
|
+
noSession: optional(flag("--no-session")),
|
|
15
|
+
}), { brief: message `Run an agent.` }),
|
|
16
|
+
impl: async ({ name, http, gui, cli, prompt, port, noSession }) => {
|
|
17
|
+
const options = {
|
|
18
|
+
name,
|
|
19
|
+
http: http === true,
|
|
20
|
+
gui: gui === true,
|
|
21
|
+
cli: cli === true,
|
|
22
|
+
noSession: noSession === true,
|
|
23
|
+
};
|
|
24
|
+
if (prompt !== null && prompt !== undefined) {
|
|
25
|
+
options.prompt = prompt;
|
|
26
|
+
}
|
|
27
|
+
if (port !== null && port !== undefined) {
|
|
28
|
+
options.port = port;
|
|
29
|
+
}
|
|
30
|
+
await runCommand(options);
|
|
31
|
+
},
|
|
32
|
+
});
|