@bibobonking/opensol 3.1.4 → 3.1.5
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/CHANGELOG.md +10 -12
- package/desktop/src-tauri/icons/128x128.png +0 -0
- package/desktop/src-tauri/icons/128x128@2x.png +0 -0
- package/desktop/src-tauri/icons/32x32.png +0 -0
- package/desktop/src-tauri/icons/64x64.png +0 -0
- package/desktop/src-tauri/icons/Square107x107Logo.png +0 -0
- package/desktop/src-tauri/icons/Square142x142Logo.png +0 -0
- package/desktop/src-tauri/icons/Square150x150Logo.png +0 -0
- package/desktop/src-tauri/icons/Square284x284Logo.png +0 -0
- package/desktop/src-tauri/icons/Square30x30Logo.png +0 -0
- package/desktop/src-tauri/icons/Square310x310Logo.png +0 -0
- package/desktop/src-tauri/icons/Square44x44Logo.png +0 -0
- package/desktop/src-tauri/icons/Square71x71Logo.png +0 -0
- package/desktop/src-tauri/icons/Square89x89Logo.png +0 -0
- package/desktop/src-tauri/icons/StoreLogo.png +0 -0
- package/desktop/src-tauri/icons/icon.icns +0 -0
- package/desktop/src-tauri/icons/icon.ico +0 -0
- package/desktop/src-tauri/icons/icon.png +0 -0
- package/dist/assets/index-C0yvs6Vl.js +281 -0
- package/dist/assets/index-CxNPLBg7.css +1 -0
- package/dist/favicon.svg +18 -15
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/public/favicon.svg +18 -15
- package/server/daemon.mjs +66 -49
- package/server/lib/deploy.mjs +222 -121
- package/server/lib/providers.mjs +23 -1
- package/server/lib/skills.mjs +385 -100
- package/server/lib/tools.mjs +0 -40
- package/src/agent/adapters/openai-responses.ts +1 -5
- package/src/agent/adapters/openai.ts +4 -11
- package/src/agent/prompt.ts +7 -14
- package/src/agent/subagent.ts +3 -14
- package/src/agent/tools.ts +2 -0
- package/src/components/ChatView.tsx +2 -1
- package/src/components/Composer.tsx +2 -1
- package/src/components/ContextBar.tsx +10 -0
- package/src/components/DeployModal.tsx +266 -252
- package/src/components/FileIcon.tsx +391 -99
- package/src/components/HomeDashboard.tsx +5 -10
- package/src/components/Logo.tsx +9 -11
- package/src/components/ModelDeck.tsx +8 -11
- package/src/components/SettingsModal.tsx +160 -122
- package/src/components/Sidebar.tsx +4 -3
- package/src/components/SystemPromptButton.tsx +198 -0
- package/src/components/ToolCallCard.tsx +2 -0
- package/src/components/TopBar.tsx +2 -19
- package/src/components/WorkspacePanel.tsx +76 -93
- package/src/core/plugins/tools/skill.ts +19 -76
- package/src/core/services/agent-loop.ts +16 -31
- package/src/core/services/subagents.ts +2 -2
- package/src/core/services/system-prompt.ts +16 -23
- package/src/core/services/tools.ts +0 -1
- package/src/lib/boot.ts +2 -0
- package/src/lib/credits.ts +33 -0
- package/src/lib/daemon.ts +40 -19
- package/src/lib/permissions.ts +1 -1
- package/src/lib/retry.test.mjs +40 -12
- package/src/lib/retry.ts +41 -8
- package/src/lib/system-prompt.ts +23 -0
- package/src/lib/usage.ts +8 -13
- package/src/store/chats.ts +0 -1
- package/src/store/settings.ts +12 -44
- package/src/store/skills.ts +79 -0
- package/src/store/ui.ts +3 -1
- package/src/types.ts +30 -15
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/assets/index-B1pQC9Fe.js +0 -288
- package/dist/assets/index-qLr4aZVC.css +0 -1
- package/src/components/SystemPromptModal.tsx +0 -168
- package/src/lib/skills.test.mjs +0 -22
- package/src/lib/skills.ts +0 -93
|
@@ -1,68 +1,78 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import {
|
|
3
|
+
AlertTriangle,
|
|
3
4
|
Check,
|
|
4
5
|
Cloud,
|
|
5
6
|
Copy,
|
|
6
7
|
ExternalLink,
|
|
7
8
|
FolderOpen,
|
|
8
9
|
Globe2,
|
|
10
|
+
Hammer,
|
|
11
|
+
PackageOpen,
|
|
9
12
|
Play,
|
|
10
13
|
Rocket,
|
|
14
|
+
ScanSearch,
|
|
11
15
|
ShieldCheck,
|
|
12
|
-
Sparkles,
|
|
13
16
|
Square,
|
|
14
|
-
Triangle,
|
|
15
17
|
Terminal,
|
|
18
|
+
Triangle,
|
|
19
|
+
UploadCloud,
|
|
16
20
|
X,
|
|
17
21
|
} from 'lucide-react'
|
|
18
|
-
import type { DeployProviderId, DeployResult } from '../types'
|
|
19
|
-
import {
|
|
22
|
+
import type { DeployDetection, DeployProviderId, DeployResult } from '../types'
|
|
23
|
+
import { detectDeployProject, streamDeploy } from '../lib/daemon'
|
|
20
24
|
import { cn } from '../lib/utils'
|
|
21
25
|
import { useSettings } from '../store/settings'
|
|
22
26
|
import { useUi } from '../store/ui'
|
|
23
27
|
import { useWorkspace } from '../store/workspace'
|
|
28
|
+
import { FolderIcon } from './FileIcon'
|
|
24
29
|
import { Modal, Spinner } from './ui'
|
|
25
30
|
|
|
26
31
|
const PROVIDERS: Array<{
|
|
27
32
|
id: DeployProviderId
|
|
28
33
|
label: string
|
|
29
|
-
short: string
|
|
30
34
|
icon: typeof Cloud
|
|
35
|
+
tint: string
|
|
31
36
|
blurb: string
|
|
32
37
|
needs: string
|
|
33
38
|
}> = [
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
short: 'Pages',
|
|
38
|
-
icon: Cloud,
|
|
39
|
-
blurb: 'Fast global hosting with a generous free tier.',
|
|
40
|
-
needs: 'API token + account id',
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
id: 'netlify',
|
|
44
|
-
label: 'Netlify',
|
|
45
|
-
short: 'Netlify',
|
|
46
|
-
icon: Globe2,
|
|
47
|
-
blurb: 'Create a site and publish a folder in one step.',
|
|
48
|
-
needs: 'Personal access token',
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: 'vercel',
|
|
52
|
-
label: 'Vercel',
|
|
53
|
-
short: 'Vercel',
|
|
54
|
-
icon: Triangle,
|
|
55
|
-
blurb: 'Production deploys through the Vercel CLI.',
|
|
56
|
-
needs: 'Access token',
|
|
57
|
-
},
|
|
39
|
+
{ id: 'cloudflare', label: 'Cloudflare Pages', icon: Cloud, tint: '#f6821f', blurb: 'Global edge, generous free tier', needs: 'API token + account id' },
|
|
40
|
+
{ id: 'netlify', label: 'Netlify', icon: Globe2, tint: '#00c7b7', blurb: 'Creates the site once, then reuses it', needs: 'Personal access token' },
|
|
41
|
+
{ id: 'vercel', label: 'Vercel', icon: Triangle, tint: '#8b95a5', blurb: 'Production deploy through the CLI', needs: 'Access token' },
|
|
58
42
|
]
|
|
59
43
|
|
|
44
|
+
const STEPS = [
|
|
45
|
+
{ id: 'prepare', label: 'Inspect', icon: ScanSearch },
|
|
46
|
+
{ id: 'build', label: 'Build', icon: Hammer },
|
|
47
|
+
{ id: 'publish', label: 'Publish', icon: UploadCloud },
|
|
48
|
+
] as const
|
|
49
|
+
|
|
50
|
+
type StepId = (typeof STEPS)[number]['id']
|
|
51
|
+
type StepState = 'idle' | 'active' | 'done' | 'error'
|
|
52
|
+
|
|
60
53
|
interface LogLine {
|
|
61
54
|
id: number
|
|
62
55
|
stream: 'stdout' | 'stderr' | 'meta'
|
|
63
56
|
text: string
|
|
64
57
|
}
|
|
65
58
|
|
|
59
|
+
const inputClass =
|
|
60
|
+
'h-9 w-full rounded-lg border border-line bg-panel px-2.5 font-mono text-[11px] text-ink outline-none transition-colors placeholder:text-faint focus:border-accent/50 disabled:opacity-60'
|
|
61
|
+
|
|
62
|
+
function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) {
|
|
63
|
+
return (
|
|
64
|
+
<div>
|
|
65
|
+
<p className="mb-1.5 flex items-baseline gap-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-faint">
|
|
66
|
+
{label}
|
|
67
|
+
{hint && <span className="font-normal normal-case tracking-normal">{hint}</span>}
|
|
68
|
+
</p>
|
|
69
|
+
{children}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const IDLE: Record<StepId, StepState> = { prepare: 'idle', build: 'idle', publish: 'idle' }
|
|
75
|
+
|
|
66
76
|
export function DeployModal() {
|
|
67
77
|
const open = useUi((state) => state.deployOpen)
|
|
68
78
|
const setOpen = useUi((state) => state.setDeployOpen)
|
|
@@ -79,9 +89,10 @@ export function DeployModal() {
|
|
|
79
89
|
const [outputDir, setOutputDir] = useState('')
|
|
80
90
|
const [buildCommand, setBuildCommand] = useState('')
|
|
81
91
|
const [projectName, setProjectName] = useState('')
|
|
82
|
-
const [
|
|
83
|
-
const [
|
|
92
|
+
const [detected, setDetected] = useState<DeployDetection | null>(null)
|
|
93
|
+
const [detectError, setDetectError] = useState('')
|
|
84
94
|
const [running, setRunning] = useState(false)
|
|
95
|
+
const [steps, setSteps] = useState<Record<StepId, StepState>>(IDLE)
|
|
85
96
|
const [logs, setLogs] = useState<LogLine[]>([])
|
|
86
97
|
const [result, setResult] = useState<DeployResult | null>(null)
|
|
87
98
|
const [error, setError] = useState('')
|
|
@@ -89,66 +100,77 @@ export function DeployModal() {
|
|
|
89
100
|
const abortRef = useRef<AbortController | null>(null)
|
|
90
101
|
const logRef = useRef<HTMLDivElement>(null)
|
|
91
102
|
const logId = useRef(0)
|
|
103
|
+
const currentStep = useRef<StepId | null>(null)
|
|
92
104
|
|
|
93
105
|
const meta = PROVIDERS.find((entry) => entry.id === provider) ?? PROVIDERS[0]
|
|
94
106
|
const tool = deployTools?.[provider]
|
|
95
107
|
const configured = Boolean(deploySecrets?.[provider]?.configured)
|
|
96
|
-
const lastUrl = deploySecrets?.[provider]?.fields?.lastUrl ?? ''
|
|
97
108
|
|
|
98
109
|
useEffect(() => {
|
|
99
110
|
if (!open) return
|
|
100
111
|
setPath(workspaceRoot)
|
|
101
|
-
setOutputDir('')
|
|
102
|
-
setBuildCommand('')
|
|
103
|
-
setProjectName('')
|
|
104
|
-
setInspection(null)
|
|
105
112
|
setResult(null)
|
|
106
113
|
setError('')
|
|
107
114
|
setLogs([])
|
|
108
115
|
setRunning(false)
|
|
109
116
|
setCopied(false)
|
|
117
|
+
setSteps(IDLE)
|
|
110
118
|
}, [open, workspaceRoot])
|
|
111
119
|
|
|
112
|
-
|
|
113
|
-
if (!target.trim()) return
|
|
114
|
-
setInspecting(true)
|
|
115
|
-
try {
|
|
116
|
-
const info = await inspectDeploy(target.trim())
|
|
117
|
-
setInspection(info)
|
|
118
|
-
if (info.buildCommand) setBuildCommand((value) => value || info.buildCommand)
|
|
119
|
-
if (info.outputDir) setOutputDir((value) => value || info.outputDir)
|
|
120
|
-
if (info.name) setProjectName((value) => value || info.name)
|
|
121
|
-
} catch (cause) {
|
|
122
|
-
setInspection(null)
|
|
123
|
-
setError(cause instanceof Error ? cause.message : 'Could not inspect that folder.')
|
|
124
|
-
} finally {
|
|
125
|
-
setInspecting(false)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Fill the form from the real project once the path settles.
|
|
120
|
+
// Look at the folder so the build command and output folder are suggested instead of guessed.
|
|
130
121
|
useEffect(() => {
|
|
131
|
-
if (!open || !path.trim())
|
|
132
|
-
|
|
133
|
-
|
|
122
|
+
if (!open || !path.trim()) {
|
|
123
|
+
setDetected(null)
|
|
124
|
+
setDetectError('')
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
let cancelled = false
|
|
128
|
+
const timer = window.setTimeout(() => {
|
|
129
|
+
detectDeployProject(path.trim())
|
|
130
|
+
.then((project) => {
|
|
131
|
+
if (cancelled) return
|
|
132
|
+
setDetected(project)
|
|
133
|
+
setDetectError('')
|
|
134
|
+
})
|
|
135
|
+
.catch((cause) => {
|
|
136
|
+
if (cancelled) return
|
|
137
|
+
setDetected(null)
|
|
138
|
+
setDetectError(cause instanceof Error ? cause.message : 'Could not read that folder.')
|
|
139
|
+
})
|
|
140
|
+
}, 350)
|
|
141
|
+
return () => {
|
|
142
|
+
cancelled = true
|
|
143
|
+
window.clearTimeout(timer)
|
|
144
|
+
}
|
|
134
145
|
}, [open, path])
|
|
135
146
|
|
|
136
147
|
useEffect(() => {
|
|
137
|
-
if (!open) return
|
|
138
148
|
const node = logRef.current
|
|
139
149
|
if (node) node.scrollTop = node.scrollHeight
|
|
140
|
-
}, [logs
|
|
150
|
+
}, [logs])
|
|
141
151
|
|
|
142
|
-
useEffect(() =>
|
|
143
|
-
return () => abortRef.current?.abort()
|
|
144
|
-
}, [])
|
|
152
|
+
useEffect(() => () => abortRef.current?.abort(), [])
|
|
145
153
|
|
|
146
154
|
const append = (stream: LogLine['stream'], text: string) => {
|
|
147
155
|
const clean = text.trimEnd()
|
|
148
156
|
if (!clean) return
|
|
149
157
|
logId.current += 1
|
|
150
158
|
const id = logId.current
|
|
151
|
-
setLogs((current) => [...current.slice(-
|
|
159
|
+
setLogs((current) => [...current.slice(-600), { id, stream, text: clean }])
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const startStep = (id: string) => {
|
|
163
|
+
const mapped: StepId = id === 'prepare' || id === 'publish' ? id : 'build'
|
|
164
|
+
setSteps((current) => {
|
|
165
|
+
const next = { ...current }
|
|
166
|
+
for (const step of STEPS) {
|
|
167
|
+
if (step.id === mapped) next[step.id] = 'active'
|
|
168
|
+
else if (next[step.id] === 'active') next[step.id] = 'done'
|
|
169
|
+
}
|
|
170
|
+
if (mapped === 'publish' && next.build === 'idle') next.build = 'done'
|
|
171
|
+
return next
|
|
172
|
+
})
|
|
173
|
+
currentStep.current = mapped
|
|
152
174
|
}
|
|
153
175
|
|
|
154
176
|
const close = () => {
|
|
@@ -166,9 +188,12 @@ export function DeployModal() {
|
|
|
166
188
|
setResult(null)
|
|
167
189
|
setError('')
|
|
168
190
|
setLogs([])
|
|
191
|
+
setSteps({ ...IDLE, prepare: 'active' })
|
|
192
|
+
currentStep.current = 'prepare'
|
|
169
193
|
const controller = new AbortController()
|
|
170
194
|
abortRef.current = controller
|
|
171
|
-
append('meta', `
|
|
195
|
+
append('meta', `Deploying ${path.trim()} to ${meta.label}`)
|
|
196
|
+
let failed = false
|
|
172
197
|
try {
|
|
173
198
|
for await (const event of streamDeploy(
|
|
174
199
|
{
|
|
@@ -180,13 +205,15 @@ export function DeployModal() {
|
|
|
180
205
|
},
|
|
181
206
|
controller.signal,
|
|
182
207
|
)) {
|
|
183
|
-
if (event.type === '
|
|
184
|
-
else if (event.type === '
|
|
208
|
+
if (event.type === 'step') startStep(event.id)
|
|
209
|
+
else if (event.type === 'log') append(event.stream, event.text)
|
|
185
210
|
else if (event.type === 'done') {
|
|
186
211
|
setResult(event.result)
|
|
187
|
-
|
|
212
|
+
setSteps({ prepare: 'done', build: 'done', publish: 'done' })
|
|
213
|
+
append('meta', `Live at ${event.result.url}`)
|
|
188
214
|
break
|
|
189
215
|
} else if (event.type === 'error') {
|
|
216
|
+
failed = true
|
|
190
217
|
setError(event.message)
|
|
191
218
|
append('stderr', event.message)
|
|
192
219
|
break
|
|
@@ -195,12 +222,18 @@ export function DeployModal() {
|
|
|
195
222
|
} catch (cause) {
|
|
196
223
|
if (cause instanceof DOMException && cause.name === 'AbortError') {
|
|
197
224
|
append('meta', 'Deployment stopped.')
|
|
225
|
+
setSteps(IDLE)
|
|
198
226
|
} else {
|
|
227
|
+
failed = true
|
|
199
228
|
const message = cause instanceof Error ? cause.message : 'The deployment failed.'
|
|
200
229
|
setError(message)
|
|
201
230
|
append('stderr', message)
|
|
202
231
|
}
|
|
203
232
|
} finally {
|
|
233
|
+
if (failed && currentStep.current) {
|
|
234
|
+
const failedStep = currentStep.current
|
|
235
|
+
setSteps((current) => ({ ...current, [failedStep]: 'error' }))
|
|
236
|
+
}
|
|
204
237
|
abortRef.current = null
|
|
205
238
|
setRunning(false)
|
|
206
239
|
}
|
|
@@ -217,94 +250,68 @@ export function DeployModal() {
|
|
|
217
250
|
}
|
|
218
251
|
}
|
|
219
252
|
|
|
253
|
+
const folderName = path.trim().split(/[\\/]/).filter(Boolean).pop() ?? ''
|
|
254
|
+
|
|
220
255
|
return (
|
|
221
|
-
<Modal open={open} onClose={close} labelledBy="deploy-title" wide className="h-[92dvh] p-0 sm:h-[min(90dvh,
|
|
222
|
-
<div className="flex shrink-0 items-
|
|
223
|
-
<div className="flex min-w-0 items-
|
|
224
|
-
<span className="
|
|
256
|
+
<Modal open={open} onClose={close} labelledBy="deploy-title" wide className="h-[92dvh] p-0 sm:h-[min(90dvh,780px)]">
|
|
257
|
+
<div className="flex shrink-0 items-center justify-between gap-4 border-b border-line px-5 py-3.5 sm:px-6">
|
|
258
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
259
|
+
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-accent text-on-accent shadow-pill">
|
|
225
260
|
<Rocket size={17} />
|
|
226
261
|
</span>
|
|
227
262
|
<div className="min-w-0">
|
|
228
|
-
<h2 id="deploy-title" className="font-display text-[
|
|
229
|
-
Deploy
|
|
263
|
+
<h2 id="deploy-title" className="font-display text-[19px] font-semibold leading-tight tracking-[-0.02em] text-ink">
|
|
264
|
+
Deploy
|
|
230
265
|
</h2>
|
|
231
|
-
<p className="
|
|
232
|
-
Publish the open workspace or another project folder. Tokens stay encrypted on this machine.
|
|
233
|
-
</p>
|
|
266
|
+
<p className="truncate text-[11.5px] text-muted">Build and publish a folder to a live URL. Tokens stay encrypted on this machine.</p>
|
|
234
267
|
</div>
|
|
235
268
|
</div>
|
|
236
|
-
<button type="button" className="icon-button
|
|
269
|
+
<button type="button" className="icon-button shrink-0" onClick={close} aria-label="Close deploy panel">
|
|
237
270
|
<X size={16} />
|
|
238
271
|
</button>
|
|
239
272
|
</div>
|
|
240
273
|
|
|
241
|
-
<div className="grid min-h-0 flex-1 lg:grid-cols-[minmax(0,
|
|
274
|
+
<div className="grid min-h-0 flex-1 lg:grid-cols-[minmax(0,390px)_minmax(0,1fr)]">
|
|
242
275
|
<div className="thin-scroll min-h-0 overflow-y-auto border-b border-line lg:border-b-0 lg:border-r">
|
|
243
276
|
<div className="space-y-5 p-5">
|
|
244
|
-
<
|
|
245
|
-
<
|
|
246
|
-
<div className="grid gap-1.5">
|
|
277
|
+
<Field label="Host">
|
|
278
|
+
<div className="grid grid-cols-3 gap-1.5">
|
|
247
279
|
{PROVIDERS.map((entry) => {
|
|
248
280
|
const Icon = entry.icon
|
|
249
281
|
const active = provider === entry.id
|
|
282
|
+
const ready = deploySecrets?.[entry.id]?.configured
|
|
250
283
|
return (
|
|
251
284
|
<button
|
|
252
285
|
key={entry.id}
|
|
253
286
|
type="button"
|
|
254
287
|
onClick={() => setProvider(entry.id)}
|
|
255
288
|
disabled={running}
|
|
289
|
+
aria-pressed={active}
|
|
256
290
|
className={cn(
|
|
257
|
-
'
|
|
258
|
-
active
|
|
259
|
-
? 'border-accent/40 bg-gradient-to-br from-accent-soft/80 to-panel shadow-pill'
|
|
260
|
-
: 'border-line bg-panel hover:border-line hover:bg-raised/50',
|
|
291
|
+
'relative flex flex-col items-start gap-2 rounded-xl border p-2.5 text-left transition-all disabled:opacity-60',
|
|
292
|
+
active ? 'border-accent/50 bg-accent-soft/50 shadow-pill' : 'border-line bg-panel hover:bg-raised/60',
|
|
261
293
|
)}
|
|
262
294
|
>
|
|
263
|
-
<span
|
|
264
|
-
|
|
265
|
-
'mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition-colors',
|
|
266
|
-
active ? 'bg-accent text-on-accent shadow-pill' : 'bg-raised text-muted group-hover/host:text-ink',
|
|
267
|
-
)}
|
|
268
|
-
>
|
|
269
|
-
<Icon size={13} />
|
|
270
|
-
</span>
|
|
271
|
-
<span className="min-w-0 flex-1">
|
|
272
|
-
<span className="flex items-center gap-2">
|
|
273
|
-
<span className={cn('text-[12px] font-medium', active ? 'text-accent' : 'text-ink')}>
|
|
274
|
-
{entry.label}
|
|
275
|
-
</span>
|
|
276
|
-
{deploySecrets?.[entry.id]?.configured ? (
|
|
277
|
-
<Check size={11} className="text-success" />
|
|
278
|
-
) : (
|
|
279
|
-
<span className="text-[9px] text-warning">needs token</span>
|
|
280
|
-
)}
|
|
281
|
-
</span>
|
|
282
|
-
<span className="mt-0.5 block text-[10px] leading-relaxed text-faint">{entry.blurb}</span>
|
|
295
|
+
<span className="flex h-7 w-7 items-center justify-center rounded-lg text-white" style={{ backgroundColor: entry.tint }}>
|
|
296
|
+
<Icon size={14} />
|
|
283
297
|
</span>
|
|
298
|
+
<span className="text-[11.5px] font-medium leading-tight text-ink">{entry.label}</span>
|
|
299
|
+
<span
|
|
300
|
+
className={cn('absolute right-2 top-2 h-1.5 w-1.5 rounded-full', ready ? 'bg-success' : 'bg-warning')}
|
|
301
|
+
title={ready ? 'Credentials saved' : 'Needs credentials'}
|
|
302
|
+
/>
|
|
284
303
|
</button>
|
|
285
304
|
)
|
|
286
305
|
})}
|
|
287
306
|
</div>
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
{lastUrl && (
|
|
291
|
-
<a
|
|
292
|
-
href={lastUrl}
|
|
293
|
-
target="_blank"
|
|
294
|
-
rel="noreferrer"
|
|
295
|
-
className="flex items-center gap-1.5 rounded-lg border border-line bg-raised/30 px-2.5 py-1.5 text-[9.5px] text-muted hover:text-ink"
|
|
296
|
-
>
|
|
297
|
-
<ExternalLink size={10} className="shrink-0 text-faint" />
|
|
298
|
-
<span className="truncate">Last deploy: {lastUrl}</span>
|
|
299
|
-
</a>
|
|
300
|
-
)}
|
|
307
|
+
<p className="mt-1.5 text-[10px] text-faint">{meta.blurb}</p>
|
|
308
|
+
</Field>
|
|
301
309
|
|
|
302
310
|
{!configured && (
|
|
303
|
-
<div className="rounded-xl border border-warning/25 bg-warning/[0.
|
|
311
|
+
<div className="rounded-xl border border-warning/25 bg-warning/[0.05] p-3">
|
|
304
312
|
<p className="flex items-start gap-1.5 text-[10.5px] leading-relaxed text-muted">
|
|
305
313
|
<ShieldCheck size={12} className="mt-0.5 shrink-0 text-warning" />
|
|
306
|
-
{meta.label} needs {meta.needs}. Add it once in Settings → Deploy; it is encrypted
|
|
307
|
-
and never appears in the page again.
|
|
314
|
+
{meta.label} needs {meta.needs}. Add it once in Settings → Deploy; it is encrypted and never shown again.
|
|
308
315
|
</p>
|
|
309
316
|
<button
|
|
310
317
|
type="button"
|
|
@@ -319,112 +326,104 @@ export function DeployModal() {
|
|
|
319
326
|
</div>
|
|
320
327
|
)}
|
|
321
328
|
|
|
322
|
-
<
|
|
323
|
-
<div className="mb-1.5 flex items-center justify-between gap-2">
|
|
324
|
-
<label htmlFor="deploy-path" className="text-[10px] font-semibold uppercase tracking-[0.14em] text-faint">
|
|
325
|
-
Project folder
|
|
326
|
-
</label>
|
|
327
|
-
{workspaceAvailable && (
|
|
328
|
-
<button
|
|
329
|
-
type="button"
|
|
330
|
-
onClick={() => setPath(workspaceRoot)}
|
|
331
|
-
disabled={running}
|
|
332
|
-
className="inline-flex items-center gap-1 text-[9.5px] font-medium text-accent disabled:opacity-40"
|
|
333
|
-
>
|
|
334
|
-
<FolderOpen size={10} />
|
|
335
|
-
Use open workspace
|
|
336
|
-
</button>
|
|
337
|
-
)}
|
|
338
|
-
</div>
|
|
329
|
+
<Field label="Project folder">
|
|
339
330
|
<input
|
|
340
|
-
id="deploy-path"
|
|
341
331
|
value={path}
|
|
342
332
|
onChange={(event) => setPath(event.target.value)}
|
|
343
333
|
disabled={running}
|
|
344
334
|
placeholder="/absolute/path/to/project or ~/Projects/site"
|
|
345
335
|
spellCheck={false}
|
|
346
|
-
className=
|
|
336
|
+
className={inputClass}
|
|
337
|
+
aria-label="Project folder"
|
|
347
338
|
/>
|
|
348
|
-
<div className="mt-1.5 flex items-center gap-2">
|
|
349
|
-
<p className="min-w-0
|
|
350
|
-
{workspaceAvailable ? `Open workspace: ${workspaceName}` : 'No workspace open.'}
|
|
339
|
+
<div className="mt-1.5 flex items-center justify-between gap-2">
|
|
340
|
+
<p className="min-w-0 truncate text-[10px] text-faint">
|
|
341
|
+
{workspaceAvailable ? `Open workspace: ${workspaceName}` : 'No workspace open.'}
|
|
351
342
|
</p>
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
343
|
+
{workspaceAvailable && path !== workspaceRoot && (
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
onClick={() => setPath(workspaceRoot)}
|
|
347
|
+
disabled={running}
|
|
348
|
+
className="inline-flex shrink-0 items-center gap-1 text-[10px] font-medium text-accent disabled:opacity-40"
|
|
349
|
+
>
|
|
350
|
+
<FolderOpen size={10} /> Use workspace
|
|
351
|
+
</button>
|
|
352
|
+
)}
|
|
361
353
|
</div>
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
{
|
|
366
|
-
|
|
354
|
+
|
|
355
|
+
{detected && (
|
|
356
|
+
<div className="mt-2 flex items-start gap-2.5 rounded-xl border border-line bg-raised/40 p-2.5">
|
|
357
|
+
<FolderIcon name={detected.outputDir || 'src'} open size={22} />
|
|
358
|
+
<div className="min-w-0 flex-1 text-[10.5px] leading-relaxed">
|
|
359
|
+
<p className="font-medium text-ink">
|
|
360
|
+
{detected.framework || 'Folder'}
|
|
361
|
+
{detected.packageManager && <span className="ml-1.5 font-normal text-faint">{detected.packageManager}</span>}
|
|
362
|
+
</p>
|
|
363
|
+
<p className="text-muted">
|
|
364
|
+
{detected.buildCommand ? (
|
|
365
|
+
<>
|
|
366
|
+
build <code className="font-mono text-ink">{detected.buildCommand}</code>
|
|
367
|
+
</>
|
|
368
|
+
) : (
|
|
369
|
+
'no build step'
|
|
370
|
+
)}
|
|
371
|
+
{' → '}
|
|
372
|
+
{detected.outputDir ? (
|
|
373
|
+
<code className="font-mono text-ink">{detected.outputDir}</code>
|
|
374
|
+
) : detected.expectedOutputDir ? (
|
|
375
|
+
<>
|
|
376
|
+
<code className="font-mono text-ink">{detected.expectedOutputDir}</code> <span className="text-faint">(after build)</span>
|
|
377
|
+
</>
|
|
378
|
+
) : (
|
|
379
|
+
<span className="text-warning">no output folder found</span>
|
|
380
|
+
)}
|
|
381
|
+
</p>
|
|
382
|
+
</div>
|
|
367
383
|
</div>
|
|
368
384
|
)}
|
|
369
|
-
|
|
385
|
+
{detectError && (
|
|
386
|
+
<p className="mt-2 flex items-start gap-1.5 text-[10.5px] leading-relaxed text-danger">
|
|
387
|
+
<AlertTriangle size={11} className="mt-0.5 shrink-0" />
|
|
388
|
+
{detectError}
|
|
389
|
+
</p>
|
|
390
|
+
)}
|
|
391
|
+
</Field>
|
|
370
392
|
|
|
371
393
|
<div className="grid grid-cols-2 gap-2.5">
|
|
372
|
-
<
|
|
373
|
-
<label htmlFor="deploy-output" className="mb-1.5 block text-[10px] font-semibold uppercase tracking-[0.14em] text-faint">
|
|
374
|
-
Output folder
|
|
375
|
-
</label>
|
|
394
|
+
<Field label="Output folder" hint="auto">
|
|
376
395
|
<input
|
|
377
|
-
id="deploy-output"
|
|
378
396
|
value={outputDir}
|
|
379
397
|
onChange={(event) => setOutputDir(event.target.value)}
|
|
380
398
|
disabled={running}
|
|
381
|
-
placeholder=
|
|
399
|
+
placeholder={detected?.outputDir || detected?.expectedOutputDir || 'auto'}
|
|
382
400
|
spellCheck={false}
|
|
383
|
-
className=
|
|
401
|
+
className={inputClass}
|
|
402
|
+
aria-label="Output folder"
|
|
384
403
|
/>
|
|
385
|
-
</
|
|
386
|
-
<
|
|
387
|
-
<label htmlFor="deploy-name" className="mb-1.5 block text-[10px] font-semibold uppercase tracking-[0.14em] text-faint">
|
|
388
|
-
Project name
|
|
389
|
-
</label>
|
|
404
|
+
</Field>
|
|
405
|
+
<Field label="Project name" hint="auto">
|
|
390
406
|
<input
|
|
391
|
-
id="deploy-name"
|
|
392
407
|
value={projectName}
|
|
393
408
|
onChange={(event) => setProjectName(event.target.value)}
|
|
394
409
|
disabled={running}
|
|
395
|
-
placeholder=
|
|
410
|
+
placeholder={detected?.name || 'my-site'}
|
|
396
411
|
spellCheck={false}
|
|
397
|
-
className=
|
|
412
|
+
className={inputClass}
|
|
413
|
+
aria-label="Project name"
|
|
398
414
|
/>
|
|
399
|
-
</
|
|
400
|
-
</div>
|
|
401
|
-
|
|
402
|
-
<div className="flex flex-wrap gap-1">
|
|
403
|
-
{['auto', 'dist', 'build', 'out', '.'].map((value) => (
|
|
404
|
-
<button
|
|
405
|
-
key={value}
|
|
406
|
-
type="button"
|
|
407
|
-
onClick={() => setOutputDir(value)}
|
|
408
|
-
disabled={running}
|
|
409
|
-
className={cn('chip h-7 px-2 text-[9.5px]', (outputDir || 'auto') === value && 'chip-active')}
|
|
410
|
-
>
|
|
411
|
-
{value}
|
|
412
|
-
</button>
|
|
413
|
-
))}
|
|
415
|
+
</Field>
|
|
414
416
|
</div>
|
|
415
417
|
|
|
416
|
-
<
|
|
417
|
-
<label htmlFor="deploy-build" className="mb-1.5 block text-[10px] font-semibold uppercase tracking-[0.14em] text-faint">
|
|
418
|
-
Build command <span className="font-normal normal-case tracking-normal text-faint">optional</span>
|
|
419
|
-
</label>
|
|
418
|
+
<Field label="Build command" hint="auto when the output folder is missing">
|
|
420
419
|
<input
|
|
421
|
-
id="deploy-build"
|
|
422
420
|
value={buildCommand}
|
|
423
421
|
onChange={(event) => setBuildCommand(event.target.value)}
|
|
424
422
|
disabled={running}
|
|
425
|
-
placeholder=
|
|
423
|
+
placeholder={detected?.buildCommand || 'npm run build'}
|
|
426
424
|
spellCheck={false}
|
|
427
|
-
className=
|
|
425
|
+
className={inputClass}
|
|
426
|
+
aria-label="Build command"
|
|
428
427
|
/>
|
|
429
428
|
<div className="mt-1.5 flex flex-wrap gap-1">
|
|
430
429
|
{['npm run build', 'pnpm build', 'yarn build'].map((value) => (
|
|
@@ -433,121 +432,136 @@ export function DeployModal() {
|
|
|
433
432
|
type="button"
|
|
434
433
|
onClick={() => setBuildCommand(value)}
|
|
435
434
|
disabled={running}
|
|
436
|
-
className="rounded-md border border-line px-1.5 py-0.5 text-[
|
|
435
|
+
className="rounded-md border border-line px-1.5 py-0.5 text-[9.5px] text-faint transition-colors hover:text-ink"
|
|
437
436
|
>
|
|
438
437
|
{value}
|
|
439
438
|
</button>
|
|
440
439
|
))}
|
|
441
|
-
{buildCommand && (
|
|
440
|
+
{(buildCommand || outputDir) && (
|
|
442
441
|
<button
|
|
443
442
|
type="button"
|
|
444
|
-
onClick={() =>
|
|
443
|
+
onClick={() => {
|
|
444
|
+
setBuildCommand('')
|
|
445
|
+
setOutputDir('')
|
|
446
|
+
}}
|
|
445
447
|
disabled={running}
|
|
446
|
-
className="px-1.5 py-0.5 text-[
|
|
448
|
+
className="px-1.5 py-0.5 text-[9.5px] text-faint hover:text-danger"
|
|
447
449
|
>
|
|
448
|
-
|
|
450
|
+
reset to auto
|
|
449
451
|
</button>
|
|
450
452
|
)}
|
|
451
453
|
</div>
|
|
452
|
-
</
|
|
454
|
+
</Field>
|
|
453
455
|
|
|
454
|
-
<div className="flex items-start gap-1.5 rounded-lg border border-line bg-raised/40 px-2.5 py-2 text-[
|
|
456
|
+
<div className="flex items-start gap-1.5 rounded-lg border border-line bg-raised/40 px-2.5 py-2 text-[10px] leading-relaxed text-faint">
|
|
455
457
|
<Terminal size={11} className="mt-0.5 shrink-0" />
|
|
456
458
|
{tool?.installed
|
|
457
|
-
? `${tool.label}
|
|
458
|
-
: `${tool?.label ??
|
|
459
|
+
? `${tool.label}${tool.version ? ` ${tool.version}` : ''} found on this machine.`
|
|
460
|
+
: `${tool?.label ?? 'The CLI'} is not installed; OpenSol will fetch it with npx for this deploy.`}
|
|
459
461
|
</div>
|
|
460
462
|
</div>
|
|
461
463
|
</div>
|
|
462
464
|
|
|
463
465
|
<div className="flex min-h-0 flex-col">
|
|
464
|
-
<div className="flex shrink-0 items-center gap-
|
|
465
|
-
<
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
466
|
+
<div className="flex shrink-0 items-center gap-3 border-b border-line px-4 py-3">
|
|
467
|
+
<ol className="flex min-w-0 flex-1 items-center gap-1.5">
|
|
468
|
+
{STEPS.map((step, index) => {
|
|
469
|
+
const state = steps[step.id]
|
|
470
|
+
const Icon = step.icon
|
|
471
|
+
return (
|
|
472
|
+
<li key={step.id} className="flex min-w-0 items-center gap-1.5">
|
|
473
|
+
<span
|
|
474
|
+
className={cn(
|
|
475
|
+
'flex h-7 items-center gap-1.5 rounded-full border px-2.5 text-[10.5px] font-medium transition-colors',
|
|
476
|
+
state === 'active' && 'border-accent/40 bg-accent-soft text-accent',
|
|
477
|
+
state === 'done' && 'border-success/30 bg-success/10 text-success',
|
|
478
|
+
state === 'error' && 'border-danger/30 bg-danger/10 text-danger',
|
|
479
|
+
state === 'idle' && 'border-line text-faint',
|
|
480
|
+
)}
|
|
481
|
+
>
|
|
482
|
+
{state === 'active' ? (
|
|
483
|
+
<Spinner size={11} />
|
|
484
|
+
) : state === 'done' ? (
|
|
485
|
+
<Check size={11} />
|
|
486
|
+
) : state === 'error' ? (
|
|
487
|
+
<AlertTriangle size={11} />
|
|
488
|
+
) : (
|
|
489
|
+
<Icon size={11} />
|
|
490
|
+
)}
|
|
491
|
+
<span className="hidden sm:inline">{step.label}</span>
|
|
492
|
+
</span>
|
|
493
|
+
{index < STEPS.length - 1 && <span className={cn('h-px w-4 shrink-0', state === 'done' ? 'bg-success/40' : 'bg-line')} />}
|
|
494
|
+
</li>
|
|
495
|
+
)
|
|
496
|
+
})}
|
|
497
|
+
</ol>
|
|
498
|
+
{running ? (
|
|
470
499
|
<button
|
|
471
500
|
type="button"
|
|
472
501
|
onClick={() => abortRef.current?.abort()}
|
|
473
|
-
className="inline-flex h-
|
|
502
|
+
className="inline-flex h-8 items-center gap-1.5 rounded-lg border border-line px-3 text-[11px] font-medium text-muted hover:text-ink"
|
|
474
503
|
>
|
|
475
504
|
<Square size={9} fill="currentColor" /> Stop
|
|
476
505
|
</button>
|
|
477
|
-
)
|
|
478
|
-
{!running && (
|
|
506
|
+
) : (
|
|
479
507
|
<button
|
|
480
508
|
type="button"
|
|
481
509
|
onClick={() => void deploy()}
|
|
482
510
|
disabled={!configured || !path.trim()}
|
|
483
|
-
className="inline-flex h-
|
|
511
|
+
className="inline-flex h-8 items-center gap-1.5 rounded-lg bg-accent px-3.5 text-[11.5px] font-medium text-on-accent transition-colors hover:bg-accent/90 disabled:cursor-not-allowed disabled:opacity-40"
|
|
484
512
|
>
|
|
485
513
|
<Play size={10} fill="currentColor" />
|
|
486
|
-
Deploy
|
|
514
|
+
{result || error ? 'Deploy again' : 'Deploy'}
|
|
487
515
|
</button>
|
|
488
516
|
)}
|
|
489
517
|
</div>
|
|
490
518
|
|
|
491
519
|
{result && (
|
|
492
|
-
<div className="shrink-0 border-b border-line bg-success/[0.
|
|
493
|
-
<div className="flex items-
|
|
494
|
-
<
|
|
520
|
+
<div className="shrink-0 border-b border-line bg-success/[0.05] px-4 py-3.5">
|
|
521
|
+
<div className="flex items-center gap-3">
|
|
522
|
+
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-success/15 text-success">
|
|
523
|
+
<Check size={17} />
|
|
524
|
+
</span>
|
|
495
525
|
<div className="min-w-0 flex-1">
|
|
496
|
-
<p className="text-[
|
|
497
|
-
<a
|
|
498
|
-
href={result.url}
|
|
499
|
-
target="_blank"
|
|
500
|
-
rel="noreferrer"
|
|
501
|
-
className="mt-0.5 block truncate text-[11px] text-accent hover:underline"
|
|
502
|
-
>
|
|
526
|
+
<p className="text-[11px] font-medium text-muted">Live on {meta.label}</p>
|
|
527
|
+
<a href={result.url} target="_blank" rel="noreferrer" className="block truncate text-[14px] font-semibold text-accent hover:underline">
|
|
503
528
|
{result.url}
|
|
504
529
|
</a>
|
|
505
530
|
</div>
|
|
506
|
-
<button type="button" onClick={copyUrl} className="icon-button h-
|
|
507
|
-
{copied ? <Check size={
|
|
531
|
+
<button type="button" onClick={copyUrl} className="icon-button h-8 w-8" title="Copy URL" aria-label="Copy URL">
|
|
532
|
+
{copied ? <Check size={13} className="text-success" /> : <Copy size={13} />}
|
|
508
533
|
</button>
|
|
509
|
-
<a
|
|
510
|
-
|
|
511
|
-
target="_blank"
|
|
512
|
-
rel="noreferrer"
|
|
513
|
-
className="icon-button h-7 w-7"
|
|
514
|
-
title="Open site"
|
|
515
|
-
aria-label="Open site"
|
|
516
|
-
>
|
|
517
|
-
<ExternalLink size={12} />
|
|
534
|
+
<a href={result.url} target="_blank" rel="noreferrer" className="icon-button h-8 w-8" title="Open site" aria-label="Open site">
|
|
535
|
+
<ExternalLink size={13} />
|
|
518
536
|
</a>
|
|
519
537
|
</div>
|
|
520
538
|
</div>
|
|
521
539
|
)}
|
|
522
540
|
|
|
523
541
|
{error && (
|
|
524
|
-
<div className="shrink-0 border-b border-danger/20 bg-danger/[0.
|
|
542
|
+
<div className="flex shrink-0 items-start gap-2 border-b border-danger/20 bg-danger/[0.05] px-4 py-2.5 text-[11px] leading-relaxed text-danger">
|
|
543
|
+
<AlertTriangle size={13} className="mt-0.5 shrink-0" />
|
|
525
544
|
{error}
|
|
526
545
|
</div>
|
|
527
546
|
)}
|
|
528
547
|
|
|
529
548
|
<div ref={logRef} className="thin-scroll min-h-0 flex-1 overflow-y-auto bg-canvas/40 px-4 py-3">
|
|
530
549
|
{logs.length === 0 && !running ? (
|
|
531
|
-
<div className="flex h-full min-h-[
|
|
532
|
-
<span className="flex h-
|
|
533
|
-
<
|
|
550
|
+
<div className="flex h-full min-h-[200px] flex-col items-center justify-center gap-2 px-6 text-center">
|
|
551
|
+
<span className="flex h-11 w-11 items-center justify-center rounded-2xl bg-raised text-faint">
|
|
552
|
+
<PackageOpen size={18} />
|
|
534
553
|
</span>
|
|
535
|
-
<p className="text-[
|
|
536
|
-
<p className="max-w-[
|
|
537
|
-
|
|
538
|
-
never printed in the log.
|
|
554
|
+
<p className="text-[12px] font-medium text-muted">Ready when you are</p>
|
|
555
|
+
<p className="max-w-[280px] text-[10.5px] leading-relaxed text-faint">
|
|
556
|
+
Leave the fields on auto: OpenSol installs dependencies, builds if there is no output folder yet, then publishes it. Tokens never appear in the log.
|
|
539
557
|
</p>
|
|
540
558
|
</div>
|
|
541
559
|
) : (
|
|
542
|
-
<pre className="whitespace-pre-wrap break-words font-mono text-[
|
|
560
|
+
<pre className="whitespace-pre-wrap break-words font-mono text-[10.5px] leading-[1.7] text-muted">
|
|
543
561
|
{logs.map((line) => (
|
|
544
562
|
<span
|
|
545
563
|
key={line.id}
|
|
546
|
-
className={cn(
|
|
547
|
-
'block',
|
|
548
|
-
line.stream === 'stderr' && 'text-danger',
|
|
549
|
-
line.stream === 'meta' && 'text-accent',
|
|
550
|
-
)}
|
|
564
|
+
className={cn('block', line.stream === 'stderr' && 'text-danger', line.stream === 'meta' && 'font-medium text-accent')}
|
|
551
565
|
>
|
|
552
566
|
{line.stream === 'meta' ? '› ' : ''}
|
|
553
567
|
{line.text}
|
|
@@ -562,10 +576,10 @@ export function DeployModal() {
|
|
|
562
576
|
)}
|
|
563
577
|
</div>
|
|
564
578
|
|
|
565
|
-
<div className="flex h-9 shrink-0 items-center gap-2 border-t border-line px-4 text-[
|
|
579
|
+
<div className="flex h-9 shrink-0 items-center gap-2 border-t border-line px-4 text-[10px] text-faint">
|
|
566
580
|
<ShieldCheck size={10} />
|
|
567
581
|
Tokens encrypted at rest
|
|
568
|
-
<span className="ml-auto">{
|
|
582
|
+
<span className="ml-auto truncate">{folderName || 'No folder'}</span>
|
|
569
583
|
</div>
|
|
570
584
|
</div>
|
|
571
585
|
</div>
|