gitspace 0.2.0-rc.17 → 0.2.0-rc.19

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 (55) hide show
  1. package/.gitspace/{select → scripts/select}/01-status.sh +6 -6
  2. package/README.md +22 -7
  3. package/bun.lock +5 -5
  4. package/docs/SITE_DOCS_FIGMA_MAKE.md +7 -8
  5. package/landing-page/src/components/docs/DocsContent.tsx +2 -2
  6. package/package.json +5 -5
  7. package/src/app.web.tsx +85 -3
  8. package/src/commands/bundle.ts +10 -17
  9. package/src/commands/review.ts +787 -0
  10. package/src/components/DiffViewer.web.tsx +1192 -0
  11. package/src/components/SpacesBrowser.web.tsx +19 -2
  12. package/src/components/ThreadPanel.web.tsx +798 -0
  13. package/src/components/review-decision-colors.ts +11 -0
  14. package/src/core/__tests__/bundle.test.ts +209 -0
  15. package/src/core/__tests__/github-review.test.ts +781 -0
  16. package/src/core/bundle.ts +70 -0
  17. package/src/core/git.ts +430 -0
  18. package/src/core/github-review.ts +761 -0
  19. package/src/core/review-executor.ts +316 -0
  20. package/src/core/review.ts +407 -0
  21. package/src/core/shell.ts +11 -8
  22. package/src/hooks/__tests__/useLocalSession.tui.test.ts +4 -0
  23. package/src/hooks/useReview.web.ts +248 -0
  24. package/src/index.ts +204 -0
  25. package/src/lib/remote-session/protocol.ts +25 -2
  26. package/src/lib/remote-session/session-handler.ts +82 -10
  27. package/src/lib/tmux-lite/cli.ts +7 -2
  28. package/src/lib/tmux-lite/protocol.ts +17 -1
  29. package/src/lib/tmux-lite/server.ts +54 -6
  30. package/src/pages/ReviewPage.web.tsx +511 -0
  31. package/src/session/__tests__/backend-manager.test.ts +3 -0
  32. package/src/session/__tests__/useRemoteSessionClient.test.ts +4 -0
  33. package/src/session/__tests__/workspace-shell-hooks.integration.test.ts +268 -0
  34. package/src/session/__tests__/workspace-shell-hooks.test.ts +24 -0
  35. package/src/session/backend.ts +3 -0
  36. package/src/session/backends/local-session-backend.ts +19 -22
  37. package/src/session/backends/remote-session-backend.ts +106 -0
  38. package/src/session/events.ts +6 -1
  39. package/src/session/useRemoteSessionClient.ts +17 -0
  40. package/src/session/useSessionEngine.ts +20 -0
  41. package/src/session/workspace-shell-hooks.ts +35 -0
  42. package/src/types/errors.ts +39 -0
  43. package/src/types/review.ts +349 -0
  44. package/src/utils/__tests__/run-scripts.test.ts +106 -5
  45. package/src/utils/hunk-header.ts +17 -0
  46. package/src/utils/id.ts +9 -0
  47. package/src/utils/normalize-env-key.ts +13 -0
  48. package/src/utils/run-scripts.ts +109 -14
  49. package/src/utils/workspace-id.ts +55 -0
  50. package/web/bun.lock +97 -0
  51. package/web/package.json +1 -0
  52. package/web/tsconfig.app.json +3 -1
  53. package/web/vite.config.ts +3 -0
  54. /package/.gitspace/{setup → scripts/setup}/01-install-deps.sh +0 -0
  55. /package/.gitspace/{setup → scripts/setup}/02-typecheck.sh +0 -0
@@ -3,8 +3,8 @@
3
3
  #
4
4
  # This runs every time you switch to an existing workspace.
5
5
  #
6
- # Bundle values are available as environment variables using bundle key names.
7
- # Example: DEVELOPERNAME, EXAMPLEAPITOKEN
6
+ # Bundle values are available using exact keys and uppercase snake-case aliases.
7
+ # Example aliases: DEVELOPER_NAME, EXAMPLE_API_TOKEN
8
8
 
9
9
  WORKSPACE_NAME=$1
10
10
  REPOSITORY=$2
@@ -14,14 +14,14 @@ echo "=== Workspace: $WORKSPACE_NAME ==="
14
14
  echo ""
