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.
Files changed (36) hide show
  1. package/bun.lock +5 -5
  2. package/package.json +5 -5
  3. package/src/app/session/useWorkspaceDeleteFlow.ts +156 -0
  4. package/src/app.tui.tsx +37 -19
  5. package/src/app.web.tsx +85 -8
  6. package/src/commands/remove.ts +26 -4
  7. package/src/commands/serve.ts +20 -0
  8. package/src/components/RemoteMachineScreen.tui.tsx +85 -8
  9. package/src/components/SessionTerminal.tui.tsx +34 -4
  10. package/src/components/SessionTerminal.web.tsx +144 -3
  11. package/src/components/SpacesBrowser.tsx +9 -12
  12. package/src/components/session-terminal-page-navigation.ts +48 -0
  13. package/src/core/__tests__/workspace-lifecycle.test.ts +23 -0
  14. package/src/core/secret-runtime.ts +105 -0
  15. package/src/core/workspace-lifecycle.ts +24 -2
  16. package/src/core/workspace.ts +64 -21
  17. package/src/hooks/__tests__/useLocalSession.tui.test.ts +11 -2
  18. package/src/hooks/useLocalSession.tui.ts +7 -2
  19. package/src/index.ts +31 -13
  20. package/src/lib/remote-session/protocol.ts +3 -1
  21. package/src/lib/remote-session/session-handler.ts +105 -29
  22. package/src/session/__tests__/backend-manager.test.ts +11 -2
  23. package/src/session/__tests__/local-session-backend.test.ts +119 -0
  24. package/src/session/__tests__/remote-session-backend.test.ts +324 -0
  25. package/src/session/__tests__/session-name.test.ts +35 -0
  26. package/src/session/__tests__/useRemoteSessionClient.test.ts +10 -2
  27. package/src/session/backend.ts +14 -1
  28. package/src/session/backends/local-session-backend.ts +103 -15
  29. package/src/session/backends/remote-session-backend.ts +146 -7
  30. package/src/session/index.ts +1 -0
  31. package/src/session/session-name.ts +50 -0
  32. package/src/session/useRemoteSessionClient.ts +19 -7
  33. package/src/session/useSessionEngine.ts +41 -3
  34. package/src/tui/__tests__/session-terminal-page-navigation.test.ts +94 -0
  35. package/src/types/errors.ts +45 -0
  36. 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.12",
43
- "@gitspace/darwin-x64": "0.2.0-rc.12",
44
- "@gitspace/linux-arm64": "0.2.0-rc.12",
45
- "@gitspace/linux-x64": "0.2.0-rc.12",
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.12", "", { "os": "darwin", "cpu": "arm64", "bin": { "gssh-darwin-arm64": "bin/gssh" } }, "sha512-FUCz7f6EThB1/epRZ+X3SV7wDq/m4mFRDy2AVXGurVK6OWAA+D8tddUYeGmxvYT9+PXEZDATu5ukBh2RKpr5OQ=="],
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.14",
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.14",
21
- "@gitspace/darwin-x64": "0.2.0-rc.14",
22
- "@gitspace/linux-x64": "0.2.0-rc.14",
23
- "@gitspace/linux-arm64": "0.2.0-rc.14"
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
- flow.showLoading({ title: 'Deleting', message: 'Preparing...' });
604
-
605
- try {
606
- await deleteLocalWorkspace(currentProject, workspace.id);
607
- } catch (error) {
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, refreshWorkspaces]);
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(relayConfig?: RelayConfig): Promise<void> {
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: (workspaceIds) => {
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
- terminal.deleteWorkspace(selected.workspace.projectName, selected.workspace.id);
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
- }, [view, terminal.status, terminal.mode, showInbox, showScriptTerminal, spacesBrowserProps, flow]);
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
 
@@ -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 result = await deleteWorkspaceCore(currentProject, workspaceName, {
118
- nonInteractive: false, // CLI is interactive
119
- keepBranch: options.keepBranch,
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(
@@ -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;