gitspace 0.2.0-rc.15 → 0.2.0-rc.17
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 +170 -0
- package/src/app.tui.tsx +52 -21
- package/src/app.web.tsx +77 -10
- package/src/commands/migrate.ts +52 -0
- package/src/commands/remove.ts +26 -4
- package/src/commands/serve.ts +20 -0
- package/src/components/RemoteMachineScreen.tui.tsx +116 -9
- package/src/components/SessionTerminal.web.tsx +82 -0
- package/src/components/SpacesBrowser.tsx +9 -12
- package/src/core/__tests__/workspace-lifecycle.test.ts +23 -0
- package/src/core/secret-runtime.ts +167 -0
- package/src/core/workspace-lifecycle.ts +24 -2
- package/src/core/workspace.ts +86 -23
- package/src/hooks/__tests__/useLocalSession.tui.test.ts +11 -2
- package/src/hooks/useLocalSession.tui.ts +7 -2
- package/src/index.ts +53 -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/useBundleRefreshAttachFlow.ts +6 -12
- package/src/session/useRemoteSessionClient.ts +19 -7
- package/src/session/useSessionEngine.ts +41 -3
- package/src/types/errors.ts +45 -0
- package/src/utils/__tests__/workspace-setup.integration.test.ts +70 -0
- package/src/utils/secrets.ts +489 -121
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.17",
|
|
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.17",
|
|
21
|
+
"@gitspace/darwin-x64": "0.2.0-rc.17",
|
|
22
|
+
"@gitspace/linux-x64": "0.2.0-rc.17",
|
|
23
|
+
"@gitspace/linux-arm64": "0.2.0-rc.17"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"cli",
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
showLoadingDuringDelete?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UseWorkspaceDeleteFlowResult {
|
|
37
|
+
deleteWorkspaceWithPrompt: (target: WorkspaceDeleteTarget) => Promise<boolean>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getErrorCode(error: unknown): string | undefined {
|
|
41
|
+
if (!error || typeof error !== 'object') {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const candidate = error as { code?: unknown };
|
|
46
|
+
return typeof candidate.code === 'string' ? candidate.code : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toErrorMessage(error: unknown): string {
|
|
50
|
+
if (error instanceof Error && error.message) {
|
|
51
|
+
return error.message;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof error === 'string' && error.length > 0) {
|
|
55
|
+
return error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return 'Failed to delete workspace';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function promptRemoveScriptFailure(
|
|
62
|
+
flow: UseWorkspaceDeleteFlowOptions['flow'],
|
|
63
|
+
workspaceName: string,
|
|
64
|
+
message: string
|
|
65
|
+
): Promise<boolean> {
|
|
66
|
+
return new Promise<boolean>((resolve) => {
|
|
67
|
+
flow.showConfirm({
|
|
68
|
+
title: 'Remove Scripts Failed',
|
|
69
|
+
message:
|
|
70
|
+
`Cleanup scripts failed for workspace "${workspaceName}".\n\n${message}\n\nRemove anyway and skip cleanup scripts?`,
|
|
71
|
+
variant: 'warning',
|
|
72
|
+
confirmLabel: 'Remove anyway',
|
|
73
|
+
cancelLabel: 'Keep workspace',
|
|
74
|
+
onConfirm: () => resolve(true),
|
|
75
|
+
onCancel: () => resolve(false),
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function useWorkspaceDeleteFlow(
|
|
81
|
+
options: UseWorkspaceDeleteFlowOptions
|
|
82
|
+
): UseWorkspaceDeleteFlowResult {
|
|
83
|
+
const {
|
|
84
|
+
flow,
|
|
85
|
+
deleteWorkspace,
|
|
86
|
+
onBeforeDelete,
|
|
87
|
+
onDeleteSuccess,
|
|
88
|
+
onDeleteCancelled,
|
|
89
|
+
onDeleteError,
|
|
90
|
+
showLoadingDuringDelete = false,
|
|
91
|
+
} = options;
|
|
92
|
+
|
|
93
|
+
const executeDelete = useCallback(async (
|
|
94
|
+
target: WorkspaceDeleteTarget,
|
|
95
|
+
params: DeleteWorkspaceParams,
|
|
96
|
+
isRetry: boolean
|
|
97
|
+
): Promise<boolean> => {
|
|
98
|
+
const context: WorkspaceDeleteContext = {
|
|
99
|
+
target,
|
|
100
|
+
params,
|
|
101
|
+
isRetry,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
await onBeforeDelete?.(context);
|
|
105
|
+
if (showLoadingDuringDelete) {
|
|
106
|
+
flow.showLoading({
|
|
107
|
+
title: 'Deleting Workspace',
|
|
108
|
+
message:
|
|
109
|
+
params.scriptPolicy === 'skip'
|
|
110
|
+
? 'Removing workspace without cleanup scripts...'
|
|
111
|
+
: `Running cleanup scripts for "${target.workspaceName}"...`,
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
flow.close();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await deleteWorkspace(target.projectName, target.workspaceId, params);
|
|
119
|
+
flow.close();
|
|
120
|
+
await onDeleteSuccess?.(context);
|
|
121
|
+
return true;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const message = toErrorMessage(error);
|
|
124
|
+
const code = getErrorCode(error);
|
|
125
|
+
|
|
126
|
+
if (code === 'REMOVE_SCRIPT_FAILED' && params.scriptPolicy !== 'skip') {
|
|
127
|
+
const removeAnyway = await promptRemoveScriptFailure(flow, target.workspaceName, message);
|
|
128
|
+
if (!removeAnyway) {
|
|
129
|
+
await onDeleteCancelled?.(context);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return executeDelete(target, { scriptPolicy: 'skip' }, true);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
flow.close();
|
|
137
|
+
if (onDeleteError) {
|
|
138
|
+
await onDeleteError({
|
|
139
|
+
...context,
|
|
140
|
+
error,
|
|
141
|
+
message,
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
flow.showMessage({
|
|
145
|
+
title: 'Delete Failed',
|
|
146
|
+
message,
|
|
147
|
+
variant: 'error',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}, [
|
|
154
|
+
deleteWorkspace,
|
|
155
|
+
flow,
|
|
156
|
+
onBeforeDelete,
|
|
157
|
+
onDeleteCancelled,
|
|
158
|
+
onDeleteError,
|
|
159
|
+
onDeleteSuccess,
|
|
160
|
+
showLoadingDuringDelete,
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
const deleteWorkspaceWithPrompt = useCallback(async (target: WorkspaceDeleteTarget): Promise<boolean> => {
|
|
164
|
+
return executeDelete(target, { scriptPolicy: 'auto' }, false);
|
|
165
|
+
}, [executeDelete]);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
deleteWorkspaceWithPrompt,
|
|
169
|
+
};
|
|
170
|
+
}
|
package/src/app.tui.tsx
CHANGED
|
@@ -86,6 +86,11 @@ 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 {
|
|
91
|
+
consumeLegacyCleanupReminderForTui,
|
|
92
|
+
initializeSecretRuntime,
|
|
93
|
+
} from './core/secret-runtime.js';
|
|
89
94
|
import {
|
|
90
95
|
resolveInboxCommand,
|
|
91
96
|
resolveMachineListCommand,
|
|
@@ -438,6 +443,27 @@ function App({ relayConfig, onQuit }: AppProps) {
|
|
|
438
443
|
},
|
|
439
444
|
});
|
|
440
445
|
|
|
446
|
+
const { deleteWorkspaceWithPrompt } = useWorkspaceDeleteFlow({
|
|
447
|
+
flow,
|
|
448
|
+
deleteWorkspace: deleteLocalWorkspace,
|
|
449
|
+
onBeforeDelete: ({ target }) => {
|
|
450
|
+
setScriptWorkspaceName(target.workspaceName);
|
|
451
|
+
dispatch({ type: 'SET_VIEW', view: 'scripts' });
|
|
452
|
+
},
|
|
453
|
+
onDeleteSuccess: async () => {
|
|
454
|
+
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
455
|
+
await refreshWorkspaces();
|
|
456
|
+
},
|
|
457
|
+
onDeleteError: async ({ message }) => {
|
|
458
|
+
dispatch({ type: 'SET_VIEW', view: 'projects' });
|
|
459
|
+
flow.showMessage({
|
|
460
|
+
title: 'Delete Failed',
|
|
461
|
+
message,
|
|
462
|
+
variant: 'error',
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
|
|
441
467
|
// Daemon status hook (tmux-lite and serve)
|
|
442
468
|
const { status: daemonStatus } = useDaemonStatus({ pollInterval: 5000 });
|
|
443
469
|
|
|
@@ -600,26 +626,14 @@ function App({ relayConfig, onQuit }: AppProps) {
|
|
|
600
626
|
warning: workspace.sessionCount > 0 ? `This will kill ${workspace.sessionCount} active session(s)!` : undefined,
|
|
601
627
|
onConfirm: async () => {
|
|
602
628
|
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();
|
|
629
|
+
await deleteWorkspaceWithPrompt({
|
|
630
|
+
projectName: currentProject,
|
|
631
|
+
workspaceId: workspace.id,
|
|
632
|
+
workspaceName: workspace.name,
|
|
633
|
+
});
|
|
620
634
|
},
|
|
621
635
|
});
|
|
622
|
-
}, [currentProject, flow,
|
|
636
|
+
}, [currentProject, flow, deleteWorkspaceWithPrompt]);
|
|
623
637
|
|
|
624
638
|
// Delete session
|
|
625
639
|
const handleDeleteSession = useCallback((sessionId: string, sessionName: string) => {
|
|
@@ -1311,9 +1325,13 @@ function App({ relayConfig, onQuit }: AppProps) {
|
|
|
1311
1325
|
// ========== Keyboard Handlers ==========
|
|
1312
1326
|
|
|
1313
1327
|
useKeyboard(async (key) => {
|
|
1328
|
+
const localScriptTerminalRunning =
|
|
1329
|
+
state.view === 'scripts' &&
|
|
1330
|
+
(localScriptState?.isRunning ?? true);
|
|
1331
|
+
|
|
1314
1332
|
// Handle flow modals FIRST - even in terminal view
|
|
1315
1333
|
// This ensures y/n work in confirmation modals when terminal is underneath
|
|
1316
|
-
if (flow.isOpen) {
|
|
1334
|
+
if (flow.isOpen && !localScriptTerminalRunning) {
|
|
1317
1335
|
// Handle confirm modal with y/n shortcuts
|
|
1318
1336
|
if (flow.flow.type === 'confirm') {
|
|
1319
1337
|
if (key.raw === 'y' || key.name === 'return') {
|
|
@@ -2048,7 +2066,7 @@ function App({ relayConfig, onQuit }: AppProps) {
|
|
|
2048
2066
|
error={localScriptState?.error}
|
|
2049
2067
|
exitCode={localScriptState?.exitCode}
|
|
2050
2068
|
/>
|
|
2051
|
-
<FlowTUI flow={flow} />
|
|
2069
|
+
{!isRunning && <FlowTUI flow={flow} />}
|
|
2052
2070
|
<StatusBar hint={isRunning ? '[Running scripts...]' : '[Esc/n] Back to workspaces'} />
|
|
2053
2071
|
</Fragment>
|
|
2054
2072
|
);
|
|
@@ -2561,7 +2579,14 @@ function StatusBar({ hint }: { hint: string }) {
|
|
|
2561
2579
|
/** @deprecated Use RelayConfig instead */
|
|
2562
2580
|
export type TUIRelayConfig = RelayConfig;
|
|
2563
2581
|
|
|
2564
|
-
export async function launchTUI(
|
|
2582
|
+
export async function launchTUI(
|
|
2583
|
+
relayConfig?: RelayConfig,
|
|
2584
|
+
options: { ignoreKeychainAndSkipSecrets?: boolean } = {}
|
|
2585
|
+
): Promise<void> {
|
|
2586
|
+
await initializeSecretRuntime({
|
|
2587
|
+
ignoreKeychainAndSkipSecrets: options.ignoreKeychainAndSkipSecrets,
|
|
2588
|
+
});
|
|
2589
|
+
|
|
2565
2590
|
const renderer = await createCliRenderer({
|
|
2566
2591
|
exitOnCtrlC: false,
|
|
2567
2592
|
targetFps: 30,
|
|
@@ -2572,6 +2597,12 @@ export async function launchTUI(relayConfig?: RelayConfig): Promise<void> {
|
|
|
2572
2597
|
// Clean exit handler
|
|
2573
2598
|
const handleQuit = () => {
|
|
2574
2599
|
renderer.destroy();
|
|
2600
|
+
|
|
2601
|
+
const legacyReminder = consumeLegacyCleanupReminderForTui();
|
|
2602
|
+
if (legacyReminder) {
|
|
2603
|
+
logger.warning(legacyReminder);
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2575
2606
|
process.exit(0);
|
|
2576
2607
|
};
|
|
2577
2608
|
|
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 {
|
|
@@ -49,6 +50,13 @@ type View = "machines" | "terminal";
|
|
|
49
50
|
|
|
50
51
|
const PAGE_UP = '\x1b[5~';
|
|
51
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
|
+
]);
|
|
52
60
|
|
|
53
61
|
export default function App() {
|
|
54
62
|
const [view, setView] = useState<View>("machines");
|
|
@@ -71,6 +79,7 @@ export default function App() {
|
|
|
71
79
|
const terminalRef = useRef<SessionTerminalHandle>(null);
|
|
72
80
|
const lastScriptErrorRef = useRef<string | null>(null);
|
|
73
81
|
const lastCommandErrorRef = useRef<string | null>(null);
|
|
82
|
+
const suppressDeleteScriptFailureModalRef = useRef(false);
|
|
74
83
|
|
|
75
84
|
// Invite params from URL
|
|
76
85
|
const [inviteParams, setInviteParams] = useState<{
|
|
@@ -174,6 +183,36 @@ export default function App() {
|
|
|
174
183
|
},
|
|
175
184
|
});
|
|
176
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
|
+
|
|
177
216
|
useEffect(() => {
|
|
178
217
|
let mounted = true;
|
|
179
218
|
void browserPreferencesService.getNotificationConfig().then((config) => {
|
|
@@ -235,6 +274,11 @@ export default function App() {
|
|
|
235
274
|
return;
|
|
236
275
|
}
|
|
237
276
|
|
|
277
|
+
if (suppressDeleteScriptFailureModalRef.current) {
|
|
278
|
+
lastScriptErrorRef.current = scriptError;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
238
282
|
if (lastScriptErrorRef.current === scriptError) {
|
|
239
283
|
return;
|
|
240
284
|
}
|
|
@@ -259,11 +303,20 @@ export default function App() {
|
|
|
259
303
|
}
|
|
260
304
|
lastCommandErrorRef.current = key;
|
|
261
305
|
|
|
306
|
+
if (
|
|
307
|
+
suppressDeleteScriptFailureModalRef.current &&
|
|
308
|
+
terminal.commandError.code &&
|
|
309
|
+
DELETE_ERROR_CODES.has(terminal.commandError.code)
|
|
310
|
+
) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
262
314
|
const isScriptFailure =
|
|
263
315
|
terminal.commandError.code === 'SCRIPT_FAILED' ||
|
|
264
316
|
terminal.commandError.code === 'PRE_SCRIPT_FAILED' ||
|
|
265
317
|
terminal.commandError.code === 'SETUP_SCRIPT_FAILED' ||
|
|
266
|
-
terminal.commandError.code === 'SELECT_SCRIPT_FAILED'
|
|
318
|
+
terminal.commandError.code === 'SELECT_SCRIPT_FAILED' ||
|
|
319
|
+
terminal.commandError.code === 'REMOVE_SCRIPT_FAILED';
|
|
267
320
|
|
|
268
321
|
if (isScriptFailure) {
|
|
269
322
|
if (!terminal.scriptState) {
|
|
@@ -370,12 +423,10 @@ export default function App() {
|
|
|
370
423
|
const spacesBrowserProps = useSpacesBrowser({
|
|
371
424
|
workspaces: terminal.workspaces,
|
|
372
425
|
sessions: terminal.sessions,
|
|
373
|
-
onRequestSessions: terminal.requestSessions,
|
|
426
|
+
onRequestSessions: () => terminal.requestSessions(),
|
|
374
427
|
onAttachSession: handleAttachSession,
|
|
375
428
|
onRefresh: terminal.requestWorkspaces,
|
|
376
|
-
onRefreshSessions: (
|
|
377
|
-
workspaceIds.forEach(id => terminal.requestSessions(id));
|
|
378
|
-
},
|
|
429
|
+
onRefreshSessions: () => terminal.requestSessions(),
|
|
379
430
|
onBack: handleBackToMachines,
|
|
380
431
|
machineName: selectedMachine?.label || selectedMachine?.machineId,
|
|
381
432
|
});
|
|
@@ -446,6 +497,7 @@ export default function App() {
|
|
|
446
497
|
if (view === "terminal" && terminal.status === "established" && terminal.mode === "browsing") {
|
|
447
498
|
terminal.requestProjects();
|
|
448
499
|
terminal.requestWorkspaces();
|
|
500
|
+
terminal.requestSessions();
|
|
449
501
|
terminal.requestNotificationConfig();
|
|
450
502
|
}
|
|
451
503
|
}, [
|
|
@@ -454,6 +506,7 @@ export default function App() {
|
|
|
454
506
|
terminal.mode,
|
|
455
507
|
terminal.requestProjects,
|
|
456
508
|
terminal.requestWorkspaces,
|
|
509
|
+
terminal.requestSessions,
|
|
457
510
|
terminal.requestNotificationConfig,
|
|
458
511
|
]);
|
|
459
512
|
|
|
@@ -594,8 +647,12 @@ export default function App() {
|
|
|
594
647
|
message: `Are you sure you want to delete workspace "${selected.workspace.name}"?`,
|
|
595
648
|
confirmText: selected.workspace.name,
|
|
596
649
|
warning: sessionCount > 0 ? `This will kill ${sessionCount} active session(s)!` : undefined,
|
|
597
|
-
onConfirm: () => {
|
|
598
|
-
|
|
650
|
+
onConfirm: async () => {
|
|
651
|
+
await deleteWorkspaceWithPrompt({
|
|
652
|
+
projectName: selected.workspace.projectName,
|
|
653
|
+
workspaceId: selected.workspace.id,
|
|
654
|
+
workspaceName: selected.workspace.name,
|
|
655
|
+
});
|
|
599
656
|
},
|
|
600
657
|
});
|
|
601
658
|
}
|
|
@@ -607,7 +664,16 @@ export default function App() {
|
|
|
607
664
|
|
|
608
665
|
window.addEventListener("keydown", handleKeyDown);
|
|
609
666
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
610
|
-
}, [
|
|
667
|
+
}, [
|
|
668
|
+
view,
|
|
669
|
+
terminal.status,
|
|
670
|
+
terminal.mode,
|
|
671
|
+
showInbox,
|
|
672
|
+
showScriptTerminal,
|
|
673
|
+
spacesBrowserProps,
|
|
674
|
+
flow,
|
|
675
|
+
deleteWorkspaceWithPrompt,
|
|
676
|
+
]);
|
|
611
677
|
|
|
612
678
|
// Attached terminal mode keyboard handler (Ctrl+Esc to detach)
|
|
613
679
|
useEffect(() => {
|
|
@@ -681,18 +747,19 @@ export default function App() {
|
|
|
681
747
|
terminal.mode === 'browsing' &&
|
|
682
748
|
showScriptTerminal
|
|
683
749
|
) {
|
|
750
|
+
const isRunning = terminal.scriptState?.isRunning ?? true;
|
|
684
751
|
return (
|
|
685
752
|
<>
|
|
686
753
|
<ScriptTerminal
|
|
687
754
|
phase={terminal.scriptState?.phase ?? 'pre'}
|
|
688
755
|
workspaceName={scriptWorkspaceName}
|
|
689
|
-
isRunning={
|
|
756
|
+
isRunning={isRunning}
|
|
690
757
|
error={terminal.scriptState?.error}
|
|
691
758
|
exitCode={terminal.scriptState?.exitCode}
|
|
692
759
|
setWriteCallback={terminal.setWriteCallback}
|
|
693
760
|
onBack={() => setShowScriptTerminal(false)}
|
|
694
761
|
/>
|
|
695
|
-
<FlowWeb flow={flow} />
|
|
762
|
+
{!isRunning && <FlowWeb flow={flow} />}
|
|
696
763
|
<Toaster theme="dark" position="top-right" richColors />
|
|
697
764
|
</>
|
|
698
765
|
);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getSecretMigrationInputs } from '../core/secret-runtime.js';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
import { promptConfirm } from '../utils/prompts.js';
|
|
4
|
+
import { cleanupLegacySecretEntries, preloadAllSecrets } from '../utils/secrets.js';
|
|
5
|
+
|
|
6
|
+
export interface CleanupLegacyOptions {
|
|
7
|
+
yes?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function migrateCleanupLegacy(
|
|
11
|
+
options: CleanupLegacyOptions = {}
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const migrationInputs = getSecretMigrationInputs();
|
|
14
|
+
|
|
15
|
+
if (!options.yes) {
|
|
16
|
+
const confirmed = await promptConfirm(
|
|
17
|
+
'Delete legacy keychain entries (project:*, global, and old per-key entries)? Unified secrets are kept.',
|
|
18
|
+
false
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (!confirmed) {
|
|
22
|
+
logger.info('Cancelled');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Ensure legacy entries are copied into unified storage before cleanup.
|
|
28
|
+
await preloadAllSecrets(migrationInputs.projectNames, {
|
|
29
|
+
projectLegacyKeys: migrationInputs.projectSecretKeys,
|
|
30
|
+
globalLegacyKeys: migrationInputs.globalSecretKeys,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const result = await cleanupLegacySecretEntries(migrationInputs.projectNames, {
|
|
34
|
+
projectLegacyKeys: migrationInputs.projectSecretKeys,
|
|
35
|
+
globalLegacyKeys: migrationInputs.globalSecretKeys,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (result.errors.length > 0) {
|
|
39
|
+
logger.warning(`Legacy cleanup completed with ${result.errors.length} error(s).`);
|
|
40
|
+
for (const error of result.errors) {
|
|
41
|
+
logger.error(` ${error}`);
|
|
42
|
+
}
|
|
43
|
+
logger.info(
|
|
44
|
+
`Legacy cleanup finished with issues. Deleted ${result.deleted} entries (${result.missing} already absent).`
|
|
45
|
+
);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logger.success(
|
|
50
|
+
`Legacy cleanup complete. Deleted ${result.deleted} entries (${result.missing} already absent).`
|
|
51
|
+
);
|
|
52
|
+
}
|
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;
|