15
15
 
16
16
  # Show bundle values (proof of concept)
17
- if [ -n "$DEVELOPERNAME" ]; then
18
- echo "Welcome back, $DEVELOPERNAME!"
17
+ if [ -n "$DEVELOPER_NAME" ]; then
18
+ echo "Welcome back, $DEVELOPER_NAME!"
19
19
  fi
20
20
 
21
21
  # Show that we have access to the secret (masked)
22
- if [ -n "$EXAMPLEAPITOKEN" ]; then
22
+ if [ -n "$EXAMPLE_API_TOKEN" ]; then
23
23
  # Only show first 4 characters to prove we have access
24
- TOKEN_PREVIEW="${EXAMPLEAPITOKEN:0:4}..."
24
+ TOKEN_PREVIEW="${EXAMPLE_API_TOKEN:0:4}..."
25
25
  echo "API Token available: $TOKEN_PREVIEW (stored in OS keychain)"
26
26
  fi
27
27
 
package/README.md CHANGED
@@ -106,6 +106,22 @@ gssh switch
106
106
  gssh switch my-feature
107
107
  ```
108
108
 
109
+ ### Workspace Session Mode (`space`)
110
+
111
+ When GitSpace opens a workspace-scoped terminal session, it injects a `space` shell function (bash/zsh).
112
+
113
+ - Use `space ...` for workspace operations without repeating `--project` and `--workspace`
114
+ - `gssh` commands are restricted in this mode to avoid cross-workspace mistakes
115
+ - `gssh tmux ...` is blocked inside workspace sessions
116
+
117
+ Examples:
118
+
119
+ ```bash
120
+ space context --json
121
+ space review hunks src/app.ts --format json
122
+ space review add-hunk src/app.ts --index 1 --approve --body "Looks good"
123
+ ```
124
+
109
125
  ## Repo Config Bundles
110
126
 
111
127
  Repo config bundles allow repository owners to share onboarding configurations with their team. When someone clones a project that contains a bundle, they'll be guided through setup steps and have scripts automatically installed.
@@ -183,7 +199,7 @@ A bundle is a directory (typically `.gitspace/`) containing:
183
199
  Bundle values are passed to scripts as environment variables using the configured bundle keys:
184
200
 
185
201
  - `<KEY>` - Regular or secret value using the exact `configKey` from `bundle.json`
186
- - Legacy aliases `SPACE_VALUE_<KEY>` / `SPACE_SECRET_<KEY>` are also provided for compatibility
202
+ - `<NORMALIZED_KEY>` - Uppercase snake-case alias (for example, `teamName` -> `TEAM_NAME`)
187
203
 
188
204
  **Example script:**
189
205
 
@@ -195,12 +211,12 @@ WORKSPACE_NAME=$1
195
211
  REPOSITORY=$2
196
212
 
197
213
  # Access bundle values
198
- if [ -n "$TEAMNAME" ]; then
199
- echo "Welcome, $TEAMNAME team!"
214
+ if [ -n "$TEAM_NAME" ]; then
215
+ echo "Welcome, $TEAM_NAME team!"
200
216
  fi
201
217
 
202
218
  # Access secrets (stored securely in OS keychain)
203
- if [ -n "$APIKEY" ]; then
219
+ if [ -n "$API_KEY" ]; then
204
220
  echo "API Key configured"
205
221
  fi
206
222
  ```
@@ -393,9 +409,8 @@ inside each workspace so they can vary by branch:
393
409
  export SPACES_CURRENT_PROJECT="my-app"
394
410
 
395
411
  # Available in scripts (from bundle onboarding):
