wtsm 1.0.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.
Files changed (3) hide show
  1. package/README.md +75 -0
  2. package/index.js +427 -0
  3. package/package.json +20 -0
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # Windows Terminal Session Manager (`s`)
2
+
3
+ A CLI tool to define, save, and launch named Windows Terminal sessions with specific layouts and commands.
4
+
5
+ ## Installation
6
+
7
+ Ensure the tool is linked globally:
8
+
9
+ ```bash
10
+ npm link
11
+ ```
12
+
13
+ ## Usage Guide
14
+
15
+ ### 1. Creating & Managing Sessions
16
+
17
+ **Create a new session:**
18
+
19
+ ```bash
20
+ s create <name>
21
+ # Example:
22
+ s create work
23
+ ```
24
+
25
+ **Add current directory to a session:**
26
+ Navigate to the folder you want to add, then run:
27
+
28
+ ```bash
29
+ s add <name>
30
+ # Example:
31
+ cd C:\Projects\MyBackend
32
+ s add work
33
+ ```
34
+
35
+ _It will ask for an optional startup command (e.g., `npm start` or `git status`)._
36
+
37
+ **Add interactively:**
38
+ If you don't provide a name, it will show a list:
39
+
40
+ ```bash
41
+ s add
42
+ ```
43
+
44
+ _(Type the number or name of the session to select it)_
45
+
46
+ ### 2. Viewing Sessions
47
+
48
+ **Interactive Explorer:**
49
+
50
+ ```bash
51
+ s ls
52
+ ```
53
+
54
+ - **↑ / ↓**: Navigate the list of sessions.
55
+ - **→**: View tabs inside the selected session.
56
+ - **Ctrl+D**: Delete the selected session or tab.
57
+ - **q**, **Esc**, or **Ctrl+C**: Exit.
58
+
59
+ ### 3. Launching Sessions
60
+
61
+ **Launch a session:**
62
+
63
+ ```bash
64
+ s <name>
65
+ # Example:
66
+ s work
67
+ ```
68
+
69
+ _Opens a new Windows Terminal window with all configured tabs._
70
+
71
+ ### 4 Not implemented features
72
+
73
+ - clean scroll page,
74
+ - Automatically use current shell
75
+ - Pane capture
package/index.js ADDED
@@ -0,0 +1,427 @@
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
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "wtsm",
3
+ "version": "1.0.0",
4
+ "description": "Windows Terminal Session Manager",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "s": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "author": "",
13
+ "license": "ISC",
14
+ "type": "module",
15
+ "dependencies": {
16
+ "chalk": "^5.6.2",
17
+ "commander": "^14.0.2",
18
+ "inquirer": "^13.2.1"
19
+ }
20
+ }