ccmanager 4.0.2 → 4.0.4
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/App.js +17 -3
- package/dist/services/bunTerminal.d.ts +1 -0
- package/dist/services/bunTerminal.js +4 -3
- package/dist/services/sessionManager.d.ts +3 -4
- package/dist/services/sessionManager.effect.test.js +16 -37
- package/dist/services/sessionManager.js +51 -19
- package/dist/services/sessionManager.test.js +62 -102
- package/dist/services/stateDetector/cursor.js +13 -1
- package/dist/services/stateDetector/cursor.test.js +27 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/utils/hookExecutor.test.js +8 -0
- package/dist/utils/worktreeUtils.test.js +2 -0
- package/package.json +6 -6
package/dist/components/App.js
CHANGED
|
@@ -42,6 +42,8 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
42
42
|
const [pendingWorktreeCreation, setPendingWorktreeCreation] = useState(null);
|
|
43
43
|
// State for loading context - track flags for message composition
|
|
44
44
|
const [loadingContext, setLoadingContext] = useState({});
|
|
45
|
+
// State for streaming devcontainer up logs
|
|
46
|
+
const [devcontainerLogs, setDevcontainerLogs] = useState([]);
|
|
45
47
|
// Helper function to format error messages based on error type using _tag discrimination
|
|
46
48
|
const formatErrorMessage = (error) => {
|
|
47
49
|
switch (error._tag) {
|
|
@@ -59,8 +61,15 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
59
61
|
};
|
|
60
62
|
// Helper function to create session with Effect-based error handling
|
|
61
63
|
const createSessionWithEffect = useCallback(async (worktreePath, presetId, initialPrompt) => {
|
|
64
|
+
setDevcontainerLogs([]);
|
|
62
65
|
const sessionEffect = devcontainerConfig
|
|
63
|
-
? sessionManager.createSessionWithDevcontainerEffect(worktreePath, devcontainerConfig, presetId, initialPrompt)
|
|
66
|
+
? sessionManager.createSessionWithDevcontainerEffect(worktreePath, devcontainerConfig, presetId, initialPrompt, (line) => {
|
|
67
|
+
setDevcontainerLogs(prev => {
|
|
68
|
+
const next = [...prev, line];
|
|
69
|
+
// Keep only the last 10 lines to avoid unbounded growth
|
|
70
|
+
return next.length > 10 ? next.slice(-10) : next;
|
|
71
|
+
});
|
|
72
|
+
})
|
|
64
73
|
: sessionManager.createSessionWithPresetEffect(worktreePath, presetId, initialPrompt);
|
|
65
74
|
const result = await Effect.runPromise(Effect.either(sessionEffect));
|
|
66
75
|
if (result._tag === 'Left') {
|
|
@@ -449,6 +458,11 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
449
458
|
setLoadingContext({ deleteBranch });
|
|
450
459
|
setView('deleting-worktree');
|
|
451
460
|
setError(null);
|
|
461
|
+
// Yield to the event loop so Ink can paint `deleting-worktree` before git work runs.
|
|
462
|
+
// Otherwise the confirmation UI stays visible until deletion finishes (no spinner).
|
|
463
|
+
await new Promise(resolve => {
|
|
464
|
+
setTimeout(resolve, 0);
|
|
465
|
+
});
|
|
452
466
|
// Delete the worktrees sequentially using Effect
|
|
453
467
|
let hasError = false;
|
|
454
468
|
for (const path of worktreePaths) {
|
|
@@ -619,7 +633,7 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
619
633
|
// Use yellow color for devcontainer operations (longer duration),
|
|
620
634
|
// cyan for standard session creation
|
|
621
635
|
const color = devcontainerConfig ? 'yellow' : 'cyan';
|
|
622
|
-
return (
|
|
636
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(LoadingSpinner, { message: message, color: color }), devcontainerLogs.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: devcontainerLogs.map((line, i) => (_jsx(Text, { dimColor: true, children: line }, i))) }))] }));
|
|
623
637
|
}
|
|
624
638
|
if (view === 'creating-session-preset') {
|
|
625
639
|
// Always display preset-specific message
|
|
@@ -631,7 +645,7 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
631
645
|
: 'Creating session with preset...';
|
|
632
646
|
// Use yellow color for devcontainer, cyan for standard
|
|
633
647
|
const color = devcontainerConfig ? 'yellow' : 'cyan';
|
|
634
|
-
return (
|
|
648
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(LoadingSpinner, { message: message, color: color }), devcontainerLogs.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, marginLeft: 2, children: devcontainerLogs.map((line, i) => (_jsx(Text, { dimColor: true, children: line }, i))) }))] }));
|
|
635
649
|
}
|
|
636
650
|
if (view === 'clearing') {
|
|
637
651
|
// Render nothing during the clearing phase to ensure clean transition
|
|
@@ -57,9 +57,10 @@ class BunTerminal {
|
|
|
57
57
|
this._processBuffer();
|
|
58
58
|
},
|
|
59
59
|
});
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
60
|
+
// Most interactive CLIs work best when the PTY starts in raw mode, but
|
|
61
|
+
// terminal proxy commands such as `devcontainer exec` manage termios
|
|
62
|
+
// themselves and break if the outer PTY is forced raw first.
|
|
63
|
+
this._terminal.setRawMode(options.rawMode ?? true);
|
|
63
64
|
// Disable ONLCR in the PTY output flags to avoid double CRLF translation
|
|
64
65
|
// when forwarding PTY output to the real stdout TTY.
|
|
65
66
|
const ONLCR_FLAG = 0x0002;
|
|
@@ -59,9 +59,8 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
|
|
|
59
59
|
/**
|
|
60
60
|
* Sets up exit handler for the session process.
|
|
61
61
|
* When the process exits with code 1 and it's the primary command,
|
|
62
|
-
* it will attempt
|
|
63
|
-
* If fallbackArgs are configured,
|
|
64
|
-
* If no fallbackArgs are configured, the command will be retried with no arguments.
|
|
62
|
+
* it will attempt a single retry using the configured command with fallback args.
|
|
63
|
+
* If fallbackArgs are not configured, it retries the configured command with no args.
|
|
65
64
|
*/
|
|
66
65
|
private setupExitHandler;
|
|
67
66
|
private setupBackgroundHandler;
|
|
@@ -96,7 +95,7 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
|
|
|
96
95
|
* Create session with devcontainer integration using Effect-based error handling
|
|
97
96
|
* @returns Effect that may fail with ProcessError (container/spawn failure) or ConfigError (invalid preset)
|
|
98
97
|
*/
|
|
99
|
-
createSessionWithDevcontainerEffect(worktreePath: string, devcontainerConfig: DevcontainerConfig, presetId?: string, initialPrompt?: string): Effect.Effect<Session, ProcessError | ConfigError, never>;
|
|
98
|
+
createSessionWithDevcontainerEffect(worktreePath: string, devcontainerConfig: DevcontainerConfig, presetId?: string, initialPrompt?: string, onLog?: (line: string) => void): Effect.Effect<Session, ProcessError | ConfigError, never>;
|
|
100
99
|
destroy(): void;
|
|
101
100
|
static getSessionCounts(sessions: Session[]): SessionCounts;
|
|
102
101
|
static formatSessionCounts(counts: SessionCounts): string;
|
|
@@ -9,8 +9,17 @@ vi.mock('./bunTerminal.js', () => ({
|
|
|
9
9
|
return null;
|
|
10
10
|
}),
|
|
11
11
|
}));
|
|
12
|
+
// Helper to create a mock child process for child_process.spawn
|
|
13
|
+
function createMockChildProcess(exitCode = 0) {
|
|
14
|
+
const stdout = new EventEmitter();
|
|
15
|
+
const stderr = new EventEmitter();
|
|
16
|
+
const proc = Object.assign(new EventEmitter(), { stdout, stderr });
|
|
17
|
+
process.nextTick(() => proc.emit('close', exitCode));
|
|
18
|
+
return proc;
|
|
19
|
+
}
|
|
12
20
|
// Mock child_process
|
|
13
21
|
vi.mock('child_process', () => ({
|
|
22
|
+
spawn: vi.fn(() => createMockChildProcess(0)),
|
|
14
23
|
exec: vi.fn(),
|
|
15
24
|
execFile: vi.fn(),
|
|
16
25
|
}));
|
|
@@ -167,18 +176,6 @@ describe('SessionManager Effect-based Operations', () => {
|
|
|
167
176
|
});
|
|
168
177
|
// Setup spawn mock
|
|
169
178
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
170
|
-
// Mock exec to succeed
|
|
171
|
-
const { exec } = await import('child_process');
|
|
172
|
-
const mockExec = vi.mocked(exec);
|
|
173
|
-
mockExec.mockImplementation((cmd, options, callback) => {
|
|
174
|
-
if (typeof options === 'function') {
|
|
175
|
-
callback = options;
|
|
176
|
-
}
|
|
177
|
-
if (callback && typeof callback === 'function') {
|
|
178
|
-
callback(null, 'Container started', '');
|
|
179
|
-
}
|
|
180
|
-
return {};
|
|
181
|
-
});
|
|
182
179
|
const devcontainerConfig = {
|
|
183
180
|
upCommand: 'devcontainer up --workspace-folder .',
|
|
184
181
|
execCommand: 'devcontainer exec --workspace-folder .',
|
|
@@ -192,18 +189,9 @@ describe('SessionManager Effect-based Operations', () => {
|
|
|
192
189
|
expect(session.devcontainerConfig).toEqual(devcontainerConfig);
|
|
193
190
|
});
|
|
194
191
|
it('should return Effect that fails with ProcessError when devcontainer up fails', async () => {
|
|
195
|
-
// Mock
|
|
196
|
-
const {
|
|
197
|
-
|
|
198
|
-
mockExec.mockImplementation((cmd, options, callback) => {
|
|
199
|
-
if (typeof options === 'function') {
|
|
200
|
-
callback = options;
|
|
201
|
-
}
|
|
202
|
-
if (callback && typeof callback === 'function') {
|
|
203
|
-
callback(new Error('Container failed to start'), '', '');
|
|
204
|
-
}
|
|
205
|
-
return {};
|
|
206
|
-
});
|
|
192
|
+
// Mock spawn to return a process that exits with code 1
|
|
193
|
+
const { spawn: childSpawn } = await import('child_process');
|
|
194
|
+
vi.mocked(childSpawn).mockImplementation(() => createMockChildProcess(1));
|
|
207
195
|
const devcontainerConfig = {
|
|
208
196
|
upCommand: 'devcontainer up --workspace-folder .',
|
|
209
197
|
execCommand: 'devcontainer exec --workspace-folder .',
|
|
@@ -217,11 +205,14 @@ describe('SessionManager Effect-based Operations', () => {
|
|
|
217
205
|
expect(result.left._tag).toBe('ProcessError');
|
|
218
206
|
if (result.left._tag === 'ProcessError') {
|
|
219
207
|
expect(result.left.command).toContain('devcontainer up');
|
|
220
|
-
expect(result.left.message).toContain('
|
|
208
|
+
expect(result.left.message).toContain('Command exited with code 1');
|
|
221
209
|
}
|
|
222
210
|
}
|
|
223
211
|
});
|
|
224
212
|
it('should return Effect that fails with ConfigError when preset not found', async () => {
|
|
213
|
+
// Reset childSpawn mock to succeed (devcontainer up should pass)
|
|
214
|
+
const { spawn: childSpawn } = await import('child_process');
|
|
215
|
+
vi.mocked(childSpawn).mockImplementation(() => createMockChildProcess(0));
|
|
225
216
|
// Setup mocks - getPresetByIdEffect returns Left, getDefaultPreset returns undefined
|
|
226
217
|
vi.mocked(configReader.getPresetByIdEffect).mockReturnValue(Either.left(new ValidationError({
|
|
227
218
|
field: 'presetId',
|
|
@@ -229,18 +220,6 @@ describe('SessionManager Effect-based Operations', () => {
|
|
|
229
220
|
receivedValue: 'invalid-preset',
|
|
230
221
|
})));
|
|
231
222
|
vi.mocked(configReader.getDefaultPreset).mockReturnValue(undefined);
|
|
232
|
-
// Mock exec to succeed (devcontainer up)
|
|
233
|
-
const { exec } = await import('child_process');
|
|
234
|
-
const mockExec = vi.mocked(exec);
|
|
235
|
-
mockExec.mockImplementation((cmd, options, callback) => {
|
|
236
|
-
if (typeof options === 'function') {
|
|
237
|
-
callback = options;
|
|
238
|
-
}
|
|
239
|
-
if (callback && typeof callback === 'function') {
|
|
240
|
-
callback(null, 'Container started', '');
|
|
241
|
-
}
|
|
242
|
-
return {};
|
|
243
|
-
});
|
|
244
223
|
const devcontainerConfig = {
|
|
245
224
|
upCommand: 'devcontainer up',
|
|
246
225
|
execCommand: 'devcontainer exec',
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { spawn } from './bunTerminal.js';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
3
|
import pkg from '@xterm/headless';
|
|
4
|
-
import {
|
|
5
|
-
import { promisify } from 'util';
|
|
4
|
+
import { spawn as childSpawn } from 'child_process';
|
|
6
5
|
import { configReader } from './config/configReader.js';
|
|
7
6
|
import { executeStatusHook } from '../utils/hookExecutor.js';
|
|
8
7
|
import { createStateDetector } from './stateDetector/index.js';
|
|
@@ -17,20 +16,20 @@ import { getTerminalScreenContent } from '../utils/screenCapture.js';
|
|
|
17
16
|
import { injectTeammateMode } from '../utils/commandArgs.js';
|
|
18
17
|
import { preparePresetLaunch } from '../utils/presetPrompt.js';
|
|
19
18
|
const { Terminal } = pkg;
|
|
20
|
-
const execAsync = promisify(exec);
|
|
21
19
|
const TERMINAL_CONTENT_MAX_LINES = 300;
|
|
22
20
|
export class SessionManager extends EventEmitter {
|
|
23
21
|
sessions;
|
|
24
22
|
waitingWithBottomBorder = new Map();
|
|
25
23
|
busyTimers = new Map();
|
|
26
24
|
autoApprovalDisabledWorktrees = new Set();
|
|
27
|
-
async spawn(command, args, worktreePath) {
|
|
25
|
+
async spawn(command, args, worktreePath, options = {}) {
|
|
28
26
|
const spawnOptions = {
|
|
29
27
|
name: 'xterm-256color',
|
|
30
28
|
cols: process.stdout.columns || 80,
|
|
31
29
|
rows: process.stdout.rows || 24,
|
|
32
30
|
cwd: worktreePath,
|
|
33
31
|
env: process.env,
|
|
32
|
+
...(options.rawMode === undefined ? {} : { rawMode: options.rawMode }),
|
|
34
33
|
};
|
|
35
34
|
return spawn(command, args, spawnOptions);
|
|
36
35
|
}
|
|
@@ -208,6 +207,8 @@ export class SessionManager extends EventEmitter {
|
|
|
208
207
|
worktreePath,
|
|
209
208
|
sessionNumber: maxNumber + 1,
|
|
210
209
|
sessionName: undefined,
|
|
210
|
+
command: options.command ?? 'claude',
|
|
211
|
+
fallbackArgs: options.fallbackArgs,
|
|
211
212
|
lastAccessedAt: Date.now(),
|
|
212
213
|
process: ptyProcess,
|
|
213
214
|
output: [],
|
|
@@ -258,6 +259,8 @@ export class SessionManager extends EventEmitter {
|
|
|
258
259
|
const ptyProcess = await this.spawn(command, args, worktreePath);
|
|
259
260
|
const session = await this.createSessionInternal(worktreePath, ptyProcess, {
|
|
260
261
|
isPrimaryCommand: true,
|
|
262
|
+
command,
|
|
263
|
+
fallbackArgs: preset.fallbackArgs,
|
|
261
264
|
presetName: preset.name,
|
|
262
265
|
detectionStrategy: preset.detectionStrategy,
|
|
263
266
|
});
|
|
@@ -316,9 +319,8 @@ export class SessionManager extends EventEmitter {
|
|
|
316
319
|
/**
|
|
317
320
|
* Sets up exit handler for the session process.
|
|
318
321
|
* When the process exits with code 1 and it's the primary command,
|
|
319
|
-
* it will attempt
|
|
320
|
-
* If fallbackArgs are configured,
|
|
321
|
-
* If no fallbackArgs are configured, the command will be retried with no arguments.
|
|
322
|
+
* it will attempt a single retry using the configured command with fallback args.
|
|
323
|
+
* If fallbackArgs are not configured, it retries the configured command with no args.
|
|
322
324
|
*/
|
|
323
325
|
setupExitHandler(session) {
|
|
324
326
|
session.process.onExit(async (e) => {
|
|
@@ -326,26 +328,23 @@ export class SessionManager extends EventEmitter {
|
|
|
326
328
|
if (e.exitCode === 1 && !e.signal && session.isPrimaryCommand) {
|
|
327
329
|
try {
|
|
328
330
|
let fallbackProcess;
|
|
331
|
+
const fallbackArgs = injectTeammateMode(session.command, session.fallbackArgs ?? [], session.detectionStrategy);
|
|
329
332
|
// Check if we're in a devcontainer session
|
|
330
333
|
if (session.devcontainerConfig) {
|
|
331
334
|
// Parse the exec command to extract arguments
|
|
332
335
|
const execParts = session.devcontainerConfig.execCommand.split(/\s+/);
|
|
333
336
|
const devcontainerCmd = execParts[0] || 'devcontainer';
|
|
334
337
|
const execArgs = execParts.slice(1);
|
|
335
|
-
// Build fallback command for devcontainer
|
|
336
|
-
const fallbackClaudeArgs = injectTeammateMode('claude', [], session.detectionStrategy);
|
|
337
338
|
const fallbackFullArgs = [
|
|
338
339
|
...execArgs,
|
|
339
340
|
'--',
|
|
340
|
-
|
|
341
|
-
...
|
|
341
|
+
session.command,
|
|
342
|
+
...fallbackArgs,
|
|
342
343
|
];
|
|
343
|
-
fallbackProcess = await this.spawn(devcontainerCmd, fallbackFullArgs, session.worktreePath);
|
|
344
|
+
fallbackProcess = await this.spawn(devcontainerCmd, fallbackFullArgs, session.worktreePath, { rawMode: false });
|
|
344
345
|
}
|
|
345
346
|
else {
|
|
346
|
-
|
|
347
|
-
const fallbackArgs = injectTeammateMode('claude', [], session.detectionStrategy);
|
|
348
|
-
fallbackProcess = await this.spawn('claude', fallbackArgs, session.worktreePath);
|
|
347
|
+
fallbackProcess = await this.spawn(session.command, fallbackArgs, session.worktreePath);
|
|
349
348
|
}
|
|
350
349
|
// Replace the process
|
|
351
350
|
session.process = fallbackProcess;
|
|
@@ -635,12 +634,43 @@ export class SessionManager extends EventEmitter {
|
|
|
635
634
|
* Create session with devcontainer integration using Effect-based error handling
|
|
636
635
|
* @returns Effect that may fail with ProcessError (container/spawn failure) or ConfigError (invalid preset)
|
|
637
636
|
*/
|
|
638
|
-
createSessionWithDevcontainerEffect(worktreePath, devcontainerConfig, presetId, initialPrompt) {
|
|
637
|
+
createSessionWithDevcontainerEffect(worktreePath, devcontainerConfig, presetId, initialPrompt, onLog) {
|
|
639
638
|
return Effect.tryPromise({
|
|
640
639
|
try: async () => {
|
|
641
|
-
// Execute devcontainer up command
|
|
640
|
+
// Execute devcontainer up command, streaming output in real-time
|
|
642
641
|
try {
|
|
643
|
-
await
|
|
642
|
+
await new Promise((resolve, reject) => {
|
|
643
|
+
const parts = devcontainerConfig.upCommand.split(/\s+/);
|
|
644
|
+
const cmd = parts[0];
|
|
645
|
+
const args = parts.slice(1);
|
|
646
|
+
const proc = childSpawn(cmd, args, {
|
|
647
|
+
cwd: worktreePath,
|
|
648
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
649
|
+
shell: false,
|
|
650
|
+
});
|
|
651
|
+
const handleData = (data) => {
|
|
652
|
+
const text = data.toString();
|
|
653
|
+
for (const line of text.split('\n')) {
|
|
654
|
+
const trimmed = line.trimEnd();
|
|
655
|
+
if (trimmed) {
|
|
656
|
+
onLog?.(trimmed);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
proc.stdout?.on('data', handleData);
|
|
661
|
+
proc.stderr?.on('data', handleData);
|
|
662
|
+
proc.on('error', err => {
|
|
663
|
+
reject(err);
|
|
664
|
+
});
|
|
665
|
+
proc.on('close', code => {
|
|
666
|
+
if (code === 0) {
|
|
667
|
+
resolve();
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
reject(new Error(`Command exited with code ${code}`));
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
});
|
|
644
674
|
}
|
|
645
675
|
catch (error) {
|
|
646
676
|
throw new ProcessError({
|
|
@@ -658,9 +688,11 @@ export class SessionManager extends EventEmitter {
|
|
|
658
688
|
const presetArgs = launch.args;
|
|
659
689
|
const fullArgs = [...execArgs, '--', preset.command, ...presetArgs];
|
|
660
690
|
// Spawn the process within devcontainer
|
|
661
|
-
const ptyProcess = await this.spawn(devcontainerCmd, fullArgs, worktreePath);
|
|
691
|
+
const ptyProcess = await this.spawn(devcontainerCmd, fullArgs, worktreePath, { rawMode: false });
|
|
662
692
|
const session = await this.createSessionInternal(worktreePath, ptyProcess, {
|
|
663
693
|
isPrimaryCommand: true,
|
|
694
|
+
command: preset.command,
|
|
695
|
+
fallbackArgs: preset.fallbackArgs,
|
|
664
696
|
presetName: preset.name,
|
|
665
697
|
detectionStrategy: preset.detectionStrategy,
|
|
666
698
|
devcontainerConfig,
|
|
@@ -3,7 +3,16 @@ import { Effect, Either } from 'effect';
|
|
|
3
3
|
import { ValidationError } from '../types/errors.js';
|
|
4
4
|
import { spawn } from './bunTerminal.js';
|
|
5
5
|
import { EventEmitter } from 'events';
|
|
6
|
-
import {
|
|
6
|
+
import { spawn as childSpawn } from 'child_process';
|
|
7
|
+
// Helper to create a mock child process for child_process.spawn
|
|
8
|
+
function createMockChildProcess(exitCode = 0) {
|
|
9
|
+
const stdout = new EventEmitter();
|
|
10
|
+
const stderr = new EventEmitter();
|
|
11
|
+
const proc = Object.assign(new EventEmitter(), { stdout, stderr });
|
|
12
|
+
// Emit 'close' asynchronously so listeners can be attached
|
|
13
|
+
process.nextTick(() => proc.emit('close', exitCode));
|
|
14
|
+
return proc;
|
|
15
|
+
}
|
|
7
16
|
// Mock bunTerminal
|
|
8
17
|
vi.mock('./bunTerminal.js', () => ({
|
|
9
18
|
spawn: vi.fn(function () {
|
|
@@ -12,12 +21,11 @@ vi.mock('./bunTerminal.js', () => ({
|
|
|
12
21
|
}));
|
|
13
22
|
// Mock child_process
|
|
14
23
|
vi.mock('child_process', () => ({
|
|
15
|
-
|
|
16
|
-
return
|
|
17
|
-
}),
|
|
18
|
-
execFile: vi.fn(function () {
|
|
19
|
-
return null;
|
|
24
|
+
spawn: vi.fn(function () {
|
|
25
|
+
return createMockChildProcess(0);
|
|
20
26
|
}),
|
|
27
|
+
exec: vi.fn(),
|
|
28
|
+
execFile: vi.fn(),
|
|
21
29
|
}));
|
|
22
30
|
// Mock configuration manager
|
|
23
31
|
vi.mock('./config/configReader.js', () => ({
|
|
@@ -230,13 +238,14 @@ describe('SessionManager', () => {
|
|
|
230
238
|
// Expect createSessionWithPresetEffect to throw the original error
|
|
231
239
|
await expect(Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'))).rejects.toThrow('Command not found');
|
|
232
240
|
});
|
|
233
|
-
it('should
|
|
241
|
+
it('should retry the configured command with fallback args when main command exits with code 1', async () => {
|
|
234
242
|
// Setup mock preset with args
|
|
235
243
|
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
236
244
|
id: '1',
|
|
237
245
|
name: 'Main',
|
|
238
246
|
command: 'claude',
|
|
239
247
|
args: ['--invalid-flag'],
|
|
248
|
+
fallbackArgs: ['--safe-flag'],
|
|
240
249
|
});
|
|
241
250
|
// First spawn attempt - will exit with code 1
|
|
242
251
|
const firstMockPty = new MockPty();
|
|
@@ -254,11 +263,13 @@ describe('SessionManager', () => {
|
|
|
254
263
|
firstMockPty.emit('exit', { exitCode: 1 });
|
|
255
264
|
// Wait for fallback to occur
|
|
256
265
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
257
|
-
// Verify fallback spawn was called
|
|
266
|
+
// Verify fallback spawn was called with the configured fallback args
|
|
258
267
|
expect(spawn).toHaveBeenCalledTimes(2);
|
|
259
|
-
expect(spawn).toHaveBeenNthCalledWith(2, 'claude', ['--teammate-mode', 'in-process'], expect.objectContaining({ cwd: '/test/worktree' }));
|
|
268
|
+
expect(spawn).toHaveBeenNthCalledWith(2, 'claude', ['--safe-flag', '--teammate-mode', 'in-process'], expect.objectContaining({ cwd: '/test/worktree' }));
|
|
260
269
|
// Verify session process was replaced
|
|
261
270
|
expect(session.process).toBe(secondMockPty);
|
|
271
|
+
expect(session.command).toBe('claude');
|
|
272
|
+
expect(session.fallbackArgs).toEqual(['--safe-flag']);
|
|
262
273
|
expect(session.isPrimaryCommand).toBe(false);
|
|
263
274
|
});
|
|
264
275
|
it('should not use fallback if main command succeeds', async () => {
|
|
@@ -310,8 +321,37 @@ describe('SessionManager', () => {
|
|
|
310
321
|
expect(spawn).toHaveBeenNthCalledWith(2, 'claude', ['--teammate-mode', 'in-process'], expect.objectContaining({ cwd: '/test/worktree' }));
|
|
311
322
|
// Verify session process was replaced
|
|
312
323
|
expect(session.process).toBe(secondMockPty);
|
|
324
|
+
expect(session.command).toBe('claude');
|
|
325
|
+
expect(session.fallbackArgs).toBeUndefined();
|
|
313
326
|
expect(session.isPrimaryCommand).toBe(false);
|
|
314
327
|
});
|
|
328
|
+
it('should cleanup and emit exit when fallback command also exits with code 1', async () => {
|
|
329
|
+
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
330
|
+
id: '1',
|
|
331
|
+
name: 'Main',
|
|
332
|
+
command: 'opencode',
|
|
333
|
+
args: ['run', '--bad-flag'],
|
|
334
|
+
fallbackArgs: ['run', '--safe-mode'],
|
|
335
|
+
detectionStrategy: 'opencode',
|
|
336
|
+
});
|
|
337
|
+
const firstMockPty = new MockPty();
|
|
338
|
+
const secondMockPty = new MockPty();
|
|
339
|
+
let exitedSession = null;
|
|
340
|
+
vi.mocked(spawn)
|
|
341
|
+
.mockReturnValueOnce(firstMockPty)
|
|
342
|
+
.mockReturnValueOnce(secondMockPty);
|
|
343
|
+
sessionManager.on('sessionExit', (session) => {
|
|
344
|
+
exitedSession = session;
|
|
345
|
+
});
|
|
346
|
+
const session = await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
|
|
347
|
+
firstMockPty.emit('exit', { exitCode: 1 });
|
|
348
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
349
|
+
secondMockPty.emit('exit', { exitCode: 1 });
|
|
350
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
351
|
+
expect(spawn).toHaveBeenNthCalledWith(2, 'opencode', ['run', '--safe-mode'], expect.objectContaining({ cwd: '/test/worktree' }));
|
|
352
|
+
expect(exitedSession).toBe(session);
|
|
353
|
+
expect(sessionManager.getSessionsForWorktree('/test/worktree')).toHaveLength(0);
|
|
354
|
+
});
|
|
315
355
|
it('should handle custom command configuration', async () => {
|
|
316
356
|
// Setup mock preset with custom command
|
|
317
357
|
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
@@ -388,24 +428,8 @@ describe('SessionManager', () => {
|
|
|
388
428
|
});
|
|
389
429
|
describe('createSessionWithDevcontainerEffect', () => {
|
|
390
430
|
beforeEach(() => {
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
mockExec.shouldFail = false;
|
|
394
|
-
// Setup exec mock to work with promisify
|
|
395
|
-
mockExec.mockImplementation(((...args) => {
|
|
396
|
-
const [command, , callback] = args;
|
|
397
|
-
if (callback) {
|
|
398
|
-
// Handle callback style
|
|
399
|
-
if (command.includes('devcontainer up')) {
|
|
400
|
-
if (mockExec.shouldFail) {
|
|
401
|
-
callback(new Error('Container startup failed'));
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
callback(null, '', '');
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}));
|
|
431
|
+
// Setup childSpawn mock to return a successful mock child process
|
|
432
|
+
vi.mocked(childSpawn).mockImplementation(() => createMockChildProcess(0));
|
|
409
433
|
});
|
|
410
434
|
it('should execute devcontainer up command before creating session', async () => {
|
|
411
435
|
// Setup mock preset
|
|
@@ -434,7 +458,7 @@ describe('SessionManager', () => {
|
|
|
434
458
|
'--resume',
|
|
435
459
|
'--teammate-mode',
|
|
436
460
|
'in-process',
|
|
437
|
-
], expect.objectContaining({ cwd: '/test/worktree' }));
|
|
461
|
+
], expect.objectContaining({ cwd: '/test/worktree', rawMode: false }));
|
|
438
462
|
});
|
|
439
463
|
it('should use specific preset when ID provided', async () => {
|
|
440
464
|
// Setup mock preset
|
|
@@ -465,15 +489,14 @@ describe('SessionManager', () => {
|
|
|
465
489
|
], expect.any(Object));
|
|
466
490
|
});
|
|
467
491
|
it('should throw error when devcontainer up fails', async () => {
|
|
468
|
-
// Setup
|
|
469
|
-
|
|
470
|
-
mockExec.shouldFail = true;
|
|
492
|
+
// Setup childSpawn to return a process that exits with code 1
|
|
493
|
+
vi.mocked(childSpawn).mockImplementation(() => createMockChildProcess(1));
|
|
471
494
|
// Create session with devcontainer
|
|
472
495
|
const devcontainerConfig = {
|
|
473
496
|
upCommand: 'devcontainer up',
|
|
474
497
|
execCommand: 'devcontainer exec',
|
|
475
498
|
};
|
|
476
|
-
await expect(Effect.runPromise(sessionManager.createSessionWithDevcontainerEffect('/test/worktree', devcontainerConfig))).rejects.toThrow('Failed to start devcontainer:
|
|
499
|
+
await expect(Effect.runPromise(sessionManager.createSessionWithDevcontainerEffect('/test/worktree', devcontainerConfig))).rejects.toThrow('Failed to start devcontainer: Command exited with code 1');
|
|
477
500
|
});
|
|
478
501
|
it('should create a new session each time for multi-session support', async () => {
|
|
479
502
|
// Setup mock preset
|
|
@@ -540,17 +563,6 @@ describe('SessionManager', () => {
|
|
|
540
563
|
});
|
|
541
564
|
// Setup spawn mock
|
|
542
565
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
543
|
-
const mockExec = vi.mocked(exec);
|
|
544
|
-
mockExec.mockImplementation((cmd, options, callback) => {
|
|
545
|
-
if (typeof options === 'function') {
|
|
546
|
-
callback = options;
|
|
547
|
-
options = undefined;
|
|
548
|
-
}
|
|
549
|
-
if (callback && typeof callback === 'function') {
|
|
550
|
-
callback(null, 'Container started', '');
|
|
551
|
-
}
|
|
552
|
-
return {};
|
|
553
|
-
});
|
|
554
566
|
await Effect.runPromise(sessionManager.createSessionWithDevcontainerEffect('/test/worktree2', {
|
|
555
567
|
upCommand: 'devcontainer up --workspace-folder .',
|
|
556
568
|
execCommand: 'devcontainer exec --workspace-folder .',
|
|
@@ -566,20 +578,10 @@ describe('SessionManager', () => {
|
|
|
566
578
|
'in-process',
|
|
567
579
|
], expect.objectContaining({
|
|
568
580
|
cwd: '/test/worktree2',
|
|
581
|
+
rawMode: false,
|
|
569
582
|
}));
|
|
570
583
|
});
|
|
571
584
|
it('should use preset with devcontainer', async () => {
|
|
572
|
-
const mockExec = vi.mocked(exec);
|
|
573
|
-
mockExec.mockImplementation((cmd, options, callback) => {
|
|
574
|
-
if (typeof options === 'function') {
|
|
575
|
-
callback = options;
|
|
576
|
-
options = undefined;
|
|
577
|
-
}
|
|
578
|
-
if (callback && typeof callback === 'function') {
|
|
579
|
-
callback(null, 'Container started', '');
|
|
580
|
-
}
|
|
581
|
-
return {};
|
|
582
|
-
});
|
|
583
585
|
await Effect.runPromise(sessionManager.createSessionWithDevcontainerEffect('/test/worktree', {
|
|
584
586
|
upCommand: 'devcontainer up --workspace-folder .',
|
|
585
587
|
execCommand: 'devcontainer exec --workspace-folder .',
|
|
@@ -594,17 +596,6 @@ describe('SessionManager', () => {
|
|
|
594
596
|
});
|
|
595
597
|
});
|
|
596
598
|
it('should parse exec command and append preset command', async () => {
|
|
597
|
-
const mockExec = vi.mocked(exec);
|
|
598
|
-
mockExec.mockImplementation((cmd, options, callback) => {
|
|
599
|
-
if (typeof options === 'function') {
|
|
600
|
-
callback = options;
|
|
601
|
-
options = undefined;
|
|
602
|
-
}
|
|
603
|
-
if (callback && typeof callback === 'function') {
|
|
604
|
-
callback(null, 'Container started', '');
|
|
605
|
-
}
|
|
606
|
-
return {};
|
|
607
|
-
});
|
|
608
599
|
const config = {
|
|
609
600
|
upCommand: 'devcontainer up --workspace-folder /path/to/project',
|
|
610
601
|
execCommand: 'devcontainer exec --workspace-folder /path/to/project --user vscode',
|
|
@@ -623,17 +614,6 @@ describe('SessionManager', () => {
|
|
|
623
614
|
], expect.any(Object));
|
|
624
615
|
});
|
|
625
616
|
it('should handle preset with args in devcontainer', async () => {
|
|
626
|
-
const mockExec = vi.mocked(exec);
|
|
627
|
-
mockExec.mockImplementation((cmd, options, callback) => {
|
|
628
|
-
if (typeof options === 'function') {
|
|
629
|
-
callback = options;
|
|
630
|
-
options = undefined;
|
|
631
|
-
}
|
|
632
|
-
if (callback && typeof callback === 'function') {
|
|
633
|
-
callback(null, 'Container started', '');
|
|
634
|
-
}
|
|
635
|
-
return {};
|
|
636
|
-
});
|
|
637
617
|
vi.mocked(configReader.getPresetByIdEffect).mockReturnValue(Either.right({
|
|
638
618
|
id: 'claude-with-args',
|
|
639
619
|
name: 'Claude with Args',
|
|
@@ -657,17 +637,6 @@ describe('SessionManager', () => {
|
|
|
657
637
|
], expect.any(Object));
|
|
658
638
|
});
|
|
659
639
|
it('should use empty args as fallback in devcontainer when no fallback args specified', async () => {
|
|
660
|
-
const mockExec = vi.mocked(exec);
|
|
661
|
-
mockExec.mockImplementation((cmd, options, callback) => {
|
|
662
|
-
if (typeof options === 'function') {
|
|
663
|
-
callback = options;
|
|
664
|
-
options = undefined;
|
|
665
|
-
}
|
|
666
|
-
if (callback && typeof callback === 'function') {
|
|
667
|
-
callback(null, 'Container started', '');
|
|
668
|
-
}
|
|
669
|
-
return {};
|
|
670
|
-
});
|
|
671
640
|
// Setup preset without fallback args
|
|
672
641
|
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
673
642
|
id: '1',
|
|
@@ -713,29 +682,19 @@ describe('SessionManager', () => {
|
|
|
713
682
|
'claude',
|
|
714
683
|
'--teammate-mode',
|
|
715
684
|
'in-process',
|
|
716
|
-
], expect.objectContaining({ cwd: '/test/worktree' }));
|
|
685
|
+
], expect.objectContaining({ cwd: '/test/worktree', rawMode: false }));
|
|
717
686
|
// Verify session process was replaced
|
|
718
687
|
expect(session.process).toBe(secondMockPty);
|
|
719
688
|
expect(session.isPrimaryCommand).toBe(false);
|
|
720
689
|
});
|
|
721
|
-
it('should
|
|
722
|
-
const mockExec = vi.mocked(exec);
|
|
723
|
-
mockExec.mockImplementation((cmd, options, callback) => {
|
|
724
|
-
if (typeof options === 'function') {
|
|
725
|
-
callback = options;
|
|
726
|
-
options = undefined;
|
|
727
|
-
}
|
|
728
|
-
if (callback && typeof callback === 'function') {
|
|
729
|
-
callback(null, 'Container started', '');
|
|
730
|
-
}
|
|
731
|
-
return {};
|
|
732
|
-
});
|
|
690
|
+
it('should retry the configured command with fallback args in devcontainer when primary command exits with code 1', async () => {
|
|
733
691
|
// Setup preset with args
|
|
734
692
|
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
735
693
|
id: '1',
|
|
736
694
|
name: 'Main',
|
|
737
695
|
command: 'claude',
|
|
738
696
|
args: ['--bad-flag'],
|
|
697
|
+
fallbackArgs: ['--safe-flag'],
|
|
739
698
|
});
|
|
740
699
|
// First spawn attempt - will exit with code 1
|
|
741
700
|
const firstMockPty = new MockPty();
|
|
@@ -764,7 +723,7 @@ describe('SessionManager', () => {
|
|
|
764
723
|
firstMockPty.emit('exit', { exitCode: 1 });
|
|
765
724
|
// Wait for fallback to occur
|
|
766
725
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
767
|
-
// Verify fallback spawn was called with
|
|
726
|
+
// Verify fallback spawn was called with the configured fallback args
|
|
768
727
|
expect(spawn).toHaveBeenCalledTimes(2);
|
|
769
728
|
expect(spawn).toHaveBeenNthCalledWith(2, 'devcontainer', [
|
|
770
729
|
'exec',
|
|
@@ -772,9 +731,10 @@ describe('SessionManager', () => {
|
|
|
772
731
|
'.',
|
|
773
732
|
'--',
|
|
774
733
|
'claude',
|
|
734
|
+
'--safe-flag',
|
|
775
735
|
'--teammate-mode',
|
|
776
736
|
'in-process',
|
|
777
|
-
], expect.objectContaining({ cwd: '/test/worktree' }));
|
|
737
|
+
], expect.objectContaining({ cwd: '/test/worktree', rawMode: false }));
|
|
778
738
|
// Verify session process was replaced
|
|
779
739
|
expect(session.process).toBe(secondMockPty);
|
|
780
740
|
expect(session.isPrimaryCommand).toBe(false);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { BaseStateDetector } from './base.js';
|
|
2
|
+
// Spinner symbols used by Cursor during active processing
|
|
3
|
+
const CURSOR_SPINNER_CHARS = '⬡⬢';
|
|
4
|
+
// Like Claude's spinner activity: "<symbol> <word>ing…"; Cursor often uses ASCII dots (.. or …)
|
|
5
|
+
const SPINNER_ACTIVITY_PATTERN = new RegExp(`^\\s*[${CURSOR_SPINNER_CHARS}] \\S+ing(?:.*\u2026|.*\\.{2,})`, 'm');
|
|
2
6
|
export class CursorStateDetector extends BaseStateDetector {
|
|
3
7
|
detectState(terminal, _currentState) {
|
|
4
8
|
const content = this.getTerminalContent(terminal, 30);
|
|
@@ -9,13 +13,21 @@ export class CursorStateDetector extends BaseStateDetector {
|
|
|
9
13
|
/auto .* \(shift\+tab\)/.test(lowerContent) ||
|
|
10
14
|
/allow .+ \(y\)/.test(lowerContent) ||
|
|
11
15
|
/run .+ \(y\)/.test(lowerContent) ||
|
|
12
|
-
lowerContent.includes('skip (esc or n)')
|
|
16
|
+
lowerContent.includes('skip (esc or n)') ||
|
|
17
|
+
lowerContent.includes('write to this file?') ||
|
|
18
|
+
lowerContent.includes('reject & propose changes') ||
|
|
19
|
+
(lowerContent.includes('add write(') &&
|
|
20
|
+
lowerContent.includes('allowlist'))) {
|
|
13
21
|
return 'waiting_input';
|
|
14
22
|
}
|
|
15
23
|
// Check for busy state - Priority 2
|
|
16
24
|
if (lowerContent.includes('ctrl+c to stop')) {
|
|
17
25
|
return 'busy';
|
|
18
26
|
}
|
|
27
|
+
// Spinner activity (e.g. "⬡ Grepping..", "⬢ Reading…") — case-sensitive on original buffer
|
|
28
|
+
if (SPINNER_ACTIVITY_PATTERN.test(content)) {
|
|
29
|
+
return 'busy';
|
|
30
|
+
}
|
|
19
31
|
// Otherwise idle - Priority 3
|
|
20
32
|
return 'idle';
|
|
21
33
|
}
|
|
@@ -114,6 +114,21 @@ describe('CursorStateDetector', () => {
|
|
|
114
114
|
// Assert
|
|
115
115
|
expect(state).toBe('waiting_input');
|
|
116
116
|
});
|
|
117
|
+
it('should detect waiting_input state for Write to this file? prompt', () => {
|
|
118
|
+
// Arrange
|
|
119
|
+
terminal = createMockTerminal([
|
|
120
|
+
' │ Write to this file? │',
|
|
121
|
+
' │ in /Users/kbwo/go/projects/github.com/kbwo/ccmanager--feature-takt/src/services/stateDetector/takt.ts │',
|
|
122
|
+
' │ → Proceed (y) │',
|
|
123
|
+
' │ Reject & propose changes (esc or n or p) │',
|
|
124
|
+
' │ Add Write(/Users/.../takt.ts) to allowlist? (tab) │',
|
|
125
|
+
' │ Run Everything (shift+tab) │',
|
|
126
|
+
]);
|
|
127
|
+
// Act
|
|
128
|
+
const state = detector.detectState(terminal, 'idle');
|
|
129
|
+
// Assert
|
|
130
|
+
expect(state).toBe('waiting_input');
|
|
131
|
+
});
|
|
117
132
|
it('should detect busy state for ctrl+c to stop pattern', () => {
|
|
118
133
|
// Arrange
|
|
119
134
|
terminal = createMockTerminal([
|
|
@@ -138,6 +153,18 @@ describe('CursorStateDetector', () => {
|
|
|
138
153
|
// Assert
|
|
139
154
|
expect(state).toBe('busy');
|
|
140
155
|
});
|
|
156
|
+
it('should detect busy state for spinner activity (⬡ …ing..)', () => {
|
|
157
|
+
terminal = createMockTerminal([' ⬡ Grepping..', 'Some footer']);
|
|
158
|
+
expect(detector.detectState(terminal, 'idle')).toBe('busy');
|
|
159
|
+
});
|
|
160
|
+
it('should detect busy state for spinner activity (⬢ …ing...)', () => {
|
|
161
|
+
terminal = createMockTerminal([' ⬢ Reading...']);
|
|
162
|
+
expect(detector.detectState(terminal, 'idle')).toBe('busy');
|
|
163
|
+
});
|
|
164
|
+
it('should detect busy state for spinner activity with Unicode ellipsis', () => {
|
|
165
|
+
terminal = createMockTerminal(['⬡ Searching\u2026']);
|
|
166
|
+
expect(detector.detectState(terminal, 'idle')).toBe('busy');
|
|
167
|
+
});
|
|
141
168
|
it('should detect idle state when no patterns match', () => {
|
|
142
169
|
// Arrange
|
|
143
170
|
terminal = createMockTerminal(['Normal output', 'Some message', 'Ready']);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -377,6 +377,8 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
377
377
|
id: 'test-session-123',
|
|
378
378
|
worktreePath: tmpDir, // Use tmpDir as the worktree path
|
|
379
379
|
sessionNumber: 1,
|
|
380
|
+
command: 'claude',
|
|
381
|
+
fallbackArgs: undefined,
|
|
380
382
|
lastAccessedAt: Date.now(),
|
|
381
383
|
process: {},
|
|
382
384
|
terminal: {},
|
|
@@ -434,6 +436,8 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
434
436
|
id: 'test-session-456',
|
|
435
437
|
worktreePath: tmpDir, // Use tmpDir as the worktree path
|
|
436
438
|
sessionNumber: 1,
|
|
439
|
+
command: 'claude',
|
|
440
|
+
fallbackArgs: undefined,
|
|
437
441
|
lastAccessedAt: Date.now(),
|
|
438
442
|
process: {},
|
|
439
443
|
terminal: {},
|
|
@@ -489,6 +493,8 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
489
493
|
id: 'test-session-789',
|
|
490
494
|
worktreePath: tmpDir, // Use tmpDir as the worktree path
|
|
491
495
|
sessionNumber: 1,
|
|
496
|
+
command: 'claude',
|
|
497
|
+
fallbackArgs: undefined,
|
|
492
498
|
lastAccessedAt: Date.now(),
|
|
493
499
|
process: {},
|
|
494
500
|
terminal: {},
|
|
@@ -546,6 +552,8 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
546
552
|
id: 'test-session-failure',
|
|
547
553
|
worktreePath: tmpDir,
|
|
548
554
|
sessionNumber: 1,
|
|
555
|
+
command: 'claude',
|
|
556
|
+
fallbackArgs: undefined,
|
|
549
557
|
lastAccessedAt: Date.now(),
|
|
550
558
|
process: {},
|
|
551
559
|
terminal: {},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.4",
|
|
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.0.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "4.0.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "4.0.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "4.0.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "4.0.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "4.0.4",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "4.0.4",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "4.0.4",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "4.0.4",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "4.0.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|