396
- # <KEY> - Value by bundle config key name
397
- # SPACE_VALUE_<KEY> - Legacy alias for regular values
398
- # SPACE_SECRET_<KEY> - Legacy alias for secret values
412
+ # <KEY> - Value by exact bundle config key name
413
+ # <NORMALIZED_KEY> - Uppercase snake-case alias (e.g. teamName -> TEAM_NAME)
399
414
  ```
400
415
 
401
416
  ## Directory Structure
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.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",
42
+ "@gitspace/darwin-arm64": "0.2.0-rc.17",
43
+ "@gitspace/darwin-x64": "0.2.0-rc.17",
44
+ "@gitspace/linux-arm64": "0.2.0-rc.17",
45
+ "@gitspace/linux-x64": "0.2.0-rc.17",
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.15", "", { "os": "darwin", "cpu": "arm64", "bin": { "gssh-darwin-arm64": "bin/gssh" } }, "sha512-gjqsRdz2f7cOOTbj97wmQ4Z0nh7jWUVN4Dxj1N/Ex4fp6dJ8qu2d5izbsCHWVIPX4uN+gK/qqzxuwLwCdaEqlQ=="],
66
+ "@gitspace/darwin-arm64": ["@gitspace/darwin-arm64@0.2.0-rc.17", "", { "os": "darwin", "cpu": "arm64", "bin": { "gssh-darwin-arm64": "bin/gssh" } }, "sha512-aPei17D31di296UBdqOt59HrdvcIRL2oVGVRWT4etCIESgjTyhZQcCLsIezsWI4E4/9DQAqaLAfTif7TsuCRHQ=="],
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
 
@@ -377,15 +377,14 @@ Using bundle values in scripts:
377
377
  ```bash
378
378
  #!/bin/bash
379
379
  # Values available as environment variables:
380
- # <KEY> - Value by bundle config key name
381
- # SPACE_VALUE_<KEY> - Legacy alias for regular values
382
- # SPACE_SECRET_<KEY> - Legacy alias for secret values
380
+ # <KEY> - Value by exact bundle config key name
381
+ # <NORMALIZED_KEY> - Uppercase snake-case alias (e.g. teamName -> TEAM_NAME)
383
382
 
384
- if [ -n "$TEAMNAME" ]; then
385
- echo "Welcome, $TEAMNAME team!"
383
+ if [ -n "$TEAM_NAME" ]; then
384
+ echo "Welcome, $TEAM_NAME team!"
386
385
  fi
387
386
 
388
- if [ -n "$APIKEY" ]; then
387
+ if [ -n "$API_KEY" ]; then
389
388
  echo "API Key configured"
390
389
  fi
391
390
  ```
@@ -960,8 +959,8 @@ Bundles allow teams to share onboarding configurations. Place in `.gitspace/`:
960
959
 
961
960
  **Using values in scripts:**
962
961
  ```bash
963
- echo "Team: $TEAMNAME"
964
- echo "Has API key: $APIKEY"
962
+ echo "Team: $TEAM_NAME"
963
+ echo "Has API key: $API_KEY"
965
964
  ```
966
965
 
967
966
  ---
@@ -378,8 +378,8 @@ git status`} />
378
378
  </div>
379
379
 
380
380
  <h3 className="text-xl font-semibold text-white mb-4">Using Values in Scripts</h3>
381
- <JsonBlock code={`echo "Team: $TEAMNAME"
382
- echo "Has API key: $APIKEY"`} />
381
+ <JsonBlock code={`echo "Team: $TEAM_NAME"
382
+ echo "Has API key: $API_KEY"`} />
383
383
  </div>
384
384
  );
385
385
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitspace",
3
- "version": "0.2.0-rc.17",
3
+ "version": "0.2.0-rc.19",
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.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"
20
+ "@gitspace/darwin-arm64": "0.2.0-rc.19",
21
+ "@gitspace/darwin-x64": "0.2.0-rc.19",
22
+ "@gitspace/linux-x64": "0.2.0-rc.19",
23
+ "@gitspace/linux-arm64": "0.2.0-rc.19"
24
24
  },
