tmux-ide 1.0.0 → 1.1.0

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
@@ -1,5 +1,7 @@
1
1
  # tmux-ide
2
2
 
3
+ [![CI](https://github.com/wavyrai/tmux-ide/actions/workflows/ci.yml/badge.svg)](https://github.com/wavyrai/tmux-ide/actions/workflows/ci.yml)
4
+
3
5
  Turn any project into a tmux-powered terminal IDE with a simple `ide.yml` config file.
4
6
 
5
7
  ## Install
@@ -8,6 +10,8 @@ Turn any project into a tmux-powered terminal IDE with a simple `ide.yml` config
8
10
  npm install -g tmux-ide
9
11
  ```
10
12
 
13
+ Global install also registers the bundled Claude Code skill and enables `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` in `~/.claude/settings.json` if Claude Code is installed locally on the machine.
14
+
11
15
  ## Quick Start
12
16
 
13
17
  ```bash
@@ -16,24 +20,25 @@ tmux-ide # Launch the IDE
16
20
  tmux-ide stop # Kill the session
17
21
  tmux-ide restart # Stop and relaunch
18
22
  tmux-ide attach # Reattach to a running session
23
+ tmux-ide inspect # Inspect effective config + runtime state
19
24
  ```
20
25
 
21
26
  ## ide.yml Format
22
27
 
23
28
  ```yaml
24
- name: project-name # tmux session name
29
+ name: project-name # tmux session name
25
30
 
26
- before: pnpm install # optional pre-launch hook
31
+ before: pnpm install # optional pre-launch hook
27
32
 
28
33
  rows:
29
- - size: 70% # row height percentage
34
+ - size: 70% # row height percentage
30
35
  panes:
31
- - title: Editor # pane border label
32
- command: vim # command to run (optional)
33
- size: 60% # pane width percentage (optional)
34
- dir: apps/web # per-pane working directory (optional)
35
- focus: true # initial focus (optional)
36
- env: # environment variables (optional)
36
+ - title: Editor # pane border label
37
+ command: vim # command to run (optional)
38
+ size: 60% # pane width percentage (optional)
39
+ dir: apps/web # per-pane working directory (optional)
40
+ focus: true # initial focus (optional)
41
+ env: # environment variables (optional)
37
42
  PORT: 3000
38
43
  - title: Shell
39
44
 
@@ -43,7 +48,7 @@ rows:
43
48
  - title: Tests
44
49
  command: pnpm test
45
50
 
46
- theme: # optional color overrides
51
+ theme: # optional color overrides
47
52
  accent: colour75
48
53
  border: colour238
49
54
  bg: colour235
@@ -52,38 +57,82 @@ theme: # optional color overrides
52
57
 
53
58
  ## Commands
54
59
 
55
- | Command | Description |
56
- |---------|-------------|
57
- | `tmux-ide` | Launch IDE from `ide.yml` |
58
- | `tmux-ide <path>` | Launch from a specific directory |
59
- | `tmux-ide init [--template <name>]` | Scaffold a new `ide.yml` |
60
- | `tmux-ide stop` | Kill the current IDE session |
61
- | `tmux-ide restart` | Stop and relaunch the IDE session |
62
- | `tmux-ide attach` | Reattach to a running session |
63
- | `tmux-ide ls` | List all tmux sessions |
64
- | `tmux-ide status` | Show session status |
65
- | `tmux-ide doctor` | Check system requirements |
66
- | `tmux-ide validate` | Validate `ide.yml` |
67
- | `tmux-ide detect` | Detect project stack |
68
- | `tmux-ide detect --write` | Detect and write `ide.yml` |
69
- | `tmux-ide config` | Dump config as JSON |
70
- | `tmux-ide config set <path> <value>` | Set a config value |
71
- | `tmux-ide config add-pane --row <N>` | Add a pane to a row |
72
- | `tmux-ide config remove-pane --row <N> --pane <M>` | Remove a pane |
73
- | `tmux-ide config add-row [--size <percent>]` | Add a new row |
60
+ | Command | Description |
61
+ | -------------------------------------------------- | --------------------------------------- |
62
+ | `tmux-ide` | Launch IDE from `ide.yml` |
63
+ | `tmux-ide <path>` | Launch from a specific directory |
64
+ | `tmux-ide init [--template <name>]` | Scaffold a new `ide.yml` |
65
+ | `tmux-ide stop` | Kill the current IDE session |
66
+ | `tmux-ide restart` | Stop and relaunch the IDE session |
67
+ | `tmux-ide attach` | Reattach to a running session |
68
+ | `tmux-ide ls` | List all tmux sessions |
69
+ | `tmux-ide status` | Show session status |
70
+ | `tmux-ide inspect` | Show effective config and runtime state |
71
+ | `tmux-ide doctor` | Check system requirements |
72
+ | `tmux-ide validate` | Validate `ide.yml` |
73
+ | `tmux-ide detect` | Detect project stack and explain why |
74
+ | `tmux-ide detect --write` | Detect and write `ide.yml` |
75
+ | `tmux-ide config` | Dump config as JSON |
76
+ | `tmux-ide config set <path> <value>` | Set a config value |
77
+ | `tmux-ide config add-pane --row <N>` | Add a pane to a row |
78
+ | `tmux-ide config remove-pane --row <N> --pane <M>` | Remove a pane |
79
+ | `tmux-ide config add-row [--size <percent>]` | Add a new row |
80
+ | `tmux-ide config enable-team --name <name>` | Enable agent teams |
81
+ | `tmux-ide config disable-team` | Disable agent teams |
74
82
 
75
83
  All commands support `--json` for structured output.
76
84
 
85
+ `tmux-ide detect` now includes reasoning about the package manager, language, framework, and dev-command signals it used. `tmux-ide inspect` combines config validation, resolved layout details, and live tmux state in one command.
86
+
77
87
  ## Templates
78
88
 
79
89
  Use `tmux-ide init --template <name>` with one of:
80
90
 
81
- - `default` — General-purpose layout
82
- - `nextjs` — Next.js development
83
- - `convex` — Convex + Next.js
84
- - `vite` — Vite project
85
- - `python` — Python development
86
- - `go` — Go development
91
+ - `default` - General-purpose layout
92
+ - `nextjs` - Next.js development
93
+ - `convex` - Convex + Next.js
94
+ - `vite` - Vite project
95
+ - `python` - Python development
96
+ - `go` - Go development
97
+ - `agent-team` - Agent team with lead + teammates
98
+ - `agent-team-nextjs` - Agent team for Next.js
99
+ - `agent-team-monorepo` - Agent team for monorepos
100
+
101
+ ## Contributor Workflow
102
+
103
+ The repo now uses a pnpm workspace with a root CLI package and a separate docs app package:
104
+
105
+ ```bash
106
+ pnpm install
107
+ pnpm test
108
+ pnpm docs:build
109
+ pnpm check
110
+ pnpm pack:check
111
+ ```
112
+
113
+ `pnpm check` is the intended local pre-push command and matches the default release checklist. `npm publish` is still guarded by `prepublishOnly`, so publishing runs the same full check path automatically.
114
+
115
+ ## CI
116
+
117
+ GitHub Actions validates:
118
+
119
+ - the Node CLI test suite on Node 18, 20, and 22
120
+ - the docs site production build
121
+ - the package can be packed successfully with `npm pack --dry-run`
122
+
123
+ That keeps the release surface small but catches the main regressions for a CLI-first package.
124
+
125
+ ## Open Source Project Files
126
+
127
+ - [CONTRIBUTING.md](CONTRIBUTING.md) for local setup and contribution workflow
128
+ - [RELEASE.md](RELEASE.md) for the publish checklist
129
+ - [CHANGELOG.md](CHANGELOG.md) for release notes
130
+ - [SECURITY.md](SECURITY.md) for vulnerability reporting
131
+
132
+ Release note convention:
133
+
134
+ - Keep the next version under an `Unreleased` heading in `CHANGELOG.md` until the tag is cut.
135
+ - Move it to a dated release entry when the release is actually published.
87
136
 
88
137
  ## Requirements
89
138
 
package/bin/cli.js CHANGED
@@ -8,10 +8,12 @@ import { attach } from "../src/attach.js";
8
8
  import { ls } from "../src/ls.js";
9
9
  import { doctor } from "../src/doctor.js";
10
10
  import { status } from "../src/status.js";
11
+ import { inspect } from "../src/inspect.js";
11
12
  import { validate } from "../src/validate.js";
12
13
  import { detect } from "../src/detect.js";
13
14
  import { config } from "../src/config.js";
14
15
  import { restart } from "../src/restart.js";
16
+ import { CommandError, printCommandError } from "../src/lib/output.js";
15
17
 
16
18
  const { positionals, values } = parseArgs({
17
19
  allowPositionals: true,
@@ -31,6 +33,22 @@ const { positionals, values } = parseArgs({
31
33
  },
32
34
  });
33
35
 
36
+ const knownCommands = new Set([
37
+ "start",
38
+ "init",
39
+ "stop",
40
+ "attach",
41
+ "restart",
42
+ "ls",
43
+ "doctor",
44
+ "status",
45
+ "inspect",
46
+ "validate",
47
+ "detect",
48
+ "config",
49
+ "help",
50
+ ]);
51
+
34
52
  // --version / -v
35
53
  if (values.version) {
36
54
  const require = createRequire(import.meta.url);
@@ -39,7 +57,10 @@ if (values.version) {
39
57
  process.exit(0);
40
58
  }
41
59
 
42
- const command = positionals[0] ?? "start";
60
+ const firstPositional = positionals[0];
61
+ const hasKnownCommand = firstPositional ? knownCommands.has(firstPositional) : false;
62
+ const command = hasKnownCommand ? firstPositional : "start";
63
+ const startTargetDir = hasKnownCommand ? positionals[1] : firstPositional;
43
64
  const json = values.json ?? false;
44
65
 
45
66
  const noColor = "NO_COLOR" in process.env;
@@ -47,6 +68,11 @@ const bold = (s) => (noColor ? s : `\x1b[1m${s}\x1b[22m`);
47
68
  const cyan = (s) => (noColor ? s : `\x1b[36m${s}\x1b[39m`);
48
69
  const dim = (s) => (noColor ? s : `\x1b[2m${s}\x1b[22m`);
49
70
 
71
+ if (values.help) {
72
+ printHelp();
73
+ process.exit(0);
74
+ }
75
+
50
76
  function printHelp() {
51
77
  console.log(`${bold("tmux-ide")} — Terminal IDE powered by tmux
52
78
 
@@ -59,6 +85,7 @@ ${bold("Usage:")}
59
85
  ${cyan("tmux-ide attach")} ${dim("Reattach to a running session")}
60
86
  ${cyan("tmux-ide ls")} ${dim("List all tmux sessions")}
61
87
  ${cyan("tmux-ide status")} [--json] ${dim("Show session status")}
88
+ ${cyan("tmux-ide inspect")} [--json] ${dim("Show effective config and runtime state")}
62
89
  ${cyan("tmux-ide doctor")} ${dim("Check system requirements")}
63
90
  ${cyan("tmux-ide validate")} [--json] ${dim("Validate ide.yml")}
64
91
  ${cyan("tmux-ide detect")} [--json] ${dim("Detect project stack")}
@@ -75,94 +102,109 @@ ${bold("Flags:")}
75
102
  ${cyan("--json")} ${dim("Output as JSON (all commands)")}
76
103
  ${cyan("--template <name>")} ${dim("Use specific template for init")}
77
104
  ${cyan("--write")} ${dim("Write detected config to ide.yml")}
105
+ ${cyan("-h, --help")} ${dim("Show usage")}
78
106
  ${cyan("-v, --version")} ${dim("Show version number")}`);
79
107
  }
80
108
 
81
- switch (command) {
82
- case "start":
83
- await launch(positionals[1]);
84
- break;
85
-
86
- case "init":
87
- await init({ template: values.template, json });
88
- break;
89
-
90
- case "stop":
91
- await stop(positionals[1], { json });
92
- break;
93
-
94
- case "attach":
95
- await attach(positionals[1], { json });
96
- break;
97
-
98
- case "restart":
99
- await restart(positionals[1]);
100
- break;
101
-
102
- case "ls":
103
- await ls({ json });
104
- break;
105
-
106
- case "doctor":
107
- await doctor({ json });
108
- break;
109
-
110
- case "status":
111
- await status(positionals[1], { json });
112
- break;
113
-
114
- case "validate":
115
- await validate(positionals[1], { json });
116
- break;
117
-
118
- case "detect":
119
- await detect(positionals[1], { json, write: values.write });
120
- break;
121
-
122
- case "config": {
123
- const sub = positionals[1]; // set, add-pane, remove-pane, add-row, or undefined (dump)
124
- let action = "dump";
125
- let configArgs = [];
126
-
127
- if (sub === "set") {
128
- action = "set";
129
- configArgs = positionals.slice(2);
130
- } else if (sub === "add-pane") {
131
- action = "add-pane";
132
- // Pass named flags as args array
133
- configArgs = [];
134
- if (values.row !== undefined) configArgs.push("--row", values.row);
135
- if (values.title !== undefined) configArgs.push("--title", values.title);
136
- if (values.command !== undefined) configArgs.push("--command", values.command);
137
- if (values.size !== undefined) configArgs.push("--size", values.size);
138
- } else if (sub === "remove-pane") {
139
- action = "remove-pane";
140
- configArgs = [];
141
- if (values.row !== undefined) configArgs.push("--row", values.row);
142
- if (values.pane !== undefined) configArgs.push("--pane", values.pane);
143
- } else if (sub === "add-row") {
144
- action = "add-row";
145
- configArgs = [];
146
- if (values.size !== undefined) configArgs.push("--size", values.size);
147
- } else if (sub === "enable-team") {
148
- action = "enable-team";
149
- configArgs = [];
150
- if (values.name !== undefined) configArgs.push("--name", values.name);
151
- } else if (sub === "disable-team") {
152
- action = "disable-team";
153
- configArgs = [];
109
+ try {
110
+ switch (command) {
111
+ case "start":
112
+ await launch(startTargetDir, { json });
113
+ break;
114
+
115
+ case "init":
116
+ await init({ template: values.template, json });
117
+ break;
118
+
119
+ case "stop":
120
+ await stop(positionals[1], { json });
121
+ break;
122
+
123
+ case "attach":
124
+ await attach(positionals[1], { json });
125
+ break;
126
+
127
+ case "restart":
128
+ await restart(positionals[1], { json });
129
+ break;
130
+
131
+ case "ls":
132
+ await ls({ json });
133
+ break;
134
+
135
+ case "doctor":
136
+ await doctor({ json });
137
+ break;
138
+
139
+ case "status":
140
+ await status(positionals[1], { json });
141
+ break;
142
+
143
+ case "inspect":
144
+ await inspect(positionals[1], { json });
145
+ break;
146
+
147
+ case "validate":
148
+ await validate(positionals[1], { json });
149
+ break;
150
+
151
+ case "detect":
152
+ await detect(positionals[1], { json, write: values.write });
153
+ break;
154
+
155
+ case "config": {
156
+ const sub = positionals[1]; // set, add-pane, remove-pane, add-row, or undefined (dump)
157
+ let action = "dump";
158
+ let configArgs = [];
159
+
160
+ if (sub === "set") {
161
+ action = "set";
162
+ configArgs = positionals.slice(2);
163
+ } else if (sub === "add-pane") {
164
+ action = "add-pane";
165
+ // Pass named flags as args array
166
+ configArgs = [];
167
+ if (values.row !== undefined) configArgs.push("--row", values.row);
168
+ if (values.title !== undefined) configArgs.push("--title", values.title);
169
+ if (values.command !== undefined) configArgs.push("--command", values.command);
170
+ if (values.size !== undefined) configArgs.push("--size", values.size);
171
+ } else if (sub === "remove-pane") {
172
+ action = "remove-pane";
173
+ configArgs = [];
174
+ if (values.row !== undefined) configArgs.push("--row", values.row);
175
+ if (values.pane !== undefined) configArgs.push("--pane", values.pane);
176
+ } else if (sub === "add-row") {
177
+ action = "add-row";
178
+ configArgs = [];
179
+ if (values.size !== undefined) configArgs.push("--size", values.size);
180
+ } else if (sub === "enable-team") {
181
+ action = "enable-team";
182
+ configArgs = [];
183
+ if (values.name !== undefined) configArgs.push("--name", values.name);
184
+ } else if (sub === "disable-team") {
185
+ action = "disable-team";
186
+ configArgs = [];
187
+ }
188
+
189
+ await config(null, { json, action, args: configArgs });
190
+ break;
154
191
  }
155
192
 
156
- await config(null, { json, action, args: configArgs });
157
- break;
158
- }
159
-
160
- case "help":
161
- printHelp();
162
- break;
193
+ case "help":
194
+ printHelp();
195
+ break;
163
196
 
164
- default:
165
- console.error(`Unknown command: ${command}`);
166
- console.error('Run "tmux-ide help" for usage.');
167
- process.exit(1);
197
+ default:
198
+ throw new CommandError(`Unknown command: ${command}\nRun "tmux-ide help" for usage.`, {
199
+ code: "USAGE",
200
+ exitCode: 1,
201
+ json,
202
+ });
203
+ }
204
+ } catch (error) {
205
+ if (error instanceof CommandError) {
206
+ printCommandError(error, { json });
207
+ } else {
208
+ throw error;
209
+ }
168
210
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Turn any project into a tmux-powered terminal IDE with a simple ide.yml",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,16 +8,29 @@
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
- "src",
11
+ "src/**/*.js",
12
+ "!src/**/*.test.js",
12
13
  "scripts",
13
14
  "skill",
14
15
  "templates"
15
16
  ],
16
17
  "scripts": {
17
18
  "dev": "node bin/cli.js",
18
- "test": "node --test 'src/**/*.test.js'",
19
+ "test": "node --test src/*.test.js src/lib/*.test.js",
20
+ "test:unit": "node --test src/cli.test.js src/config-cli.test.js src/detect.test.js src/launch.test.js src/validate.test.js src/lib/*.test.js",
21
+ "test:integration": "node --test src/integration.test.js",
22
+ "lint": "eslint .",
23
+ "lint:workspace": "turbo run lint",
24
+ "format": "prettier --write .",
25
+ "format:check": "prettier --check .",
26
+ "build:workspace": "turbo run build",
27
+ "typecheck:workspace": "turbo run typecheck",
28
+ "docs:build": "turbo run build --filter=@tmux-ide/docs",
29
+ "pack:check": "npm pack --dry-run --cache /tmp/tmux-ide-npm-cache > /dev/null",
30
+ "check": "pnpm run lint:workspace && pnpm run format:check && pnpm run test:unit && pnpm run docs:build && pnpm run pack:check",
31
+ "prepublishOnly": "pnpm run check",
19
32
  "postinstall": "node scripts/postinstall.js",
20
- "docs": "cd docs && pnpm dev"
33
+ "docs": "turbo run dev --filter=@tmux-ide/docs"
21
34
  },
22
35
  "keywords": [
23
36
  "tmux",
@@ -38,7 +51,15 @@
38
51
  "url": "https://github.com/wavyrai/tmux-ide/issues"
39
52
  },
40
53
  "license": "MIT",
54
+ "packageManager": "pnpm@10.21.0",
41
55
  "dependencies": {
42
- "js-yaml": "^4.1.0"
56
+ "js-yaml": "^4.1.1"
57
+ },
58
+ "devDependencies": {
59
+ "@eslint/js": "^10.0.1",
60
+ "eslint": "^10.0.3",
61
+ "globals": "^17.4.0",
62
+ "prettier": "^3.8.1",
63
+ "turbo": "^2.3.3"
43
64
  }
44
65
  }
@@ -1,13 +1,52 @@
1
- import { existsSync, mkdirSync, copyFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { resolve, dirname } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
 
5
5
  const claudeDir = resolve(homedir(), ".claude");
6
- if (existsSync(claudeDir)) {
7
- const skillDir = resolve(claudeDir, "skills", "tmux-ide");
8
- mkdirSync(skillDir, { recursive: true });
9
- const src = resolve(dirname(import.meta.dirname), "skill", "SKILL.md");
10
- if (existsSync(src)) {
11
- copyFileSync(src, resolve(skillDir, "SKILL.md"));
6
+ if (!shouldInstallClaudeIntegration() || !existsSync(claudeDir)) {
7
+ process.exit(0);
8
+ }
9
+
10
+ const skillDir = resolve(claudeDir, "skills", "tmux-ide");
11
+ mkdirSync(skillDir, { recursive: true });
12
+ const src = resolve(dirname(import.meta.dirname), "skill", "SKILL.md");
13
+ if (existsSync(src)) {
14
+ copyFileSync(src, resolve(skillDir, "SKILL.md"));
15
+ }
16
+
17
+ const settingsPath = resolve(claudeDir, "settings.json");
18
+ let settings = {};
19
+
20
+ if (existsSync(settingsPath)) {
21
+ try {
22
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
23
+ } catch (error) {
24
+ console.warn(
25
+ `[tmux-ide] Skipping Claude settings update: could not parse ${settingsPath}: ${error.message}`,
26
+ );
27
+ process.exit(0);
12
28
  }
13
29
  }
30
+
31
+ if (settings == null || typeof settings !== "object" || Array.isArray(settings)) {
32
+ console.warn(
33
+ `[tmux-ide] Skipping Claude settings update: ${settingsPath} does not contain a JSON object.`,
34
+ );
35
+ process.exit(0);
36
+ }
37
+
38
+ const nextSettings = {
39
+ ...settings,
40
+ env: {
41
+ ...(settings.env && typeof settings.env === "object" && !Array.isArray(settings.env)
42
+ ? settings.env
43
+ : {}),
44
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1",
45
+ },
46
+ };
47
+
48
+ writeFileSync(settingsPath, `${JSON.stringify(nextSettings, null, 2)}\n`);
49
+
50
+ function shouldInstallClaudeIntegration() {
51
+ return process.env.npm_config_global === "true";
52
+ }