@townco/cli 0.1.11 → 0.1.13

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.
@@ -0,0 +1,112 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ /**
6
+ * Standardized editor fallback chain
7
+ */
8
+ const EDITOR_FALLBACKS = [
9
+ () => process.env.EDITOR,
10
+ () => process.env.VISUAL,
11
+ () => "code",
12
+ () => "vi",
13
+ ];
14
+ /**
15
+ * Parse an editor command that might include arguments
16
+ * e.g., "code --wait" -> ["code", "--wait"]
17
+ */
18
+ function parseEditorCommand(editor) {
19
+ const parts = editor.trim().split(/\s+/);
20
+ const command = parts[0] || "vi";
21
+ const args = parts.slice(1);
22
+ return [command, args];
23
+ }
24
+ /**
25
+ * Attempt to spawn an editor and wait for it to close
26
+ */
27
+ async function spawnEditor(editor, filePath, additionalArgs = []) {
28
+ return new Promise((resolve) => {
29
+ const [command, editorArgs] = parseEditorCommand(editor);
30
+ // Special handling for 'code' - add --wait if not present
31
+ const args = command === "code" && !editorArgs.includes("--wait")
32
+ ? ["--wait", ...editorArgs, ...additionalArgs, filePath]
33
+ : [...editorArgs, ...additionalArgs, filePath];
34
+ const child = spawn(command, args, {
35
+ stdio: "inherit",
36
+ });
37
+ child.on("close", (code) => {
38
+ resolve(code === 0 || code === null);
39
+ });
40
+ child.on("error", () => {
41
+ resolve(false);
42
+ });
43
+ });
44
+ }
45
+ /**
46
+ * Try editors in the fallback chain until one succeeds
47
+ */
48
+ async function tryEditorsInOrder(filePath) {
49
+ for (const getEditor of EDITOR_FALLBACKS) {
50
+ const editor = getEditor();
51
+ if (editor) {
52
+ const success = await spawnEditor(editor, filePath);
53
+ if (success) {
54
+ return true;
55
+ }
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+ /**
61
+ * Unified editor opening function that handles both content and file modes
62
+ *
63
+ * @example Content mode
64
+ * const edited = await openInEditor("Hello world", { extension: '.txt' });
65
+ * if (edited) console.log("User edited:", edited);
66
+ *
67
+ * @example File mode
68
+ * await openInEditor({ filePath: '/path/to/file.ts' });
69
+ */
70
+ export async function openInEditor(contentOrOptions, options) {
71
+ const isContentMode = typeof contentOrOptions === "string";
72
+ if (isContentMode) {
73
+ // Content mode: create temp file, edit, return content
74
+ const content = contentOrOptions;
75
+ const extension = options?.extension || ".txt";
76
+ const tempFile = join(tmpdir(), `editor-${Date.now()}${extension}`);
77
+ try {
78
+ // Write initial content
79
+ writeFileSync(tempFile, content, "utf-8");
80
+ // Try editors in fallback order
81
+ const success = await tryEditorsInOrder(tempFile);
82
+ if (success) {
83
+ // Read edited content
84
+ const editedContent = readFileSync(tempFile, "utf-8");
85
+ unlinkSync(tempFile);
86
+ return editedContent;
87
+ }
88
+ // All editors failed
89
+ unlinkSync(tempFile);
90
+ return null;
91
+ }
92
+ catch (_error) {
93
+ // Clean up temp file if it exists
94
+ try {
95
+ unlinkSync(tempFile);
96
+ }
97
+ catch {
98
+ // Ignore cleanup errors
99
+ }
100
+ return null;
101
+ }
102
+ }
103
+ else {
104
+ // File mode: edit existing file, return void
105
+ const { filePath } = contentOrOptions;
106
+ const success = await tryEditorsInOrder(filePath);
107
+ if (!success) {
108
+ throw new Error("Failed to open editor");
109
+ }
110
+ return null;
111
+ }
112
+ }
@@ -1,9 +1,9 @@
1
1
  export type MCPConfig = {
2
- name: string;
3
- transport: "stdio" | "http";
4
- command?: string;
5
- args?: string[];
6
- url?: string;
2
+ name: string;
3
+ transport: "stdio" | "http";
4
+ command?: string;
5
+ args?: string[];
6
+ url?: string;
7
7
  };
8
8
  /**
9
9
  * Save an MCP config to the store
@@ -1,7 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
-
5
4
  // ============================================================================
6
5
  // Constants
7
6
  // ============================================================================
@@ -14,40 +13,38 @@ const MCPS_FILE = join(TOWN_CONFIG_DIR, "mcps.json");
14
13
  * Ensure the config directory exists
15
14
  */
16
15
  function ensureConfigDir() {
17
- if (!existsSync(TOWN_CONFIG_DIR)) {
18
- mkdirSync(TOWN_CONFIG_DIR, { recursive: true });
19
- }
16
+ if (!existsSync(TOWN_CONFIG_DIR)) {
17
+ mkdirSync(TOWN_CONFIG_DIR, { recursive: true });
18
+ }
20
19
  }
21
20
  /**
22
21
  * Load all MCP configs from the JSON file
23
22
  */
24
23
  function loadStore() {
25
- ensureConfigDir();
26
- if (!existsSync(MCPS_FILE)) {
27
- return {};
28
- }
29
- try {
30
- const content = readFileSync(MCPS_FILE, "utf-8");
31
- return JSON.parse(content);
32
- } catch (error) {
33
- throw new Error(
34
- `Failed to load MCP configs: ${error instanceof Error ? error.message : String(error)}`,
35
- );
36
- }
24
+ ensureConfigDir();
25
+ if (!existsSync(MCPS_FILE)) {
26
+ return {};
27
+ }
28
+ try {
29
+ const content = readFileSync(MCPS_FILE, "utf-8");
30
+ return JSON.parse(content);
31
+ }
32
+ catch (error) {
33
+ throw new Error(`Failed to load MCP configs: ${error instanceof Error ? error.message : String(error)}`);
34
+ }
37
35
  }
38
36
  /**
39
37
  * Save all MCP configs to the JSON file
40
38
  */
41
39
  function saveStore(store) {
42
- ensureConfigDir();
43
- try {
44
- const content = JSON.stringify(store, null, 2);
45
- writeFileSync(MCPS_FILE, content, "utf-8");
46
- } catch (error) {
47
- throw new Error(
48
- `Failed to save MCP configs: ${error instanceof Error ? error.message : String(error)}`,
49
- );
50
- }
40
+ ensureConfigDir();
41
+ try {
42
+ const content = JSON.stringify(store, null, 2);
43
+ writeFileSync(MCPS_FILE, content, "utf-8");
44
+ }
45
+ catch (error) {
46
+ throw new Error(`Failed to save MCP configs: ${error instanceof Error ? error.message : String(error)}`);
47
+ }
51
48
  }
52
49
  // ============================================================================
53
50
  // Public API
@@ -56,54 +53,55 @@ function saveStore(store) {
56
53
  * Save an MCP config to the store
57
54
  */
58
55
  export function saveMCPConfig(config) {
59
- const store = loadStore();
60
- store[config.name] = config;
61
- saveStore(store);
56
+ const store = loadStore();
57
+ store[config.name] = config;
58
+ saveStore(store);
62
59
  }
63
60
  /**
64
61
  * Load an MCP config by name
65
62
  */
66
63
  export function loadMCPConfig(name) {
67
- const store = loadStore();
68
- return store[name] || null;
64
+ const store = loadStore();
65
+ return store[name] || null;
69
66
  }
70
67
  /**
71
68
  * Delete an MCP config by name
72
69
  */
73
70
  export function deleteMCPConfig(name) {
74
- const store = loadStore();
75
- if (store[name]) {
76
- delete store[name];
77
- saveStore(store);
78
- return true;
79
- }
80
- return false;
71
+ const store = loadStore();
72
+ if (store[name]) {
73
+ delete store[name];
74
+ saveStore(store);
75
+ return true;
76
+ }
77
+ return false;
81
78
  }
82
79
  /**
83
80
  * List all MCP configs
84
81
  */
85
82
  export function listMCPConfigs() {
86
- const store = loadStore();
87
- return Object.values(store).sort((a, b) => a.name.localeCompare(b.name));
83
+ const store = loadStore();
84
+ return Object.values(store).sort((a, b) => a.name.localeCompare(b.name));
88
85
  }
89
86
  /**
90
87
  * Check if an MCP config exists
91
88
  */
92
89
  export function mcpConfigExists(name) {
93
- const store = loadStore();
94
- return name in store;
90
+ const store = loadStore();
91
+ return name in store;
95
92
  }
96
93
  /**
97
94
  * Get a summary of an MCP config for display
98
95
  */
99
96
  export function getMCPSummary(config) {
100
- if (config.transport === "http") {
101
- return `HTTP: ${config.url}`;
102
- } else {
103
- const parts = [`Stdio: ${config.command}`];
104
- if (config.args && config.args.length > 0) {
105
- parts.push(`[${config.args.join(" ")}]`);
106
- }
107
- return parts.join(" ");
108
- }
97
+ if (config.transport === "http") {
98
+ return `HTTP: ${config.url}`;
99
+ }
100
+ else {
101
+ const parts = [`Stdio: ${config.command}`];
102
+ if (config.args && config.args.length > 0) {
103
+ parts.push(`[${config.args.join(" ")}]`);
104
+ }
105
+ return parts.join(" ");
106
+ }
109
107
  }
@@ -0,0 +1,32 @@
1
+ export type ToolConfig = {
2
+ name: string;
3
+ path: string;
4
+ };
5
+ /**
6
+ * Ensure the tools directory exists
7
+ */
8
+ export declare function ensureToolsDir(): void;
9
+ /**
10
+ * Get the tools directory path
11
+ */
12
+ export declare function getToolsDir(): string;
13
+ /**
14
+ * Save a tool config to the store
15
+ */
16
+ export declare function saveToolConfig(config: ToolConfig): void;
17
+ /**
18
+ * Load a tool config by name
19
+ */
20
+ export declare function loadToolConfig(name: string): ToolConfig | null;
21
+ /**
22
+ * Delete a tool config by name
23
+ */
24
+ export declare function deleteToolConfig(name: string): boolean;
25
+ /**
26
+ * List all tool configs
27
+ */
28
+ export declare function listToolConfigs(): ToolConfig[];
29
+ /**
30
+ * Check if a tool config exists
31
+ */
32
+ export declare function toolConfigExists(name: string): boolean;
@@ -0,0 +1,107 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ // ============================================================================
5
+ // Constants
6
+ // ============================================================================
7
+ const TOWN_CONFIG_DIR = join(homedir(), ".config", "town");
8
+ const TOOLS_FILE = join(TOWN_CONFIG_DIR, "tools.json");
9
+ const TOOLS_DIR = join(TOWN_CONFIG_DIR, "tools");
10
+ // ============================================================================
11
+ // Helper Functions
12
+ // ============================================================================
13
+ /**
14
+ * Ensure the config directory exists
15
+ */
16
+ function ensureConfigDir() {
17
+ if (!existsSync(TOWN_CONFIG_DIR)) {
18
+ mkdirSync(TOWN_CONFIG_DIR, { recursive: true });
19
+ }
20
+ }
21
+ /**
22
+ * Ensure the tools directory exists
23
+ */
24
+ export function ensureToolsDir() {
25
+ if (!existsSync(TOOLS_DIR)) {
26
+ mkdirSync(TOOLS_DIR, { recursive: true });
27
+ }
28
+ }
29
+ /**
30
+ * Get the tools directory path
31
+ */
32
+ export function getToolsDir() {
33
+ return TOOLS_DIR;
34
+ }
35
+ /**
36
+ * Load all tool configs from the JSON file
37
+ */
38
+ function loadStore() {
39
+ ensureConfigDir();
40
+ if (!existsSync(TOOLS_FILE)) {
41
+ return {};
42
+ }
43
+ try {
44
+ const content = readFileSync(TOOLS_FILE, "utf-8");
45
+ return JSON.parse(content);
46
+ }
47
+ catch (error) {
48
+ throw new Error(`Failed to load tool configs: ${error instanceof Error ? error.message : String(error)}`);
49
+ }
50
+ }
51
+ /**
52
+ * Save all tool configs to the JSON file
53
+ */
54
+ function saveStore(store) {
55
+ ensureConfigDir();
56
+ try {
57
+ const content = JSON.stringify(store, null, 2);
58
+ writeFileSync(TOOLS_FILE, content, "utf-8");
59
+ }
60
+ catch (error) {
61
+ throw new Error(`Failed to save tool configs: ${error instanceof Error ? error.message : String(error)}`);
62
+ }
63
+ }
64
+ // ============================================================================
65
+ // Public API
66
+ // ============================================================================
67
+ /**
68
+ * Save a tool config to the store
69
+ */
70
+ export function saveToolConfig(config) {
71
+ const store = loadStore();
72
+ store[config.name] = config;
73
+ saveStore(store);
74
+ }
75
+ /**
76
+ * Load a tool config by name
77
+ */
78
+ export function loadToolConfig(name) {
79
+ const store = loadStore();
80
+ return store[name] || null;
81
+ }
82
+ /**
83
+ * Delete a tool config by name
84
+ */
85
+ export function deleteToolConfig(name) {
86
+ const store = loadStore();
87
+ if (store[name]) {
88
+ delete store[name];
89
+ saveStore(store);
90
+ return true;
91
+ }
92
+ return false;
93
+ }
94
+ /**
95
+ * List all tool configs
96
+ */
97
+ export function listToolConfigs() {
98
+ const store = loadStore();
99
+ return Object.values(store).sort((a, b) => a.name.localeCompare(b.name));
100
+ }
101
+ /**
102
+ * Check if a tool config exists
103
+ */
104
+ export function toolConfigExists(name) {
105
+ const store = loadStore();
106
+ return name in store;
107
+ }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@townco/cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "bin": {
6
- "townco": "./dist/index.js"
6
+ "town": "./dist/index.js"
7
7
  },
8
8
  "files": [
9
9
  "dist",
@@ -19,16 +19,16 @@
19
19
  "build": "tsc"
20
20
  },
21
21
  "devDependencies": {
22
- "@townco/tsconfig": "0.1.3",
22
+ "@townco/tsconfig": "0.1.5",
23
23
  "@types/bun": "^1.3.1",
24
24
  "@types/react": "^19.2.2"
25
25
  },
26
26
  "dependencies": {
27
27
  "@optique/core": "^0.6.2",
28
28
  "@optique/run": "^0.6.2",
29
- "@townco/agent": "0.1.11",
30
- "@townco/secret": "0.1.6",
31
- "@townco/ui": "0.1.6",
29
+ "@townco/agent": "0.1.13",
30
+ "@townco/secret": "0.1.8",
31
+ "@townco/ui": "0.1.8",
32
32
  "@types/inquirer": "^9.0.9",
33
33
  "ink": "^6.4.0",
34
34
  "ink-text-input": "^6.0.0",