start-ai-cli 0.1.1 → 0.2.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 +54 -5
- package/bin/start-ai-cli.js +511 -112
- package/package.json +48 -41
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/start-ai-cli)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
`start-ai-cli` is an npm package and command line tool for launching
|
|
7
|
+
`start-ai-cli` is an npm package and command line tool for launching selected AI coding CLIs from the same project directory. It can open Codex CLI, Claude Code, and Cursor CLI in separate terminal tabs or windows so every assistant starts in the current workspace.
|
|
8
8
|
|
|
9
|
-
Use it when you want one command to start your AI coding agents:
|
|
9
|
+
Use it when you want one command to choose and start your AI coding agents:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npx start-ai-cli
|
|
@@ -14,11 +14,11 @@ npx start-ai-cli
|
|
|
14
14
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
17
|
-
-
|
|
17
|
+
- Interactively chooses which Codex CLI, Claude Code CLI, and Cursor CLI tools to launch.
|
|
18
|
+
- Remembers your enabled tools in a global config file for the next run.
|
|
18
19
|
- Opens each available tool in its own Windows Terminal tab or macOS Terminal window.
|
|
19
20
|
- Skips missing CLIs instead of failing when at least one supported tool is available.
|
|
20
21
|
- Works as a global npm command or one-off `npx` command.
|
|
21
|
-
- Has no runtime dependencies.
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
@@ -53,7 +53,7 @@ On Windows, `start-ai-cli` uses Windows Terminal (`wt.exe`) with PowerShell. On
|
|
|
53
53
|
## Requirements
|
|
54
54
|
|
|
55
55
|
- Windows or macOS
|
|
56
|
-
- Node.js
|
|
56
|
+
- Node.js 22 or newer
|
|
57
57
|
- Windows Terminal available as `wt.exe` on Windows
|
|
58
58
|
- PowerShell 7 available as `pwsh.exe`, or Windows PowerShell, on Windows
|
|
59
59
|
- Terminal.app and `osascript` available on macOS
|
|
@@ -62,10 +62,35 @@ On Windows, `start-ai-cli` uses Windows Terminal (`wt.exe`) with PowerShell. On
|
|
|
62
62
|
## Options
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
+
start-ai-cli
|
|
66
|
+
start-ai-cli --all
|
|
67
|
+
start-ai-cli --no-interactive
|
|
65
68
|
start-ai-cli --help
|
|
66
69
|
start-ai-cli --version
|
|
67
70
|
```
|
|
68
71
|
|
|
72
|
+
By default, `start-ai-cli` opens an interactive selector. Use Up/Down to move, Space to enable or disable an available CLI, Enter to open the selected tools, or `q`/Esc to cancel. Missing CLIs are shown as unavailable and cannot be selected.
|
|
73
|
+
|
|
74
|
+
Use `--all` or `--no-interactive` in scripts and automation to launch every available CLI without prompting.
|
|
75
|
+
|
|
76
|
+
## Global Config
|
|
77
|
+
|
|
78
|
+
Interactive selections are saved to:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
~/.start-ai-cli/config.json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The config stores enabled tool ids:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"enabledClis": ["codex", "claude", "cursor"]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Delete this file to reset the saved defaults.
|
|
93
|
+
|
|
69
94
|
## Troubleshooting
|
|
70
95
|
|
|
71
96
|
### `start-ai-cli cannot start: no CLI tools found in PATH`
|
|
@@ -76,6 +101,30 @@ Install or expose at least one supported command in your shell:
|
|
|
76
101
|
- `claude` for Claude Code
|
|
77
102
|
- `agent` for Cursor CLI
|
|
78
103
|
|
|
104
|
+
### A CLI does not appear as available in the prompt
|
|
105
|
+
|
|
106
|
+
Make sure its command is installed and available in `PATH`:
|
|
107
|
+
|
|
108
|
+
- `codex` for Codex CLI
|
|
109
|
+
- `claude` for Claude Code
|
|
110
|
+
- `agent` for Cursor CLI
|
|
111
|
+
|
|
112
|
+
### `start-ai-cli cannot prompt in a non-interactive terminal`
|
|
113
|
+
|
|
114
|
+
Run with `--all` or `--no-interactive` when using CI, scripts, or shell pipelines:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
start-ai-cli --all
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `start-ai-cli cannot start: no selected CLI tools are available`
|
|
121
|
+
|
|
122
|
+
Choose at least one installed CLI in the prompt, or run `start-ai-cli --all` to launch every available tool.
|
|
123
|
+
|
|
124
|
+
### `start-ai-cli cancelled`
|
|
125
|
+
|
|
126
|
+
The interactive selector was closed with `q` or Esc. Run `start-ai-cli` again and press Enter after selecting at least one available CLI.
|
|
127
|
+
|
|
79
128
|
### Windows Terminal was not found
|
|
80
129
|
|
|
81
130
|
Install Windows Terminal and make sure `wt.exe` is available in `PATH`.
|
package/bin/start-ai-cli.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
-
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
5
6
|
import { dirname, resolve, win32 as pathWin32 } from 'node:path';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
import { Box, Text, render, useApp, useInput } from 'ink';
|
|
7
10
|
|
|
8
11
|
const COMMAND = 'start-ai-cli';
|
|
9
12
|
|
|
@@ -12,29 +15,37 @@ const HARD_REQUIREMENTS_BY_PLATFORM = {
|
|
|
12
15
|
darwin: [{ command: 'osascript', label: 'AppleScript runner (osascript)' }]
|
|
13
16
|
};
|
|
14
17
|
|
|
15
|
-
const CLI_COMMANDS = [
|
|
16
|
-
{ command: 'codex', label: 'Codex CLI (codex)', title: 'Codex' },
|
|
17
|
-
{ command: 'claude', label: 'Claude Code CLI (claude)', title: 'Claude' },
|
|
18
|
-
{ command: 'agent', label: 'Cursor CLI (agent)', title: 'Cursor' }
|
|
19
|
-
];
|
|
20
|
-
|
|
18
|
+
const CLI_COMMANDS = [
|
|
19
|
+
{ id: 'codex', command: 'codex', label: 'Codex CLI (codex)', title: 'Codex' },
|
|
20
|
+
{ id: 'claude', command: 'claude', label: 'Claude Code CLI (claude)', title: 'Claude' },
|
|
21
|
+
{ id: 'cursor', command: 'agent', label: 'Cursor CLI (agent)', title: 'Cursor' }
|
|
22
|
+
];
|
|
23
|
+
|
|
21
24
|
const DEFAULT_WINDOWS_SHELL_COMMAND = 'pwsh.exe';
|
|
25
|
+
const DEFAULT_ENABLED_CLI_IDS = CLI_COMMANDS.map(({ id }) => id);
|
|
26
|
+
const EMPTY_SELECTION_MESSAGE = 'Select at least one available CLI.';
|
|
22
27
|
|
|
23
28
|
const HELP_TEXT = `Usage:
|
|
24
29
|
${COMMAND}
|
|
30
|
+
${COMMAND} --all
|
|
25
31
|
${COMMAND} --help
|
|
26
32
|
${COMMAND} --version
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
Interactively choose which CLI tools to open in the current directory:
|
|
29
35
|
- Codex: runs "codex"
|
|
30
36
|
- Claude: runs "claude"
|
|
31
37
|
- Cursor: runs "agent"
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
|
|
39
|
+
Options:
|
|
40
|
+
--all, --no-interactive Open every available CLI without prompting
|
|
41
|
+
--help, -h Show this help
|
|
42
|
+
--version, -v Show the package version
|
|
43
|
+
|
|
44
|
+
Requirements:
|
|
45
|
+
- Windows with Windows Terminal (wt.exe) and PowerShell, or macOS with Terminal.app
|
|
46
|
+
- Codex CLI available as "codex"
|
|
47
|
+
- Claude Code CLI available as "claude"
|
|
48
|
+
- Cursor CLI available as "agent"
|
|
38
49
|
`;
|
|
39
50
|
|
|
40
51
|
export function parseArgs(args) {
|
|
@@ -46,12 +57,14 @@ export function parseArgs(args) {
|
|
|
46
57
|
return { action: 'version' };
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
const
|
|
60
|
+
const nonInteractive = args.includes('--all') || args.includes('--no-interactive');
|
|
61
|
+
const known = new Set(['--all', '--no-interactive']);
|
|
62
|
+
const unknown = args.filter((arg) => arg.startsWith('-') && !known.has(arg));
|
|
50
63
|
if (unknown.length > 0) {
|
|
51
64
|
return { action: 'error', message: `Unknown option: ${unknown.join(', ')}` };
|
|
52
65
|
}
|
|
53
66
|
|
|
54
|
-
return { action: 'open' };
|
|
67
|
+
return { action: 'open', interactive: !nonInteractive };
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
export function getPackageVersion() {
|
|
@@ -60,27 +73,350 @@ export function getPackageVersion() {
|
|
|
60
73
|
return packageJson.version;
|
|
61
74
|
}
|
|
62
75
|
|
|
63
|
-
export function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
export function getConfigPath({ homeDirectory = homedir() } = {}) {
|
|
77
|
+
return resolve(homeDirectory, '.start-ai-cli', 'config.json');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function normalizeEnabledCliIds(enabledCliIds, cliCommands = CLI_COMMANDS) {
|
|
81
|
+
const validIds = new Set(cliCommands.map(({ id }) => id));
|
|
82
|
+
if (!Array.isArray(enabledCliIds)) {
|
|
83
|
+
return cliCommands.map(({ id }) => id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const normalized = [];
|
|
87
|
+
for (const id of enabledCliIds) {
|
|
88
|
+
if (typeof id === 'string' && validIds.has(id) && !normalized.includes(id)) {
|
|
89
|
+
normalized.push(id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return normalized;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function readConfig({
|
|
97
|
+
configPath = getConfigPath(),
|
|
98
|
+
readFileFn = readFileSync
|
|
99
|
+
} = {}) {
|
|
100
|
+
try {
|
|
101
|
+
const config = JSON.parse(readFileFn(configPath, 'utf8'));
|
|
102
|
+
return {
|
|
103
|
+
enabledClis: normalizeEnabledCliIds(config.enabledClis)
|
|
104
|
+
};
|
|
105
|
+
} catch {
|
|
106
|
+
return {
|
|
107
|
+
enabledClis: [...DEFAULT_ENABLED_CLI_IDS]
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function writeConfig({
|
|
113
|
+
configPath = getConfigPath(),
|
|
114
|
+
enabledClis,
|
|
115
|
+
mkdirFn = mkdirSync,
|
|
116
|
+
writeFileFn = writeFileSync
|
|
117
|
+
} = {}) {
|
|
118
|
+
const config = {
|
|
119
|
+
enabledClis: normalizeEnabledCliIds(enabledClis)
|
|
120
|
+
};
|
|
121
|
+
mkdirFn(dirname(configPath), { recursive: true });
|
|
122
|
+
writeFileFn(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
123
|
+
return config;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function inspectCliCommands({
|
|
127
|
+
env = process.env,
|
|
128
|
+
platform = process.platform,
|
|
129
|
+
commandExistsFn = commandExists,
|
|
130
|
+
cliCommands = CLI_COMMANDS
|
|
131
|
+
} = {}) {
|
|
132
|
+
return cliCommands.map((cli) => ({
|
|
133
|
+
...cli,
|
|
134
|
+
available: commandExistsFn(cli.command, { env, platform })
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getAvailableTabsFromInspection(inspectedClis) {
|
|
139
|
+
return inspectedClis.filter(({ available }) => available);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getMissingLabelsFromInspection(inspectedClis) {
|
|
143
|
+
return inspectedClis
|
|
144
|
+
.filter(({ available }) => !available)
|
|
145
|
+
.map(({ label }) => label);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function filterTabsByEnabledCliIds(tabs, enabledCliIds) {
|
|
149
|
+
const enabled = new Set(normalizeEnabledCliIds(enabledCliIds));
|
|
150
|
+
return tabs.filter(({ id }) => enabled.has(id));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function isInteractiveTerminal({
|
|
154
|
+
input = process.stdin,
|
|
155
|
+
output = process.stdout
|
|
156
|
+
} = {}) {
|
|
157
|
+
return Boolean(input.isTTY && output.isTTY);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class CliSelectionCancelledError extends Error {
|
|
161
|
+
constructor() {
|
|
162
|
+
super('CLI selection cancelled.');
|
|
163
|
+
this.name = 'CliSelectionCancelledError';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getFirstAvailableCliIndex(inspectedClis) {
|
|
168
|
+
const index = inspectedClis.findIndex(({ available }) => available);
|
|
169
|
+
return index === -1 ? 0 : index;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getAvailableCliIds(inspectedClis) {
|
|
173
|
+
return new Set(inspectedClis.filter(({ available }) => available).map(({ id }) => id));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function orderSelectedIds(selectedIds, inspectedClis) {
|
|
177
|
+
const selected = new Set(selectedIds);
|
|
178
|
+
return inspectedClis.filter(({ id }) => selected.has(id)).map(({ id }) => id);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function createCliSelectionState({
|
|
182
|
+
inspectedClis,
|
|
183
|
+
defaultEnabledCliIds
|
|
184
|
+
}) {
|
|
185
|
+
const availableIds = getAvailableCliIds(inspectedClis);
|
|
186
|
+
return {
|
|
187
|
+
cursorIndex: getFirstAvailableCliIndex(inspectedClis),
|
|
188
|
+
selectedIds: normalizeEnabledCliIds(defaultEnabledCliIds, inspectedClis)
|
|
189
|
+
.filter((id) => availableIds.has(id)),
|
|
190
|
+
errorMessage: null
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function moveCliSelectionCursor({
|
|
195
|
+
state,
|
|
196
|
+
inspectedClis,
|
|
197
|
+
direction
|
|
198
|
+
}) {
|
|
199
|
+
if (inspectedClis.every(({ available }) => !available)) {
|
|
200
|
+
return state;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let cursorIndex = state.cursorIndex;
|
|
204
|
+
for (let i = 0; i < inspectedClis.length; i++) {
|
|
205
|
+
cursorIndex = (cursorIndex + direction + inspectedClis.length) % inspectedClis.length;
|
|
206
|
+
if (inspectedClis[cursorIndex].available) {
|
|
207
|
+
return {
|
|
208
|
+
...state,
|
|
209
|
+
cursorIndex,
|
|
210
|
+
errorMessage: null
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return state;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function toggleCliSelectionAtCursor({
|
|
219
|
+
state,
|
|
220
|
+
inspectedClis
|
|
221
|
+
}) {
|
|
222
|
+
const cli = inspectedClis[state.cursorIndex];
|
|
223
|
+
if (!cli?.available) {
|
|
224
|
+
return state;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const selected = new Set(state.selectedIds);
|
|
228
|
+
if (selected.has(cli.id)) {
|
|
229
|
+
selected.delete(cli.id);
|
|
230
|
+
} else {
|
|
231
|
+
selected.add(cli.id);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
...state,
|
|
236
|
+
selectedIds: orderSelectedIds(selected, inspectedClis),
|
|
237
|
+
errorMessage: null
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function confirmCliSelection({
|
|
242
|
+
state,
|
|
243
|
+
inspectedClis
|
|
244
|
+
}) {
|
|
245
|
+
const availableIds = getAvailableCliIds(inspectedClis);
|
|
246
|
+
const selectedIds = orderSelectedIds(
|
|
247
|
+
state.selectedIds.filter((id) => availableIds.has(id)),
|
|
248
|
+
inspectedClis
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
if (selectedIds.length === 0) {
|
|
252
|
+
return {
|
|
253
|
+
confirmed: false,
|
|
254
|
+
state: {
|
|
255
|
+
...state,
|
|
256
|
+
selectedIds,
|
|
257
|
+
errorMessage: EMPTY_SELECTION_MESSAGE
|
|
258
|
+
}
|
|
259
|
+
};
|
|
80
260
|
}
|
|
81
|
-
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
confirmed: true,
|
|
264
|
+
selectedIds
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function CliSelectionPrompt({
|
|
269
|
+
inspectedClis,
|
|
270
|
+
defaultEnabledCliIds,
|
|
271
|
+
onSubmit,
|
|
272
|
+
onCancel
|
|
273
|
+
}) {
|
|
274
|
+
const { exit } = useApp();
|
|
275
|
+
const [state, setState] = useState(() => createCliSelectionState({
|
|
276
|
+
inspectedClis,
|
|
277
|
+
defaultEnabledCliIds
|
|
278
|
+
}));
|
|
279
|
+
|
|
280
|
+
useInput((input, key) => {
|
|
281
|
+
if (key.upArrow) {
|
|
282
|
+
setState((current) => moveCliSelectionCursor({
|
|
283
|
+
state: current,
|
|
284
|
+
inspectedClis,
|
|
285
|
+
direction: -1
|
|
286
|
+
}));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (key.downArrow) {
|
|
291
|
+
setState((current) => moveCliSelectionCursor({
|
|
292
|
+
state: current,
|
|
293
|
+
inspectedClis,
|
|
294
|
+
direction: 1
|
|
295
|
+
}));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (input === ' ') {
|
|
300
|
+
setState((current) => toggleCliSelectionAtCursor({
|
|
301
|
+
state: current,
|
|
302
|
+
inspectedClis
|
|
303
|
+
}));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (key.return) {
|
|
308
|
+
const result = confirmCliSelection({
|
|
309
|
+
state,
|
|
310
|
+
inspectedClis
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (result.confirmed) {
|
|
314
|
+
onSubmit(result.selectedIds);
|
|
315
|
+
exit();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
setState(result.state);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (input === 'q' || key.escape) {
|
|
324
|
+
onCancel();
|
|
325
|
+
exit();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const selected = new Set(state.selectedIds);
|
|
330
|
+
return React.createElement(
|
|
331
|
+
Box,
|
|
332
|
+
{ flexDirection: 'column' },
|
|
333
|
+
React.createElement(Text, { bold: true }, 'Select CLI tools to launch'),
|
|
334
|
+
...inspectedClis.map((cli, index) => {
|
|
335
|
+
const focused = index === state.cursorIndex;
|
|
336
|
+
const marker = focused ? '>' : ' ';
|
|
337
|
+
const checked = selected.has(cli.id) ? '[x]' : '[ ]';
|
|
338
|
+
const availability = cli.available ? 'available' : 'not found in PATH';
|
|
339
|
+
const color = cli.available && focused ? 'cyan' : undefined;
|
|
340
|
+
|
|
341
|
+
return React.createElement(
|
|
342
|
+
Text,
|
|
343
|
+
{
|
|
344
|
+
key: cli.id,
|
|
345
|
+
color,
|
|
346
|
+
dimColor: !cli.available
|
|
347
|
+
},
|
|
348
|
+
`${marker} ${checked} ${cli.label} - ${availability}`
|
|
349
|
+
);
|
|
350
|
+
}),
|
|
351
|
+
state.errorMessage
|
|
352
|
+
? React.createElement(Text, { color: 'red' }, state.errorMessage)
|
|
353
|
+
: null,
|
|
354
|
+
React.createElement(
|
|
355
|
+
Text,
|
|
356
|
+
{ dimColor: true },
|
|
357
|
+
'Use Up/Down to move, Space to toggle, Enter to open, q/Esc to cancel.'
|
|
358
|
+
)
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function promptForEnabledCliIds({
|
|
363
|
+
input = process.stdin,
|
|
364
|
+
output = process.stdout,
|
|
365
|
+
inspectedClis,
|
|
366
|
+
defaultEnabledCliIds,
|
|
367
|
+
renderFn = render
|
|
368
|
+
}) {
|
|
369
|
+
return new Promise((resolve, reject) => {
|
|
370
|
+
let app;
|
|
371
|
+
let settled = false;
|
|
372
|
+
const settle = (callback, value) => {
|
|
373
|
+
if (settled) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
settled = true;
|
|
378
|
+
callback(value);
|
|
379
|
+
app?.unmount();
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
app = renderFn(
|
|
383
|
+
React.createElement(CliSelectionPrompt, {
|
|
384
|
+
inspectedClis,
|
|
385
|
+
defaultEnabledCliIds,
|
|
386
|
+
onSubmit: (selectedIds) => settle(resolve, selectedIds),
|
|
387
|
+
onCancel: () => settle(reject, new CliSelectionCancelledError())
|
|
388
|
+
}),
|
|
389
|
+
{
|
|
390
|
+
stdin: input,
|
|
391
|
+
stdout: output,
|
|
392
|
+
stderr: output,
|
|
393
|
+
exitOnCtrlC: false
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
});
|
|
82
397
|
}
|
|
83
398
|
|
|
399
|
+
export function buildWtArgs({ cwd, tabs, shellCommand = DEFAULT_WINDOWS_SHELL_COMMAND }) {
|
|
400
|
+
const result = [];
|
|
401
|
+
for (let i = 0; i < tabs.length; i++) {
|
|
402
|
+
if (i > 0) result.push(';');
|
|
403
|
+
result.push(
|
|
404
|
+
'new-tab',
|
|
405
|
+
'--title',
|
|
406
|
+
tabs[i].title,
|
|
407
|
+
'-d',
|
|
408
|
+
cwd,
|
|
409
|
+
shellCommand,
|
|
410
|
+
'-NoExit',
|
|
411
|
+
'-ExecutionPolicy',
|
|
412
|
+
'Bypass',
|
|
413
|
+
'-Command',
|
|
414
|
+
tabs[i].command
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
|
|
84
420
|
function shellQuote(value) {
|
|
85
421
|
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
86
422
|
}
|
|
@@ -108,7 +444,7 @@ export function buildMacTerminalArgs({ cwd, tabs }) {
|
|
|
108
444
|
return ['-e', buildMacTerminalScript({ cwd, tabs })];
|
|
109
445
|
}
|
|
110
446
|
|
|
111
|
-
export function commandExists(command, { env = process.env, platform = process.platform } = {}) {
|
|
447
|
+
export function commandExists(command, { env = process.env, platform = process.platform } = {}) {
|
|
112
448
|
const executable = platform === 'win32' ? 'where.exe' : 'sh';
|
|
113
449
|
const args = platform === 'win32'
|
|
114
450
|
? [command]
|
|
@@ -119,82 +455,94 @@ export function commandExists(command, { env = process.env, platform = process.p
|
|
|
119
455
|
stdio: 'ignore',
|
|
120
456
|
windowsHide: true
|
|
121
457
|
});
|
|
122
|
-
|
|
123
|
-
return result.status === 0;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function getSystemWindowsPowerShellPath(env = process.env) {
|
|
127
|
-
const systemRoot = env.SystemRoot ?? env.SYSTEMROOT ?? env.windir ?? env.WINDIR;
|
|
128
|
-
if (!systemRoot) {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return pathWin32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function getWindowsShellCommand({
|
|
136
|
-
env = process.env,
|
|
137
|
-
platform = process.platform,
|
|
138
|
-
commandExistsFn = commandExists,
|
|
139
|
-
fileExistsFn = existsSync
|
|
140
|
-
} = {}) {
|
|
141
|
-
if (platform !== 'win32') {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (commandExistsFn('pwsh.exe', { env, platform })) {
|
|
146
|
-
return 'pwsh.exe';
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (commandExistsFn('powershell.exe', { env, platform })) {
|
|
150
|
-
return 'powershell.exe';
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const systemPowerShellPath = getSystemWindowsPowerShellPath(env);
|
|
154
|
-
if (systemPowerShellPath && fileExistsFn(systemPowerShellPath)) {
|
|
155
|
-
return systemPowerShellPath;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function getMissingRequirements({ platform = process.platform, env = process.env } = {}) {
|
|
162
|
-
const requirements = HARD_REQUIREMENTS_BY_PLATFORM[platform];
|
|
163
|
-
if (!requirements) {
|
|
164
|
-
return ['Windows or macOS is required.'];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const missing = requirements
|
|
168
|
-
.filter(({ command }) => !commandExists(command, { env, platform }))
|
|
169
|
-
.map(({ label }) => `${label} was not found in PATH.`);
|
|
170
|
-
|
|
171
|
-
if (platform === 'win32' && !getWindowsShellCommand({ env, platform })) {
|
|
172
|
-
missing.push('PowerShell (pwsh.exe or powershell.exe) was not found.');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return missing;
|
|
176
|
-
}
|
|
177
458
|
|
|
178
|
-
|
|
179
|
-
|
|
459
|
+
return result.status === 0;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function getSystemWindowsPowerShellPath(env = process.env) {
|
|
463
|
+
const systemRoot = env.SystemRoot ?? env.SYSTEMROOT ?? env.windir ?? env.WINDIR;
|
|
464
|
+
if (!systemRoot) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return pathWin32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
180
469
|
}
|
|
181
470
|
|
|
182
|
-
export function
|
|
471
|
+
export function getWindowsShellCommand({
|
|
472
|
+
env = process.env,
|
|
473
|
+
platform = process.platform,
|
|
474
|
+
commandExistsFn = commandExists,
|
|
475
|
+
fileExistsFn = existsSync
|
|
476
|
+
} = {}) {
|
|
477
|
+
if (platform !== 'win32') {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (commandExistsFn('pwsh.exe', { env, platform })) {
|
|
482
|
+
return 'pwsh.exe';
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (commandExistsFn('powershell.exe', { env, platform })) {
|
|
486
|
+
return 'powershell.exe';
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const systemPowerShellPath = getSystemWindowsPowerShellPath(env);
|
|
490
|
+
if (systemPowerShellPath && fileExistsFn(systemPowerShellPath)) {
|
|
491
|
+
return systemPowerShellPath;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function getMissingRequirements({
|
|
498
|
+
platform = process.platform,
|
|
499
|
+
env = process.env,
|
|
500
|
+
commandExistsFn = commandExists
|
|
501
|
+
} = {}) {
|
|
502
|
+
const requirements = HARD_REQUIREMENTS_BY_PLATFORM[platform];
|
|
503
|
+
if (!requirements) {
|
|
504
|
+
return ['Windows or macOS is required.'];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const missing = requirements
|
|
508
|
+
.filter(({ command }) => !commandExistsFn(command, { env, platform }))
|
|
509
|
+
.map(({ label }) => `${label} was not found in PATH.`);
|
|
510
|
+
|
|
511
|
+
if (platform === 'win32' && !getWindowsShellCommand({ env, platform, commandExistsFn })) {
|
|
512
|
+
missing.push('PowerShell (pwsh.exe or powershell.exe) was not found.');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return missing;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export function getAvailableCliTabs({
|
|
519
|
+
env = process.env,
|
|
520
|
+
platform = process.platform,
|
|
521
|
+
commandExistsFn = commandExists
|
|
522
|
+
} = {}) {
|
|
523
|
+
return CLI_COMMANDS.filter(({ command }) => commandExistsFn(command, { env, platform }));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function getMissingCliLabels({
|
|
527
|
+
env = process.env,
|
|
528
|
+
platform = process.platform,
|
|
529
|
+
commandExistsFn = commandExists
|
|
530
|
+
} = {}) {
|
|
183
531
|
return CLI_COMMANDS
|
|
184
|
-
.filter(({ command }) => !
|
|
532
|
+
.filter(({ command }) => !commandExistsFn(command, { env, platform }))
|
|
185
533
|
.map(({ label }) => label);
|
|
186
534
|
}
|
|
187
535
|
|
|
188
|
-
export function launchTerminals({ cwd = process.cwd(), env = process.env, platform = process.platform, tabs = CLI_COMMANDS } = {}) {
|
|
189
|
-
const executable = platform === 'win32' ? 'wt.exe' : 'osascript';
|
|
190
|
-
const shellCommand = platform === 'win32' ? getWindowsShellCommand({ env, platform }) : undefined;
|
|
191
|
-
if (platform === 'win32' && !shellCommand) {
|
|
192
|
-
throw new Error('PowerShell (pwsh.exe or powershell.exe) was not found.');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const args = platform === 'win32'
|
|
196
|
-
? buildWtArgs({ cwd, tabs, shellCommand })
|
|
197
|
-
: buildMacTerminalArgs({ cwd, tabs });
|
|
536
|
+
export function launchTerminals({ cwd = process.cwd(), env = process.env, platform = process.platform, tabs = CLI_COMMANDS } = {}) {
|
|
537
|
+
const executable = platform === 'win32' ? 'wt.exe' : 'osascript';
|
|
538
|
+
const shellCommand = platform === 'win32' ? getWindowsShellCommand({ env, platform }) : undefined;
|
|
539
|
+
if (platform === 'win32' && !shellCommand) {
|
|
540
|
+
throw new Error('PowerShell (pwsh.exe or powershell.exe) was not found.');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const args = platform === 'win32'
|
|
544
|
+
? buildWtArgs({ cwd, tabs, shellCommand })
|
|
545
|
+
: buildMacTerminalArgs({ cwd, tabs });
|
|
198
546
|
|
|
199
547
|
const child = spawn(executable, args, {
|
|
200
548
|
cwd,
|
|
@@ -207,7 +555,7 @@ export function launchTerminals({ cwd = process.cwd(), env = process.env, platfo
|
|
|
207
555
|
child.unref();
|
|
208
556
|
}
|
|
209
557
|
|
|
210
|
-
export function main(args = process.argv.slice(2), options = {}) {
|
|
558
|
+
export async function main(args = process.argv.slice(2), options = {}) {
|
|
211
559
|
const parsed = parseArgs(args);
|
|
212
560
|
|
|
213
561
|
if (parsed.action === 'help') {
|
|
@@ -235,19 +583,63 @@ export function main(args = process.argv.slice(2), options = {}) {
|
|
|
235
583
|
return 1;
|
|
236
584
|
}
|
|
237
585
|
|
|
238
|
-
const
|
|
586
|
+
const inspectedClis = inspectCliCommands(options);
|
|
587
|
+
const missingClis = getMissingLabelsFromInspection(inspectedClis);
|
|
239
588
|
for (const label of missingClis) {
|
|
240
589
|
console.warn(`Warning: ${label} was not found in PATH, skipping.`);
|
|
241
590
|
}
|
|
242
591
|
|
|
243
|
-
const availableTabs =
|
|
592
|
+
const availableTabs = getAvailableTabsFromInspection(inspectedClis);
|
|
244
593
|
if (availableTabs.length === 0) {
|
|
245
594
|
console.error(`${COMMAND} cannot start: no CLI tools found in PATH.`);
|
|
246
595
|
return 1;
|
|
247
596
|
}
|
|
248
597
|
|
|
249
|
-
|
|
250
|
-
|
|
598
|
+
let tabsToLaunch = availableTabs;
|
|
599
|
+
if (parsed.interactive) {
|
|
600
|
+
const input = options.input ?? process.stdin;
|
|
601
|
+
const output = options.output ?? process.stdout;
|
|
602
|
+
if (!isInteractiveTerminal({ input, output })) {
|
|
603
|
+
console.error(`${COMMAND} cannot prompt in a non-interactive terminal.`);
|
|
604
|
+
console.error(`Run "${COMMAND} --all" to open every available CLI without prompting.`);
|
|
605
|
+
return 1;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const config = readConfig(options);
|
|
609
|
+
let enabledClis;
|
|
610
|
+
try {
|
|
611
|
+
enabledClis = await promptForEnabledCliIds({
|
|
612
|
+
input,
|
|
613
|
+
output,
|
|
614
|
+
inspectedClis,
|
|
615
|
+
defaultEnabledCliIds: config.enabledClis,
|
|
616
|
+
renderFn: options.renderFn
|
|
617
|
+
});
|
|
618
|
+
} catch (error) {
|
|
619
|
+
if (error instanceof CliSelectionCancelledError) {
|
|
620
|
+
console.error(`${COMMAND} cancelled.`);
|
|
621
|
+
return 1;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
try {
|
|
628
|
+
writeConfig({ ...options, enabledClis });
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.warn(`Warning: could not save CLI selection: ${error.message}`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
tabsToLaunch = filterTabsByEnabledCliIds(availableTabs, enabledClis);
|
|
634
|
+
if (tabsToLaunch.length === 0) {
|
|
635
|
+
console.error(`${COMMAND} cannot start: no selected CLI tools are available.`);
|
|
636
|
+
return 1;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const launchTerminalsFn = options.launchTerminalsFn ?? launchTerminals;
|
|
641
|
+
launchTerminalsFn({ ...options, tabs: tabsToLaunch });
|
|
642
|
+
const launched = tabsToLaunch.map(({ title }) => title).join(', ');
|
|
251
643
|
const terminalName = (options.platform ?? process.platform) === 'win32' ? 'Windows Terminal' : 'Terminal.app';
|
|
252
644
|
console.log(`Opened ${launched} in ${terminalName}.`);
|
|
253
645
|
return 0;
|
|
@@ -265,5 +657,12 @@ export function isEntrypoint(argvPath = process.argv[1], moduleUrl = import.meta
|
|
|
265
657
|
}
|
|
266
658
|
|
|
267
659
|
if (isEntrypoint()) {
|
|
268
|
-
|
|
660
|
+
main()
|
|
661
|
+
.then((exitCode) => {
|
|
662
|
+
process.exitCode = exitCode;
|
|
663
|
+
})
|
|
664
|
+
.catch((error) => {
|
|
665
|
+
console.error(error.message);
|
|
666
|
+
process.exitCode = 1;
|
|
667
|
+
});
|
|
269
668
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "start-ai-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Launch Codex CLI, Claude Code, and Cursor CLI
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "bin/start-ai-cli.js",
|
|
7
|
-
"exports": "./bin/start-ai-cli.js",
|
|
8
|
-
"bin": {
|
|
9
|
-
"start-ai-cli": "bin/start-ai-cli.js"
|
|
10
|
-
},
|
|
2
|
+
"name": "start-ai-cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Launch selected Codex CLI, Claude Code, and Cursor CLI tools from one project directory in Windows Terminal or macOS Terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "bin/start-ai-cli.js",
|
|
7
|
+
"exports": "./bin/start-ai-cli.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"start-ai-cli": "bin/start-ai-cli.js"
|
|
10
|
+
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "node --test",
|
|
13
13
|
"pack:dry-run": "npm pack --dry-run",
|
|
@@ -18,37 +18,37 @@
|
|
|
18
18
|
"README.md",
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
|
-
"keywords": [
|
|
22
|
-
"start-ai-cli",
|
|
23
|
-
"open-ai-cli",
|
|
24
|
-
"ai-cli",
|
|
25
|
-
"ai-agent",
|
|
26
|
-
"ai-coding",
|
|
27
|
-
"ai-coding-agent",
|
|
28
|
-
"coding-agent",
|
|
29
|
-
"codex",
|
|
30
|
-
"codex-cli",
|
|
31
|
-
"openai-codex",
|
|
32
|
-
"claude",
|
|
33
|
-
"claude-code",
|
|
34
|
-
"claude-code-cli",
|
|
35
|
-
"cursor",
|
|
36
|
-
"cursor-cli",
|
|
37
|
-
"cursor-agent",
|
|
38
|
-
"cli",
|
|
39
|
-
"cli-launcher",
|
|
40
|
-
"developer-tools",
|
|
41
|
-
"terminal",
|
|
42
|
-
"terminal-tabs",
|
|
43
|
-
"windows-terminal",
|
|
44
|
-
"macos",
|
|
45
|
-
"macos-terminal",
|
|
46
|
-
"productivity"
|
|
47
|
-
],
|
|
48
|
-
"author": {
|
|
49
|
-
"name": "guoxiao0521",
|
|
50
|
-
"url": "https://github.com/guoxiao0521"
|
|
51
|
-
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"start-ai-cli",
|
|
23
|
+
"open-ai-cli",
|
|
24
|
+
"ai-cli",
|
|
25
|
+
"ai-agent",
|
|
26
|
+
"ai-coding",
|
|
27
|
+
"ai-coding-agent",
|
|
28
|
+
"coding-agent",
|
|
29
|
+
"codex",
|
|
30
|
+
"codex-cli",
|
|
31
|
+
"openai-codex",
|
|
32
|
+
"claude",
|
|
33
|
+
"claude-code",
|
|
34
|
+
"claude-code-cli",
|
|
35
|
+
"cursor",
|
|
36
|
+
"cursor-cli",
|
|
37
|
+
"cursor-agent",
|
|
38
|
+
"cli",
|
|
39
|
+
"cli-launcher",
|
|
40
|
+
"developer-tools",
|
|
41
|
+
"terminal",
|
|
42
|
+
"terminal-tabs",
|
|
43
|
+
"windows-terminal",
|
|
44
|
+
"macos",
|
|
45
|
+
"macos-terminal",
|
|
46
|
+
"productivity"
|
|
47
|
+
],
|
|
48
|
+
"author": {
|
|
49
|
+
"name": "guoxiao0521",
|
|
50
|
+
"url": "https://github.com/guoxiao0521"
|
|
51
|
+
},
|
|
52
52
|
"license": "MIT",
|
|
53
53
|
"repository": {
|
|
54
54
|
"type": "git",
|
|
@@ -62,8 +62,15 @@
|
|
|
62
62
|
"win32",
|
|
63
63
|
"darwin"
|
|
64
64
|
],
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"ink": "^7.0.6",
|
|
67
|
+
"react": "^19.2.7"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"ink-testing-library": "^4.0.0"
|
|
71
|
+
},
|
|
65
72
|
"engines": {
|
|
66
|
-
"node": ">=
|
|
73
|
+
"node": ">=22"
|
|
67
74
|
},
|
|
68
75
|
"publishConfig": {
|
|
69
76
|
"registry": "https://registry.npmjs.org/"
|