gitspace 0.2.0-rc.15 → 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 (33) 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 +74 -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.web.tsx +82 -0
  10. package/src/components/SpacesBrowser.tsx +9 -12
  11. package/src/core/__tests__/workspace-lifecycle.test.ts +23 -0
  12. package/src/core/secret-runtime.ts +105 -0
  13. package/src/core/workspace-lifecycle.ts +24 -2
  14. package/src/core/workspace.ts +64 -21
  15. package/src/hooks/__tests__/useLocalSession.tui.test.ts +11 -2
  16. package/src/hooks/useLocalSession.tui.ts +7 -2
  17. package/src/index.ts +31 -13
  18. package/src/lib/remote-session/protocol.ts +3 -1
  19. package/src/lib/remote-session/session-handler.ts +105 -29
  20. package/src/session/__tests__/backend-manager.test.ts +11 -2
  21. package/src/session/__tests__/local-session-backend.test.ts +119 -0
  22. package/src/session/__tests__/remote-session-backend.test.ts +324 -0
  23. package/src/session/__tests__/session-name.test.ts +35 -0
  24. package/src/session/__tests__/useRemoteSessionClient.test.ts +10 -2
  25. package/src/session/backend.ts +14 -1
  26. package/src/session/backends/local-session-backend.ts +103 -15
  27. package/src/session/backends/remote-session-backend.ts +146 -7
  28. package/src/session/index.ts +1 -0
  29. package/src/session/session-name.ts +50 -0
  30. package/src/session/useRemoteSessionClient.ts +19 -7
  31. package/src/session/useSessionEngine.ts +41 -3
  32. package/src/types/errors.ts +45 -0
  33. 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.15",
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.15",
21
- "@gitspace/darwin-x64": "0.2.0-rc.15",
22
- "@gitspace/linux-x64": "0.2.0-rc.15",
23
- "@gitspace/linux-arm64": "0.2.0-rc.15"
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 {
@@ -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: (workspaceIds) => {
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
- 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
+ });
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
- }, [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
+ ]);
611
677
 
612
678
  // Attached terminal mode keyboard handler (Ctrl+Esc to detach)
613
679
  useEffect(() => {
@@ -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;