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.
- package/.claude/settings.local.json +21 -0
- package/.gitspace/bundle.json +50 -0
- package/.gitspace/select/01-status.sh +40 -0
- package/.gitspace/setup/01-install-deps.sh +12 -0
- package/.gitspace/setup/02-typecheck.sh +16 -0
- package/AGENTS.md +439 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +25 -0
- package/README.md +607 -0
- package/bin/gssh +62 -0
- package/bun.lock +647 -0
- package/docs/CONNECTION.md +623 -0
- package/docs/GATEWAY-WORKER.md +319 -0
- package/docs/GETTING-STARTED.md +448 -0
- package/docs/GITSPACE-PLATFORM.md +1819 -0
- package/docs/INFRASTRUCTURE.md +1347 -0
- package/docs/PROTOCOL.md +619 -0
- package/docs/QUICKSTART.md +174 -0
- package/docs/RELAY.md +327 -0
- package/docs/REMOTE-DESIGN.md +549 -0
- package/docs/ROADMAP.md +564 -0
- package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
- package/docs/STACK-DESIGN.md +588 -0
- package/docs/UNIFIED_ARCHITECTURE.md +292 -0
- package/experiments/pty-benchmark.ts +148 -0
- package/experiments/pty-latency.ts +100 -0
- package/experiments/router/client.ts +199 -0
- package/experiments/router/protocol.ts +74 -0
- package/experiments/router/router.ts +217 -0
- package/experiments/router/session.ts +180 -0
- package/experiments/router/test.ts +133 -0
- package/experiments/socket-bandwidth.ts +77 -0
- package/homebrew/gitspace.rb +45 -0
- package/landing-page/ATTRIBUTIONS.md +3 -0
- package/landing-page/README.md +11 -0
- package/landing-page/bun.lock +801 -0
- package/landing-page/guidelines/Guidelines.md +61 -0
- package/landing-page/index.html +37 -0
- package/landing-page/package.json +90 -0
- package/landing-page/postcss.config.mjs +15 -0
- package/landing-page/public/_redirects +1 -0
- package/landing-page/public/favicon.png +0 -0
- package/landing-page/src/app/App.tsx +53 -0
- package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
- package/landing-page/src/app/components/ui/accordion.tsx +66 -0
- package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
- package/landing-page/src/app/components/ui/alert.tsx +66 -0
- package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
- package/landing-page/src/app/components/ui/avatar.tsx +53 -0
- package/landing-page/src/app/components/ui/badge.tsx +46 -0
- package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
- package/landing-page/src/app/components/ui/button.tsx +57 -0
- package/landing-page/src/app/components/ui/calendar.tsx +75 -0
- package/landing-page/src/app/components/ui/card.tsx +92 -0
- package/landing-page/src/app/components/ui/carousel.tsx +241 -0
- package/landing-page/src/app/components/ui/chart.tsx +353 -0
- package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
- package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
- package/landing-page/src/app/components/ui/command.tsx +177 -0
- package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
- package/landing-page/src/app/components/ui/dialog.tsx +135 -0
- package/landing-page/src/app/components/ui/drawer.tsx +132 -0
- package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
- package/landing-page/src/app/components/ui/form.tsx +168 -0
- package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
- package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
- package/landing-page/src/app/components/ui/input.tsx +21 -0
- package/landing-page/src/app/components/ui/label.tsx +24 -0
- package/landing-page/src/app/components/ui/menubar.tsx +276 -0
- package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
- package/landing-page/src/app/components/ui/pagination.tsx +127 -0
- package/landing-page/src/app/components/ui/popover.tsx +48 -0
- package/landing-page/src/app/components/ui/progress.tsx +31 -0
- package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
- package/landing-page/src/app/components/ui/resizable.tsx +56 -0
- package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
- package/landing-page/src/app/components/ui/select.tsx +189 -0
- package/landing-page/src/app/components/ui/separator.tsx +28 -0
- package/landing-page/src/app/components/ui/sheet.tsx +139 -0
- package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
- package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
- package/landing-page/src/app/components/ui/slider.tsx +63 -0
- package/landing-page/src/app/components/ui/sonner.tsx +25 -0
- package/landing-page/src/app/components/ui/switch.tsx +31 -0
- package/landing-page/src/app/components/ui/table.tsx +116 -0
- package/landing-page/src/app/components/ui/tabs.tsx +66 -0
- package/landing-page/src/app/components/ui/textarea.tsx +18 -0
- package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
- package/landing-page/src/app/components/ui/toggle.tsx +47 -0
- package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
- package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
- package/landing-page/src/app/components/ui/utils.ts +6 -0
- package/landing-page/src/components/docs/DocsContent.tsx +718 -0
- package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
- package/landing-page/src/components/landing/CTA.tsx +59 -0
- package/landing-page/src/components/landing/Comparison.tsx +84 -0
- package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
- package/landing-page/src/components/landing/Features.tsx +201 -0
- package/landing-page/src/components/landing/Hero.tsx +142 -0
- package/landing-page/src/components/landing/Pricing.tsx +140 -0
- package/landing-page/src/components/landing/Roadmap.tsx +86 -0
- package/landing-page/src/components/landing/Security.tsx +81 -0
- package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
- package/landing-page/src/components/landing/UseCases.tsx +55 -0
- package/landing-page/src/components/landing/Workflow.tsx +101 -0
- package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
- package/landing-page/src/components/layout/Footer.tsx +55 -0
- package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
- package/landing-page/src/components/ui/badge.tsx +39 -0
- package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
- package/landing-page/src/components/ui/button.tsx +57 -0
- package/landing-page/src/components/ui/card.tsx +79 -0
- package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
- package/landing-page/src/components/ui/separator.tsx +28 -0
- package/landing-page/src/lib/utils.ts +6 -0
- package/landing-page/src/main.tsx +10 -0
- package/landing-page/src/pages/Dashboard.tsx +133 -0
- package/landing-page/src/pages/DocsPage.tsx +79 -0
- package/landing-page/src/pages/LandingPage.tsx +31 -0
- package/landing-page/src/pages/TerminalView.tsx +106 -0
- package/landing-page/src/styles/fonts.css +0 -0
- package/landing-page/src/styles/index.css +3 -0
- package/landing-page/src/styles/tailwind.css +4 -0
- package/landing-page/src/styles/theme.css +181 -0
- package/landing-page/vite.config.ts +19 -0
- package/npm/darwin-arm64/bin/gssh +0 -0
- package/npm/darwin-arm64/package.json +20 -0
- package/package.json +74 -0
- package/scripts/build.ts +284 -0
- package/scripts/release.ts +140 -0
- package/src/__tests__/test-utils.ts +298 -0
- package/src/commands/__tests__/serve-messages.test.ts +190 -0
- package/src/commands/access.ts +298 -0
- package/src/commands/add.ts +452 -0
- package/src/commands/auth.ts +364 -0
- package/src/commands/connect.ts +287 -0
- package/src/commands/directory.ts +16 -0
- package/src/commands/host.ts +396 -0
- package/src/commands/identity.ts +184 -0
- package/src/commands/list.ts +200 -0
- package/src/commands/relay.ts +315 -0
- package/src/commands/remove.ts +241 -0
- package/src/commands/serve.ts +1493 -0
- package/src/commands/share.ts +456 -0
- package/src/commands/status.ts +125 -0
- package/src/commands/switch.ts +353 -0
- package/src/commands/tmux.ts +317 -0
- package/src/core/__tests__/access.test.ts +240 -0
- package/src/core/access.ts +277 -0
- package/src/core/bundle.ts +342 -0
- package/src/core/config.ts +510 -0
- package/src/core/git.ts +317 -0
- package/src/core/github.ts +151 -0
- package/src/core/identity.ts +631 -0
- package/src/core/linear.ts +225 -0
- package/src/core/shell.ts +161 -0
- package/src/core/trusted-relays.ts +315 -0
- package/src/index.ts +821 -0
- package/src/lib/remote-session/index.ts +7 -0
- package/src/lib/remote-session/protocol.ts +267 -0
- package/src/lib/remote-session/session-handler.ts +581 -0
- package/src/lib/remote-session/workspace-scanner.ts +167 -0
- package/src/lib/tmux-lite/README.md +81 -0
- package/src/lib/tmux-lite/cli.ts +796 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
- package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
- package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
- package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
- package/src/lib/tmux-lite/crypto/frames.ts +141 -0
- package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
- package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
- package/src/lib/tmux-lite/crypto/identity.ts +286 -0
- package/src/lib/tmux-lite/crypto/index.ts +51 -0
- package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
- package/src/lib/tmux-lite/crypto/invites.ts +215 -0
- package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
- package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
- package/src/lib/tmux-lite/crypto/keys.ts +47 -0
- package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
- package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
- package/src/lib/tmux-lite/handshake-handler.ts +451 -0
- package/src/lib/tmux-lite/protocol.test.ts +307 -0
- package/src/lib/tmux-lite/protocol.ts +266 -0
- package/src/lib/tmux-lite/relay-client.ts +506 -0
- package/src/lib/tmux-lite/server.ts +1250 -0
- package/src/lib/tmux-lite/shell-integration.sh +37 -0
- package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
- package/src/lib/tmux-lite/terminal-queries.ts +49 -0
- package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
- package/src/relay/__tests__/helpers/auth.ts +354 -0
- package/src/relay/__tests__/helpers/ports.ts +51 -0
- package/src/relay/__tests__/protocol-validation.test.ts +265 -0
- package/src/relay/authorization.ts +303 -0
- package/src/relay/embedded-assets.generated.d.ts +15 -0
- package/src/relay/identity.ts +352 -0
- package/src/relay/index.ts +57 -0
- package/src/relay/pipes.test.ts +427 -0
- package/src/relay/pipes.ts +195 -0
- package/src/relay/protocol.ts +804 -0
- package/src/relay/registries.test.ts +437 -0
- package/src/relay/registries.ts +593 -0
- package/src/relay/server.test.ts +1323 -0
- package/src/relay/server.ts +1092 -0
- package/src/relay/signing.ts +238 -0
- package/src/relay/types.ts +69 -0
- package/src/serve/client-session-manager.ts +622 -0
- package/src/serve/daemon.ts +497 -0
- package/src/serve/pty-session.ts +236 -0
- package/src/serve/types.ts +169 -0
- package/src/shared/components/Flow.tsx +453 -0
- package/src/shared/components/Flow.tui.tsx +343 -0
- package/src/shared/components/Flow.web.tsx +442 -0
- package/src/shared/components/Inbox.tsx +446 -0
- package/src/shared/components/Inbox.tui.tsx +262 -0
- package/src/shared/components/Inbox.web.tsx +329 -0
- package/src/shared/components/MachineList.tsx +187 -0
- package/src/shared/components/MachineList.tui.tsx +161 -0
- package/src/shared/components/MachineList.web.tsx +210 -0
- package/src/shared/components/ProjectList.tsx +176 -0
- package/src/shared/components/ProjectList.tui.tsx +109 -0
- package/src/shared/components/ProjectList.web.tsx +143 -0
- package/src/shared/components/SpacesBrowser.tsx +332 -0
- package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
- package/src/shared/components/SpacesBrowser.web.tsx +221 -0
- package/src/shared/components/index.ts +103 -0
- package/src/shared/hooks/index.ts +16 -0
- package/src/shared/hooks/useNavigation.ts +226 -0
- package/src/shared/index.ts +122 -0
- package/src/shared/providers/LocalMachineProvider.ts +425 -0
- package/src/shared/providers/MachineProvider.ts +165 -0
- package/src/shared/providers/RemoteMachineProvider.ts +444 -0
- package/src/shared/providers/index.ts +26 -0
- package/src/shared/types.ts +145 -0
- package/src/tui/adapters.ts +120 -0
- package/src/tui/app.tsx +1816 -0
- package/src/tui/components/Terminal.tsx +580 -0
- package/src/tui/hooks/index.ts +35 -0
- package/src/tui/hooks/useAppState.ts +314 -0
- package/src/tui/hooks/useDaemonStatus.ts +174 -0
- package/src/tui/hooks/useInboxTUI.ts +113 -0
- package/src/tui/hooks/useRemoteMachines.ts +209 -0
- package/src/tui/index.ts +24 -0
- package/src/tui/state.ts +299 -0
- package/src/tui/terminal-bracketed-paste.test.ts +45 -0
- package/src/tui/terminal-bracketed-paste.ts +47 -0
- package/src/types/bundle.ts +112 -0
- package/src/types/config.ts +89 -0
- package/src/types/errors.ts +206 -0
- package/src/types/identity.ts +284 -0
- package/src/types/workspace-fuzzy.ts +49 -0
- package/src/types/workspace.ts +151 -0
- package/src/utils/bun-socket-writer.ts +80 -0
- package/src/utils/deps.ts +127 -0
- package/src/utils/fuzzy-match.ts +125 -0
- package/src/utils/logger.ts +127 -0
- package/src/utils/markdown.ts +254 -0
- package/src/utils/onboarding.ts +229 -0
- package/src/utils/prompts.ts +114 -0
- package/src/utils/run-commands.ts +112 -0
- package/src/utils/run-scripts.ts +142 -0
- package/src/utils/sanitize.ts +98 -0
- package/src/utils/secrets.ts +122 -0
- package/src/utils/shell-escape.ts +40 -0
- package/src/utils/utf8.ts +79 -0
- package/src/utils/workspace-state.ts +47 -0
- package/src/web/README.md +73 -0
- package/src/web/bun.lock +575 -0
- package/src/web/eslint.config.js +23 -0
- package/src/web/index.html +16 -0
- package/src/web/package.json +37 -0
- package/src/web/public/vite.svg +1 -0
- package/src/web/src/App.tsx +604 -0
- package/src/web/src/assets/react.svg +1 -0
- package/src/web/src/components/Terminal.tsx +207 -0
- package/src/web/src/hooks/useRelayConnection.ts +224 -0
- package/src/web/src/hooks/useTerminal.ts +699 -0
- package/src/web/src/index.css +55 -0
- package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
- package/src/web/src/lib/crypto/frames.ts +205 -0
- package/src/web/src/lib/crypto/handshake.ts +396 -0
- package/src/web/src/lib/crypto/identity.ts +128 -0
- package/src/web/src/lib/crypto/keyexchange.ts +246 -0
- package/src/web/src/lib/crypto/relay-signing.ts +53 -0
- package/src/web/src/lib/invite.ts +58 -0
- package/src/web/src/lib/storage/identity-store.ts +94 -0
- package/src/web/src/main.tsx +10 -0
- package/src/web/src/types/identity.ts +45 -0
- package/src/web/tsconfig.app.json +28 -0
- package/src/web/tsconfig.json +7 -0
- package/src/web/tsconfig.node.json +26 -0
- package/src/web/vite.config.ts +31 -0
- package/todo-security.md +92 -0
- package/tsconfig.json +23 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
- package/worker/bun.lock +237 -0
- package/worker/package.json +22 -0
- package/worker/schema.sql +96 -0
- package/worker/src/handlers/auth.ts +451 -0
- package/worker/src/handlers/subdomains.ts +376 -0
- package/worker/src/handlers/user.ts +98 -0
- package/worker/src/index.ts +70 -0
- package/worker/src/middleware/auth.ts +152 -0
- package/worker/src/services/cloudflare.ts +609 -0
- package/worker/src/types.ts +96 -0
- package/worker/tsconfig.json +15 -0
- 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
|
+
}
|