gitspace 0.2.0-rc.1

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 (318) hide show
  1. package/.claude/settings.local.json +21 -0
  2. package/.gitspace/bundle.json +50 -0
  3. package/.gitspace/select/01-status.sh +40 -0
  4. package/.gitspace/setup/01-install-deps.sh +12 -0
  5. package/.gitspace/setup/02-typecheck.sh +16 -0
  6. package/AGENTS.md +439 -0
  7. package/CLAUDE.md +1 -0
  8. package/LICENSE +25 -0
  9. package/README.md +607 -0
  10. package/bin/gssh +62 -0
  11. package/bun.lock +647 -0
  12. package/docs/CONNECTION.md +623 -0
  13. package/docs/GATEWAY-WORKER.md +319 -0
  14. package/docs/GETTING-STARTED.md +448 -0
  15. package/docs/GITSPACE-PLATFORM.md +1819 -0
  16. package/docs/INFRASTRUCTURE.md +1347 -0
  17. package/docs/PROTOCOL.md +619 -0
  18. package/docs/QUICKSTART.md +174 -0
  19. package/docs/RELAY.md +327 -0
  20. package/docs/REMOTE-DESIGN.md +549 -0
  21. package/docs/ROADMAP.md +564 -0
  22. package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
  23. package/docs/STACK-DESIGN.md +588 -0
  24. package/docs/UNIFIED_ARCHITECTURE.md +292 -0
  25. package/experiments/pty-benchmark.ts +148 -0
  26. package/experiments/pty-latency.ts +100 -0
  27. package/experiments/router/client.ts +199 -0
  28. package/experiments/router/protocol.ts +74 -0
  29. package/experiments/router/router.ts +217 -0
  30. package/experiments/router/session.ts +180 -0
  31. package/experiments/router/test.ts +133 -0
  32. package/experiments/socket-bandwidth.ts +77 -0
  33. package/homebrew/gitspace.rb +45 -0
  34. package/landing-page/ATTRIBUTIONS.md +3 -0
  35. package/landing-page/README.md +11 -0
  36. package/landing-page/bun.lock +801 -0
  37. package/landing-page/guidelines/Guidelines.md +61 -0
  38. package/landing-page/index.html +37 -0
  39. package/landing-page/package.json +90 -0
  40. package/landing-page/postcss.config.mjs +15 -0
  41. package/landing-page/public/_redirects +1 -0
  42. package/landing-page/public/favicon.png +0 -0
  43. package/landing-page/src/app/App.tsx +53 -0
  44. package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
  45. package/landing-page/src/app/components/ui/accordion.tsx +66 -0
  46. package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
  47. package/landing-page/src/app/components/ui/alert.tsx +66 -0
  48. package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
  49. package/landing-page/src/app/components/ui/avatar.tsx +53 -0
  50. package/landing-page/src/app/components/ui/badge.tsx +46 -0
  51. package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
  52. package/landing-page/src/app/components/ui/button.tsx +57 -0
  53. package/landing-page/src/app/components/ui/calendar.tsx +75 -0
  54. package/landing-page/src/app/components/ui/card.tsx +92 -0
  55. package/landing-page/src/app/components/ui/carousel.tsx +241 -0
  56. package/landing-page/src/app/components/ui/chart.tsx +353 -0
  57. package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
  58. package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
  59. package/landing-page/src/app/components/ui/command.tsx +177 -0
  60. package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
  61. package/landing-page/src/app/components/ui/dialog.tsx +135 -0
  62. package/landing-page/src/app/components/ui/drawer.tsx +132 -0
  63. package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
  64. package/landing-page/src/app/components/ui/form.tsx +168 -0
  65. package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
  66. package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
  67. package/landing-page/src/app/components/ui/input.tsx +21 -0
  68. package/landing-page/src/app/components/ui/label.tsx +24 -0
  69. package/landing-page/src/app/components/ui/menubar.tsx +276 -0
  70. package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
  71. package/landing-page/src/app/components/ui/pagination.tsx +127 -0
  72. package/landing-page/src/app/components/ui/popover.tsx +48 -0
  73. package/landing-page/src/app/components/ui/progress.tsx +31 -0
  74. package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
  75. package/landing-page/src/app/components/ui/resizable.tsx +56 -0
  76. package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
  77. package/landing-page/src/app/components/ui/select.tsx +189 -0
  78. package/landing-page/src/app/components/ui/separator.tsx +28 -0
  79. package/landing-page/src/app/components/ui/sheet.tsx +139 -0
  80. package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
  81. package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
  82. package/landing-page/src/app/components/ui/slider.tsx +63 -0
  83. package/landing-page/src/app/components/ui/sonner.tsx +25 -0
  84. package/landing-page/src/app/components/ui/switch.tsx +31 -0
  85. package/landing-page/src/app/components/ui/table.tsx +116 -0
  86. package/landing-page/src/app/components/ui/tabs.tsx +66 -0
  87. package/landing-page/src/app/components/ui/textarea.tsx +18 -0
  88. package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
  89. package/landing-page/src/app/components/ui/toggle.tsx +47 -0
  90. package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
  91. package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
  92. package/landing-page/src/app/components/ui/utils.ts +6 -0
  93. package/landing-page/src/components/docs/DocsContent.tsx +718 -0
  94. package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
  95. package/landing-page/src/components/landing/CTA.tsx +59 -0
  96. package/landing-page/src/components/landing/Comparison.tsx +84 -0
  97. package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
  98. package/landing-page/src/components/landing/Features.tsx +201 -0
  99. package/landing-page/src/components/landing/Hero.tsx +142 -0
  100. package/landing-page/src/components/landing/Pricing.tsx +140 -0
  101. package/landing-page/src/components/landing/Roadmap.tsx +86 -0
  102. package/landing-page/src/components/landing/Security.tsx +81 -0
  103. package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
  104. package/landing-page/src/components/landing/UseCases.tsx +55 -0
  105. package/landing-page/src/components/landing/Workflow.tsx +101 -0
  106. package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
  107. package/landing-page/src/components/layout/Footer.tsx +55 -0
  108. package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
  109. package/landing-page/src/components/ui/badge.tsx +39 -0
  110. package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
  111. package/landing-page/src/components/ui/button.tsx +57 -0
  112. package/landing-page/src/components/ui/card.tsx +79 -0
  113. package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
  114. package/landing-page/src/components/ui/separator.tsx +28 -0
  115. package/landing-page/src/lib/utils.ts +6 -0
  116. package/landing-page/src/main.tsx +10 -0
  117. package/landing-page/src/pages/Dashboard.tsx +133 -0
  118. package/landing-page/src/pages/DocsPage.tsx +79 -0
  119. package/landing-page/src/pages/LandingPage.tsx +31 -0
  120. package/landing-page/src/pages/TerminalView.tsx +106 -0
  121. package/landing-page/src/styles/fonts.css +0 -0
  122. package/landing-page/src/styles/index.css +3 -0
  123. package/landing-page/src/styles/tailwind.css +4 -0
  124. package/landing-page/src/styles/theme.css +181 -0
  125. package/landing-page/vite.config.ts +19 -0
  126. package/npm/darwin-arm64/bin/gssh +0 -0
  127. package/npm/darwin-arm64/package.json +20 -0
  128. package/package.json +74 -0
  129. package/scripts/build.ts +284 -0
  130. package/scripts/release.ts +140 -0
  131. package/src/__tests__/test-utils.ts +298 -0
  132. package/src/commands/__tests__/serve-messages.test.ts +190 -0
  133. package/src/commands/access.ts +298 -0
  134. package/src/commands/add.ts +452 -0
  135. package/src/commands/auth.ts +364 -0
  136. package/src/commands/connect.ts +287 -0
  137. package/src/commands/directory.ts +16 -0
  138. package/src/commands/host.ts +396 -0
  139. package/src/commands/identity.ts +184 -0
  140. package/src/commands/list.ts +200 -0
  141. package/src/commands/relay.ts +315 -0
  142. package/src/commands/remove.ts +241 -0
  143. package/src/commands/serve.ts +1493 -0
  144. package/src/commands/share.ts +456 -0
  145. package/src/commands/status.ts +125 -0
  146. package/src/commands/switch.ts +353 -0
  147. package/src/commands/tmux.ts +317 -0
  148. package/src/core/__tests__/access.test.ts +240 -0
  149. package/src/core/access.ts +277 -0
  150. package/src/core/bundle.ts +342 -0
  151. package/src/core/config.ts +510 -0
  152. package/src/core/git.ts +317 -0
  153. package/src/core/github.ts +151 -0
  154. package/src/core/identity.ts +631 -0
  155. package/src/core/linear.ts +225 -0
  156. package/src/core/shell.ts +161 -0
  157. package/src/core/trusted-relays.ts +315 -0
  158. package/src/index.ts +821 -0
  159. package/src/lib/remote-session/index.ts +7 -0
  160. package/src/lib/remote-session/protocol.ts +267 -0
  161. package/src/lib/remote-session/session-handler.ts +581 -0
  162. package/src/lib/remote-session/workspace-scanner.ts +167 -0
  163. package/src/lib/tmux-lite/README.md +81 -0
  164. package/src/lib/tmux-lite/cli.ts +796 -0
  165. package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
  166. package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
  167. package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
  168. package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
  169. package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
  170. package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
  171. package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
  172. package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
  173. package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
  174. package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
  175. package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
  176. package/src/lib/tmux-lite/crypto/frames.ts +141 -0
  177. package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
  178. package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
  179. package/src/lib/tmux-lite/crypto/identity.ts +286 -0
  180. package/src/lib/tmux-lite/crypto/index.ts +51 -0
  181. package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
  182. package/src/lib/tmux-lite/crypto/invites.ts +215 -0
  183. package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
  184. package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
  185. package/src/lib/tmux-lite/crypto/keys.ts +47 -0
  186. package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
  187. package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
  188. package/src/lib/tmux-lite/handshake-handler.ts +451 -0
  189. package/src/lib/tmux-lite/protocol.test.ts +307 -0
  190. package/src/lib/tmux-lite/protocol.ts +266 -0
  191. package/src/lib/tmux-lite/relay-client.ts +506 -0
  192. package/src/lib/tmux-lite/server.ts +1250 -0
  193. package/src/lib/tmux-lite/shell-integration.sh +37 -0
  194. package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
  195. package/src/lib/tmux-lite/terminal-queries.ts +49 -0
  196. package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
  197. package/src/relay/__tests__/helpers/auth.ts +354 -0
  198. package/src/relay/__tests__/helpers/ports.ts +51 -0
  199. package/src/relay/__tests__/protocol-validation.test.ts +265 -0
  200. package/src/relay/authorization.ts +303 -0
  201. package/src/relay/embedded-assets.generated.d.ts +15 -0
  202. package/src/relay/identity.ts +352 -0
  203. package/src/relay/index.ts +57 -0
  204. package/src/relay/pipes.test.ts +427 -0
  205. package/src/relay/pipes.ts +195 -0
  206. package/src/relay/protocol.ts +804 -0
  207. package/src/relay/registries.test.ts +437 -0
  208. package/src/relay/registries.ts +593 -0
  209. package/src/relay/server.test.ts +1323 -0
  210. package/src/relay/server.ts +1092 -0
  211. package/src/relay/signing.ts +238 -0
  212. package/src/relay/types.ts +69 -0
  213. package/src/serve/client-session-manager.ts +622 -0
  214. package/src/serve/daemon.ts +497 -0
  215. package/src/serve/pty-session.ts +236 -0
  216. package/src/serve/types.ts +169 -0
  217. package/src/shared/components/Flow.tsx +453 -0
  218. package/src/shared/components/Flow.tui.tsx +343 -0
  219. package/src/shared/components/Flow.web.tsx +442 -0
  220. package/src/shared/components/Inbox.tsx +446 -0
  221. package/src/shared/components/Inbox.tui.tsx +262 -0
  222. package/src/shared/components/Inbox.web.tsx +329 -0
  223. package/src/shared/components/MachineList.tsx +187 -0
  224. package/src/shared/components/MachineList.tui.tsx +161 -0
  225. package/src/shared/components/MachineList.web.tsx +210 -0
  226. package/src/shared/components/ProjectList.tsx +176 -0
  227. package/src/shared/components/ProjectList.tui.tsx +109 -0
  228. package/src/shared/components/ProjectList.web.tsx +143 -0
  229. package/src/shared/components/SpacesBrowser.tsx +332 -0
  230. package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
  231. package/src/shared/components/SpacesBrowser.web.tsx +221 -0
  232. package/src/shared/components/index.ts +103 -0
  233. package/src/shared/hooks/index.ts +16 -0
  234. package/src/shared/hooks/useNavigation.ts +226 -0
  235. package/src/shared/index.ts +122 -0
  236. package/src/shared/providers/LocalMachineProvider.ts +425 -0
  237. package/src/shared/providers/MachineProvider.ts +165 -0
  238. package/src/shared/providers/RemoteMachineProvider.ts +444 -0
  239. package/src/shared/providers/index.ts +26 -0
  240. package/src/shared/types.ts +145 -0
  241. package/src/tui/adapters.ts +120 -0
  242. package/src/tui/app.tsx +1816 -0
  243. package/src/tui/components/Terminal.tsx +580 -0
  244. package/src/tui/hooks/index.ts +35 -0
  245. package/src/tui/hooks/useAppState.ts +314 -0
  246. package/src/tui/hooks/useDaemonStatus.ts +174 -0
  247. package/src/tui/hooks/useInboxTUI.ts +113 -0
  248. package/src/tui/hooks/useRemoteMachines.ts +209 -0
  249. package/src/tui/index.ts +24 -0
  250. package/src/tui/state.ts +299 -0
  251. package/src/tui/terminal-bracketed-paste.test.ts +45 -0
  252. package/src/tui/terminal-bracketed-paste.ts +47 -0
  253. package/src/types/bundle.ts +112 -0
  254. package/src/types/config.ts +89 -0
  255. package/src/types/errors.ts +206 -0
  256. package/src/types/identity.ts +284 -0
  257. package/src/types/workspace-fuzzy.ts +49 -0
  258. package/src/types/workspace.ts +151 -0
  259. package/src/utils/bun-socket-writer.ts +80 -0
  260. package/src/utils/deps.ts +127 -0
  261. package/src/utils/fuzzy-match.ts +125 -0
  262. package/src/utils/logger.ts +127 -0
  263. package/src/utils/markdown.ts +254 -0
  264. package/src/utils/onboarding.ts +229 -0
  265. package/src/utils/prompts.ts +114 -0
  266. package/src/utils/run-commands.ts +112 -0
  267. package/src/utils/run-scripts.ts +142 -0
  268. package/src/utils/sanitize.ts +98 -0
  269. package/src/utils/secrets.ts +122 -0
  270. package/src/utils/shell-escape.ts +40 -0
  271. package/src/utils/utf8.ts +79 -0
  272. package/src/utils/workspace-state.ts +47 -0
  273. package/src/web/README.md +73 -0
  274. package/src/web/bun.lock +575 -0
  275. package/src/web/eslint.config.js +23 -0
  276. package/src/web/index.html +16 -0
  277. package/src/web/package.json +37 -0
  278. package/src/web/public/vite.svg +1 -0
  279. package/src/web/src/App.tsx +604 -0
  280. package/src/web/src/assets/react.svg +1 -0
  281. package/src/web/src/components/Terminal.tsx +207 -0
  282. package/src/web/src/hooks/useRelayConnection.ts +224 -0
  283. package/src/web/src/hooks/useTerminal.ts +699 -0
  284. package/src/web/src/index.css +55 -0
  285. package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
  286. package/src/web/src/lib/crypto/frames.ts +205 -0
  287. package/src/web/src/lib/crypto/handshake.ts +396 -0
  288. package/src/web/src/lib/crypto/identity.ts +128 -0
  289. package/src/web/src/lib/crypto/keyexchange.ts +246 -0
  290. package/src/web/src/lib/crypto/relay-signing.ts +53 -0
  291. package/src/web/src/lib/invite.ts +58 -0
  292. package/src/web/src/lib/storage/identity-store.ts +94 -0
  293. package/src/web/src/main.tsx +10 -0
  294. package/src/web/src/types/identity.ts +45 -0
  295. package/src/web/tsconfig.app.json +28 -0
  296. package/src/web/tsconfig.json +7 -0
  297. package/src/web/tsconfig.node.json +26 -0
  298. package/src/web/vite.config.ts +31 -0
  299. package/todo-security.md +92 -0
  300. package/tsconfig.json +23 -0
  301. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
  302. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
  303. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
  304. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
  305. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
  306. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
  307. package/worker/bun.lock +237 -0
  308. package/worker/package.json +22 -0
  309. package/worker/schema.sql +96 -0
  310. package/worker/src/handlers/auth.ts +451 -0
  311. package/worker/src/handlers/subdomains.ts +376 -0
  312. package/worker/src/handlers/user.ts +98 -0
  313. package/worker/src/index.ts +70 -0
  314. package/worker/src/middleware/auth.ts +152 -0
  315. package/worker/src/services/cloudflare.ts +609 -0
  316. package/worker/src/types.ts +96 -0
  317. package/worker/tsconfig.json +15 -0
  318. package/worker/wrangler.toml +26 -0
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Bundle loading, validation, and script management
3
+ */
4
+
5
+ import {
6
+ existsSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ copyFileSync,
10
+ chmodSync,
11
+ mkdirSync,
12
+ statSync,
13
+ writeFileSync,
14
+ rmSync,
15
+ } from 'fs';
16
+ import { join, basename, resolve, sep } from 'path';
17
+ import { tmpdir } from 'os';
18
+ import { SpacesError } from '../types/errors.js';
19
+ import { logger } from '../utils/logger.js';
20
+ import type { SpacesBundle, LoadedBundle } from '../types/bundle.js';
21
+ import { getScriptsPhaseDir } from './config.js';
22
+
23
+ const BUNDLE_FILENAME = 'bundle.json';
24
+ const BUNDLE_SUBDIRS = ['.gitspace', '.gitspace-config', 'gitspace-config', '.spaces-config', 'spaces-config', '.spaces'];
25
+ const SCRIPT_PHASES = ['pre', 'setup', 'select', 'remove'] as const;
26
+
27
+ function assertSafeExtractedPaths(rootDir: string): void {
28
+ const rootResolved = resolve(rootDir);
29
+ const rootPrefix = rootResolved.endsWith(sep) ? rootResolved : `${rootResolved}${sep}`;
30
+ const stack: string[] = [''];
31
+
32
+ while (stack.length > 0) {
33
+ const relativeDir = stack.pop() ?? '';
34
+ const currentDir = join(rootResolved, relativeDir);
35
+ const entries = readdirSync(currentDir, { withFileTypes: true });
36
+
37
+ for (const entry of entries) {
38
+ const entryRelative = relativeDir ? join(relativeDir, entry.name) : entry.name;
39
+ const resolvedPath = resolve(rootResolved, entryRelative);
40
+
41
+ if (!resolvedPath.startsWith(rootPrefix)) {
42
+ throw new Error(`Zip slip detected: ${entryRelative}`);
43
+ }
44
+
45
+ if (entry.isSymbolicLink()) {
46
+ throw new Error(`Zip slip detected: symlink ${entryRelative}`);
47
+ }
48
+
49
+ if (entry.isDirectory()) {
50
+ stack.push(entryRelative);
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Detect bundle in cloned repository
58
+ * Checks common subdirectory names for bundle.json
59
+ */
60
+ export function detectBundleInRepo(baseDir: string): string | null {
61
+ logger.debug(`Checking for bundle in: ${baseDir}`);
62
+
63
+ for (const subdir of BUNDLE_SUBDIRS) {
64
+ const bundlePath = join(baseDir, subdir, BUNDLE_FILENAME);
65
+ logger.debug(` Checking: ${bundlePath}`);
66
+ if (existsSync(bundlePath)) {
67
+ logger.debug(` Found bundle at: ${bundlePath}`);
68
+ return join(baseDir, subdir);
69
+ }
70
+ }
71
+
72
+ // Check root level
73
+ const rootBundlePath = join(baseDir, BUNDLE_FILENAME);
74
+ logger.debug(` Checking root: ${rootBundlePath}`);
75
+ if (existsSync(rootBundlePath)) {
76
+ logger.debug(` Found bundle at root`);
77
+ return baseDir;
78
+ }
79
+
80
+ logger.debug(` No bundle found`);
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * Load bundle manifest from local path
86
+ */
87
+ export function loadBundleFromPath(bundleDir: string): LoadedBundle {
88
+ const manifestPath = join(bundleDir, BUNDLE_FILENAME);
89
+
90
+ if (!existsSync(manifestPath)) {
91
+ throw new SpacesError(
92
+ `Bundle manifest not found: ${manifestPath}`,
93
+ 'USER_ERROR',
94
+ 1
95
+ );
96
+ }
97
+
98
+ try {
99
+ const content = readFileSync(manifestPath, 'utf-8');
100
+ const bundle = JSON.parse(content) as SpacesBundle;
101
+ validateBundle(bundle);
102
+
103
+ return {
104
+ bundle,
105
+ bundleDir,
106
+ source: bundleDir,
107
+ };
108
+ } catch (error) {
109
+ if (error instanceof SpacesError) throw error;
110
+ throw new SpacesError(
111
+ `Failed to parse bundle manifest: ${error instanceof Error ? error.message : 'Unknown error'}`,
112
+ 'USER_ERROR',
113
+ 1
114
+ );
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Download and extract bundle from remote URL (zip archive)
120
+ */
121
+ export async function loadBundleFromUrl(url: string): Promise<LoadedBundle> {
122
+ const tempDir = join(tmpdir(), `spaces-bundle-${Date.now()}`);
123
+
124
+ try {
125
+ logger.info('Downloading bundle...');
126
+
127
+ const response = await fetch(url);
128
+ if (!response.ok) {
129
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
130
+ }
131
+
132
+ // Create temp directory
133
+ mkdirSync(tempDir, { recursive: true });
134
+
135
+ // Get the response as array buffer
136
+ const arrayBuffer = await response.arrayBuffer();
137
+ const zipPath = join(tempDir, 'bundle.zip');
138
+
139
+ // Write zip file
140
+ writeFileSync(zipPath, Buffer.from(arrayBuffer));
141
+
142
+ // Extract using unzip command
143
+ const { exec } = await import('child_process');
144
+ const { promisify } = await import('util');
145
+ const execAsync = promisify(exec);
146
+
147
+ await execAsync(`unzip -q "${zipPath}" -d "${tempDir}"`);
148
+ assertSafeExtractedPaths(tempDir);
149
+
150
+ // Find the bundle manifest (might be in root or a subdirectory)
151
+ let bundleDir = tempDir;
152
+ if (!existsSync(join(tempDir, BUNDLE_FILENAME))) {
153
+ // Check if there's a single directory that contains the manifest
154
+ const entries = readdirSync(tempDir);
155
+ for (const entry of entries) {
156
+ const entryPath = join(tempDir, entry);
157
+ if (statSync(entryPath).isDirectory() && existsSync(join(entryPath, BUNDLE_FILENAME))) {
158
+ bundleDir = entryPath;
159
+ break;
160
+ }
161
+ }
162
+ }
163
+
164
+ const manifestPath = join(bundleDir, BUNDLE_FILENAME);
165
+ if (!existsSync(manifestPath)) {
166
+ throw new Error('Bundle manifest (bundle.json) not found in archive');
167
+ }
168
+
169
+ const content = readFileSync(manifestPath, 'utf-8');
170
+ const bundle = JSON.parse(content) as SpacesBundle;
171
+ validateBundle(bundle);
172
+
173
+ logger.success('Bundle downloaded and extracted');
174
+
175
+ return {
176
+ bundle,
177
+ bundleDir,
178
+ source: url,
179
+ };
180
+ } catch (error) {
181
+ // Clean up temp directory on error
182
+ if (existsSync(tempDir)) {
183
+ try {
184
+ rmSync(tempDir, { recursive: true, force: true });
185
+ } catch {
186
+ // Ignore cleanup errors
187
+ }
188
+ }
189
+
190
+ if (error instanceof SpacesError) throw error;
191
+ throw new SpacesError(
192
+ `Failed to fetch bundle from ${url}: ${error instanceof Error ? error.message : 'Unknown error'}`,
193
+ 'SERVICE_ERROR',
194
+ 3
195
+ );
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Validate bundle manifest schema
201
+ */
202
+ export function validateBundle(bundle: SpacesBundle): void {
203
+ if (!bundle.version || bundle.version !== '1.0') {
204
+ throw new SpacesError(
205
+ `Unsupported bundle version: ${bundle.version}. Expected "1.0"`,
206
+ 'USER_ERROR',
207
+ 1
208
+ );
209
+ }
210
+
211
+ if (!bundle.name) {
212
+ throw new SpacesError('Bundle must have a name', 'USER_ERROR', 1);
213
+ }
214
+
215
+ // Validate onboarding steps if present
216
+ if (bundle.onboarding) {
217
+ const ids = new Set<string>();
218
+ for (const step of bundle.onboarding) {
219
+ if (!step.id) {
220
+ throw new SpacesError('Each onboarding step must have an id', 'USER_ERROR', 1);
221
+ }
222
+ if (ids.has(step.id)) {
223
+ throw new SpacesError(`Duplicate onboarding step id: ${step.id}`, 'USER_ERROR', 1);
224
+ }
225
+ ids.add(step.id);
226
+
227
+ if (!['info', 'confirm', 'secret', 'input'].includes(step.type)) {
228
+ throw new SpacesError(`Invalid step type: ${step.type}`, 'USER_ERROR', 1);
229
+ }
230
+
231
+ // Validate configKey for secret/input steps
232
+ if (step.type === 'secret' || step.type === 'input') {
233
+ // Cast to access configKey since TypeScript knows these types should have it
234
+ const stepWithKey = step as { configKey?: string };
235
+ if (!stepWithKey.configKey) {
236
+ throw new SpacesError(
237
+ `Step "${step.id}" of type "${step.type}" must have a configKey`,
238
+ 'USER_ERROR',
239
+ 1
240
+ );
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Discover executable scripts in a bundle phase directory
249
+ */
250
+ function discoverBundleScripts(bundleDir: string, phase: string): string[] {
251
+ const phaseDir = join(bundleDir, phase);
252
+
253
+ if (!existsSync(phaseDir)) {
254
+ return [];
255
+ }
256
+
257
+ try {
258
+ const files = readdirSync(phaseDir);
259
+ const scripts: string[] = [];
260
+
261
+ for (const file of files) {
262
+ const filePath = join(phaseDir, file);
263
+ const stats = statSync(filePath);
264
+
265
+ // Include files (check execute permission for Unix)
266
+ if (stats.isFile()) {
267
+ scripts.push(file);
268
+ }
269
+ }
270
+
271
+ // Sort alphabetically for predictable order
272
+ scripts.sort();
273
+ return scripts;
274
+ } catch (error) {
275
+ logger.debug(`Error discovering bundle scripts in ${phase}: ${error}`);
276
+ return [];
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Copy scripts from bundle to project scripts directory
282
+ */
283
+ export function copyBundleScripts(
284
+ bundleDir: string,
285
+ projectName: string
286
+ ): { copied: number; skipped: number } {
287
+ let copied = 0;
288
+ let skipped = 0;
289
+
290
+ for (const phase of SCRIPT_PHASES) {
291
+ const scripts = discoverBundleScripts(bundleDir, phase);
292
+ if (scripts.length === 0) continue;
293
+
294
+ const targetDir = getScriptsPhaseDir(projectName, phase);
295
+
296
+ // Ensure target directory exists
297
+ if (!existsSync(targetDir)) {
298
+ mkdirSync(targetDir, { recursive: true });
299
+ }
300
+
301
+ for (const scriptFile of scripts) {
302
+ const sourcePath = join(bundleDir, phase, scriptFile);
303
+ const targetPath = join(targetDir, scriptFile);
304
+
305
+ // Skip if target already exists
306
+ if (existsSync(targetPath)) {
307
+ logger.debug(`Script already exists, skipping: ${scriptFile}`);
308
+ skipped++;
309
+ continue;
310
+ }
311
+
312
+ copyFileSync(sourcePath, targetPath);
313
+ chmodSync(targetPath, 0o755);
314
+ logger.debug(`Copied script: ${scriptFile} -> ${phase}/`);
315
+ copied++;
316
+ }
317
+ }
318
+
319
+ if (copied > 0) {
320
+ logger.success(`Copied ${copied} bundle script${copied === 1 ? '' : 's'}`);
321
+ }
322
+ if (skipped > 0) {
323
+ logger.dim(`Skipped ${skipped} existing script${skipped === 1 ? '' : 's'}`);
324
+ }
325
+
326
+ return { copied, skipped };
327
+ }
328
+
329
+ /**
330
+ * Clean up temporary bundle directory (for URL bundles)
331
+ */
332
+ export function cleanupBundleDir(bundleDir: string): void {
333
+ // Only clean up if it's in the temp directory
334
+ if (bundleDir.startsWith(tmpdir())) {
335
+ try {
336
+ rmSync(bundleDir, { recursive: true, force: true });
337
+ logger.debug('Cleaned up temporary bundle directory');
338
+ } catch (error) {
339
+ logger.debug(`Failed to clean up bundle directory: ${error}`);
340
+ }
341
+ }
342
+ }