gitspace 0.2.0-rc.4 → 0.2.0-rc.6

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/src/index.ts CHANGED
@@ -128,7 +128,13 @@ addCommand
128
128
  .action(async (workspaceName, options) => {
129
129
  await checkFirstTimeSetup()
130
130
  try {
131
- await addWorkspace(workspaceName, options)
131
+ // Map commander option names to CreateWorkspaceOptions property names
132
+ await addWorkspace(workspaceName, {
133
+ branchName: options.branch,
134
+ fromBranch: options.from,
135
+ noShell: options.shell === false,
136
+ noSetup: options.setup === false,
137
+ })
132
138
  } catch (error) {
133
139
  handleError(error)
134
140
  }
@@ -31,9 +31,12 @@ import {
31
31
  // Import project loading
32
32
  import { loadProjects } from "../../tui/state";
33
33
 
34
- // Import workspace removal
35
- import { removeWorktree, deleteLocalBranch, getWorktreeInfo } from "../../core/git";
36
- import { getProjectWorkspacesDir, getProjectBaseDir } from "../../core/config";
34
+ // Import workspace operations
35
+ import { deleteWorkspaceCore } from "../../core/workspace";
36
+ import { readProjectConfig } from "../../core/config";
37
+
38
+ // Import script execution
39
+ import { runWorkspaceScripts } from "../../utils/run-workspace-scripts";
37
40
 
38
41
  /**
39
42
  * Session state for a connected client
@@ -326,6 +329,24 @@ export class RemoteSessionHandler {
326
329
  return;
327
330
  }
328
331
 
332
+ // Run setup or select scripts for the workspace
333
+ const config = readProjectConfig(workspace.projectName);
334
+
335
+ console.log(`[remote-session] Running workspace scripts for: ${workspace.id}`);
336
+ const scriptResult = await runWorkspaceScripts({
337
+ projectName: workspace.projectName,
338
+ workspacePath: workspace.path,
339
+ workspaceName: workspace.id,
340
+ repository: config.repository,
341
+ interactive: false, // Remote context - scripts can't prompt for input
342
+ });
343
+
344
+ if (!scriptResult.success) {
345
+ console.error(`[remote-session] ${scriptResult.phase} scripts failed:`, scriptResult.error);
346
+ await this.sendError(session, sendResponse, "SCRIPT_FAILED", `Workspace scripts failed during ${scriptResult.phase} phase: ${scriptResult.error}`);
347
+ return;
348
+ }
349
+
329
350
  // Create session name: use provided name or auto-generate
330
351
  let sessionName: string;
331
352
  if (msg.sessionName) {
@@ -449,25 +470,14 @@ export class RemoteSessionHandler {
449
470
  sendResponse: (data: Uint8Array) => void
450
471
  ): Promise<void> {
451
472
  try {
452
- const workspacesDir = getProjectWorkspacesDir(projectName);
453
- const baseDir = getProjectBaseDir(projectName);
454
- const workspacePath = `${workspacesDir}/${workspaceId}`;
455
-
456
- // Get workspace info for branch name
457
- const info = await getWorktreeInfo(workspacePath);
458
- if (!info) {
459
- await this.sendError(session, sendResponse, "NOT_FOUND", "Workspace not found");
460
- return;
461
- }
462
-
463
- // Remove worktree
464
- await removeWorktree(baseDir, workspacePath, true);
473
+ const result = await deleteWorkspaceCore(projectName, workspaceId, {
474
+ nonInteractive: true, // Remote context - scripts can't prompt for input
475
+ });
465
476
 
466
- // Try to delete the local branch
467
- try {
468
- await deleteLocalBranch(baseDir, info.branch, true);
469
- } catch {
470
- // Branch deletion is best-effort
477
+ if (!result.success) {
478
+ const errorCode = result.error?.includes("not found") ? "NOT_FOUND" : "DELETE_FAILED";
479
+ await this.sendError(session, sendResponse, errorCode, result.error || "Failed to delete workspace");
480
+ return;
471
481
  }
472
482
 
473
483
  await this.sendMessage(session, sendResponse, {
package/src/tui/app.tsx CHANGED
@@ -57,15 +57,18 @@ import {
57
57
  projectExists,
58
58
  updateProjectConfig,
59
59
  } from '../core/config.js';
60
- import { removeWorkspace, removeProject } from '../commands/remove.js';
61
60
 
62
- // Git and workspace creation
63
- import { listRemoteBranches, createWorktree, checkRemoteBranch, getDefaultBranch } from '../core/git.js';
61
+ // Git and workspace operations
62
+ import { listRemoteBranches, createWorktree, getDefaultBranch } from '../core/git.js';
63
+ import { deleteWorkspaceCore, deleteProjectCore } from '../core/workspace.js';
64
64
  import { fetchUnstartedIssues } from '../core/linear.js';
65
65
  import { generateMarkdown } from '../utils/markdown.js';
66
- import { sanitizeForFileSystem, generateWorkspaceName, isValidWorkspaceName, extractRepoName } from '../utils/sanitize.js';
66
+ import { sanitizeForFileSystem, generateWorkspaceName, isValidWorkspaceName, isValidBranchName, extractRepoName } from '../utils/sanitize.js';
67
67
  import { existsSync, mkdirSync, writeFileSync } from 'fs';
68
68
  import { join } from 'path';
69
+
70
+ // Script execution
71
+ import { runWorkspaceScripts } from '../utils/run-workspace-scripts.js';
69
72
  import type { LinearIssue } from '../types/workspace.js';
70
73
 
71
74
  // Project creation
@@ -98,7 +101,8 @@ type WorkspaceFlowState =
98
101
  | { type: 'loading'; title: string; message: string }
99
102
  | { type: 'branch-select'; branches: string[]; selectedIndex: number }
100
103
  | { type: 'linear-select'; issues: LinearIssue[]; selectedIndex: number }
101
- | { type: 'manual-input'; inputValue: string; error: string | null }
104
+ | { type: 'manual-name-input'; inputValue: string; error: string | null }
105
+ | { type: 'manual-branch-input'; workspaceName: string; inputValue: string }
102
106
  | { type: 'creating'; workspaceName: string };
103
107
 
104
108
  /** Project flow states - explicit state machine for project creation */
@@ -371,7 +375,33 @@ function App({ relayConfig, onQuit }: AppProps) {
371
375
  warning: 'This will delete all workspaces in this project!',
372
376
  onConfirm: async () => {
373
377
  flow.showLoading({ title: 'Deleting', message: 'Removing project...' });
374
- await removeProject(project.name, { force: false });
378
+
379
+ try {
380
+ const result = await deleteProjectCore(project.name, {
381
+ nonInteractive: true, // TUI is non-interactive for scripts
382
+ });
383
+
384
+ if (!result.success && result.errors.length > 0) {
385
+ console.error('[tui] Project deletion errors:', result.errors);
386
+ flow.close();
387
+ flow.showMessage({
388
+ title: 'Delete Failed',
389
+ message: `Failed to delete project "${project.name}". Check logs for details.`,
390
+ variant: 'error',
391
+ });
392
+ return;
393
+ }
394
+ } catch (error) {
395
+ console.error('[tui] Failed to delete project:', error);
396
+ flow.close();
397
+ flow.showMessage({
398
+ title: 'Delete Failed',
399
+ message: `An unexpected error occurred while deleting project "${project.name}".`,
400
+ variant: 'error',
401
+ });
402
+ return;
403
+ }
404
+
375
405
  flow.close();
376
406
  await refreshProjects();
377
407
  },
@@ -419,7 +449,7 @@ function App({ relayConfig, onQuit }: AppProps) {
419
449
  } else if (params.workspaceId) {
420
450
  // Create new session
421
451
  const workspace = state.workspaces.find(w => w.name === params.workspaceId);
422
- if (workspace) {
452
+ if (workspace && state.currentProject) {
423
453
  flow.showInput({
424
454
  title: 'New Session',
425
455
  label: 'Session name (optional):',
@@ -427,12 +457,36 @@ function App({ relayConfig, onQuit }: AppProps) {
427
457
  onSubmit: async (name) => {
428
458
  const sessionName = name || `${state.currentProject}:${workspace.name}:${Date.now()}`;
429
459
  try {
460
+ const config = readProjectConfig(state.currentProject!);
461
+
462
+ // Run workspace scripts (pre+setup for first time, select for existing)
463
+ const scriptResult = await runWorkspaceScripts({
464
+ projectName: state.currentProject!,
465
+ workspacePath: workspace.path,
466
+ workspaceName: workspace.name,
467
+ repository: config.repository,
468
+ interactive: false, // TUI context - scripts can't prompt for input
469
+ });
470
+
471
+ if (!scriptResult.success) {
472
+ flow.showMessage({
473
+ title: 'Script Failed',
474
+ message: `Workspace scripts failed during ${scriptResult.phase} phase: ${scriptResult.error}`,
475
+ variant: 'error',
476
+ });
477
+ return;
478
+ }
479
+
430
480
  const session = await createSession(sessionName, workspace.path);
431
481
  // Attach to newly created session
432
482
  dispatch({ type: 'SET_ATTACHED_SESSION', session });
433
483
  dispatch({ type: 'SET_VIEW', view: 'terminal' });
434
484
  } catch (err) {
435
- dispatch({ type: 'SET_ERROR', error: err instanceof Error ? err.message : 'Failed to create session' });
485
+ flow.showMessage({
486
+ title: 'Session Failed',
487
+ message: err instanceof Error ? err.message : 'Failed to create session',
488
+ variant: 'error',
489
+ });
436
490
  }
437
491
  },
438
492
  });
@@ -492,7 +546,33 @@ function App({ relayConfig, onQuit }: AppProps) {
492
546
  onConfirm: async () => {
493
547
  if (!state.currentProject) return;
494
548
  flow.showLoading({ title: 'Deleting', message: 'Removing workspace...' });
495
- await removeWorkspace(workspace.name, { force: false });
549
+
550
+ try {
551
+ const result = await deleteWorkspaceCore(state.currentProject, workspace.name, {
552
+ nonInteractive: true, // TUI is non-interactive for scripts
553
+ });
554
+
555
+ if (!result.success) {
556
+ console.error('[tui] Failed to delete workspace:', result.error);
557
+ flow.close();
558
+ flow.showMessage({
559
+ title: 'Delete Failed',
560
+ message: result.error ?? `Failed to delete workspace "${workspace.name}".`,
561
+ variant: 'error',
562
+ });
563
+ return;
564
+ }
565
+ } catch (error) {
566
+ console.error('[tui] Failed to delete workspace:', error);
567
+ flow.close();
568
+ flow.showMessage({
569
+ title: 'Delete Failed',
570
+ message: error instanceof Error ? error.message : `Failed to delete workspace "${workspace.name}".`,
571
+ variant: 'error',
572
+ });
573
+ return;
574
+ }
575
+
496
576
  flow.close();
497
577
  await refreshWorkspaces();
498
578
  },
@@ -556,6 +636,25 @@ function App({ relayConfig, onQuit }: AppProps) {
556
636
  writeFileSync(join(promptDir, 'issue.md'), markdown, 'utf-8');
557
637
  }
558
638
 
639
+ // Run workspace scripts (pre+setup for new workspace)
640
+ const scriptResult = await runWorkspaceScripts({
641
+ projectName: state.currentProject,
642
+ workspacePath,
643
+ workspaceName,
644
+ repository: config.repository,
645
+ interactive: false, // TUI context - scripts can't prompt for input
646
+ });
647
+
648
+ if (!scriptResult.success) {
649
+ setWorkspaceFlow({ type: 'closed' });
650
+ flow.showMessage({
651
+ title: 'Script Failed',
652
+ message: `Workspace scripts failed during ${scriptResult.phase} phase: ${scriptResult.error}`,
653
+ variant: 'error',
654
+ });
655
+ return;
656
+ }
657
+
559
658
  setWorkspaceFlow({ type: 'closed' });
560
659
  await refreshWorkspaces();
561
660
 
@@ -566,7 +665,11 @@ function App({ relayConfig, onQuit }: AppProps) {
566
665
  dispatch({ type: 'SET_VIEW', view: 'terminal' });
567
666
  } catch (err) {
568
667
  setWorkspaceFlow({ type: 'closed' });
569
- dispatch({ type: 'SET_ERROR', error: err instanceof Error ? err.message : 'Failed to create workspace' });
668
+ flow.showMessage({
669
+ title: 'Workspace Failed',
670
+ message: err instanceof Error ? err.message : 'Failed to create workspace',
671
+ variant: 'error',
672
+ });
570
673
  }
571
674
  }, [state.currentProject, refreshWorkspaces]);
572
675
 
@@ -639,7 +742,7 @@ function App({ relayConfig, onQuit }: AppProps) {
639
742
  setWorkspaceFlow({ type: 'closed' });
640
743
  }
641
744
  } else if (source === 'manual') {
642
- setWorkspaceFlow({ type: 'manual-input', inputValue: '', error: null });
745
+ setWorkspaceFlow({ type: 'manual-name-input', inputValue: '', error: null });
643
746
  }
644
747
  }, [state.currentProject, flow]);
645
748
 
@@ -655,17 +758,29 @@ function App({ relayConfig, onQuit }: AppProps) {
655
758
  await createWorkspaceAndOpenSession(workspaceName, workspaceName, false, issue);
656
759
  }, [createWorkspaceAndOpenSession]);
657
760
 
658
- // Handle manual name submission
659
- const handleManualSubmit = useCallback(async (name: string) => {
660
- if (!name || name.trim().length === 0) {
661
- setWorkspaceFlow(prev => prev.type === 'manual-input' ? { ...prev, error: 'Workspace name is required' } : prev);
761
+ // Handle manual workspace name submission (advances to branch input)
762
+ const handleManualNameSubmit = useCallback((name: string) => {
763
+ const trimmedName = name.trim();
764
+ if (!trimmedName) {
765
+ setWorkspaceFlow(prev => prev.type === 'manual-name-input' ? { ...prev, error: 'Workspace name is required' } : prev);
662
766
  return;
663
767
  }
664
- if (!isValidWorkspaceName(name)) {
665
- setWorkspaceFlow(prev => prev.type === 'manual-input' ? { ...prev, error: 'Use only letters, numbers, hyphens, underscores' } : prev);
768
+ if (!isValidWorkspaceName(trimmedName)) {
769
+ setWorkspaceFlow(prev => prev.type === 'manual-name-input' ? { ...prev, error: 'Use only letters, numbers, hyphens, underscores' } : prev);
666
770
  return;
667
771
  }
668
- await createWorkspaceAndOpenSession(name, name, false);
772
+ // Advance to branch input step, pre-fill with workspace name
773
+ setWorkspaceFlow({ type: 'manual-branch-input', workspaceName: trimmedName, inputValue: trimmedName });
774
+ }, []);
775
+
776
+ // Handle manual branch name submission (creates the workspace)
777
+ const handleManualBranchSubmit = useCallback(async (workspaceName: string, branchName: string) => {
778
+ const finalBranch = branchName.trim() || workspaceName;
779
+ if (!isValidBranchName(finalBranch)) {
780
+ setWorkspaceFlow(prev => prev.type === 'manual-branch-input' ? { ...prev, error: 'Invalid branch name (no spaces, .., or special chars like : ? * [ \\ ~)' } : prev);
781
+ return;
782
+ }
783
+ await createWorkspaceAndOpenSession(workspaceName, finalBranch, false);
669
784
  }, [createWorkspaceAndOpenSession]);
670
785
 
671
786
  // Main handler to start new workspace flow
@@ -1155,9 +1270,9 @@ function App({ relayConfig, onQuit }: AppProps) {
1155
1270
  return;
1156
1271
  }
1157
1272
 
1158
- if (workspaceFlow.type === 'manual-input') {
1273
+ if (workspaceFlow.type === 'manual-name-input') {
1159
1274
  if (key.name === 'return') {
1160
- await handleManualSubmit(workspaceFlow.inputValue);
1275
+ handleManualNameSubmit(workspaceFlow.inputValue);
1161
1276
  } else if (key.name === 'backspace') {
1162
1277
  setWorkspaceFlow({
1163
1278
  ...workspaceFlow,
@@ -1174,6 +1289,23 @@ function App({ relayConfig, onQuit }: AppProps) {
1174
1289
  return;
1175
1290
  }
1176
1291
 
1292
+ if (workspaceFlow.type === 'manual-branch-input') {
1293
+ if (key.name === 'return') {
1294
+ await handleManualBranchSubmit(workspaceFlow.workspaceName, workspaceFlow.inputValue);
1295
+ } else if (key.name === 'backspace') {
1296
+ setWorkspaceFlow({
1297
+ ...workspaceFlow,
1298
+ inputValue: workspaceFlow.inputValue.slice(0, -1),
1299
+ });
1300
+ } else if (key.raw && key.raw.length === 1 && !key.ctrl && !key.meta) {
1301
+ setWorkspaceFlow({
1302
+ ...workspaceFlow,
1303
+ inputValue: workspaceFlow.inputValue + key.raw,
1304
+ });
1305
+ }
1306
+ return;
1307
+ }
1308
+
1177
1309
  // For loading/creating states, just wait (escape to cancel handled above)
1178
1310
  return;
1179
1311
  }
@@ -1484,8 +1616,10 @@ function WorkspaceFlowModal({ flow }: { flow: WorkspaceFlowState }) {
1484
1616
  // Calculate modal height based on content:
1485
1617
  // - source-select: title + spacer + (options * 2 lines each) + (spacers between) + spacer + hint + border/padding
1486
1618
  // - branch/linear-select: title + items (scrollable) + hint + border/padding
1487
- // - manual-input: title + label + input box + error? + hint + border/padding
1488
- const modalHeight = flow.type === 'manual-input' ? 10 :
1619
+ // - manual-name-input: title + label + input box + error? + hint + border/padding
1620
+ // - manual-branch-input: title + label + input box + workspace display + hint + border/padding
1621
+ const modalHeight = flow.type === 'manual-name-input' ? 10 :
1622
+ flow.type === 'manual-branch-input' ? 12 :
1489
1623
  flow.type === 'loading' || flow.type === 'creating' ? 6 :
1490
1624
  flow.type === 'source-select' ? 6 + flow.options.length * 3 :
1491
1625
  flow.type === 'branch-select' ? Math.min(16, 6 + flow.branches.length) :
@@ -1584,10 +1718,10 @@ function WorkspaceFlowModal({ flow }: { flow: WorkspaceFlowState }) {
1584
1718
  </>
1585
1719
  )}
1586
1720
 
1587
- {/* Manual input */}
1588
- {flow.type === 'manual-input' && (
1721
+ {/* Manual workspace name input */}
1722
+ {flow.type === 'manual-name-input' && (
1589
1723
  <>
1590
- <text fg={COLORS.title} height={1}>New Workspace</text>
1724
+ <text fg={COLORS.title} height={1}>New Workspace (1/2)</text>
1591
1725
  <text fg={COLORS.text} height={1} marginTop={1}>Enter workspace name:</text>
1592
1726
  <box
1593
1727
  marginTop={1}
@@ -1599,6 +1733,25 @@ function WorkspaceFlowModal({ flow }: { flow: WorkspaceFlowState }) {
1599
1733
  <text fg={COLORS.text} height={1}>{flow.inputValue || ' '}_</text>
1600
1734
  </box>
1601
1735
  {flow.error && <text fg={COLORS.error} height={1} marginTop={1}>{flow.error}</text>}
1736
+ <text fg={COLORS.textDim} height={1} marginTop={1}>[Enter] Next [Esc] Cancel</text>
1737
+ </>
1738
+ )}
1739
+
1740
+ {/* Manual branch name input */}
1741
+ {flow.type === 'manual-branch-input' && (
1742
+ <>
1743
+ <text fg={COLORS.title} height={1}>New Workspace (2/2)</text>
1744
+ <text fg={COLORS.text} height={1} marginTop={1}>Enter branch name (slashes allowed):</text>
1745
+ <box
1746
+ marginTop={1}
1747
+ borderStyle="rounded"
1748
+ borderColor={COLORS.border}
1749
+ padding={0}
1750
+ width="100%"
1751
+ >
1752
+ <text fg={COLORS.text} height={1}>{flow.inputValue || ' '}_</text>
1753
+ </box>
1754
+ <text fg={COLORS.textDim} height={1} marginTop={1}>Workspace: {flow.workspaceName}</text>
1602
1755
  <text fg={COLORS.textDim} height={1} marginTop={1}>[Enter] Create [Esc] Cancel</text>
1603
1756
  </>
1604
1757
  )}