wtsm 1.0.2 → 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 +3 -3
- package/dist/index.mjs +363 -0
- package/package.json +35 -7
- package/index.js +0 -427
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ A CLI tool to define, save, and launch named Windows Terminal sessions with spec
|
|
|
7
7
|
Ensure the tool is linked globally:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm
|
|
10
|
+
npm i wtsm -g
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Usage Guide
|
|
@@ -70,6 +70,6 @@ _Opens a new Windows Terminal window with all configured tabs._
|
|
|
70
70
|
|
|
71
71
|
### 4 Not implemented features
|
|
72
72
|
|
|
73
|
-
-
|
|
74
|
-
- Automatically use current shell
|
|
73
|
+
- currently only supports powershell
|
|
74
|
+
- Automatically use current shell profile
|
|
75
75
|
- Pane capture
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import inquirer from "inquirer";
|
|
8
|
+
import readline from "readline";
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
|
|
11
|
+
//#region src/lib/session.ts
|
|
12
|
+
const SESSION_FILE = path.join(os.homedir(), ".wts-sessions.json");
|
|
13
|
+
function loadSessions() {
|
|
14
|
+
if (!fs.existsSync(SESSION_FILE)) return {};
|
|
15
|
+
try {
|
|
16
|
+
const data = fs.readFileSync(SESSION_FILE, "utf-8");
|
|
17
|
+
return JSON.parse(data);
|
|
18
|
+
} catch {
|
|
19
|
+
console.error("Error reading session file.");
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function saveSessions(sessions) {
|
|
24
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(sessions, null, 2));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/commands/create.ts
|
|
29
|
+
function createCommand() {
|
|
30
|
+
const cmd = new Command("create");
|
|
31
|
+
cmd.arguments("<name>");
|
|
32
|
+
cmd.description("Create a new session config");
|
|
33
|
+
cmd.action((name) => {
|
|
34
|
+
const sessions = loadSessions();
|
|
35
|
+
if (sessions[name]) {
|
|
36
|
+
console.log(chalk.yellow(`Session '${name}' already exists.`));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
sessions[name] = [];
|
|
40
|
+
saveSessions(sessions);
|
|
41
|
+
console.log(chalk.green(`Session '${name}' created.`));
|
|
42
|
+
});
|
|
43
|
+
return cmd;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/commands/add.ts
|
|
48
|
+
async function addToSession(sessionName, cwd) {
|
|
49
|
+
const sessions = loadSessions();
|
|
50
|
+
if (!sessions[sessionName]) {
|
|
51
|
+
console.log(chalk.red(`Session '${sessionName}' not found. Use 's create ${sessionName}' first.`));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const handleExit = (str, key) => {
|
|
55
|
+
if (key && (key.ctrl && key.name === "c" || key.name === "q" || key.name === "escape")) process.exit(0);
|
|
56
|
+
};
|
|
57
|
+
process.stdin.on("keypress", handleExit);
|
|
58
|
+
try {
|
|
59
|
+
const { command } = await inquirer.prompt([{
|
|
60
|
+
type: "input",
|
|
61
|
+
name: "command",
|
|
62
|
+
message: " Command to run on start (optional):"
|
|
63
|
+
}]);
|
|
64
|
+
const newTab = {
|
|
65
|
+
path: cwd,
|
|
66
|
+
command: command.trim() || null
|
|
67
|
+
};
|
|
68
|
+
sessions[sessionName].push(newTab);
|
|
69
|
+
saveSessions(sessions);
|
|
70
|
+
console.log(chalk.green(`Added current path to session '${sessionName}'.`));
|
|
71
|
+
} finally {
|
|
72
|
+
process.stdin.removeListener("keypress", handleExit);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function addCommand() {
|
|
76
|
+
const cmd = new Command("add");
|
|
77
|
+
cmd.arguments("[name]");
|
|
78
|
+
cmd.description("Add current path to a session");
|
|
79
|
+
cmd.action(async (nameOrIndex) => {
|
|
80
|
+
const sessions = loadSessions();
|
|
81
|
+
const sessionNames = Object.keys(sessions);
|
|
82
|
+
if (sessionNames.length === 0) {
|
|
83
|
+
console.log(chalk.red("No sessions found. Create one first with 's create <name>'"));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let targetSession = nameOrIndex;
|
|
87
|
+
if (!targetSession) {
|
|
88
|
+
console.log(chalk.cyan.bold(" Available Sessions"));
|
|
89
|
+
console.log(chalk.gray(" ------------------"));
|
|
90
|
+
sessionNames.forEach((name, idx) => {
|
|
91
|
+
console.log(` ${chalk.yellow(idx + 1)}. ${chalk.white(name)}`);
|
|
92
|
+
});
|
|
93
|
+
console.log();
|
|
94
|
+
const handleExit = (str, key) => {
|
|
95
|
+
if (key && (key.ctrl && key.name === "c" || key.name === "q" || key.name === "escape")) process.exit(0);
|
|
96
|
+
};
|
|
97
|
+
process.stdin.on("keypress", handleExit);
|
|
98
|
+
let answer;
|
|
99
|
+
try {
|
|
100
|
+
answer = await inquirer.prompt([{
|
|
101
|
+
type: "input",
|
|
102
|
+
name: "input",
|
|
103
|
+
message: " Select session (number or name):",
|
|
104
|
+
validate: (input) => {
|
|
105
|
+
if (input === "q") return true;
|
|
106
|
+
if (!input) return "Please enter a value";
|
|
107
|
+
const num = parseInt(input, 10);
|
|
108
|
+
if (!isNaN(num)) {
|
|
109
|
+
if (num < 1 || num > sessionNames.length) return "Invalid number";
|
|
110
|
+
} else if (!sessions[input]) return "Session not found";
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}]);
|
|
114
|
+
} finally {
|
|
115
|
+
process.stdin.removeListener("keypress", handleExit);
|
|
116
|
+
}
|
|
117
|
+
if (answer.input === "q") process.exit(0);
|
|
118
|
+
const num = parseInt(answer.input, 10);
|
|
119
|
+
if (!isNaN(num)) targetSession = sessionNames[num - 1];
|
|
120
|
+
else targetSession = answer.input;
|
|
121
|
+
} else if (nameOrIndex) {
|
|
122
|
+
const num = parseInt(nameOrIndex, 10);
|
|
123
|
+
if (!isNaN(num)) {
|
|
124
|
+
if (num >= 1 && num <= sessionNames.length) targetSession = sessionNames[num - 1];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (targetSession) await addToSession(targetSession, process.cwd());
|
|
128
|
+
});
|
|
129
|
+
return cmd;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/commands/list.ts
|
|
134
|
+
function interactiveList() {
|
|
135
|
+
const sessions = loadSessions();
|
|
136
|
+
const sessionNames = Object.keys(sessions);
|
|
137
|
+
if (sessionNames.length === 0) {
|
|
138
|
+
console.log("No sessions found.");
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
}
|
|
141
|
+
let view = "sessions";
|
|
142
|
+
let selectedIndex = 0;
|
|
143
|
+
let activeSessionName = null;
|
|
144
|
+
let tabs = [];
|
|
145
|
+
let renderedLines = 0;
|
|
146
|
+
const { stdin, stdout } = process;
|
|
147
|
+
if (!process.stdin.isTTY) {
|
|
148
|
+
console.log(chalk.red("Interactive mode requires a TTY. Please run in a terminal."));
|
|
149
|
+
return Promise.resolve();
|
|
150
|
+
}
|
|
151
|
+
readline.emitKeypressEvents(stdin);
|
|
152
|
+
stdin.setRawMode(true);
|
|
153
|
+
stdin.resume();
|
|
154
|
+
stdin.setEncoding("utf8");
|
|
155
|
+
stdout.write("\x1B[?25l");
|
|
156
|
+
function cleanup() {
|
|
157
|
+
stdout.write("\x1B[?25h");
|
|
158
|
+
stdin.setRawMode(false);
|
|
159
|
+
stdin.pause();
|
|
160
|
+
stdin.removeListener("keypress", handleInput);
|
|
161
|
+
}
|
|
162
|
+
function render() {
|
|
163
|
+
if (renderedLines === 0) stdout.write("\x1B[2J\x1B[H");
|
|
164
|
+
else stdout.write(`\x1b[${renderedLines}A\x1b[0J`);
|
|
165
|
+
if (view === "sessions") {
|
|
166
|
+
console.log(chalk.cyan.bold(" Windows Terminal Sessions"));
|
|
167
|
+
console.log(chalk.gray(" -------------------------"));
|
|
168
|
+
sessionNames.forEach((name, idx) => {
|
|
169
|
+
if (idx === selectedIndex) console.log(chalk.green.bold(`> ${name}`));
|
|
170
|
+
else console.log(` ${name}`);
|
|
171
|
+
});
|
|
172
|
+
console.log(chalk.gray("\n (↑/↓: Move, Enter/→: Open, Ctrl+D: Delete, q/Esc: Exit)"));
|
|
173
|
+
renderedLines = 4 + sessionNames.length;
|
|
174
|
+
} else if (view === "tabs") {
|
|
175
|
+
console.log(chalk.cyan.bold(` Session: ${activeSessionName}`));
|
|
176
|
+
console.log(chalk.gray(" -------------------------"));
|
|
177
|
+
if (tabs.length === 0) console.log(" (Empty Session)");
|
|
178
|
+
else tabs.forEach((tab, idx) => {
|
|
179
|
+
const title = tab.path + (tab.command ? ` [${tab.command}]` : "");
|
|
180
|
+
if (idx === selectedIndex) console.log(chalk.green.bold(`> ${idx + 1}. ${title}`));
|
|
181
|
+
else console.log(` ${idx + 1}. ${title}`);
|
|
182
|
+
});
|
|
183
|
+
console.log(chalk.gray("\n (←: Back, Ctrl+D: Delete, q/Esc: Exit)"));
|
|
184
|
+
renderedLines = 4 + tabs.length;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const handleInput = (str, key) => {
|
|
188
|
+
if (!key) return;
|
|
189
|
+
if (key.sequence === "" || key.name === "q" || key.name === "escape") {
|
|
190
|
+
cleanup();
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
if (key.ctrl && key.name === "d") {
|
|
194
|
+
if (view === "sessions") {
|
|
195
|
+
const nameToDelete = sessionNames[selectedIndex];
|
|
196
|
+
if (nameToDelete) {
|
|
197
|
+
delete sessions[nameToDelete];
|
|
198
|
+
saveSessions(sessions);
|
|
199
|
+
const idx = sessionNames.indexOf(nameToDelete);
|
|
200
|
+
sessionNames.splice(idx, 1);
|
|
201
|
+
if (selectedIndex >= sessionNames.length) selectedIndex = Math.max(0, sessionNames.length - 1);
|
|
202
|
+
render();
|
|
203
|
+
}
|
|
204
|
+
} else if (view === "tabs") {
|
|
205
|
+
const currentTabs = sessions[activeSessionName];
|
|
206
|
+
if (currentTabs && currentTabs.length > 0) {
|
|
207
|
+
currentTabs.splice(selectedIndex, 1);
|
|
208
|
+
saveSessions(sessions);
|
|
209
|
+
tabs = currentTabs;
|
|
210
|
+
if (selectedIndex >= tabs.length) selectedIndex = Math.max(0, tabs.length - 1);
|
|
211
|
+
render();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (key.name === "up") {
|
|
217
|
+
selectedIndex--;
|
|
218
|
+
const max = view === "sessions" ? sessionNames.length : (sessions[activeSessionName] || []).length;
|
|
219
|
+
if (max > 0) {
|
|
220
|
+
if (selectedIndex < 0) selectedIndex = max - 1;
|
|
221
|
+
render();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (key.name === "down") {
|
|
225
|
+
selectedIndex++;
|
|
226
|
+
const max = view === "sessions" ? sessionNames.length : (sessions[activeSessionName] || []).length;
|
|
227
|
+
if (max > 0) {
|
|
228
|
+
if (selectedIndex >= max) selectedIndex = 0;
|
|
229
|
+
render();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (key.name === "right" || key.name === "return" || key.name === "enter") {
|
|
233
|
+
if (view === "sessions") {
|
|
234
|
+
activeSessionName = sessionNames[selectedIndex];
|
|
235
|
+
tabs = sessions[activeSessionName];
|
|
236
|
+
if (tabs) {
|
|
237
|
+
view = "tabs";
|
|
238
|
+
selectedIndex = 0;
|
|
239
|
+
render();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (key.name === "left") {
|
|
244
|
+
if (view === "tabs") {
|
|
245
|
+
view = "sessions";
|
|
246
|
+
const prevIdx = sessionNames.indexOf(activeSessionName);
|
|
247
|
+
selectedIndex = prevIdx >= 0 ? prevIdx : 0;
|
|
248
|
+
activeSessionName = null;
|
|
249
|
+
tabs = [];
|
|
250
|
+
render();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
render();
|
|
255
|
+
stdin.on("keypress", handleInput);
|
|
256
|
+
return new Promise(() => {});
|
|
257
|
+
}
|
|
258
|
+
function listCommand() {
|
|
259
|
+
const cmd = new Command("ls");
|
|
260
|
+
cmd.alias("list");
|
|
261
|
+
cmd.arguments("[name]");
|
|
262
|
+
cmd.description("List sessions (interactive) or tabs in a session");
|
|
263
|
+
cmd.action((name) => {
|
|
264
|
+
if (!name) interactiveList();
|
|
265
|
+
else {
|
|
266
|
+
const sessions = loadSessions();
|
|
267
|
+
if (!sessions[name]) {
|
|
268
|
+
console.log(chalk.red(`Session '${name}' not found.`));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
console.log(chalk.cyan(`Session: ${name}`));
|
|
272
|
+
const tabs = sessions[name];
|
|
273
|
+
if (tabs.length === 0) console.log(" (Empty)");
|
|
274
|
+
else tabs.forEach((tab, idx) => {
|
|
275
|
+
console.log(` ${chalk.bold(idx + 1)}. Path: ${tab.path}`);
|
|
276
|
+
if (tab.command) console.log(` Cmd: ${chalk.gray(tab.command)}`);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
return cmd;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/commands/restore.ts
|
|
285
|
+
function restoreSession(sessionName) {
|
|
286
|
+
const sessions = loadSessions();
|
|
287
|
+
if (!sessions[sessionName]) {
|
|
288
|
+
console.log(chalk.red(`Session '${sessionName}' not found.`));
|
|
289
|
+
console.log("Available sessions:", Object.keys(sessions).join(", "));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const tabs = sessions[sessionName];
|
|
293
|
+
if (tabs.length === 0) {
|
|
294
|
+
console.log(chalk.yellow(`Session '${sessionName}' is empty.`));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const args = [];
|
|
298
|
+
tabs.forEach((tab, index) => {
|
|
299
|
+
if (index > 0) {
|
|
300
|
+
args.push(";");
|
|
301
|
+
args.push("new-tab");
|
|
302
|
+
}
|
|
303
|
+
args.push("-p");
|
|
304
|
+
args.push("Windows PowerShell");
|
|
305
|
+
if (tab.path) {
|
|
306
|
+
args.push("-d");
|
|
307
|
+
args.push(tab.path);
|
|
308
|
+
}
|
|
309
|
+
if (tab.command) {
|
|
310
|
+
args.push("powershell");
|
|
311
|
+
args.push("-NoExit");
|
|
312
|
+
args.push("-Command");
|
|
313
|
+
args.push(tab.command);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
console.log(chalk.blue(`Launching session '${sessionName}'...`));
|
|
317
|
+
spawn("wt", args, {
|
|
318
|
+
detached: true,
|
|
319
|
+
stdio: "ignore",
|
|
320
|
+
shell: false
|
|
321
|
+
}).unref();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/lib/utils.ts
|
|
326
|
+
function setupKeypressEvents() {
|
|
327
|
+
readline.emitKeypressEvents(process.stdin);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/cli.ts
|
|
332
|
+
setupKeypressEvents();
|
|
333
|
+
function createCLI() {
|
|
334
|
+
const program = new Command();
|
|
335
|
+
program.name("s").description("Windows Terminal Session Manager").version("1.0.3");
|
|
336
|
+
program.addCommand(createCommand());
|
|
337
|
+
program.addCommand(addCommand());
|
|
338
|
+
program.addCommand(listCommand());
|
|
339
|
+
return program;
|
|
340
|
+
}
|
|
341
|
+
const rawArgs = process.argv.slice(2);
|
|
342
|
+
if (rawArgs.length > 0 && ![
|
|
343
|
+
"create",
|
|
344
|
+
"add",
|
|
345
|
+
"ls",
|
|
346
|
+
"list",
|
|
347
|
+
"help",
|
|
348
|
+
"--help",
|
|
349
|
+
"-h",
|
|
350
|
+
"--version",
|
|
351
|
+
"-V"
|
|
352
|
+
].includes(rawArgs[0])) {
|
|
353
|
+
const sessionName = rawArgs[0];
|
|
354
|
+
if (rawArgs[1] === "add") addToSession(sessionName, process.cwd());
|
|
355
|
+
else restoreSession(sessionName);
|
|
356
|
+
} else createCLI().parse(process.argv);
|
|
357
|
+
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/index.ts
|
|
360
|
+
createCLI();
|
|
361
|
+
|
|
362
|
+
//#endregion
|
|
363
|
+
export { };
|
package/package.json
CHANGED
|
@@ -1,20 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wtsm",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Windows Terminal Session Manager",
|
|
5
|
-
"
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.mjs",
|
|
6
7
|
"bin": {
|
|
7
|
-
"s": "./index.
|
|
8
|
+
"s": "./dist/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
8
15
|
},
|
|
9
16
|
"scripts": {
|
|
10
|
-
"
|
|
17
|
+
"build": "tsdown",
|
|
18
|
+
"dev": "tsx src/index.ts",
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
11
20
|
},
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
"keywords": [
|
|
22
|
+
"windows-terminal",
|
|
23
|
+
"session-manager",
|
|
24
|
+
"cli",
|
|
25
|
+
"terminal",
|
|
26
|
+
"wt"
|
|
27
|
+
],
|
|
28
|
+
"homepage": "https://github.com/adityacodepublic/wts-cli#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/adityacodepublic/wts-cli/issues"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/adityacodepublic/wts-cli.git"
|
|
35
|
+
},
|
|
36
|
+
"author": "adityacodepublic (https://github.com/adityacodepublic)",
|
|
37
|
+
"license": "MIT",
|
|
15
38
|
"dependencies": {
|
|
16
39
|
"chalk": "^5.6.2",
|
|
17
40
|
"commander": "^14.0.2",
|
|
18
41
|
"inquirer": "^13.2.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"tsdown": "^0.20.1",
|
|
45
|
+
"tsx": "^4.21.0",
|
|
46
|
+
"typescript": "^5.9.3"
|
|
19
47
|
}
|
|
20
48
|
}
|
package/index.js
DELETED
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import inquirer from 'inquirer';
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
import fs from 'fs';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
import os from 'os';
|
|
9
|
-
import { spawn } from 'child_process';
|
|
10
|
-
import readline from 'readline';
|
|
11
|
-
|
|
12
|
-
const SESSION_FILE = path.join(os.homedir(), '.wts-sessions.json');
|
|
13
|
-
const program = new Command();
|
|
14
|
-
readline.emitKeypressEvents(process.stdin);
|
|
15
|
-
|
|
16
|
-
// --- Data Helpers ---
|
|
17
|
-
|
|
18
|
-
function loadSessions() {
|
|
19
|
-
if (!fs.existsSync(SESSION_FILE)) {
|
|
20
|
-
return {};
|
|
21
|
-
}
|
|
22
|
-
try {
|
|
23
|
-
return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
|
24
|
-
} catch (e) {
|
|
25
|
-
console.error(chalk.red('Error reading session file.'));
|
|
26
|
-
return {};
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function saveSessions(sessions) {
|
|
31
|
-
fs.writeFileSync(SESSION_FILE, JSON.stringify(sessions, null, 2));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// --- Logic Helpers ---
|
|
35
|
-
|
|
36
|
-
async function addToSession(sessionName, cwd) {
|
|
37
|
-
const sessions = loadSessions();
|
|
38
|
-
if (!sessions[sessionName]) {
|
|
39
|
-
console.log(chalk.red(`Session '${sessionName}' not found. Use 's create ${sessionName}' first.`));
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const handleExit = (str, key) => {
|
|
44
|
-
if (key && (key.ctrl && key.name === 'c' || key.name === 'q' || key.name === 'escape')) {
|
|
45
|
-
process.exit(0);
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
process.stdin.on('keypress', handleExit);
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
const { command } = await inquirer.prompt([
|
|
52
|
-
{
|
|
53
|
-
type: 'input',
|
|
54
|
-
name: 'command',
|
|
55
|
-
message: ' Command to run on start (optional):',
|
|
56
|
-
}
|
|
57
|
-
]);
|
|
58
|
-
|
|
59
|
-
sessions[sessionName].push({
|
|
60
|
-
path: cwd,
|
|
61
|
-
command: command.trim() || null
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
saveSessions(sessions);
|
|
65
|
-
console.log(chalk.green(`Added current path to session '${sessionName}'.`));
|
|
66
|
-
} finally {
|
|
67
|
-
process.stdin.removeListener('keypress', handleExit);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function restoreSession(sessionName) {
|
|
72
|
-
const sessions = loadSessions();
|
|
73
|
-
if (!sessions[sessionName]) {
|
|
74
|
-
console.log(chalk.red(`Session '${sessionName}' not found.`));
|
|
75
|
-
console.log('Available sessions:', Object.keys(sessions).join(', '));
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const tabs = sessions[sessionName];
|
|
80
|
-
if (tabs.length === 0) {
|
|
81
|
-
console.log(chalk.yellow(`Session '${sessionName}' is empty.`));
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const args = [];
|
|
86
|
-
|
|
87
|
-
tabs.forEach((tab, index) => {
|
|
88
|
-
if (index > 0) {
|
|
89
|
-
args.push(';');
|
|
90
|
-
args.push('new-tab');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Force PowerShell profile
|
|
94
|
-
args.push('-p');
|
|
95
|
-
args.push('Windows PowerShell');
|
|
96
|
-
|
|
97
|
-
if (tab.path) {
|
|
98
|
-
args.push('-d');
|
|
99
|
-
args.push(tab.path);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (tab.command) {
|
|
103
|
-
// PowerShell command execution
|
|
104
|
-
args.push('powershell'); // Redundant if profile is set, but ensures command syntax works if profile is weird
|
|
105
|
-
args.push('-NoExit');
|
|
106
|
-
args.push('-Command');
|
|
107
|
-
args.push(tab.command);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
console.log(chalk.blue(`Launching session '${sessionName}'...`));
|
|
112
|
-
const subprocess = spawn('wt', args, { detached: true, stdio: 'ignore', shell: false });
|
|
113
|
-
subprocess.unref();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
async function interactiveList() {
|
|
119
|
-
const sessions = loadSessions();
|
|
120
|
-
const sessionNames = Object.keys(sessions);
|
|
121
|
-
|
|
122
|
-
if (sessionNames.length === 0) {
|
|
123
|
-
console.log("No sessions found.");
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// State
|
|
128
|
-
let view = 'sessions'; // 'sessions' | 'tabs'
|
|
129
|
-
let selectedIndex = 0;
|
|
130
|
-
let activeSessionName = null;
|
|
131
|
-
let tabs = [];
|
|
132
|
-
|
|
133
|
-
// Input Handling
|
|
134
|
-
const { stdin, stdout } = process;
|
|
135
|
-
readline.emitKeypressEvents(stdin); // Required for keypress events
|
|
136
|
-
stdin.setRawMode(true);
|
|
137
|
-
stdin.resume();
|
|
138
|
-
stdin.setEncoding('utf8');
|
|
139
|
-
|
|
140
|
-
// Helper to hide cursor
|
|
141
|
-
stdout.write('\x1B[?25l');
|
|
142
|
-
|
|
143
|
-
function cleanup() {
|
|
144
|
-
stdout.write('\x1B[?25h'); // Show cursor
|
|
145
|
-
stdin.setRawMode(false);
|
|
146
|
-
stdin.pause();
|
|
147
|
-
stdin.removeListener('keypress', handleInput);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function render() {
|
|
151
|
-
// Clear screen for full-screen feel, or use clearDown if preferred.
|
|
152
|
-
// console.clear() is robust.
|
|
153
|
-
console.clear();
|
|
154
|
-
|
|
155
|
-
if (view === 'sessions') {
|
|
156
|
-
console.log(chalk.cyan.bold(" Windows Terminal Sessions"));
|
|
157
|
-
console.log(chalk.gray(" -------------------------"));
|
|
158
|
-
|
|
159
|
-
sessionNames.forEach((name, idx) => {
|
|
160
|
-
if (idx === selectedIndex) {
|
|
161
|
-
console.log(chalk.green.bold(`> ${name}`));
|
|
162
|
-
} else {
|
|
163
|
-
console.log(` ${name}`);
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
console.log(chalk.gray("\n (↑/↓: Move, Enter/→: Open, Ctrl+D: Delete, q/Esc: Exit)"));
|
|
168
|
-
|
|
169
|
-
} else if (view === 'tabs') {
|
|
170
|
-
console.log(chalk.cyan.bold(` Session: ${activeSessionName}`));
|
|
171
|
-
console.log(chalk.gray(" -------------------------"));
|
|
172
|
-
|
|
173
|
-
if (tabs.length === 0) {
|
|
174
|
-
console.log(" (Empty Session)");
|
|
175
|
-
} else {
|
|
176
|
-
tabs.forEach((tab, idx) => {
|
|
177
|
-
const title = tab.path + (tab.command ? ` [${tab.command}]` : '');
|
|
178
|
-
if (idx === selectedIndex) {
|
|
179
|
-
console.log(chalk.green.bold(`> ${idx + 1}. ${title}`));
|
|
180
|
-
} else {
|
|
181
|
-
console.log(` ${idx + 1}. ${title}`);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
console.log(chalk.gray("\n (←: Back, Ctrl+D: Delete, q/Esc: Exit)"));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const handleInput = (str, key) => {
|
|
191
|
-
if (!key) return;
|
|
192
|
-
|
|
193
|
-
// Ctrl+C (End of Text) or 'q' or Esc
|
|
194
|
-
if (key.sequence === '\u0003' || key.name === 'q' || key.name === 'escape') {
|
|
195
|
-
cleanup();
|
|
196
|
-
process.exit(0);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Ctrl+D (Delete)
|
|
200
|
-
if (key.ctrl && key.name === 'd') {
|
|
201
|
-
if (view === 'sessions') {
|
|
202
|
-
const nameToDelete = sessionNames[selectedIndex];
|
|
203
|
-
if (nameToDelete) {
|
|
204
|
-
delete sessions[nameToDelete];
|
|
205
|
-
saveSessions(sessions);
|
|
206
|
-
// Refresh list
|
|
207
|
-
const idx = sessionNames.indexOf(nameToDelete);
|
|
208
|
-
sessionNames.splice(idx, 1);
|
|
209
|
-
if (selectedIndex >= sessionNames.length) selectedIndex = Math.max(0, sessionNames.length - 1);
|
|
210
|
-
render();
|
|
211
|
-
}
|
|
212
|
-
} else if (view === 'tabs') {
|
|
213
|
-
const currentTabs = sessions[activeSessionName];
|
|
214
|
-
if (currentTabs && currentTabs.length > 0) {
|
|
215
|
-
currentTabs.splice(selectedIndex, 1);
|
|
216
|
-
saveSessions(sessions);
|
|
217
|
-
if (selectedIndex >= currentTabs.length) selectedIndex = Math.max(0, currentTabs.length - 1);
|
|
218
|
-
render();
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Up Arrow
|
|
225
|
-
if (key.name === 'up') {
|
|
226
|
-
selectedIndex--;
|
|
227
|
-
const max = view === 'sessions' ? sessionNames.length : (sessions[activeSessionName] || []).length;
|
|
228
|
-
if (max > 0) {
|
|
229
|
-
if (selectedIndex < 0) selectedIndex = max - 1; // Wrap top
|
|
230
|
-
render();
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Down Arrow
|
|
235
|
-
if (key.name === 'down') {
|
|
236
|
-
selectedIndex++;
|
|
237
|
-
const max = view === 'sessions' ? sessionNames.length : (sessions[activeSessionName] || []).length;
|
|
238
|
-
if (max > 0) {
|
|
239
|
-
if (selectedIndex >= max) selectedIndex = 0; // Wrap bottom
|
|
240
|
-
render();
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Right Arrow or Enter
|
|
245
|
-
if (key.name === 'right' || key.name === 'return' || key.name === 'enter') {
|
|
246
|
-
if (view === 'sessions') {
|
|
247
|
-
activeSessionName = sessionNames[selectedIndex];
|
|
248
|
-
tabs = sessions[activeSessionName];
|
|
249
|
-
if (tabs) { // Only switch if valid
|
|
250
|
-
view = 'tabs';
|
|
251
|
-
selectedIndex = 0; // Reset index for tabs list
|
|
252
|
-
render();
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Left Arrow
|
|
258
|
-
if (key.name === 'left') {
|
|
259
|
-
if (view === 'tabs') {
|
|
260
|
-
view = 'sessions';
|
|
261
|
-
// Try to restore index of previous session
|
|
262
|
-
const prevIdx = sessionNames.indexOf(activeSessionName);
|
|
263
|
-
selectedIndex = prevIdx >= 0 ? prevIdx : 0;
|
|
264
|
-
|
|
265
|
-
activeSessionName = null;
|
|
266
|
-
tabs = [];
|
|
267
|
-
render();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
// Initial Render
|
|
273
|
-
render();
|
|
274
|
-
stdin.on('keypress', handleInput);
|
|
275
|
-
|
|
276
|
-
// Return a promise that never resolves so the program waits for input
|
|
277
|
-
return new Promise(() => { });
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// --- Commands ---
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
program
|
|
287
|
-
.name('s')
|
|
288
|
-
.description('Windows Terminal Session Manager')
|
|
289
|
-
.version('1.0.0');
|
|
290
|
-
|
|
291
|
-
// 1. s create <name>
|
|
292
|
-
program
|
|
293
|
-
.command('create <name>')
|
|
294
|
-
.description('Create a new session config')
|
|
295
|
-
.action((name) => {
|
|
296
|
-
const sessions = loadSessions();
|
|
297
|
-
if (sessions[name]) {
|
|
298
|
-
console.log(chalk.yellow(`Session '${name}' already exists.`));
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
sessions[name] = [];
|
|
302
|
-
saveSessions(sessions);
|
|
303
|
-
console.log(chalk.green(`Session '${name}' created.`));
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// 2. s add [name] (Modified)
|
|
307
|
-
program
|
|
308
|
-
.command('add [name]')
|
|
309
|
-
.description('Add current path to a session')
|
|
310
|
-
.action(async (nameOrIndex) => {
|
|
311
|
-
const sessions = loadSessions();
|
|
312
|
-
const sessionNames = Object.keys(sessions);
|
|
313
|
-
|
|
314
|
-
if (sessionNames.length === 0) {
|
|
315
|
-
console.log(chalk.red("No sessions found. Create one first with 's create <name>'"));
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
let targetSession = nameOrIndex;
|
|
320
|
-
|
|
321
|
-
if (!targetSession) {
|
|
322
|
-
// Print numbered list
|
|
323
|
-
console.log(chalk.cyan.bold(" Available Sessions"));
|
|
324
|
-
console.log(chalk.gray(" ------------------"));
|
|
325
|
-
sessionNames.forEach((name, idx) => {
|
|
326
|
-
console.log(` ${chalk.yellow(idx + 1)}. ${chalk.white(name)}`);
|
|
327
|
-
});
|
|
328
|
-
console.log();
|
|
329
|
-
|
|
330
|
-
const handleExit = (str, key) => {
|
|
331
|
-
if (key && (key.ctrl && key.name === 'c' || key.name === 'q' || key.name === 'escape')) {
|
|
332
|
-
process.exit(0);
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
process.stdin.on('keypress', handleExit);
|
|
336
|
-
|
|
337
|
-
let answer;
|
|
338
|
-
try {
|
|
339
|
-
answer = await inquirer.prompt([
|
|
340
|
-
{
|
|
341
|
-
type: 'input',
|
|
342
|
-
name: 'input',
|
|
343
|
-
message: ' Select session (number or name):',
|
|
344
|
-
validate: (input) => {
|
|
345
|
-
if (input === 'q') return true;
|
|
346
|
-
if (!input) return "Please enter a value";
|
|
347
|
-
const num = parseInt(input, 10);
|
|
348
|
-
if (!isNaN(num)) {
|
|
349
|
-
if (num < 1 || num > sessionNames.length) return "Invalid number";
|
|
350
|
-
} else {
|
|
351
|
-
if (!sessions[input]) return "Session not found";
|
|
352
|
-
}
|
|
353
|
-
return true;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
]);
|
|
357
|
-
} finally {
|
|
358
|
-
process.stdin.removeListener('keypress', handleExit);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (answer.input === 'q') process.exit(0);
|
|
362
|
-
|
|
363
|
-
const num = parseInt(answer.input, 10);
|
|
364
|
-
if (!isNaN(num)) {
|
|
365
|
-
targetSession = sessionNames[num - 1];
|
|
366
|
-
} else {
|
|
367
|
-
targetSession = answer.input;
|
|
368
|
-
}
|
|
369
|
-
} else {
|
|
370
|
-
// Check if user provided a number directly in CLI: `s add 1`
|
|
371
|
-
const num = parseInt(nameOrIndex, 10);
|
|
372
|
-
if (!isNaN(num)) {
|
|
373
|
-
if (num >= 1 && num <= sessionNames.length) {
|
|
374
|
-
targetSession = sessionNames[num - 1];
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
await addToSession(targetSession, process.cwd());
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// 5. s ls (Renamed & Interactive)
|
|
383
|
-
program
|
|
384
|
-
.command('ls [name]')
|
|
385
|
-
.alias('list')
|
|
386
|
-
.description('List sessions (interactive) or tabs in a session')
|
|
387
|
-
.action((name) => {
|
|
388
|
-
if (!name) {
|
|
389
|
-
// Interactive Mode
|
|
390
|
-
interactiveList();
|
|
391
|
-
} else {
|
|
392
|
-
// List tabs in specific session (Legacy/Scriptable mode)
|
|
393
|
-
const sessions = loadSessions();
|
|
394
|
-
if (!sessions[name]) {
|
|
395
|
-
console.log(chalk.red(`Session '${name}' not found.`));
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
console.log(chalk.cyan(`Session: ${name}`));
|
|
399
|
-
const tabs = sessions[name];
|
|
400
|
-
if (tabs.length === 0) {
|
|
401
|
-
console.log(" (Empty)");
|
|
402
|
-
} else {
|
|
403
|
-
tabs.forEach((tab, idx) => {
|
|
404
|
-
console.log(` ${chalk.bold(idx + 1)}. Path: ${tab.path}`);
|
|
405
|
-
if (tab.command) console.log(` Cmd: ${chalk.gray(tab.command)}`);
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// --- Custom Dispatcher ---
|
|
413
|
-
const rawArgs = process.argv.slice(2);
|
|
414
|
-
const knownCommands = ['create', 'add', 'ls', 'list', 'help', '--help', '-h', '--version', '-V'];
|
|
415
|
-
|
|
416
|
-
if (rawArgs.length > 0 && !knownCommands.includes(rawArgs[0])) {
|
|
417
|
-
const sessionName = rawArgs[0];
|
|
418
|
-
const secondArg = rawArgs[1];
|
|
419
|
-
|
|
420
|
-
if (secondArg === 'add') {
|
|
421
|
-
addToSession(sessionName, process.cwd());
|
|
422
|
-
} else {
|
|
423
|
-
restoreSession(sessionName);
|
|
424
|
-
}
|
|
425
|
-
} else {
|
|
426
|
-
program.parse(process.argv);
|
|
427
|
-
}
|