ccmanager 4.1.6 → 4.1.7
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/dist/components/Session.js +7 -10
- package/dist/services/sessionManager.autoApproval.test.js +14 -2
- package/dist/services/sessionManager.d.ts +3 -0
- package/dist/services/sessionManager.effect.test.js +14 -2
- package/dist/services/sessionManager.js +64 -5
- package/dist/services/sessionManager.test.js +85 -4
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/hookExecutor.test.js +4 -0
- package/dist/utils/worktreeUtils.test.js +1 -0
- package/package.json +6 -6
|
@@ -65,9 +65,9 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
65
65
|
stdout.write('\x1B[2J\x1B[H');
|
|
66
66
|
// Restore the current terminal state from the headless xterm snapshot.
|
|
67
67
|
// The xterm serialize addon relies on auto-wrap (DECAWM) being enabled to
|
|
68
|
-
// render wrapped lines
|
|
69
|
-
// characters to naturally overflow to the next line
|
|
70
|
-
//
|
|
68
|
+
// render wrapped lines. It omits row separators for wrapped rows and expects
|
|
69
|
+
// characters to naturally overflow to the next line, so auto-wrap must stay
|
|
70
|
+
// enabled while writing the snapshot and only be disabled afterward.
|
|
71
71
|
const handleSessionRestore = (restoredSession, restoreSnapshot) => {
|
|
72
72
|
if (restoredSession.id === session.id) {
|
|
73
73
|
if (restoreSnapshot.length > 0) {
|
|
@@ -109,15 +109,12 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
109
109
|
/* empty */
|
|
110
110
|
}
|
|
111
111
|
// Mark session as active after resizing so the restore snapshot matches
|
|
112
|
-
// the current terminal dimensions.
|
|
113
|
-
//
|
|
114
|
-
// we proceed.
|
|
112
|
+
// the current terminal dimensions. setSessionActive synchronously emits the
|
|
113
|
+
// restore event, so the snapshot is written to stdout before we proceed.
|
|
115
114
|
sessionManager.setSessionActive(session.id, true);
|
|
116
115
|
// Prevent line wrapping from drifting redraws in TUIs that rely on
|
|
117
|
-
// cursor-up clears.
|
|
118
|
-
//
|
|
119
|
-
// enabled — it omits row separators for wrapped rows, expecting characters
|
|
120
|
-
// to naturally overflow to the next line.
|
|
116
|
+
// cursor-up clears. This must happen after the restore snapshot write,
|
|
117
|
+
// otherwise wrapped restore content can overlap on the same row.
|
|
121
118
|
stdout.write('\x1b[?7l');
|
|
122
119
|
// Handle terminal resize
|
|
123
120
|
const handleResize = () => {
|
|
@@ -69,13 +69,25 @@ vi.mock('@xterm/addon-serialize', () => ({
|
|
|
69
69
|
vi.mock('@xterm/headless', () => ({
|
|
70
70
|
default: {
|
|
71
71
|
Terminal: vi.fn().mockImplementation(function () {
|
|
72
|
+
const normalBuffer = {
|
|
73
|
+
type: 'normal',
|
|
74
|
+
baseY: 0,
|
|
75
|
+
cursorY: 0,
|
|
76
|
+
cursorX: 0,
|
|
77
|
+
length: 0,
|
|
78
|
+
getLine: vi.fn(),
|
|
79
|
+
};
|
|
72
80
|
return {
|
|
73
81
|
rows: 24,
|
|
74
82
|
cols: 80,
|
|
75
83
|
buffer: {
|
|
76
|
-
active:
|
|
77
|
-
|
|
84
|
+
active: normalBuffer,
|
|
85
|
+
normal: normalBuffer,
|
|
86
|
+
alternate: {
|
|
87
|
+
type: 'alternate',
|
|
78
88
|
baseY: 0,
|
|
89
|
+
cursorY: 0,
|
|
90
|
+
cursorX: 0,
|
|
79
91
|
length: 0,
|
|
80
92
|
getLine: vi.fn(),
|
|
81
93
|
},
|
|
@@ -16,6 +16,8 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
|
|
|
16
16
|
private waitingWithBottomBorder;
|
|
17
17
|
private busyTimers;
|
|
18
18
|
private autoApprovalDisabledWorktrees;
|
|
19
|
+
private restoringSessions;
|
|
20
|
+
private bufferedRestoreData;
|
|
19
21
|
private spawn;
|
|
20
22
|
private resolvePreset;
|
|
21
23
|
detectTerminalState(session: Session): SessionState;
|
|
@@ -35,6 +37,7 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
|
|
|
35
37
|
private updateSessionState;
|
|
36
38
|
constructor();
|
|
37
39
|
private createTerminal;
|
|
40
|
+
private shouldResetRestoreScrollback;
|
|
38
41
|
private getRestoreSnapshot;
|
|
39
42
|
private createSessionInternal;
|
|
40
43
|
/**
|
|
@@ -46,13 +46,25 @@ vi.mock('@xterm/addon-serialize', () => ({
|
|
|
46
46
|
vi.mock('@xterm/headless', () => ({
|
|
47
47
|
default: {
|
|
48
48
|
Terminal: vi.fn().mockImplementation(function () {
|
|
49
|
+
const normalBuffer = {
|
|
50
|
+
type: 'normal',
|
|
51
|
+
baseY: 0,
|
|
52
|
+
cursorY: 0,
|
|
53
|
+
cursorX: 0,
|
|
54
|
+
length: 0,
|
|
55
|
+
getLine: vi.fn(),
|
|
56
|
+
};
|
|
49
57
|
return {
|
|
50
58
|
rows: 24,
|
|
51
59
|
cols: 80,
|
|
52
60
|
buffer: {
|
|
53
|
-
active:
|
|
54
|
-
|
|
61
|
+
active: normalBuffer,
|
|
62
|
+
normal: normalBuffer,
|
|
63
|
+
alternate: {
|
|
64
|
+
type: 'alternate',
|
|
55
65
|
baseY: 0,
|
|
66
|
+
cursorY: 0,
|
|
67
|
+
cursorX: 0,
|
|
56
68
|
length: 0,
|
|
57
69
|
getLine: vi.fn(),
|
|
58
70
|
},
|
|
@@ -20,11 +20,14 @@ import { preparePresetLaunch } from '../utils/presetPrompt.js';
|
|
|
20
20
|
const { Terminal } = pkg;
|
|
21
21
|
const TERMINAL_CONTENT_MAX_LINES = 300;
|
|
22
22
|
const TERMINAL_SCROLLBACK_LINES = 5000;
|
|
23
|
+
const TERMINAL_RESTORE_SCROLLBACK_LINES = 200;
|
|
23
24
|
export class SessionManager extends EventEmitter {
|
|
24
25
|
sessions;
|
|
25
26
|
waitingWithBottomBorder = new Map();
|
|
26
27
|
busyTimers = new Map();
|
|
27
28
|
autoApprovalDisabledWorktrees = new Set();
|
|
29
|
+
restoringSessions = new Set();
|
|
30
|
+
bufferedRestoreData = new Map();
|
|
28
31
|
async spawn(command, args, worktreePath, options = {}) {
|
|
29
32
|
const spawnOptions = {
|
|
30
33
|
name: 'xterm-256color',
|
|
@@ -197,10 +200,36 @@ export class SessionManager extends EventEmitter {
|
|
|
197
200
|
logLevel: 'off',
|
|
198
201
|
});
|
|
199
202
|
}
|
|
203
|
+
shouldResetRestoreScrollback(data) {
|
|
204
|
+
return (data.includes('\x1b[2J') ||
|
|
205
|
+
data.includes('\x1b[3J') ||
|
|
206
|
+
data.includes('\x1bc'));
|
|
207
|
+
}
|
|
200
208
|
getRestoreSnapshot(session) {
|
|
201
|
-
|
|
202
|
-
|
|
209
|
+
const activeBuffer = session.terminal.buffer.active;
|
|
210
|
+
if (activeBuffer.type !== 'normal') {
|
|
211
|
+
return session.serializer.serialize({
|
|
212
|
+
scrollback: 0,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const normalBuffer = session.terminal.buffer.normal;
|
|
216
|
+
const bufferLength = normalBuffer.length;
|
|
217
|
+
if (bufferLength === 0) {
|
|
218
|
+
return '';
|
|
219
|
+
}
|
|
220
|
+
const scrollbackStart = Math.max(0, normalBuffer.baseY - TERMINAL_RESTORE_SCROLLBACK_LINES);
|
|
221
|
+
const rangeStart = Math.max(session.restoreScrollbackBaseLine, scrollbackStart);
|
|
222
|
+
const rangeEnd = bufferLength - 1;
|
|
223
|
+
const snapshot = session.serializer.serialize({
|
|
224
|
+
range: {
|
|
225
|
+
start: rangeStart,
|
|
226
|
+
end: rangeEnd,
|
|
227
|
+
},
|
|
228
|
+
excludeAltBuffer: true,
|
|
203
229
|
});
|
|
230
|
+
const cursorRow = normalBuffer.cursorY + 1;
|
|
231
|
+
const cursorCol = normalBuffer.cursorX + 1;
|
|
232
|
+
return `${snapshot}\x1b[${cursorRow};${cursorCol}H`;
|
|
204
233
|
}
|
|
205
234
|
async createSessionInternal(worktreePath, ptyProcess, options = {}) {
|
|
206
235
|
const existingSessions = this.getSessionsForWorktree(worktreePath);
|
|
@@ -224,6 +253,7 @@ export class SessionManager extends EventEmitter {
|
|
|
224
253
|
isActive: false,
|
|
225
254
|
terminal,
|
|
226
255
|
serializer,
|
|
256
|
+
restoreScrollbackBaseLine: 0,
|
|
227
257
|
stateCheckInterval: undefined, // Will be set in setupBackgroundHandler
|
|
228
258
|
isPrimaryCommand: options.isPrimaryCommand ?? true,
|
|
229
259
|
presetName: options.presetName,
|
|
@@ -299,9 +329,19 @@ export class SessionManager extends EventEmitter {
|
|
|
299
329
|
session.process.onData((data) => {
|
|
300
330
|
// Write data to virtual terminal
|
|
301
331
|
session.terminal.write(data);
|
|
332
|
+
if (this.shouldResetRestoreScrollback(data)) {
|
|
333
|
+
session.restoreScrollbackBaseLine =
|
|
334
|
+
session.terminal.buffer.normal.baseY;
|
|
335
|
+
}
|
|
302
336
|
session.lastActivity = new Date();
|
|
303
337
|
// Only emit data events when session is active
|
|
304
338
|
if (session.isActive) {
|
|
339
|
+
if (this.restoringSessions.has(session.id)) {
|
|
340
|
+
const bufferedData = this.bufferedRestoreData.get(session.id) ?? [];
|
|
341
|
+
bufferedData.push(data);
|
|
342
|
+
this.bufferedRestoreData.set(session.id, bufferedData);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
305
345
|
this.emit('sessionData', session, data);
|
|
306
346
|
}
|
|
307
347
|
});
|
|
@@ -436,10 +476,27 @@ export class SessionManager extends EventEmitter {
|
|
|
436
476
|
session.isActive = active;
|
|
437
477
|
if (active) {
|
|
438
478
|
session.lastAccessedAt = Date.now();
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
this.
|
|
479
|
+
this.restoringSessions.add(session.id);
|
|
480
|
+
try {
|
|
481
|
+
const restoreSnapshot = this.getRestoreSnapshot(session);
|
|
482
|
+
if (restoreSnapshot.length > 0) {
|
|
483
|
+
this.emit('sessionRestore', session, restoreSnapshot);
|
|
484
|
+
}
|
|
442
485
|
}
|
|
486
|
+
finally {
|
|
487
|
+
this.restoringSessions.delete(session.id);
|
|
488
|
+
const bufferedData = this.bufferedRestoreData.get(session.id);
|
|
489
|
+
if (bufferedData && bufferedData.length > 0) {
|
|
490
|
+
this.bufferedRestoreData.delete(session.id);
|
|
491
|
+
for (const chunk of bufferedData) {
|
|
492
|
+
this.emit('sessionData', session, chunk);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
this.restoringSessions.delete(session.id);
|
|
499
|
+
this.bufferedRestoreData.delete(session.id);
|
|
443
500
|
}
|
|
444
501
|
}
|
|
445
502
|
}
|
|
@@ -512,6 +569,8 @@ export class SessionManager extends EventEmitter {
|
|
|
512
569
|
}
|
|
513
570
|
this.sessions.delete(sessionId);
|
|
514
571
|
this.waitingWithBottomBorder.delete(sessionId);
|
|
572
|
+
this.restoringSessions.delete(sessionId);
|
|
573
|
+
this.bufferedRestoreData.delete(sessionId);
|
|
515
574
|
this.emit('sessionDestroyed', session);
|
|
516
575
|
}
|
|
517
576
|
}
|
|
@@ -50,13 +50,27 @@ vi.mock('@xterm/addon-serialize', () => ({
|
|
|
50
50
|
vi.mock('@xterm/headless', () => ({
|
|
51
51
|
default: {
|
|
52
52
|
Terminal: vi.fn(function () {
|
|
53
|
+
const normalBuffer = {
|
|
54
|
+
type: 'normal',
|
|
55
|
+
baseY: 0,
|
|
56
|
+
cursorY: 0,
|
|
57
|
+
cursorX: 0,
|
|
58
|
+
length: 0,
|
|
59
|
+
getLine: vi.fn(function () {
|
|
60
|
+
return null;
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
53
63
|
return {
|
|
54
64
|
rows: 24,
|
|
55
65
|
cols: 80,
|
|
56
66
|
buffer: {
|
|
57
|
-
active:
|
|
58
|
-
|
|
67
|
+
active: normalBuffer,
|
|
68
|
+
normal: normalBuffer,
|
|
69
|
+
alternate: {
|
|
70
|
+
type: 'alternate',
|
|
59
71
|
baseY: 0,
|
|
72
|
+
cursorY: 0,
|
|
73
|
+
cursorX: 0,
|
|
60
74
|
length: 0,
|
|
61
75
|
getLine: vi.fn(function () {
|
|
62
76
|
return null;
|
|
@@ -760,7 +774,7 @@ describe('SessionManager', () => {
|
|
|
760
774
|
});
|
|
761
775
|
});
|
|
762
776
|
describe('session restore snapshots', () => {
|
|
763
|
-
it('should emit
|
|
777
|
+
it('should emit a bounded normal-buffer restore snapshot and restore the cursor position', async () => {
|
|
764
778
|
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
765
779
|
id: '1',
|
|
766
780
|
name: 'Main',
|
|
@@ -768,14 +782,44 @@ describe('SessionManager', () => {
|
|
|
768
782
|
});
|
|
769
783
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
770
784
|
const session = await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
|
|
785
|
+
const normalBuffer = session.terminal.buffer.normal;
|
|
786
|
+
normalBuffer.baseY = 260;
|
|
787
|
+
normalBuffer.length = 300;
|
|
788
|
+
normalBuffer.cursorY = 7;
|
|
789
|
+
normalBuffer.cursorX = 11;
|
|
790
|
+
session.restoreScrollbackBaseLine = 120;
|
|
771
791
|
const serializeMock = vi
|
|
772
792
|
.spyOn(session.serializer, 'serialize')
|
|
773
793
|
.mockReturnValue('\u001b[31mrestored\u001b[0m');
|
|
774
794
|
const restoreHandler = vi.fn();
|
|
775
795
|
sessionManager.on('sessionRestore', restoreHandler);
|
|
776
796
|
sessionManager.setSessionActive(session.id, true);
|
|
797
|
+
expect(serializeMock).toHaveBeenCalledWith({
|
|
798
|
+
range: {
|
|
799
|
+
start: 120,
|
|
800
|
+
end: 299,
|
|
801
|
+
},
|
|
802
|
+
excludeAltBuffer: true,
|
|
803
|
+
});
|
|
804
|
+
expect(restoreHandler).toHaveBeenCalledWith(session, '\u001b[31mrestored\u001b[0m\u001b[8;12H');
|
|
805
|
+
});
|
|
806
|
+
it('should keep viewport-only restore behavior for alternate screen sessions', async () => {
|
|
807
|
+
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
808
|
+
id: '1',
|
|
809
|
+
name: 'Main',
|
|
810
|
+
command: 'claude',
|
|
811
|
+
});
|
|
812
|
+
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
813
|
+
const session = await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
|
|
814
|
+
session.terminal.buffer.active = session.terminal.buffer.alternate;
|
|
815
|
+
const serializeMock = vi
|
|
816
|
+
.spyOn(session.serializer, 'serialize')
|
|
817
|
+
.mockReturnValue('\u001b[31malt\u001b[0m');
|
|
818
|
+
const restoreHandler = vi.fn();
|
|
819
|
+
sessionManager.on('sessionRestore', restoreHandler);
|
|
820
|
+
sessionManager.setSessionActive(session.id, true);
|
|
777
821
|
expect(serializeMock).toHaveBeenCalledWith({ scrollback: 0 });
|
|
778
|
-
expect(restoreHandler).toHaveBeenCalledWith(session, '\u001b[
|
|
822
|
+
expect(restoreHandler).toHaveBeenCalledWith(session, '\u001b[31malt\u001b[0m');
|
|
779
823
|
});
|
|
780
824
|
it('should skip restore event when serialized output is empty', async () => {
|
|
781
825
|
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
@@ -791,6 +835,43 @@ describe('SessionManager', () => {
|
|
|
791
835
|
sessionManager.setSessionActive(session.id, true);
|
|
792
836
|
expect(restoreHandler).not.toHaveBeenCalled();
|
|
793
837
|
});
|
|
838
|
+
it('should reset restore scrollback baseline after a clear-screen sequence', async () => {
|
|
839
|
+
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
840
|
+
id: '1',
|
|
841
|
+
name: 'Main',
|
|
842
|
+
command: 'claude',
|
|
843
|
+
});
|
|
844
|
+
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
845
|
+
const session = await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
|
|
846
|
+
session.terminal.buffer.normal.baseY = 17;
|
|
847
|
+
mockPty.emit('data', '\x1b[2J\x1b[Hfresh');
|
|
848
|
+
expect(session.restoreScrollbackBaseLine).toBe(17);
|
|
849
|
+
});
|
|
850
|
+
it('should flush live session data after the restore snapshot completes', async () => {
|
|
851
|
+
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
852
|
+
id: '1',
|
|
853
|
+
name: 'Main',
|
|
854
|
+
command: 'claude',
|
|
855
|
+
});
|
|
856
|
+
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
857
|
+
const session = await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
|
|
858
|
+
session.terminal.buffer.normal.length = 1;
|
|
859
|
+
vi.spyOn(session.serializer, 'serialize').mockReturnValue('restored');
|
|
860
|
+
const eventOrder = [];
|
|
861
|
+
sessionManager.on('sessionRestore', restoredSession => {
|
|
862
|
+
if (restoredSession.id === session.id) {
|
|
863
|
+
eventOrder.push('restore');
|
|
864
|
+
mockPty.emit('data', 'live-output');
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
sessionManager.on('sessionData', activeSession => {
|
|
868
|
+
if (activeSession.id === session.id) {
|
|
869
|
+
eventOrder.push('data');
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
sessionManager.setSessionActive(session.id, true);
|
|
873
|
+
expect(eventOrder).toEqual(['restore', 'data']);
|
|
874
|
+
});
|
|
794
875
|
});
|
|
795
876
|
describe('static methods', () => {
|
|
796
877
|
describe('getSessionCounts', () => {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -383,6 +383,7 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
383
383
|
process: {},
|
|
384
384
|
terminal: {},
|
|
385
385
|
serializer: {},
|
|
386
|
+
restoreScrollbackBaseLine: 0,
|
|
386
387
|
output: [],
|
|
387
388
|
stateCheckInterval: undefined,
|
|
388
389
|
isPrimaryCommand: true,
|
|
@@ -442,6 +443,7 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
442
443
|
process: {},
|
|
443
444
|
terminal: {},
|
|
444
445
|
serializer: {},
|
|
446
|
+
restoreScrollbackBaseLine: 0,
|
|
445
447
|
output: [],
|
|
446
448
|
stateCheckInterval: undefined,
|
|
447
449
|
isPrimaryCommand: true,
|
|
@@ -499,6 +501,7 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
499
501
|
process: {},
|
|
500
502
|
terminal: {},
|
|
501
503
|
serializer: {},
|
|
504
|
+
restoreScrollbackBaseLine: 0,
|
|
502
505
|
output: [],
|
|
503
506
|
stateCheckInterval: undefined,
|
|
504
507
|
isPrimaryCommand: true,
|
|
@@ -558,6 +561,7 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
558
561
|
process: {},
|
|
559
562
|
terminal: {},
|
|
560
563
|
serializer: {},
|
|
564
|
+
restoreScrollbackBaseLine: 0,
|
|
561
565
|
output: [],
|
|
562
566
|
stateCheckInterval: undefined,
|
|
563
567
|
isPrimaryCommand: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.7",
|
|
4
4
|
"description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kodai Kabasawa",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"bin"
|
|
42
42
|
],
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@kodaikabasawa/ccmanager-darwin-arm64": "4.1.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "4.1.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "4.1.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "4.1.7",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.7",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.7",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "4.1.7",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "4.1.7"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|