summon-cli 0.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 ADDED
@@ -0,0 +1,47 @@
1
+ # summon-cli
2
+
3
+ <p align="center">
4
+ <img src="assets/screenshot.png" alt="summon-cli picker" width="720">
5
+ </p>
6
+
7
+ Terminal launcher for local AI CLIs. Run `summon`, pick a tool, launch it.
8
+
9
+ Supported: Codex CLI, Claude Code, Antigravity CLI, Cursor CLI, GitHub Copilot CLI, opencode CLI. Missing tools show dimmed.
10
+
11
+ > Pre-release (0.1.0).
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm install -g summon-cli
17
+ summon
18
+ ```
19
+
20
+ Move with arrows or `j/k` or `1-9`. `Enter` launches, `Esc` quits.
21
+
22
+ ## Commands
23
+
24
+ - `summon` open the picker (or your default)
25
+ - `summon menu` always open the picker
26
+ - `summon reorder` set the order
27
+ - `summon default <tool>` launch one directly (`off` clears, no arg = pick)
28
+ - `summon alias <name>` add another command name (e.g. `summon alias cli`)
29
+ - `summon help`
30
+
31
+ Flag: `--no-logo`. Args after `--` go to the launched tool.
32
+
33
+ ## Config
34
+
35
+ `~/.config/summon-cli/config.json`
36
+
37
+ ## Requirements
38
+
39
+ Node 18+, a TrueColor terminal, the target CLIs on PATH.
40
+
41
+ ## Trademarks
42
+
43
+ Unofficial, not affiliated with Codex CLI, Claude Code, Antigravity CLI, Cursor CLI, GitHub Copilot CLI, opencode CLI or their makers. Names, logos, colors belong to their owners and are used only to identify what you launch. Open an issue to request changes.
44
+
45
+ ## License
46
+
47
+ GPL-3.0-only. See [LICENSE](LICENSE). Copyright (c) 2026 sk1gl4a.
package/bin/cli.mjs ADDED
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {spawn, spawnSync} from 'node:child_process';
4
+ import process from 'node:process';
5
+ import fs from 'node:fs';
6
+ import {homedir} from 'node:os';
7
+ import {join} from 'node:path';
8
+ import {fileURLToPath} from 'node:url';
9
+ import React from 'react';
10
+ import {render} from 'ink';
11
+ import {App, ReorderApp, renderSnapshot, tools, orderTools} from '../src/app.mjs';
12
+ import {loadConfig, saveConfig, configLocation} from '../src/config.mjs';
13
+
14
+ const PROG = process.env.CLI_LEVEL_NAME || 'summon';
15
+ const SCRIPT = fileURLToPath(import.meta.url);
16
+
17
+ const passthroughIndex = process.argv.indexOf('--');
18
+ const forwardedArgs = passthroughIndex === -1 ? [] : process.argv.slice(passthroughIndex + 1);
19
+ const rawArgs = passthroughIndex === -1 ? process.argv.slice(2) : process.argv.slice(2, passthroughIndex);
20
+
21
+ const flags = new Set(rawArgs.filter(arg => arg.startsWith('-')));
22
+ const args = rawArgs.filter(arg => !arg.startsWith('-'));
23
+ const command = args[0];
24
+
25
+ const config = loadConfig();
26
+ const logo = flags.has('--no-logo') ? false : (flags.has('--logo') || config.logo);
27
+ const items = orderTools(config.order);
28
+
29
+ if (process.env.CLI_LEVEL_SNAPSHOT === '1') {
30
+ process.stdout.write(renderSnapshot(Number(process.env.CLI_LEVEL_ACTIVE || 0)));
31
+ process.exit(0);
32
+ }
33
+
34
+ if (flags.has('--help') || flags.has('-h') || command === 'help') {
35
+ printHelp();
36
+ process.exit(0);
37
+ }
38
+
39
+ const canRenderTui = Boolean(process.stdin.isTTY && process.stdout.isTTY && process.env.TERM !== 'dumb');
40
+
41
+ switch (command) {
42
+ case 'reorder':
43
+ await runReorder();
44
+ break;
45
+ case 'default':
46
+ await runDefault(args[1]);
47
+ break;
48
+ case 'alias':
49
+ runAlias(args[1]);
50
+ break;
51
+ case 'menu':
52
+ await runMenu({forceMenu: true});
53
+ break;
54
+ case undefined:
55
+ await runMenu({forceMenu: false});
56
+ break;
57
+ default:
58
+ process.stderr.write(`${PROG}: unknown command '${command}'. Try '${PROG} --help'.\n`);
59
+ process.exit(2);
60
+ }
61
+
62
+ async function runMenu({forceMenu}) {
63
+ if (!forceMenu && config.default) {
64
+ const tool = tools.find(item => item.id === config.default);
65
+ if (tool && commandExists(tool.command)) {
66
+ process.exitCode = await runCommand(tool.command, forwardedArgs);
67
+ return;
68
+ }
69
+ process.stderr.write(`${PROG}: default '${config.default}' unavailable, opening menu.\n`);
70
+ }
71
+
72
+ if (!canRenderTui) {
73
+ process.stdout.write(renderSnapshot(0));
74
+ process.stderr.write(`${PROG}: interactive terminal required for selection.\n`);
75
+ process.exit(2);
76
+ }
77
+
78
+ clearScreen();
79
+ const selected = await chooseTool();
80
+ if (!selected) {
81
+ process.exit(0);
82
+ }
83
+
84
+ clearScreen();
85
+ process.exitCode = await runCommand(selected.command, forwardedArgs);
86
+ }
87
+
88
+ async function runReorder() {
89
+ requireTui('reorder');
90
+ clearScreen();
91
+ const order = await new Promise(resolve => {
92
+ let instance;
93
+ instance = render(React.createElement(ReorderApp, {
94
+ onCancel: () => {
95
+ instance.unmount();
96
+ resolve(null);
97
+ },
98
+ onDone: result => {
99
+ instance.unmount();
100
+ resolve(result);
101
+ }
102
+ }));
103
+ });
104
+
105
+ if (!order) {
106
+ process.stdout.write('Reorder cancelled.\n');
107
+ return;
108
+ }
109
+
110
+ saveConfig({order});
111
+ process.stdout.write(`Saved order: ${order.join(' › ')}\n`);
112
+ }
113
+
114
+ async function runDefault(target) {
115
+ if (target === 'off' || target === 'none') {
116
+ saveConfig({default: null});
117
+ process.stdout.write(`Default cleared. '${PROG}' now opens the menu.\n`);
118
+ return;
119
+ }
120
+
121
+ if (target) {
122
+ const tool = tools.find(item => item.id === target || item.command === target);
123
+ if (!tool) {
124
+ process.stderr.write(`${PROG}: no tool '${target}'. Options: ${tools.map(t => t.id).join(', ')}.\n`);
125
+ process.exit(2);
126
+ }
127
+ saveConfig({default: tool.id});
128
+ process.stdout.write(`Default set to ${tool.label}. '${PROG}' now launches it directly (use '${PROG} menu' for the picker).\n`);
129
+ return;
130
+ }
131
+
132
+ requireTui('default');
133
+ clearScreen();
134
+ const selected = await chooseTool();
135
+ if (!selected) {
136
+ process.stdout.write('No change.\n');
137
+ return;
138
+ }
139
+ saveConfig({default: selected.id});
140
+ process.stdout.write(`Default set to ${selected.label}. '${PROG}' now launches it directly (use '${PROG} menu' for the picker).\n`);
141
+ }
142
+
143
+ function runAlias(name) {
144
+ if (!name || !/^[a-zA-Z0-9._-]+$/.test(name)) {
145
+ process.stderr.write(`${PROG}: usage: ${PROG} alias <name>\n`);
146
+ process.exit(2);
147
+ }
148
+
149
+ const binDir = join(homedir(), '.local', 'bin');
150
+ const target = join(binDir, name);
151
+ const shim = `#!/usr/bin/env sh\nCLI_LEVEL_NAME=${name} exec ${process.execPath} ${SCRIPT} "$@"\n`;
152
+
153
+ fs.mkdirSync(binDir, {recursive: true});
154
+ fs.writeFileSync(target, shim);
155
+ fs.chmodSync(target, 0o755);
156
+ process.stdout.write(`Created '${name}' at ${target}.\n`);
157
+ if (!`${process.env.PATH}`.split(':').includes(binDir)) {
158
+ process.stdout.write(`Note: ${binDir} is not on your PATH yet.\n`);
159
+ }
160
+ }
161
+
162
+ function chooseTool() {
163
+ return new Promise(resolve => {
164
+ let instance;
165
+ instance = render(React.createElement(App, {
166
+ items,
167
+ logo,
168
+ onCancel: () => {
169
+ instance.unmount();
170
+ resolve(null);
171
+ },
172
+ onSelect: tool => {
173
+ instance.unmount();
174
+ resolve(tool);
175
+ }
176
+ }));
177
+ });
178
+ }
179
+
180
+ function requireTui(what) {
181
+ if (!canRenderTui) {
182
+ process.stderr.write(`${PROG}: '${what}' needs an interactive terminal.\n`);
183
+ process.exit(2);
184
+ }
185
+ }
186
+
187
+ function commandExists(cmd) {
188
+ const result = spawnSync('sh', ['-lc', `command -v ${shellQuote(cmd)} >/dev/null 2>&1`], {stdio: 'ignore'});
189
+ return result.status === 0;
190
+ }
191
+
192
+ function runCommand(cmd, cmdArgs) {
193
+ const child = spawn(cmd, cmdArgs, {stdio: 'inherit', shell: false});
194
+
195
+ let signal = null;
196
+ let status = null;
197
+
198
+ child.on('exit', (code, receivedSignal) => {
199
+ status = code;
200
+ signal = receivedSignal;
201
+ });
202
+
203
+ return waitForChild(child).then(() => {
204
+ if (signal) {
205
+ process.kill(process.pid, signal);
206
+ return 128;
207
+ }
208
+ return status ?? 0;
209
+ });
210
+ }
211
+
212
+ function waitForChild(child) {
213
+ return new Promise((resolve, reject) => {
214
+ child.once('error', error => {
215
+ if (error.code === 'ENOENT') {
216
+ process.stderr.write(`${PROG}: command not found: ${child.spawnfile}\n`);
217
+ } else {
218
+ process.stderr.write(`${PROG}: failed to start ${child.spawnfile}: ${error.message}\n`);
219
+ }
220
+ reject(error);
221
+ });
222
+ child.once('close', resolve);
223
+ }).catch(error => {
224
+ process.exitCode = error.code === 'ENOENT' ? 127 : 1;
225
+ });
226
+ }
227
+
228
+ function clearScreen() {
229
+ process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
230
+ }
231
+
232
+ function shellQuote(value) {
233
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
234
+ }
235
+
236
+ function printHelp() {
237
+ process.stdout.write(`Usage: ${PROG} [command] [--no-logo] [-- args...]
238
+
239
+ Summon your AI CLI. A terminal launcher.
240
+
241
+ Commands:
242
+ (none) Open the picker, or launch your default if one is set
243
+ menu Always open the picker (ignore the default)
244
+ reorder Set the order tools appear in
245
+ default [tool] Launch <tool> directly on '${PROG}'; no tool = pick one;
246
+ 'off' clears it
247
+ alias <name> Install a second command name for this launcher
248
+ help Show this help
249
+
250
+ Options:
251
+ --no-logo Hide the side logo (shown by default)
252
+
253
+ Anything after -- is passed to the launched tool, e.g. ${PROG} -- --version
254
+ Config: ${configLocation()}
255
+ `);
256
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "summon-cli",
3
+ "version": "0.1.0",
4
+ "description": "Summon your AI CLI. Terminal launcher for Codex CLI, Claude Code, Antigravity CLI, Cursor CLI, GitHub Copilot CLI, and opencode CLI.",
5
+ "type": "module",
6
+ "bin": {
7
+ "summon": "./bin/cli.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "keywords": [
19
+ "summon",
20
+ "cli",
21
+ "tui",
22
+ "ink",
23
+ "react",
24
+ "terminal",
25
+ "launcher",
26
+ "selector",
27
+ "ai",
28
+ "agent",
29
+ "developer-tools",
30
+ "codex-cli",
31
+ "claude-code",
32
+ "github-copilot-cli",
33
+ "cursor-cli",
34
+ "opencode",
35
+ "antigravity"
36
+ ],
37
+ "license": "GPL-3.0-only",
38
+ "author": "sk1gl4a",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/sk1gl4a/summon-cli.git"
42
+ },
43
+ "scripts": {
44
+ "check": "node --check bin/cli.mjs && node --check src/app.mjs && node --check src/config.mjs"
45
+ },
46
+ "dependencies": {
47
+ "ink": "^7.0.5",
48
+ "react": "^19.2.7"
49
+ }
50
+ }