gitspace 0.2.0-rc.14 → 0.2.0-rc.16
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/bun.lock +5 -5
- package/package.json +5 -5
- package/src/app/session/useWorkspaceDeleteFlow.ts +156 -0
- package/src/app.tui.tsx +37 -19
- package/src/app.web.tsx +85 -8
- package/src/commands/remove.ts +26 -4
- package/src/commands/serve.ts +20 -0
- package/src/components/RemoteMachineScreen.tui.tsx +85 -8
- package/src/components/SessionTerminal.tui.tsx +34 -4
- package/src/components/SessionTerminal.web.tsx +144 -3
- package/src/components/SpacesBrowser.tsx +9 -12
- package/src/components/session-terminal-page-navigation.ts +48 -0
- package/src/core/__tests__/workspace-lifecycle.test.ts +23 -0
- package/src/core/secret-runtime.ts +105 -0
- package/src/core/workspace-lifecycle.ts +24 -2
- package/src/core/workspace.ts +64 -21
- package/src/hooks/__tests__/useLocalSession.tui.test.ts +11 -2
- package/src/hooks/useLocalSession.tui.ts +7 -2
- package/src/index.ts +31 -13
- package/src/lib/remote-session/protocol.ts +3 -1
- package/src/lib/remote-session/session-handler.ts +105 -29
- package/src/session/__tests__/backend-manager.test.ts +11 -2
- package/src/session/__tests__/local-session-backend.test.ts +119 -0
- package/src/session/__tests__/remote-session-backend.test.ts +324 -0
- package/src/session/__tests__/session-name.test.ts +35 -0
- package/src/session/__tests__/useRemoteSessionClient.test.ts +10 -2
- package/src/session/backend.ts +14 -1
- package/src/session/backends/local-session-backend.ts +103 -15
- package/src/session/backends/remote-session-backend.ts +146 -7
- package/src/session/index.ts +1 -0
- package/src/session/session-name.ts +50 -0
- package/src/session/useRemoteSessionClient.ts +19 -7
- package/src/session/useSessionEngine.ts +41 -3
- package/src/tui/__tests__/session-terminal-page-navigation.test.ts +94 -0
- package/src/types/errors.ts +45 -0
- package/src/utils/__tests__/workspace-setup.integration.test.ts +44 -0
package/bun.lock
CHANGED
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"typescript": "^5.9.3",
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@gitspace/darwin-arm64": "0.2.0-rc.
|
|
43
|
-
"@gitspace/darwin-x64": "0.2.0-rc.
|
|
44
|
-
"@gitspace/linux-arm64": "0.2.0-rc.
|
|
45
|
-
"@gitspace/linux-x64": "0.2.0-rc.
|
|
42
|
+
"@gitspace/darwin-arm64": "0.2.0-rc.15",
|
|
43
|
+
"@gitspace/darwin-x64": "0.2.0-rc.15",
|
|
44
|
+
"@gitspace/linux-arm64": "0.2.0-rc.15",
|
|
45
|
+
"@gitspace/linux-x64": "0.2.0-rc.15",
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
},
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
|
|
64
64
|
"@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="],
|
|
65
65
|
|
|
66
|
-
"@gitspace/darwin-arm64": ["@gitspace/darwin-arm64@0.2.0-rc.
|
|
66
|
+
"@gitspace/darwin-arm64": ["@gitspace/darwin-arm64@0.2.0-rc.15", "", { "os": "darwin", "cpu": "arm64", "bin": { "gssh-darwin-arm64": "bin/gssh" } }, "sha512-gjqsRdz2f7cOOTbj97wmQ4Z0nh7jWUVN4Dxj1N/Ex4fp6dJ8qu2d5izbsCHWVIPX4uN+gK/qqzxuwLwCdaEqlQ=="],
|
|
67
67
|
|
|
68
68
|
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
|
69
69
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitspace",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.16",
|
|
4
4
|
"description": "CLI for managing GitHub workspaces with git worktrees and secure remote terminal access",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gssh": "./bin/gssh"
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"relay": "bun src/relay/index.ts"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@gitspace/darwin-arm64": "0.2.0-rc.
|
|
21
|
-
"@gitspace/darwin-x64": "0.2.0-rc.
|
|
22
|
-
"@gitspace/linux-x64": "0.2.0-rc.
|
|
23
|
-
"@gitspace/linux-arm64": "0.2.0-rc.
|
|
20
|
+
"@gitspace/darwin-arm64": "0.2.0-rc.16",
|
|
21
|
+
"@gitspace/darwin-x64": "0.2.0-rc.16",
|
|
22
|
+
"@gitspace/linux-x64": "0.2.0-rc.16",
|
|
23
|
+
"@gitspace/linux-arm64": "0.2.0-rc.16"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"cli",
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import type { UseFlowReturn } from '../../components/Flow.js';
|
|
3
|
+
import type { DeleteWorkspaceParams } from '../../session/backend.js';
|
|
4
|
+
|
|
5
|
+
export interface WorkspaceDeleteTarget {
|
|
6
|
+
projectName: string;
|
|
7
|
+
workspaceId: string;
|
|
8
|
+
workspaceName: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WorkspaceDeleteContext {
|
|
12
|
+
target: WorkspaceDeleteTarget;
|
|
13
|
+
params: DeleteWorkspaceParams;
|
|
14
|
+
isRetry: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WorkspaceDeleteErrorContext extends WorkspaceDeleteContext {
|
|
18
|
+
error: unknown;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseWorkspaceDeleteFlowOptions {
|
|
23
|
+
flow: Pick<UseFlowReturn, 'showLoading' | 'showConfirm' | 'showMessage' | 'close'>;
|
|
24
|
+
deleteWorkspace: (
|
|
25
|
+
projectName: string,
|
|
26
|
+
workspaceId: string,
|
|
27
|
+
params?: DeleteWorkspaceParams
|
|
28
|
+
) => Promise<void>;
|
|
29
|
+
onBeforeDelete?: (context: WorkspaceDeleteContext) => void | Promise<void>;
|
|
30
|
+
onDeleteSuccess?: (context: WorkspaceDeleteContext) => void | Promise<void>;
|
|
31
|
+
onDeleteCancelled?: (context: WorkspaceDeleteContext) => void | Promise<void>;
|
|
32
|
+
onDeleteError?: (context: WorkspaceDeleteErrorContext) => void | Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UseWorkspaceDeleteFlowResult {
|
|
36
|
+
deleteWorkspaceWithPrompt: (target: WorkspaceDeleteTarget) => Promise<boolean>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getErrorCode(error: unknown): string | undefined {
|
|
40
|
+
if (!error || typeof error !== 'object') {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const candidate = error as { code?: unknown };
|
|
45
|
+
return typeof candidate.code === 'string' ? candidate.code : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toErrorMessage(error: unknown): string {
|
|
49
|
+
if (error instanceof Error && error.message) {
|
|
50
|
+
return error.message;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof error === 'string' && error.length > 0) {
|
|
54
|
+
return error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return 'Failed to delete workspace';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function promptRemoveScriptFailure(
|
|
61
|
+
flow: UseWorkspaceDeleteFlowOptions['flow'],
|
|
62
|
+
workspaceName: string,
|
|
63
|
+
message: string
|
|
64
|
+
): Promise<boolean> {
|
|
65
|
+
return new Promise<boolean>((resolve) => {
|
|
66
|
+
flow.showConfirm({
|
|
67
|
+
title: 'Remove Scripts Failed',
|
|
68
|
+
message:
|
|
69
|
+
`Cleanup scripts failed for workspace "${workspaceName}".\n\n${message}\n\nRemove anyway and skip cleanup scripts?`,
|
|
70
|
+
variant: 'warning',
|
|
71
|
+
confirmLabel: 'Remove anyway',
|
|
72
|
+
cancelLabel: 'Keep workspace',
|
|
73
|
+
onConfirm: () => resolve(true),
|
|
74
|
+
onCancel: () => resolve(false),
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function useWorkspaceDeleteFlow(
|
|
80
|
+
options: UseWorkspaceDeleteFlowOptions
|
|
81
|
+
): UseWorkspaceDeleteFlowResult {
|
|
82
|
+
const {
|
|
83
|
+
flow,
|
|
84
|
+
deleteWorkspace,
|
|
85
|
+
onBeforeDelete,
|
|
86
|
+
onDeleteSuccess,
|
|
87
|
+
onDeleteCancelled,
|
|
88
|
+
onDeleteError,
|
|
89
|
+
} = options;
|
|
90
|
+
|
|
91
|
+
const executeDelete = useCallback(async (
|
|
92
|
+
target: WorkspaceDeleteTarget,
|
|
93
|
+
params: DeleteWorkspaceParams,
|
|
94
|
+
isRetry: boolean
|
|
95
|
+
): Promise<boolean> => {
|
|
96
|
+
const context: WorkspaceDeleteContext = {
|
|
97
|
+
target,
|
|
98
|
+
params,
|
|
99
|
+
isRetry,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
await onBeforeDelete?.(context);
|
|
103
|
+
flow.showLoading({
|
|
104
|
+
title: 'Deleting Workspace',
|
|
105
|
+
message:
|
|
106
|
+
params.scriptPolicy === 'skip'
|
|
107
|
+
? 'Removing workspace without cleanup scripts...'
|
|
108
|
+
: `Running cleanup scripts for "${target.workspaceName}"...`,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await deleteWorkspace(target.projectName, target.workspaceId, params);
|
|
113
|
+
flow.close();
|
|
114
|
+
await onDeleteSuccess?.(context);
|
|
115
|
+
return true;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const message = toErrorMessage(error);
|
|
118
|
+
const code = getErrorCode(error);
|
|
119
|
+
|
|
120
|
+
if (code === 'REMOVE_SCRIPT_FAILED' && params.scriptPolicy !== 'skip') {
|
|
121
|
+
const removeAnyway = await promptRemoveScriptFailure(flow, target.workspaceName, message);
|
|
122
|
+
if (!removeAnyway) {
|
|
123
|
+
await onDeleteCancelled?.(context);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return executeDelete(target, { scriptPolicy: 'skip' }, true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
flow.close();
|
|
131
|
+
if (onDeleteError) {
|
|
132
|
+
await onDeleteError({
|
|
133
|
+
...context,
|
|
134
|
+
error,
|
|
135
|
+
message,
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
flow.showMessage({
|
|
139
|
+
title: 'Delete Failed',
|
|
140
|
+
message,
|
|
141
|
+
variant: 'error',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}, [deleteWorkspace, flow, onBeforeDelete, onDeleteCancelled, onDeleteError, onDeleteSuccess]);
|
|
148
|
+
|
|
149
|
+
const deleteWorkspaceWithPrompt = useCallback(async (target: WorkspaceDeleteTarget): Promise<boolean> => {
|
|
150
|
+
return executeDelete(target, { scriptPolicy: 'auto' }, false);
|
|
151
|
+
}, [executeDelete]);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
deleteWorkspaceWithPrompt,
|
|
155
|
+
};
|
|
156
|
+
}
|
package/src/app.tui.tsx
CHANGED
|
@@ -86,6 +86,8 @@ import { useLocalSession } from './hooks/useLocalSession.tui.js';
|
|
|
86
86
|
import { useUserActivity } from './hooks/index.js';
|
|
87
87
|
import { useBundleRefreshAttachFlow } from './session/index.js';
|
|
88
88
|
import { useAttachController } from './app/session/useAttachController.js';
|
|
89
|
+
import { useWorkspaceDeleteFlow } from './app/session/useWorkspaceDeleteFlow.js';
|
|
90
|
+
import { initializeSecretRuntime } from './core/secret-runtime.js';
|
|
89
91
|
import {
|
|
90
92
|
resolveInboxCommand,
|
|
91
93
|
resolveMachineListCommand,
|
|
@@ -438,6 +440,27 @@ function App({ relayConfig, onQuit }: AppProps) {
|
|
|
438
440
|
},
|
|
439
441
|
});
|
|
440
442
|
|
|
443
|
+
const { deleteWorkspaceWithPrompt } = useWorkspaceDeleteFlow({
|
|
444
|
+
flow,
|
|
445
|
+
deleteWorkspace: deleteLocalWorkspace,
|
|
446
|
+
onBeforeDelete: ({ target }) => {
|
|
447
|
+
setScriptWorkspaceName(target.workspaceName);
|
|
448
|
+
dispatch({ type: 'SET_VIEW', view: 'scripts' });
|
|
449
|
+
},
|
|
450
|
+
onDeleteSuccess: async () => {
|
|
451
|
+
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
452
|
+
await refreshWorkspaces();
|
|
453
|
+
},
|
|
454
|
+
onDeleteError: async ({ message }) => {
|
|
455
|
+
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
456
|
+
flow.showMessage({
|
|
457
|
+
title: 'Delete Failed',
|
|
458
|
+
message,
|
|
459
|
+
variant: 'error',
|
|
460
|
+
});
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
|
|
441
464
|
// Daemon status hook (tmux-lite and serve)
|
|
442
465
|
const { status: daemonStatus } = useDaemonStatus({ pollInterval: 5000 });
|
|
443
466
|
|
|
@@ -600,26 +623,14 @@ function App({ relayConfig, onQuit }: AppProps) {
|
|
|
600
623
|
warning: workspace.sessionCount > 0 ? `This will kill ${workspace.sessionCount} active session(s)!` : undefined,
|
|
601
624
|
onConfirm: async () => {
|
|
602
625
|
if (!currentProject) return;
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
}
|
|
608
|
-
console.error('[tui] Failed to delete workspace:', error);
|
|
609
|
-
flow.close();
|
|
610
|
-
flow.showMessage({
|
|
611
|
-
title: 'Delete Failed',
|
|
612
|
-
message: error instanceof Error ? error.message : `Failed to delete workspace "${workspace.name}".`,
|
|
613
|
-
variant: 'error',
|
|
614
|
-
});
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
flow.close();
|
|
619
|
-
await refreshWorkspaces();
|
|
626
|
+
await deleteWorkspaceWithPrompt({
|
|
627
|
+
projectName: currentProject,
|
|
628
|
+
workspaceId: workspace.id,
|
|
629
|
+
workspaceName: workspace.name,
|
|
630
|
+
});
|
|
620
631
|
},
|
|
621
632
|
});
|
|
622
|
-
}, [currentProject, flow,
|
|
633
|
+
}, [currentProject, flow, deleteWorkspaceWithPrompt]);
|
|
623
634
|
|
|
624
635
|
// Delete session
|
|
625
636
|
const handleDeleteSession = useCallback((sessionId: string, sessionName: string) => {
|
|
@@ -2561,7 +2572,14 @@ function StatusBar({ hint }: { hint: string }) {
|
|
|
2561
2572
|
/** @deprecated Use RelayConfig instead */
|
|
2562
2573
|
export type TUIRelayConfig = RelayConfig;
|
|
2563
2574
|
|
|
2564
|
-
export async function launchTUI(
|
|
2575
|
+
export async function launchTUI(
|
|
2576
|
+
relayConfig?: RelayConfig,
|
|
2577
|
+
options: { ignoreKeychainAndSkipSecrets?: boolean } = {}
|
|
2578
|
+
): Promise<void> {
|
|
2579
|
+
await initializeSecretRuntime({
|
|
2580
|
+
ignoreKeychainAndSkipSecrets: options.ignoreKeychainAndSkipSecrets,
|
|
2581
|
+
});
|
|
2582
|
+
|
|
2565
2583
|
const renderer = await createCliRenderer({
|
|
2566
2584
|
exitOnCtrlC: false,
|
|
2567
2585
|
targetFps: 30,
|
package/src/app.web.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import { applyDeviceClasses, isMobileLayout, isTouchDevice } from "./utils/devic
|
|
|
18
18
|
import { useUserActivity } from "./hooks/index.js";
|
|
19
19
|
import { useBundleRefreshAttachFlow } from './session/useBundleRefreshAttachFlow.js';
|
|
20
20
|
import { useAttachController } from './app/session/useAttachController.js';
|
|
21
|
+
import { useWorkspaceDeleteFlow } from './app/session/useWorkspaceDeleteFlow.js';
|
|
21
22
|
|
|
22
23
|
// Import shared components and hooks
|
|
23
24
|
import {
|
|
@@ -47,6 +48,16 @@ import {
|
|
|
47
48
|
|
|
48
49
|
type View = "machines" | "terminal";
|
|
49
50
|
|
|
51
|
+
const PAGE_UP = '\x1b[5~';
|
|
52
|
+
const PAGE_DOWN = '\x1b[6~';
|
|
53
|
+
const DELETE_ERROR_CODES = new Set([
|
|
54
|
+
'REMOVE_SCRIPT_FAILED',
|
|
55
|
+
'DELETE_FAILED',
|
|
56
|
+
'WORKSPACE_NOT_FOUND',
|
|
57
|
+
'RESOURCE_NOT_FOUND',
|
|
58
|
+
'NOT_FOUND',
|
|
59
|
+
]);
|
|
60
|
+
|
|
50
61
|
export default function App() {
|
|
51
62
|
const [view, setView] = useState<View>("machines");
|
|
52
63
|
const [selectedMachine, setSelectedMachine] = useState<MachineInfo | null>(null);
|
|
@@ -68,6 +79,7 @@ export default function App() {
|
|
|
68
79
|
const terminalRef = useRef<SessionTerminalHandle>(null);
|
|
69
80
|
const lastScriptErrorRef = useRef<string | null>(null);
|
|
70
81
|
const lastCommandErrorRef = useRef<string | null>(null);
|
|
82
|
+
const suppressDeleteScriptFailureModalRef = useRef(false);
|
|
71
83
|
|
|
72
84
|
// Invite params from URL
|
|
73
85
|
const [inviteParams, setInviteParams] = useState<{
|
|
@@ -171,6 +183,36 @@ export default function App() {
|
|
|
171
183
|
},
|
|
172
184
|
});
|
|
173
185
|
|
|
186
|
+
const { deleteWorkspaceWithPrompt } = useWorkspaceDeleteFlow({
|
|
187
|
+
flow,
|
|
188
|
+
deleteWorkspace: terminal.deleteWorkspace,
|
|
189
|
+
onBeforeDelete: ({ target }) => {
|
|
190
|
+
suppressDeleteScriptFailureModalRef.current = true;
|
|
191
|
+
setShowInbox(false);
|
|
192
|
+
setScriptWorkspaceName(target.workspaceName);
|
|
193
|
+
setShowScriptTerminal(true);
|
|
194
|
+
},
|
|
195
|
+
onDeleteSuccess: async () => {
|
|
196
|
+
suppressDeleteScriptFailureModalRef.current = false;
|
|
197
|
+
setShowScriptTerminal(false);
|
|
198
|
+
terminal.requestWorkspaces();
|
|
199
|
+
terminal.requestSessions();
|
|
200
|
+
},
|
|
201
|
+
onDeleteCancelled: async () => {
|
|
202
|
+
suppressDeleteScriptFailureModalRef.current = false;
|
|
203
|
+
setShowScriptTerminal(false);
|
|
204
|
+
},
|
|
205
|
+
onDeleteError: async ({ message }) => {
|
|
206
|
+
suppressDeleteScriptFailureModalRef.current = false;
|
|
207
|
+
setShowScriptTerminal(false);
|
|
208
|
+
flow.showMessage({
|
|
209
|
+
title: 'Delete Failed',
|
|
210
|
+
message,
|
|
211
|
+
variant: 'error',
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
174
216
|
useEffect(() => {
|
|
175
217
|
let mounted = true;
|
|
176
218
|
void browserPreferencesService.getNotificationConfig().then((config) => {
|
|
@@ -232,6 +274,11 @@ export default function App() {
|
|
|
232
274
|
return;
|
|
233
275
|
}
|
|
234
276
|
|
|
277
|
+
if (suppressDeleteScriptFailureModalRef.current) {
|
|
278
|
+
lastScriptErrorRef.current = scriptError;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
235
282
|
if (lastScriptErrorRef.current === scriptError) {
|
|
236
283
|
return;
|
|
237
284
|
}
|
|
@@ -256,11 +303,20 @@ export default function App() {
|
|
|
256
303
|
}
|
|
257
304
|
lastCommandErrorRef.current = key;
|
|
258
305
|
|
|
306
|
+
if (
|
|
307
|
+
suppressDeleteScriptFailureModalRef.current &&
|
|
308
|
+
terminal.commandError.code &&
|
|
309
|
+
DELETE_ERROR_CODES.has(terminal.commandError.code)
|
|
310
|
+
) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
259
314
|
const isScriptFailure =
|
|
260
315
|
terminal.commandError.code === 'SCRIPT_FAILED' ||
|
|
261
316
|
terminal.commandError.code === 'PRE_SCRIPT_FAILED' ||
|
|
262
317
|
terminal.commandError.code === 'SETUP_SCRIPT_FAILED' ||
|
|
263
|
-
terminal.commandError.code === 'SELECT_SCRIPT_FAILED'
|
|
318
|
+
terminal.commandError.code === 'SELECT_SCRIPT_FAILED' ||
|
|
319
|
+
terminal.commandError.code === 'REMOVE_SCRIPT_FAILED';
|
|
264
320
|
|
|
265
321
|
if (isScriptFailure) {
|
|
266
322
|
if (!terminal.scriptState) {
|
|
@@ -367,12 +423,10 @@ export default function App() {
|
|
|
367
423
|
const spacesBrowserProps = useSpacesBrowser({
|
|
368
424
|
workspaces: terminal.workspaces,
|
|
369
425
|
sessions: terminal.sessions,
|
|
370
|
-
onRequestSessions: terminal.requestSessions,
|
|
426
|
+
onRequestSessions: () => terminal.requestSessions(),
|
|
371
427
|
onAttachSession: handleAttachSession,
|
|
372
428
|
onRefresh: terminal.requestWorkspaces,
|
|
373
|
-
onRefreshSessions: (
|
|
374
|
-
workspaceIds.forEach(id => terminal.requestSessions(id));
|
|
375
|
-
},
|
|
429
|
+
onRefreshSessions: () => terminal.requestSessions(),
|
|
376
430
|
onBack: handleBackToMachines,
|
|
377
431
|
machineName: selectedMachine?.label || selectedMachine?.machineId,
|
|
378
432
|
});
|
|
@@ -443,6 +497,7 @@ export default function App() {
|
|
|
443
497
|
if (view === "terminal" && terminal.status === "established" && terminal.mode === "browsing") {
|
|
444
498
|
terminal.requestProjects();
|
|
445
499
|
terminal.requestWorkspaces();
|
|
500
|
+
terminal.requestSessions();
|
|
446
501
|
terminal.requestNotificationConfig();
|
|
447
502
|
}
|
|
448
503
|
}, [
|
|
@@ -451,6 +506,7 @@ export default function App() {
|
|
|
451
506
|
terminal.mode,
|
|
452
507
|
terminal.requestProjects,
|
|
453
508
|
terminal.requestWorkspaces,
|
|
509
|
+
terminal.requestSessions,
|
|
454
510
|
terminal.requestNotificationConfig,
|
|
455
511
|
]);
|
|
456
512
|
|
|
@@ -591,8 +647,12 @@ export default function App() {
|
|
|
591
647
|
message: `Are you sure you want to delete workspace "${selected.workspace.name}"?`,
|
|
592
648
|
confirmText: selected.workspace.name,
|
|
593
649
|
warning: sessionCount > 0 ? `This will kill ${sessionCount} active session(s)!` : undefined,
|
|
594
|
-
onConfirm: () => {
|
|
595
|
-
|
|
650
|
+
onConfirm: async () => {
|
|
651
|
+
await deleteWorkspaceWithPrompt({
|
|
652
|
+
projectName: selected.workspace.projectName,
|
|
653
|
+
workspaceId: selected.workspace.id,
|
|
654
|
+
workspaceName: selected.workspace.name,
|
|
655
|
+
});
|
|
596
656
|
},
|
|
597
657
|
});
|
|
598
658
|
}
|
|
@@ -604,7 +664,16 @@ export default function App() {
|
|
|
604
664
|
|
|
605
665
|
window.addEventListener("keydown", handleKeyDown);
|
|
606
666
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
607
|
-
}, [
|
|
667
|
+
}, [
|
|
668
|
+
view,
|
|
669
|
+
terminal.status,
|
|
670
|
+
terminal.mode,
|
|
671
|
+
showInbox,
|
|
672
|
+
showScriptTerminal,
|
|
673
|
+
spacesBrowserProps,
|
|
674
|
+
flow,
|
|
675
|
+
deleteWorkspaceWithPrompt,
|
|
676
|
+
]);
|
|
608
677
|
|
|
609
678
|
// Attached terminal mode keyboard handler (Ctrl+Esc to detach)
|
|
610
679
|
useEffect(() => {
|
|
@@ -762,6 +831,14 @@ export default function App() {
|
|
|
762
831
|
if (view === "terminal" && terminal.status === "established" && terminal.mode === "attached") {
|
|
763
832
|
// Handler for sending data from mobile controls (already processed)
|
|
764
833
|
const handleSendData = (data: string) => {
|
|
834
|
+
if (data === PAGE_UP && terminalRef.current?.pageUp()) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (data === PAGE_DOWN && terminalRef.current?.pageDown()) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
765
842
|
terminal.send(new TextEncoder().encode(data));
|
|
766
843
|
};
|
|
767
844
|
|
package/src/commands/remove.ts
CHANGED
|
@@ -114,10 +114,32 @@ export async function removeWorkspace(
|
|
|
114
114
|
|
|
115
115
|
// Delegate to core deletion logic (interactive mode for CLI)
|
|
116
116
|
logger.info('Removing workspace...')
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
const runDelete = async (scriptPolicy: 'enforce' | 'skip') => {
|
|
118
|
+
return deleteWorkspaceCore(currentProject, workspaceName, {
|
|
119
|
+
nonInteractive: false, // CLI is interactive
|
|
120
|
+
keepBranch: options.keepBranch,
|
|
121
|
+
removeScriptPolicy: scriptPolicy,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let result = await runDelete('enforce')
|
|
126
|
+
|
|
127
|
+
if (!result.success && result.errorCode === 'REMOVE_SCRIPT_FAILED') {
|
|
128
|
+
logger.warning(result.error || 'Remove scripts failed')
|
|
129
|
+
const removeAnyway = options.force
|
|
130
|
+
? true
|
|
131
|
+
: await promptConfirm(
|
|
132
|
+
`Remove workspace "${workspaceName}" anyway and skip cleanup scripts?`,
|
|
133
|
+
false
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if (!removeAnyway) {
|
|
137
|
+
logger.info('Cancelled')
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
result = await runDelete('skip')
|
|
142
|
+
}
|
|
121
143
|
|
|
122
144
|
if (!result.success) {
|
|
123
145
|
throw new SpacesError(
|
package/src/commands/serve.ts
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
ensureServeDaemonDir,
|
|
62
62
|
type StatusResponse,
|
|
63
63
|
} from '../serve/daemon.js';
|
|
64
|
+
import { initializeSecretRuntime } from '../core/secret-runtime.js';
|
|
64
65
|
|
|
65
66
|
/** Package version for daemon status */
|
|
66
67
|
const PACKAGE_VERSION = '1.0.0';
|
|
@@ -463,6 +464,7 @@ function stopCloudflared(): void {
|
|
|
463
464
|
export async function serve(options: {
|
|
464
465
|
relay?: string;
|
|
465
466
|
relayPubkey?: string;
|
|
467
|
+
ignoreKeychainAndSkipSecrets?: boolean;
|
|
466
468
|
} = {}): Promise<void> {
|
|
467
469
|
// Step 1: Load machine identity
|
|
468
470
|
if (!keypairExists()) {
|
|
@@ -503,6 +505,10 @@ export async function serve(options: {
|
|
|
503
505
|
const entries = readAccessList();
|
|
504
506
|
accessList.import(entries);
|
|
505
507
|
|
|
508
|
+
await initializeSecretRuntime({
|
|
509
|
+
ignoreKeychainAndSkipSecrets: options.ignoreKeychainAndSkipSecrets,
|
|
510
|
+
});
|
|
511
|
+
|
|
506
512
|
// Step 3: Check for gitspace.sh hosting or explicit relay
|
|
507
513
|
const hostConfig = readHostConfig();
|
|
508
514
|
const relayUrl = options.relay; // No default - must use hosting or explicit --relay
|
|
@@ -1156,6 +1162,7 @@ export async function serveStart(options: {
|
|
|
1156
1162
|
relayPubkey?: string;
|
|
1157
1163
|
passwordStdin?: boolean;
|
|
1158
1164
|
foreground?: boolean;
|
|
1165
|
+
ignoreKeychainAndSkipSecrets?: boolean;
|
|
1159
1166
|
} = {}): Promise<void> {
|
|
1160
1167
|
// Check if already running
|
|
1161
1168
|
if (isServeRunning()) {
|
|
@@ -1234,6 +1241,9 @@ export async function serveStart(options: {
|
|
|
1234
1241
|
const serveArgs = ['serve', 'start', '--foreground'];
|
|
1235
1242
|
if (options.relay) serveArgs.push('--relay', options.relay);
|
|
1236
1243
|
if (options.relayPubkey) serveArgs.push('--relay-pubkey', options.relayPubkey);
|
|
1244
|
+
if (options.ignoreKeychainAndSkipSecrets) {
|
|
1245
|
+
serveArgs.push('--ignore-keychain-and-skip-secrets');
|
|
1246
|
+
}
|
|
1237
1247
|
serveArgs.push('--password-stdin');
|
|
1238
1248
|
|
|
1239
1249
|
// Build command: compiled binary runs directly, dev mode uses bun
|
|
@@ -1294,6 +1304,16 @@ export async function serveStart(options: {
|
|
|
1294
1304
|
const entries = readAccessList();
|
|
1295
1305
|
accessList.import(entries);
|
|
1296
1306
|
|
|
1307
|
+
try {
|
|
1308
|
+
await initializeSecretRuntime({
|
|
1309
|
+
ignoreKeychainAndSkipSecrets: options.ignoreKeychainAndSkipSecrets,
|
|
1310
|
+
});
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
stopStatusServer();
|
|
1313
|
+
cleanupServeFiles();
|
|
1314
|
+
throw error;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1297
1317
|
// Get config
|
|
1298
1318
|
const machineIdentity = readMachineIdentity();
|
|
1299
1319
|
const machineId = machineIdentity?.machineId ?? identity.id;
|