25
25
  "keywords": [
26
26
  "cli",
package/src/app.web.tsx CHANGED
@@ -19,6 +19,7 @@ import { useUserActivity } from "./hooks/index.js";
19
19
  import { useBundleRefreshAttachFlow } from './session/useBundleRefreshAttachFlow.js';
20
20
  import { useAttachController } from './app/session/useAttachController.js';
21
21
  import { useWorkspaceDeleteFlow } from './app/session/useWorkspaceDeleteFlow.js';
22
+ import { ReviewPage } from './pages/ReviewPage.web.js';
22
23
 
23
24
  // Import shared components and hooks
24
25
  import {
@@ -27,6 +28,7 @@ import {
27
28
  useFlow,
28
29
  getDefaultShortcuts,
29
30
  type MachineInfo,
31
+ type WorkspaceInfo,
30
32
  } from "./components/index.js";
31
33
  import { MachineListWeb } from "./components/MachineList.web.js";
32
34
  import { SpacesBrowserWeb } from "./components/SpacesBrowser.web.js";
@@ -46,7 +48,7 @@ import {
46
48
  resolveSessionBrowserCommand,
47
49
  } from './app/input/sessionCommands.js';
48
50
 
49
- type View = "machines" | "terminal";
51
+ type View = "machines" | "terminal" | "review";
50
52
 
51
53
  const PAGE_UP = '\x1b[5~';
52
54
  const PAGE_DOWN = '\x1b[6~';
@@ -88,6 +90,13 @@ export default function App() {
88
90
  inviteToken?: string;
89
91
  } | null>(null);
90
92
 
93
+ // Review workspace/project state
94
+ const [reviewWorkspace, setReviewWorkspace] = useState<{
95
+ projectName: string;
96
+ workspaceId: string;
97
+ workspaceLabel?: string;
98
+ } | null>(null);
99
+
91
100
  // Relay connection (for machine list)
92
101
  const relay = useRelayConnection();
93
102
 
@@ -234,7 +243,7 @@ export default function App() {
234
243
  setLocalNotificationConfig(terminal.notificationConfig);
235
244
  }, [terminal.notificationConfig]);
236
245
 
237
- // Parse invite from URL hash on load
246
+ // Parse invite from URL hash on load, and review params from query string
238
247
  useEffect(() => {
239
248
  const hash = window.location.hash;
240
249
  if (hash.startsWith("#invite=")) {
@@ -248,6 +257,16 @@ export default function App() {
248
257
  }
249
258
  });
250
259
  }
260
+
261
+ const params = new URLSearchParams(window.location.search);
262
+ if (params.get('view') === 'review') {
263
+ const ws = params.get('workspace');
264
+ const proj = params.get('project');
265
+ if (ws && proj) {
266
+ setReviewWorkspace({ projectName: proj, workspaceId: ws, workspaceLabel: ws });
267
+ setView('review');
268
+ }
269
+ }
251
270
  }, []);
252
271
 
253
272
  // Auto-connect on load (no token required for personal relays)
@@ -419,6 +438,16 @@ export default function App() {
419
438
  await attachController.attachFromSelection(params);
420
439
  }, [attachController]);
421
440
 
441
+ // Handle opening review for a workspace
442
+ const handleOpenReview = useCallback((workspace: WorkspaceInfo) => {
443
+ setReviewWorkspace({
444
+ projectName: workspace.projectName,
445
+ workspaceId: workspace.id,
446
+ workspaceLabel: workspace.name,
447
+ });
448
+ setView('review');
449
+ }, []);
450
+
422
451
  // Spaces browser hook
