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,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown generation for Linear issues
|
|
3
|
+
* Generates markdown files in the specified template format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFileSync } from 'fs'
|
|
7
|
+
import { join, extname } from 'path'
|
|
8
|
+
import type { LinearIssue as Issue, LinearAttachment } from '../types/workspace'
|
|
9
|
+
import { logger } from './logger.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert a string to kebab-case for branch names
|
|
13
|
+
*/
|
|
14
|
+
function toKebabCase(str: string, maxLength = 60): string {
|
|
15
|
+
const kebab = str
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^\w\s-]/g, '') // Remove special chars
|
|
18
|
+
.replace(/\s+/g, '-') // Replace spaces with dashes
|
|
19
|
+
.replace(/-+/g, '-') // Collapse multiple dashes
|
|
20
|
+
.replace(/^-|-$/g, '') // Remove leading/trailing dashes
|
|
21
|
+
|
|
22
|
+
// Truncate to max length at word boundary
|
|
23
|
+
if (kebab.length <= maxLength) {
|
|
24
|
+
return kebab
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const truncated = kebab.substring(0, maxLength)
|
|
28
|
+
const lastDash = truncated.lastIndexOf('-')
|
|
29
|
+
|
|
30
|
+
return lastDash > 0 ? truncated.substring(0, lastDash) : truncated
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Clean markdown text to plaintext while preserving structure
|
|
35
|
+
* Keeps lists and code fences readable
|
|
36
|
+
*/
|
|
37
|
+
function cleanMarkdown(text: string): string {
|
|
38
|
+
// For now, just return the text as-is
|
|
39
|
+
// More sophisticated markdown-to-plaintext conversion can be added later
|
|
40
|
+
return text.trim()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Download an image from a URL and save it to disk
|
|
45
|
+
* Supports authenticated Linear image downloads
|
|
46
|
+
*/
|
|
47
|
+
async function downloadImage(
|
|
48
|
+
url: string,
|
|
49
|
+
filepath: string,
|
|
50
|
+
linearApiKey?: string
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
try {
|
|
53
|
+
// Check if this is a Linear upload URL that requires authentication
|
|
54
|
+
const isLinearUpload = url.includes('uploads.linear.app')
|
|
55
|
+
|
|
56
|
+
// Prepare fetch options
|
|
57
|
+
const fetchOptions: RequestInit = {}
|
|
58
|
+
|
|
59
|
+
if (isLinearUpload && linearApiKey) {
|
|
60
|
+
// Linear API keys (lin_api_...) don't use Bearer prefix
|
|
61
|
+
// Only OAuth tokens use Bearer prefix
|
|
62
|
+
const authHeader = linearApiKey.startsWith('lin_api_')
|
|
63
|
+
? linearApiKey
|
|
64
|
+
: `Bearer ${linearApiKey}`
|
|
65
|
+
|
|
66
|
+
fetchOptions.headers = {
|
|
67
|
+
Authorization: authHeader,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const response = await fetch(url, fetchOptions)
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Failed to download image: ${response.status} ${response.statusText}`
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const arrayBuffer = await response.arrayBuffer()
|
|
80
|
+
const buffer = Buffer.from(arrayBuffer)
|
|
81
|
+
|
|
82
|
+
writeFileSync(filepath, buffer)
|
|
83
|
+
logger.debug(`Downloaded image: ${url} -> ${filepath}`)
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logger.warning(
|
|
86
|
+
`Failed to download image from ${url}: ${
|
|
87
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
88
|
+
}`
|
|
89
|
+
)
|
|
90
|
+
throw error
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get file extension from URL or default to .png
|
|
96
|
+
*/
|
|
97
|
+
function getExtensionFromUrl(url: string): string {
|
|
98
|
+
try {
|
|
99
|
+
const urlObj = new URL(url)
|
|
100
|
+
const pathname = urlObj.pathname
|
|
101
|
+
const ext = extname(pathname)
|
|
102
|
+
|
|
103
|
+
// Common image extensions
|
|
104
|
+
if (
|
|
105
|
+
['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'].includes(
|
|
106
|
+
ext.toLowerCase()
|
|
107
|
+
)
|
|
108
|
+
) {
|
|
109
|
+
return ext.toLowerCase()
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Invalid URL, fall through to default
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return '.png'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Download and localize images in markdown and from attachments
|
|
120
|
+
* Replaces image URLs with local file paths
|
|
121
|
+
*/
|
|
122
|
+
async function downloadAndLocalizeImages(
|
|
123
|
+
description: string,
|
|
124
|
+
attachments: LinearAttachment[],
|
|
125
|
+
promptDir: string,
|
|
126
|
+
linearApiKey?: string
|
|
127
|
+
): Promise<string> {
|
|
128
|
+
let localizedDescription = description
|
|
129
|
+
let imageCounter = 1
|
|
130
|
+
|
|
131
|
+
// Regular expression to match markdown images: 
|
|
132
|
+
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g
|
|
133
|
+
|
|
134
|
+
// Extract all image URLs from markdown
|
|
135
|
+
const imageMatches = Array.from(description.matchAll(imageRegex))
|
|
136
|
+
|
|
137
|
+
// Download images from markdown description
|
|
138
|
+
for (const match of imageMatches) {
|
|
139
|
+
const [fullMatch, altText, imageUrl] = match
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const ext = getExtensionFromUrl(imageUrl)
|
|
143
|
+
const filename = `image-${imageCounter}${ext}`
|
|
144
|
+
const filepath = join(promptDir, filename)
|
|
145
|
+
|
|
146
|
+
await downloadImage(imageUrl, filepath, linearApiKey)
|
|
147
|
+
|
|
148
|
+
// Replace URL with local path in markdown
|
|
149
|
+
localizedDescription = localizedDescription.replace(
|
|
150
|
+
fullMatch,
|
|
151
|
+
``
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
imageCounter++
|
|
155
|
+
} catch (error) {
|
|
156
|
+
logger.warning(`Skipping image: ${imageUrl}`)
|
|
157
|
+
// Keep original URL if download fails
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Download images from Linear attachments (if they're image URLs)
|
|
162
|
+
for (const attachment of attachments) {
|
|
163
|
+
// Check if attachment is an image based on URL
|
|
164
|
+
const isImage = /\.(png|jpg|jpeg|gif|svg|webp)$/i.test(attachment.url)
|
|
165
|
+
|
|
166
|
+
if (isImage) {
|
|
167
|
+
try {
|
|
168
|
+
const ext = getExtensionFromUrl(attachment.url)
|
|
169
|
+
const filename = `attachment-${imageCounter}${ext}`
|
|
170
|
+
const filepath = join(promptDir, filename)
|
|
171
|
+
|
|
172
|
+
await downloadImage(attachment.url, filepath, linearApiKey)
|
|
173
|
+
|
|
174
|
+
// Add attachment to markdown if not already present
|
|
175
|
+
const attachmentTitle = attachment.title || `Attachment ${imageCounter}`
|
|
176
|
+
const attachmentMarkdown = `\n\n`
|
|
177
|
+
|
|
178
|
+
// Only add if this URL isn't already in the description
|
|
179
|
+
if (!description.includes(attachment.url)) {
|
|
180
|
+
localizedDescription += attachmentMarkdown
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
imageCounter++
|
|
184
|
+
} catch (error) {
|
|
185
|
+
logger.warning(`Skipping attachment: ${attachment.url}`)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return localizedDescription
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Generate markdown content for an issue
|
|
195
|
+
* Follows the exact template format from the specification
|
|
196
|
+
*
|
|
197
|
+
* @param issue - The issue to generate markdown for
|
|
198
|
+
* @param promptDir - Directory where images will be saved (optional, if provided downloads images)
|
|
199
|
+
* @param linearApiKey - Linear API key for authenticated image downloads (optional)
|
|
200
|
+
* @returns Markdown formatted string
|
|
201
|
+
*/
|
|
202
|
+
export async function generateMarkdown(
|
|
203
|
+
issue: Issue,
|
|
204
|
+
promptDir?: string,
|
|
205
|
+
linearApiKey?: string
|
|
206
|
+
): Promise<string> {
|
|
207
|
+
const assignee = await issue.assignee
|
|
208
|
+
const state = await issue.state
|
|
209
|
+
|
|
210
|
+
const assigneeName = assignee?.name || 'unassigned'
|
|
211
|
+
|
|
212
|
+
let description = issue.description || 'No description provided.'
|
|
213
|
+
|
|
214
|
+
// Download and localize images if promptDir is provided
|
|
215
|
+
if (promptDir && issue.description) {
|
|
216
|
+
try {
|
|
217
|
+
// Fetch attachments (lazy-loaded - only called when needed)
|
|
218
|
+
const attachments = await issue.attachments()
|
|
219
|
+
|
|
220
|
+
description = await downloadAndLocalizeImages(
|
|
221
|
+
issue.description,
|
|
222
|
+
attachments,
|
|
223
|
+
promptDir,
|
|
224
|
+
linearApiKey
|
|
225
|
+
)
|
|
226
|
+
} catch (error) {
|
|
227
|
+
logger.warning('Failed to download some images, using original URLs')
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const fullDescription = cleanMarkdown(description)
|
|
232
|
+
const branchName = `${issue.identifier}-${toKebabCase(issue.title)}`
|
|
233
|
+
|
|
234
|
+
return `# ${issue.identifier}: ${issue.title}
|
|
235
|
+
|
|
236
|
+
**linear url:** ${issue.url}
|
|
237
|
+
**assignee:** ${assigneeName}
|
|
238
|
+
**state:** ${state?.name ?? 'Unknown'}
|
|
239
|
+
|
|
240
|
+
## description
|
|
241
|
+
|
|
242
|
+
${fullDescription}
|
|
243
|
+
|
|
244
|
+
## acceptance criteria (fill in)
|
|
245
|
+
|
|
246
|
+
- [ ] criterion 1
|
|
247
|
+
- [ ] criterion 2
|
|
248
|
+
|
|
249
|
+
## implementation notes (auto-generated)
|
|
250
|
+
|
|
251
|
+
- branch name: \`${branchName}\`
|
|
252
|
+
- source issue id: \`${issue.id}\`
|
|
253
|
+
`
|
|
254
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding step execution engine
|
|
3
|
+
* Runs interactive onboarding steps from bundle manifests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execFile } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import { logger } from './logger.js';
|
|
9
|
+
import { promptInput, promptConfirm, promptPassword } from './prompts.js';
|
|
10
|
+
import type {
|
|
11
|
+
OnboardingStep,
|
|
12
|
+
OnboardingResult,
|
|
13
|
+
InfoStep,
|
|
14
|
+
ConfirmStep,
|
|
15
|
+
SecretStep,
|
|
16
|
+
InputStep,
|
|
17
|
+
} from '../types/bundle.js';
|
|
18
|
+
|
|
19
|
+
const execFileAsync = promisify(execFile);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Execute all onboarding steps
|
|
23
|
+
*/
|
|
24
|
+
export async function runOnboarding(
|
|
25
|
+
steps: OnboardingStep[]
|
|
26
|
+
): Promise<OnboardingResult> {
|
|
27
|
+
const result: OnboardingResult = {
|
|
28
|
+
configValues: {},
|
|
29
|
+
completed: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
logger.bold('\n=== Project Onboarding ===\n');
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < steps.length; i++) {
|
|
35
|
+
const step = steps[i];
|
|
36
|
+
const stepNumber = i + 1;
|
|
37
|
+
const totalSteps = steps.length;
|
|
38
|
+
|
|
39
|
+
logger.log(`\n[${stepNumber}/${totalSteps}] ${step.title}`);
|
|
40
|
+
logger.dim(step.description);
|
|
41
|
+
logger.log('');
|
|
42
|
+
|
|
43
|
+
const stepResult = await executeStep(step);
|
|
44
|
+
|
|
45
|
+
if (stepResult === null) {
|
|
46
|
+
// User cancelled
|
|
47
|
+
result.cancelledAt = step.id;
|
|
48
|
+
logger.warning('\nOnboarding cancelled');
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Store values for secret/input steps
|
|
53
|
+
if (step.type === 'secret' || step.type === 'input') {
|
|
54
|
+
const configKey = (step as SecretStep | InputStep).configKey;
|
|
55
|
+
result.configValues[configKey] = stepResult;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
result.completed = true;
|
|
60
|
+
logger.success('\nOnboarding complete!');
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Execute a single onboarding step
|
|
66
|
+
* Returns collected value or null if cancelled
|
|
67
|
+
*/
|
|
68
|
+
async function executeStep(step: OnboardingStep): Promise<string | null> {
|
|
69
|
+
switch (step.type) {
|
|
70
|
+
case 'info':
|
|
71
|
+
return executeInfoStep(step);
|
|
72
|
+
case 'confirm':
|
|
73
|
+
return executeConfirmStep(step);
|
|
74
|
+
case 'secret':
|
|
75
|
+
return executeSecretStep(step);
|
|
76
|
+
case 'input':
|
|
77
|
+
return executeInputStep(step);
|
|
78
|
+
default:
|
|
79
|
+
logger.warning('Unknown step type, skipping');
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Execute info step - just wait for acknowledgment
|
|
86
|
+
*/
|
|
87
|
+
async function executeInfoStep(step: InfoStep): Promise<string | null> {
|
|
88
|
+
const confirmed = await promptConfirm('Press Enter to continue...', true);
|
|
89
|
+
return confirmed ? '' : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Execute confirm step - optionally check command
|
|
94
|
+
*/
|
|
95
|
+
async function executeConfirmStep(step: ConfirmStep): Promise<string | null> {
|
|
96
|
+
// Check if command exists (if specified)
|
|
97
|
+
if (step.checkCommand) {
|
|
98
|
+
const exists = await checkCommandExists(step.checkCommand);
|
|
99
|
+
|
|
100
|
+
if (exists) {
|
|
101
|
+
logger.success(`✓ ${step.checkCommand} is installed`);
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
logger.warning(`✗ ${step.checkCommand} not found in PATH`);
|
|
106
|
+
|
|
107
|
+
if (step.installUrl) {
|
|
108
|
+
logger.log(`\nInstall instructions: ${step.installUrl}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Ask user to confirm they've installed it
|
|
112
|
+
const prompt = step.confirmPrompt || `Have you installed ${step.checkCommand}?`;
|
|
113
|
+
|
|
114
|
+
while (true) {
|
|
115
|
+
const confirmed = await promptConfirm(prompt, false);
|
|
116
|
+
|
|
117
|
+
if (!confirmed) {
|
|
118
|
+
// User said no or cancelled
|
|
119
|
+
if (step.required !== false) {
|
|
120
|
+
logger.warning('This step is required. Please install and try again.');
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Re-check if they say yes
|
|
127
|
+
const nowExists = await checkCommandExists(step.checkCommand);
|
|
128
|
+
if (nowExists) {
|
|
129
|
+
logger.success(`✓ ${step.checkCommand} is now available`);
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
logger.warning(`${step.checkCommand} still not found. Please ensure it's in your PATH.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// No command check, just confirm
|
|
138
|
+
const prompt = step.confirmPrompt || 'Continue?';
|
|
139
|
+
const confirmed = await promptConfirm(prompt, true);
|
|
140
|
+
return confirmed ? '' : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Execute secret step - collect masked input
|
|
145
|
+
*/
|
|
146
|
+
async function executeSecretStep(step: SecretStep): Promise<string | null> {
|
|
147
|
+
const validator = step.validationPattern
|
|
148
|
+
? createValidator(step.validationPattern, step.validationMessage)
|
|
149
|
+
: undefined;
|
|
150
|
+
|
|
151
|
+
while (true) {
|
|
152
|
+
const value = await promptPassword(`Enter ${step.title}:`);
|
|
153
|
+
|
|
154
|
+
if (value === null) {
|
|
155
|
+
return null; // Cancelled
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!value && step.required !== false) {
|
|
159
|
+
logger.warning('This field is required');
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (validator && value) {
|
|
164
|
+
const validationResult = validator(value);
|
|
165
|
+
if (validationResult !== true) {
|
|
166
|
+
logger.warning(typeof validationResult === 'string' ? validationResult : 'Invalid input');
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Execute input step - collect plain text input
|
|
177
|
+
*/
|
|
178
|
+
async function executeInputStep(step: InputStep): Promise<string | null> {
|
|
179
|
+
const validator = step.validationPattern
|
|
180
|
+
? createValidator(step.validationPattern, step.validationMessage)
|
|
181
|
+
: undefined;
|
|
182
|
+
|
|
183
|
+
const value = await promptInput(`Enter ${step.title}:`, {
|
|
184
|
+
default: step.defaultValue,
|
|
185
|
+
validate: (input) => {
|
|
186
|
+
if (!input && step.required !== false) {
|
|
187
|
+
return 'This field is required';
|
|
188
|
+
}
|
|
189
|
+
if (validator && input) {
|
|
190
|
+
return validator(input);
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if a command exists in PATH
|
|
201
|
+
*/
|
|
202
|
+
async function checkCommandExists(command: string): Promise<boolean> {
|
|
203
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(command)) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await execFileAsync('which', [command]);
|
|
209
|
+
return true;
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Create a validator function from regex pattern
|
|
217
|
+
*/
|
|
218
|
+
function createValidator(
|
|
219
|
+
pattern: string,
|
|
220
|
+
message?: string
|
|
221
|
+
): (value: string) => boolean | string {
|
|
222
|
+
const regex = new RegExp(pattern);
|
|
223
|
+
return (value: string) => {
|
|
224
|
+
if (regex.test(value)) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
return message || `Value must match pattern: ${pattern}`;
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User prompt utilities using @inquirer/prompts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { search, input, confirm, password } from '@inquirer/prompts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Select an item from a searchable list
|
|
9
|
+
* @param items Array of items to select from
|
|
10
|
+
* @param message Prompt message
|
|
11
|
+
* @returns Selected item or null if cancelled
|
|
12
|
+
*/
|
|
13
|
+
export async function selectItem(
|
|
14
|
+
items: string[],
|
|
15
|
+
message: string
|
|
16
|
+
): Promise<string | null> {
|
|
17
|
+
if (items.length === 0) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const selected = await search({
|
|
23
|
+
message,
|
|
24
|
+
source: async (input) => {
|
|
25
|
+
if (!input) {
|
|
26
|
+
return items.map((item) => ({ name: item, value: item }));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Filter items based on input
|
|
30
|
+
const filtered = items.filter((item) =>
|
|
31
|
+
item.toLowerCase().includes(input.toLowerCase())
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return filtered.map((item) => ({ name: item, value: item }));
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return selected;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// User cancelled (Ctrl+C)
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Prompt for text input
|
|
47
|
+
* @param message Prompt message
|
|
48
|
+
* @param options Additional options
|
|
49
|
+
* @returns Input value or null if cancelled
|
|
50
|
+
*/
|
|
51
|
+
export async function promptInput(
|
|
52
|
+
message: string,
|
|
53
|
+
options: {
|
|
54
|
+
default?: string;
|
|
55
|
+
validate?: (value: string) => boolean | string;
|
|
56
|
+
} = {}
|
|
57
|
+
): Promise<string | null> {
|
|
58
|
+
try {
|
|
59
|
+
const value = await input({
|
|
60
|
+
message,
|
|
61
|
+
default: options.default,
|
|
62
|
+
validate: options.validate,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return value;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// User cancelled (Ctrl+C)
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Prompt for confirmation
|
|
74
|
+
* @param message Prompt message
|
|
75
|
+
* @param defaultValue Default value
|
|
76
|
+
* @returns Boolean response
|
|
77
|
+
*/
|
|
78
|
+
export async function promptConfirm(
|
|
79
|
+
message: string,
|
|
80
|
+
defaultValue = false
|
|
81
|
+
): Promise<boolean> {
|
|
82
|
+
try {
|
|
83
|
+
const value = await confirm({
|
|
84
|
+
message,
|
|
85
|
+
default: defaultValue,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return value;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// User cancelled (Ctrl+C), treat as false
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Prompt for password input
|
|
97
|
+
* @param message Prompt message
|
|
98
|
+
* @returns Password value or null if cancelled
|
|
99
|
+
*/
|
|
100
|
+
export async function promptPassword(
|
|
101
|
+
message: string
|
|
102
|
+
): Promise<string | null> {
|
|
103
|
+
try {
|
|
104
|
+
const value = await password({
|
|
105
|
+
message,
|
|
106
|
+
mask: true,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return value;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// User cancelled (Ctrl+C)
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run commands in the current terminal (not tmux)
|
|
3
|
+
* Intelligently runs setup commands only once, then select commands
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { SpacesError } from '../types/errors.js';
|
|
8
|
+
import { logger } from './logger.js';
|
|
9
|
+
import { hasSetupBeenRun, markSetupComplete } from './workspace-state.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Run a single command in the workspace directory
|
|
13
|
+
* Streams output to current terminal
|
|
14
|
+
*/
|
|
15
|
+
async function runCommand(
|
|
16
|
+
command: string,
|
|
17
|
+
workspacePath: string
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
logger.dim(` $ ${command}`);
|
|
21
|
+
|
|
22
|
+
// Parse command and args
|
|
23
|
+
const [cmd, ...args] = command.split(' ');
|
|
24
|
+
|
|
25
|
+
const child = spawn(cmd, args, {
|
|
26
|
+
cwd: workspacePath,
|
|
27
|
+
stdio: 'inherit', // Stream output to current terminal
|
|
28
|
+
shell: true, // Use shell to handle complex commands
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
child.on('close', (code) => {
|
|
32
|
+
if (code !== 0) {
|
|
33
|
+
reject(
|
|
34
|
+
new SpacesError(
|
|
35
|
+
`Command failed with exit code ${code}: ${command}`,
|
|
36
|
+
'SYSTEM_ERROR',
|
|
37
|
+
2
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
} else {
|
|
41
|
+
resolve();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
child.on('error', (error) => {
|
|
46
|
+
reject(
|
|
47
|
+
new SpacesError(
|
|
48
|
+
`Failed to run command: ${error.message}`,
|
|
49
|
+
'SYSTEM_ERROR',
|
|
50
|
+
2
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run commands in terminal with smart setup tracking
|
|
59
|
+
* Runs setup commands only once (first time), then select commands on subsequent runs
|
|
60
|
+
*/
|
|
61
|
+
export async function runCommandsInTerminal(
|
|
62
|
+
workspacePath: string,
|
|
63
|
+
setupCommands: string[] = [],
|
|
64
|
+
selectCommands: string[] = []
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
const setupAlreadyRun = hasSetupBeenRun(workspacePath);
|
|
67
|
+
|
|
68
|
+
let commandsToRun: string[] = [];
|
|
69
|
+
let commandType: string;
|
|
70
|
+
|
|
71
|
+
if (setupAlreadyRun) {
|
|
72
|
+
// Setup has been run before, use select commands
|
|
73
|
+
commandsToRun = selectCommands;
|
|
74
|
+
commandType = 'select';
|
|
75
|
+
} else {
|
|
76
|
+
// First time setup, run setup commands
|
|
77
|
+
commandsToRun = setupCommands;
|
|
78
|
+
commandType = 'setup';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (commandsToRun.length === 0) {
|
|
82
|
+
logger.debug('No commands to run');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
logger.info(`Running ${commandType} commands...`);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// Run commands sequentially
|
|
90
|
+
for (const cmd of commandsToRun) {
|
|
91
|
+
await runCommand(cmd, workspacePath);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Mark setup as complete if we just ran setup commands
|
|
95
|
+
if (!setupAlreadyRun && setupCommands.length > 0) {
|
|
96
|
+
markSetupComplete(workspacePath);
|
|
97
|
+
logger.debug('Setup marked as complete');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
logger.success('Commands completed');
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error instanceof SpacesError) {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new SpacesError(
|
|
107
|
+
`Failed to run commands: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
108
|
+
'SYSTEM_ERROR',
|
|
109
|
+
2
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|