423
452
  const spacesBrowserProps = useSpacesBrowser({
424
453
  workspaces: terminal.workspaces,
@@ -740,6 +769,59 @@ export default function App() {
740
769
  return () => window.removeEventListener("keydown", handleKeyDown);
741
770
  }, [notifications.activeToast, notifications.attachToActiveToast, flow]);
742
771
 
772
+ // ========== Review View ==========
773
+ if (view === 'review' && reviewWorkspace) {
774
+ if (terminal.status === 'established') {
775
+ return (
776
+ <>
777
+ <ReviewPage
778
+ projectName={reviewWorkspace.projectName}
779
+ workspaceName={reviewWorkspace.workspaceId}
780
+ workspaceLabel={reviewWorkspace.workspaceLabel}
781
+ machineName={selectedMachine?.label || selectedMachine?.machineId}
782
+ sendReviewRequest={terminal.sendReviewRequest}
783
+ onBack={() => {
784
+ setView('terminal');
785
+ setReviewWorkspace(null);
786
+ }}
787
+ />
788
+ <Toaster theme="dark" position="top-right" richColors />
789
+ </>
790
+ );
791
+ }
792
+
793
+ // Connection not yet established — show a targeted connecting screen
794
+ // rather than falling through to the generic machine list.
795
+ const statusMessage = {
796
+ disconnected: "Disconnected",
797
+ connecting: "Connecting to relay...",
798
+ connected: "Connected, authenticating...",
799
+ handshaking: "Establishing secure connection...",
800
+ established: "Connected!",
801
+ error: "Connection failed",
802
+ }[terminal.status];
803
+
804
+ return (
805
+ <>
806
+ <div className="h-screen w-screen flex flex-col items-center justify-center bg-[#0d1117] px-4">
807
+ <div className="text-center">
808
+ <div className="text-lg text-[#e6edf3] mb-2">
809
+ Loading review for <span className="text-[#58a6ff]">{reviewWorkspace.workspaceLabel ?? reviewWorkspace.workspaceId}</span>
810
+ </div>
811
+ <div className="text-sm text-[#8b949e]">{statusMessage}</div>
812
+ <button
813
+ onClick={() => { setView('machines'); setReviewWorkspace(null); }}
814
+ className="mt-4 px-6 py-3 text-base bg-[#21262d] hover:bg-[#30363d] rounded-lg text-[#e6edf3] min-h-[48px] border border-[#30363d]"
815
+ >
816
+ Back to Machines
817
+ </button>
818
+ </div>
819
+ </div>
820
+ <Toaster theme="dark" position="top-right" richColors />
821
+ </>
822
+ );
823
+ }
824
+
743
825
  // ========== Spaces Browser View (browsing mode) ==========
744
826
  if (
745
827
  view === 'terminal' &&
@@ -819,7 +901,7 @@ export default function App() {
819
901
  </div>
820
902
  </div>
821
903
  <div className="flex-1 overflow-hidden">
822
- <SpacesBrowserWeb {...spacesBrowserProps} />
904
+ <SpacesBrowserWeb {...spacesBrowserProps} onReview={handleOpenReview} />
823
905
  </div>
824
906
  </div>
825
907
  <FlowWeb flow={flow} />
@@ -8,15 +8,16 @@ import { exec } from 'child_process';
8
8
  import { promisify } from 'util';
9
9
  import { logger } from '../utils/logger.js';
10
10
  import { SpacesError } from '../types/errors.js';
11
- import { getCurrentProject, getProjectBaseDir, getProjectWorkspacesDir } from '../core/config.js';
11
+ import { getCurrentProject, getGitspaceDir, getProjectBaseDir, getProjectWorkspacesDir } from '../core/config.js';
12
12
  import {
13
13
  detectBundleChanges,
14
14
  formatBundleChangeDetails,
15
15
  refreshBundle,
16
16
  type BundleRefreshOptions,
17
17
  } from '../core/bundle-refresh.js';
18
- import { join, resolve } from 'path';
18
+ import { join } from 'path';
19
19
  import { existsSync } from 'fs';
20
+ import { detectWorkspaceContextFromCwd } from '../utils/workspace-id.js';
20
21
 
21
22
  const execAsync = promisify(exec);
22
23
 
@@ -43,24 +44,16 @@ export interface BundleStatusOptions {
43
44
  * Returns the workspace path if found, undefined otherwise
44
45
  */
45
46
  function detectWorkspaceFromCwd(projectName: string): string | undefined {
46
- const cwd = process.cwd();
47
47
  const workspacesDir = getProjectWorkspacesDir(projectName);
48
48
 
49
- // Check if cwd is inside the workspaces directory
50
- const resolvedCwd = resolve(cwd);
51
- const resolvedWorkspacesDir = resolve(workspacesDir);
52
-
53
- if (resolvedCwd.startsWith(resolvedWorkspacesDir + '/') || resolvedCwd === resolvedWorkspacesDir) {
54
- // Extract the workspace name (first path segment after workspaces/)
55
- const relativePath = resolvedCwd.slice(resolvedWorkspacesDir.length + 1);
56
- const workspaceName = relativePath.split('/')[0];
49
+ const context = detectWorkspaceContextFromCwd(process.cwd(), getGitspaceDir());
50
+ if (!context || context.projectName !== projectName) {
51
+ return undefined;
52
+ }
57
53
 
58
- if (workspaceName) {
59
- const workspacePath = join(workspacesDir, workspaceName);
60
- if (existsSync(workspacePath)) {
61
- return workspacePath;
62
- }
63
- }
54
+ const workspacePath = join(workspacesDir, context.workspaceName);
55
+ if (existsSync(workspacePath)) {
56
+ return workspacePath;
64
57
  }
65
58
 
66
59
  return undefined;