@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
package/server/lib/deploy.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
-
import { readFile, realpath, stat } from 'node:fs/promises'
|
|
2
|
+
import { readFile, readdir, realpath, stat } from 'node:fs/promises'
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { HttpError } from './http.mjs'
|
|
@@ -13,7 +13,7 @@ const CLIS = {
|
|
|
13
13
|
cloudflare: { bin: 'wrangler', pkg: 'wrangler@latest', label: 'Wrangler' },
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function slugify(value, fallback = '
|
|
16
|
+
function slugify(value, fallback = 'maxcode-site') {
|
|
17
17
|
const slug = String(value ?? '')
|
|
18
18
|
.toLowerCase()
|
|
19
19
|
.replace(/[^a-z0-9-]+/g, '-')
|
|
@@ -26,7 +26,14 @@ function suffix() {
|
|
|
26
26
|
return Math.random().toString(36).slice(2, 7)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
const IS_WINDOWS = process.platform === 'win32'
|
|
30
|
+
|
|
29
31
|
async function commandExists(bin) {
|
|
32
|
+
// Direct spawn first: it is fast and works on Windows (.cmd shims). Fall back to a login shell
|
|
33
|
+
// so tools installed through nvm/asdf/Homebrew profiles are still found.
|
|
34
|
+
const direct = await runCapture(bin, ['--version'], { timeoutMs: 15_000, shell: IS_WINDOWS })
|
|
35
|
+
if (direct.code === 0) return { installed: true, version: direct.stdout.split('\n')[0]?.trim() ?? '' }
|
|
36
|
+
if (IS_WINDOWS) return { installed: false, version: '' }
|
|
30
37
|
const result = await runCapture('bash', ['-lc', `command -v ${bin} >/dev/null 2>&1 && ${bin} --version`], {
|
|
31
38
|
timeoutMs: 20_000,
|
|
32
39
|
})
|
|
@@ -43,15 +50,16 @@ export async function deployToolStatus() {
|
|
|
43
50
|
return Object.fromEntries(entries)
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
function runCapture(command, args, { cwd, env, timeoutMs = 120_000 } = {}) {
|
|
53
|
+
function runCapture(command, args, { cwd, env, timeoutMs = 120_000, shell = false } = {}) {
|
|
47
54
|
return new Promise((resolve) => {
|
|
48
55
|
let stdout = ''
|
|
49
56
|
let stderr = ''
|
|
50
57
|
const child = spawn(command, args, {
|
|
51
58
|
cwd,
|
|
52
|
-
env: { ...process.env, ...(env ?? {}) },
|
|
59
|
+
env: cleanEnv({ ...process.env, ...(env ?? {}) }),
|
|
53
60
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
54
61
|
detached: process.platform !== 'win32',
|
|
62
|
+
shell,
|
|
55
63
|
})
|
|
56
64
|
const timer = setTimeout(() => {
|
|
57
65
|
try {
|
|
@@ -78,17 +86,33 @@ function runCapture(command, args, { cwd, env, timeoutMs = 120_000 } = {}) {
|
|
|
78
86
|
})
|
|
79
87
|
}
|
|
80
88
|
|
|
89
|
+
/** spawn() rejects undefined env values on some Node versions; drop them. */
|
|
90
|
+
function cleanEnv(env) {
|
|
91
|
+
return Object.fromEntries(Object.entries(env).filter(([, value]) => typeof value === 'string'))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function winQuote(arg) {
|
|
95
|
+
return /[\s"&|<>^()]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg
|
|
96
|
+
}
|
|
97
|
+
|
|
81
98
|
function runStreaming(command, args, { cwd, env, onLog, timeoutMs = 15 * 60 * 1000, signal }) {
|
|
82
99
|
return new Promise((resolve, reject) => {
|
|
83
100
|
let stdout = ''
|
|
84
101
|
let stderr = ''
|
|
85
102
|
let settled = false
|
|
86
|
-
const child =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
const child = IS_WINDOWS
|
|
104
|
+
? spawn([command, ...args].map(winQuote).join(' '), {
|
|
105
|
+
cwd,
|
|
106
|
+
env: cleanEnv({ ...process.env, ...(env ?? {}), CI: '1' }),
|
|
107
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
108
|
+
shell: true,
|
|
109
|
+
})
|
|
110
|
+
: spawn(command, args, {
|
|
111
|
+
cwd,
|
|
112
|
+
env: cleanEnv({ ...process.env, ...(env ?? {}), CI: '1' }),
|
|
113
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
114
|
+
detached: true,
|
|
115
|
+
})
|
|
92
116
|
const finish = (result) => {
|
|
93
117
|
if (settled) return
|
|
94
118
|
settled = true
|
|
@@ -169,68 +193,18 @@ async function fetchJson(url, options = {}) {
|
|
|
169
193
|
return payload
|
|
170
194
|
}
|
|
171
195
|
|
|
172
|
-
|
|
173
|
-
const matches = text.match(/https:\/\/[^\s"'<>)\]]+/g) ?? []
|
|
174
|
-
const filtered = matches.filter(
|
|
175
|
-
(url) =>
|
|
176
|
-
!/vercel\.com\/(docs|cli|pricing)/i.test(url) &&
|
|
177
|
-
!/netlify\.com\/(docs|pricing|app)/i.test(url) &&
|
|
178
|
-
!/developers\.cloudflare\.com/i.test(url) &&
|
|
179
|
-
!/wrangler/i.test(url) &&
|
|
180
|
-
!/github\.com/i.test(url),
|
|
181
|
-
)
|
|
182
|
-
return filtered[filtered.length - 1] ?? ''
|
|
183
|
-
}
|
|
196
|
+
const ANSI = /\u001b\[[0-9;]*[A-Za-z]/g
|
|
184
197
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
{ dep: 'next', name: 'Next.js', output: 'out' },
|
|
188
|
-
{ dep: 'nuxt', name: 'Nuxt', output: '.output/public' },
|
|
189
|
-
{ dep: 'astro', name: 'Astro', output: 'dist' },
|
|
190
|
-
{ dep: '@sveltejs/kit', name: 'SvelteKit', output: 'build' },
|
|
191
|
-
{ dep: 'vite', name: 'Vite', output: 'dist' },
|
|
192
|
-
{ dep: 'react-scripts', name: 'Create React App', output: 'build' },
|
|
193
|
-
]
|
|
194
|
-
|
|
195
|
-
async function packageManager(root) {
|
|
196
|
-
for (const [file, command] of [
|
|
197
|
-
['pnpm-lock.yaml', 'pnpm'],
|
|
198
|
-
['yarn.lock', 'yarn'],
|
|
199
|
-
['bun.lockb', 'bun'],
|
|
200
|
-
['bun.lock', 'bun'],
|
|
201
|
-
['package-lock.json', 'npm'],
|
|
202
|
-
]) {
|
|
203
|
-
if (await stat(path.join(root, file)).catch(() => null)) return command
|
|
204
|
-
}
|
|
205
|
-
return 'npm'
|
|
198
|
+
function urlsIn(text) {
|
|
199
|
+
return (String(text).replace(ANSI, '').match(/https:\/\/[^\s"'<>)\]]+/g) ?? []).map((url) => url.replace(/[.,;]+$/, ''))
|
|
206
200
|
}
|
|
207
201
|
|
|
208
|
-
/**
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
.catch(() => null)
|
|
215
|
-
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) }
|
|
216
|
-
const framework = FRAMEWORKS.find((entry) => deps[entry.dep])
|
|
217
|
-
const found = []
|
|
218
|
-
for (const candidate of OUTPUT_CANDIDATES) {
|
|
219
|
-
const info = await stat(path.join(absolute, candidate)).catch(() => null)
|
|
220
|
-
if (info?.isDirectory()) found.push(candidate)
|
|
221
|
-
}
|
|
222
|
-
const manager = await packageManager(absolute)
|
|
223
|
-
const script = typeof pkg?.scripts?.build === 'string' ? pkg.scripts.build : ''
|
|
224
|
-
const buildCommand = script ? (manager === 'npm' ? 'npm run build' : `${manager} build`) : ''
|
|
225
|
-
return {
|
|
226
|
-
path: absolute,
|
|
227
|
-
name: slugify(path.basename(absolute), ''),
|
|
228
|
-
hasPackageJson: Boolean(pkg),
|
|
229
|
-
framework: framework?.name ?? (pkg ? 'Node project' : 'Static folder'),
|
|
230
|
-
buildCommand,
|
|
231
|
-
outputDir: found[0] ?? framework?.output ?? '',
|
|
232
|
-
existingOutputs: found,
|
|
233
|
-
}
|
|
202
|
+
/** Picks the live site URL from CLI output, ignoring dashboards, docs and build-log links. */
|
|
203
|
+
function liveUrl(text, preferred, ignored = /(app\.netlify\.com|vercel\.com|dash\.cloudflare\.com|developers\.cloudflare\.com|docs\.|github\.com|npmjs\.)/i) {
|
|
204
|
+
const urls = urlsIn(text).filter((url) => !ignored.test(url))
|
|
205
|
+
const preferredMatches = urls.filter((url) => preferred.test(url))
|
|
206
|
+
const pool = preferredMatches.length > 0 ? preferredMatches : urls
|
|
207
|
+
return pool[pool.length - 1] ?? ''
|
|
234
208
|
}
|
|
235
209
|
|
|
236
210
|
async function resolveCli(provider) {
|
|
@@ -251,31 +225,96 @@ async function resolveCli(provider) {
|
|
|
251
225
|
return { command: 'npx', prefix: ['--yes', def.pkg], label: def.label }
|
|
252
226
|
}
|
|
253
227
|
|
|
254
|
-
async function
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (result.code !== 0) {
|
|
265
|
-
throw new HttpError(500, `Build failed with exit code ${result.code}.`, 'build_failed')
|
|
228
|
+
async function pathInfo(target) {
|
|
229
|
+
return stat(target).catch(() => null)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function readPackageJson(cwd) {
|
|
233
|
+
try {
|
|
234
|
+
const parsed = JSON.parse(await readFile(path.join(cwd, 'package.json'), 'utf8'))
|
|
235
|
+
return parsed && typeof parsed === 'object' ? parsed : null
|
|
236
|
+
} catch {
|
|
237
|
+
return null
|
|
266
238
|
}
|
|
267
239
|
}
|
|
268
240
|
|
|
269
|
-
async function
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
241
|
+
async function packageManager(cwd) {
|
|
242
|
+
if (await pathInfo(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
243
|
+
if (await pathInfo(path.join(cwd, 'yarn.lock'))) return 'yarn'
|
|
244
|
+
if ((await pathInfo(path.join(cwd, 'bun.lockb'))) || (await pathInfo(path.join(cwd, 'bun.lock')))) return 'bun'
|
|
245
|
+
return 'npm'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const OUTPUT_CANDIDATES = ['dist', 'build', 'out', '.output/public', 'public']
|
|
249
|
+
|
|
250
|
+
async function folderHasFiles(dir) {
|
|
251
|
+
try {
|
|
252
|
+
const entries = await readdir(dir)
|
|
253
|
+
return entries.length > 0
|
|
254
|
+
} catch {
|
|
255
|
+
return false
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Looks at a project folder and suggests how it should be built and which folder to publish. */
|
|
260
|
+
export async function detectProject(raw) {
|
|
261
|
+
const cwd = await resolveDeployPath(raw)
|
|
262
|
+
await ensureDir(cwd)
|
|
263
|
+
const pkg = await readPackageJson(cwd)
|
|
264
|
+
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) }
|
|
265
|
+
const framework =
|
|
266
|
+
deps.next ? 'Next.js'
|
|
267
|
+
: deps.nuxt ? 'Nuxt'
|
|
268
|
+
: deps['@sveltejs/kit'] ? 'SvelteKit'
|
|
269
|
+
: deps.astro ? 'Astro'
|
|
270
|
+
: deps.vite ? 'Vite'
|
|
271
|
+
: deps['react-scripts'] ? 'Create React App'
|
|
272
|
+
: deps['@angular/core'] ? 'Angular'
|
|
273
|
+
: deps.vue ? 'Vue'
|
|
274
|
+
: pkg ? 'Node project'
|
|
275
|
+
: ''
|
|
276
|
+
const manager = await packageManager(cwd)
|
|
277
|
+
const hasBuild = typeof pkg?.scripts?.build === 'string'
|
|
278
|
+
const buildCommand = hasBuild ? (manager === 'npm' ? 'npm run build' : `${manager} build`) : ''
|
|
279
|
+
|
|
280
|
+
let outputDir = ''
|
|
281
|
+
for (const candidate of OUTPUT_CANDIDATES) {
|
|
282
|
+
const dir = path.join(cwd, ...candidate.split('/'))
|
|
283
|
+
if ((await pathInfo(dir))?.isDirectory() && (await folderHasFiles(dir))) {
|
|
284
|
+
// `public` is only a real output for projects without a build step; otherwise it is source assets.
|
|
285
|
+
if (candidate === 'public' && hasBuild) continue
|
|
286
|
+
outputDir = candidate
|
|
287
|
+
break
|
|
276
288
|
}
|
|
277
|
-
return cwd
|
|
278
289
|
}
|
|
290
|
+
const staticSite = !hasBuild && Boolean(await pathInfo(path.join(cwd, 'index.html')))
|
|
291
|
+
if (!outputDir && staticSite) outputDir = '.'
|
|
292
|
+
const expected = hasBuild ? (deps['react-scripts'] ? 'build' : deps.nuxt ? '.output/public' : deps.next ? 'out' : 'dist') : ''
|
|
293
|
+
return {
|
|
294
|
+
path: cwd,
|
|
295
|
+
name: slugify(pkg?.name ? String(pkg.name).replace(/^@[^/]+\//, '') : path.basename(cwd), ''),
|
|
296
|
+
framework: framework || (staticSite ? 'Static site' : ''),
|
|
297
|
+
packageManager: pkg ? manager : '',
|
|
298
|
+
hasNodeModules: Boolean(await pathInfo(path.join(cwd, 'node_modules'))),
|
|
299
|
+
buildCommand,
|
|
300
|
+
outputDir,
|
|
301
|
+
expectedOutputDir: outputDir || expected,
|
|
302
|
+
built: Boolean(outputDir),
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function runBuild(cwd, command, onLog, signal) {
|
|
307
|
+
const result = IS_WINDOWS
|
|
308
|
+
? await runStreaming(command, [], { cwd, onLog, signal, env: { MAXCODE_DEPLOY: '1' } })
|
|
309
|
+
: await runStreaming('/bin/bash', ['-lc', command], { cwd, onLog, signal, env: { MAXCODE_DEPLOY: '1' } })
|
|
310
|
+
if (result.code !== 0) {
|
|
311
|
+
throw new HttpError(500, `"${command}" failed with exit code ${result.code}. Fix the error in the log, then deploy again.`, 'build_failed')
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function outputDirFor(cwd, outputDir) {
|
|
316
|
+
const clean = typeof outputDir === 'string' ? outputDir.trim() : ''
|
|
317
|
+
if (!clean) return cwd
|
|
279
318
|
const resolved = path.resolve(cwd, clean)
|
|
280
319
|
if (!resolved.startsWith(cwd.endsWith(path.sep) ? cwd : `${cwd}${path.sep}`) && resolved !== cwd) {
|
|
281
320
|
throw new HttpError(400, 'The output folder must stay inside the deploy folder.', 'bad_output')
|
|
@@ -285,13 +324,7 @@ async function outputDirFor(cwd, outputDir) {
|
|
|
285
324
|
|
|
286
325
|
async function ensureDir(target) {
|
|
287
326
|
const info = await stat(target).catch(() => null)
|
|
288
|
-
if (!info?.isDirectory())
|
|
289
|
-
throw new HttpError(
|
|
290
|
-
400,
|
|
291
|
-
`The folder "${path.basename(target)}" does not exist. Run a build command or choose the folder that actually contains the site.`,
|
|
292
|
-
'bad_deploy_path',
|
|
293
|
-
)
|
|
294
|
-
}
|
|
327
|
+
if (!info?.isDirectory()) throw new HttpError(400, 'The folder to deploy does not exist.', 'bad_deploy_path')
|
|
295
328
|
}
|
|
296
329
|
|
|
297
330
|
/**
|
|
@@ -319,8 +352,20 @@ async function netlifyDeploy({ cwd, targetDir, projectName, siteId, onLog, signa
|
|
|
319
352
|
if (!token) {
|
|
320
353
|
throw new HttpError(400, 'Add a Netlify personal access token in Settings → Deploy.', 'missing_deploy_token')
|
|
321
354
|
}
|
|
322
|
-
|
|
355
|
+
// An explicit site id wins; otherwise reuse the site from the last deploy instead of creating a new one every time.
|
|
356
|
+
const requested = typeof siteId === 'string' ? siteId.trim() : ''
|
|
357
|
+
let site = requested || (typeof secrets.siteId === 'string' ? secrets.siteId : '')
|
|
323
358
|
let siteUrl = ''
|
|
359
|
+
if (site && !requested) {
|
|
360
|
+
const existing = await fetchJson(`https://api.netlify.com/api/v1/sites/${encodeURIComponent(site)}`, {
|
|
361
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
362
|
+
}).catch(() => null)
|
|
363
|
+
if (existing) siteUrl = String(existing.ssl_url ?? existing.url ?? '')
|
|
364
|
+
else {
|
|
365
|
+
onLog({ stream: 'meta', text: 'The site from the last deploy no longer exists. Creating a new one.' })
|
|
366
|
+
site = ''
|
|
367
|
+
}
|
|
368
|
+
}
|
|
324
369
|
if (!site) {
|
|
325
370
|
const name = slugify(projectName || path.basename(cwd), `maxcode-${suffix()}`)
|
|
326
371
|
onLog({ stream: 'meta', text: `Creating Netlify site "${name}"…` })
|
|
@@ -348,11 +393,6 @@ async function netlifyDeploy({ cwd, targetDir, projectName, siteId, onLog, signa
|
|
|
348
393
|
siteUrl = String(created.ssl_url ?? created.url ?? '')
|
|
349
394
|
if (!site) throw new HttpError(502, 'Netlify did not return a site id.', 'deploy_api_failed')
|
|
350
395
|
onLog({ stream: 'meta', text: `Created site ${siteUrl || site}` })
|
|
351
|
-
} else {
|
|
352
|
-
const info = await fetchJson(`https://api.netlify.com/api/v1/sites/${encodeURIComponent(site)}`, {
|
|
353
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
354
|
-
}).catch(() => null)
|
|
355
|
-
if (info) siteUrl = String(info.ssl_url ?? info.url ?? '')
|
|
356
396
|
}
|
|
357
397
|
|
|
358
398
|
await setDeploySecret('netlify', { siteId: site })
|
|
@@ -364,6 +404,7 @@ async function netlifyDeploy({ cwd, targetDir, projectName, siteId, onLog, signa
|
|
|
364
404
|
[
|
|
365
405
|
...cli.prefix,
|
|
366
406
|
'deploy',
|
|
407
|
+
'--no-build',
|
|
367
408
|
'--prod',
|
|
368
409
|
'--dir',
|
|
369
410
|
targetDir,
|
|
@@ -377,7 +418,7 @@ async function netlifyDeploy({ cwd, targetDir, projectName, siteId, onLog, signa
|
|
|
377
418
|
if (result.code !== 0) {
|
|
378
419
|
throw new HttpError(500, `Netlify deploy failed (exit ${result.code}).`, 'deploy_failed')
|
|
379
420
|
}
|
|
380
|
-
const url =
|
|
421
|
+
const url = liveUrl(result.stdout, /\.netlify\.app/i) || siteUrl
|
|
381
422
|
if (url) await setDeploySecret('netlify', { lastUrl: url })
|
|
382
423
|
return { provider: 'netlify', url, siteId: site }
|
|
383
424
|
}
|
|
@@ -390,21 +431,24 @@ async function vercelDeploy({ cwd, targetDir, projectName, onLog, signal }) {
|
|
|
390
431
|
}
|
|
391
432
|
const cli = await resolveCli('vercel')
|
|
392
433
|
const teamId = (typeof secrets.teamId === 'string' && secrets.teamId) || ''
|
|
393
|
-
const args = [...cli.prefix, 'deploy', targetDir, '--prod', '--yes']
|
|
394
|
-
if (teamId) args.push('--scope', teamId)
|
|
395
434
|
const name = slugify(projectName || path.basename(cwd), '')
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
signal,
|
|
402
|
-
|
|
403
|
-
|
|
435
|
+
const base = [...cli.prefix, 'deploy', targetDir, '--prod', '--yes', '--token', token]
|
|
436
|
+
if (teamId) base.push('--scope', teamId)
|
|
437
|
+
const run = (args) => {
|
|
438
|
+
onLog({ stream: 'meta', text: `Deploying ${targetDir} with ${cli.label}${name ? ` as "${name}"` : ''}…` })
|
|
439
|
+
// The token goes through the environment too; never log the argument list.
|
|
440
|
+
return runStreaming(cli.command, args, { cwd, onLog, signal, env: { VERCEL_TOKEN: token } })
|
|
441
|
+
}
|
|
442
|
+
// VERCEL_ORG_ID must not be set alone (the CLI then demands VERCEL_PROJECT_ID), so the team goes through --scope.
|
|
443
|
+
let result = await run(name ? [...base, '--name', name] : base)
|
|
444
|
+
if (result.code !== 0 && name && /(unknown|unexpected|unrecognized).{0,40}--?name|--name.{0,40}(unknown|not)/i.test(`${result.stderr}\n${result.stdout}`)) {
|
|
445
|
+
onLog({ stream: 'meta', text: 'This Vercel CLI no longer accepts --name; retrying without it.' })
|
|
446
|
+
result = await run(base)
|
|
447
|
+
}
|
|
404
448
|
if (result.code !== 0) {
|
|
405
|
-
throw new HttpError(500, `Vercel deploy failed (exit ${result.code}).`, 'deploy_failed')
|
|
449
|
+
throw new HttpError(500, `Vercel deploy failed (exit ${result.code}). See the log above.`, 'deploy_failed')
|
|
406
450
|
}
|
|
407
|
-
const url =
|
|
451
|
+
const url = liveUrl(`${result.stdout}\n${result.stderr}`, /\.vercel\.app/i)
|
|
408
452
|
if (!url) throw new HttpError(502, 'Vercel did not report a deployment URL.', 'deploy_url_missing')
|
|
409
453
|
await setDeploySecret('vercel', { projectName: name || projectName || '', lastUrl: url })
|
|
410
454
|
return { provider: 'vercel', url, project: name || projectName || '' }
|
|
@@ -477,26 +521,83 @@ async function cloudflareDeploy({ cwd, targetDir, projectName, onLog, signal })
|
|
|
477
521
|
if (result.code !== 0) {
|
|
478
522
|
throw new HttpError(500, `Cloudflare Pages deploy failed (exit ${result.code}).`, 'deploy_failed')
|
|
479
523
|
}
|
|
480
|
-
const url =
|
|
524
|
+
const url = `https://${name}.pages.dev`
|
|
525
|
+
const reported = liveUrl(`${result.stdout}\n${result.stderr}`, /\.pages\.dev/i)
|
|
526
|
+
onLog({ stream: 'meta', text: reported && reported !== url ? `This deployment: ${reported}` : `Production alias: ${url}` })
|
|
481
527
|
await setDeploySecret('cloudflare', { lastUrl: url })
|
|
482
528
|
return { provider: 'cloudflare', url, project: name }
|
|
483
529
|
}
|
|
484
530
|
|
|
485
|
-
export async function runDeploy(input, { onLog = () => undefined, signal } = {}) {
|
|
531
|
+
export async function runDeploy(input, { onLog = () => undefined, onStep = () => undefined, signal } = {}) {
|
|
486
532
|
const provider = String(input?.provider ?? '')
|
|
487
533
|
if (!['cloudflare', 'netlify', 'vercel'].includes(provider)) {
|
|
488
534
|
throw new HttpError(400, 'Choose Cloudflare Pages, Netlify or Vercel.', 'bad_deploy_provider')
|
|
489
535
|
}
|
|
536
|
+
onStep({ id: 'prepare', label: 'Inspect project' })
|
|
490
537
|
const absolute = await resolveDeployPath(input?.path)
|
|
491
538
|
await ensureDir(absolute)
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
539
|
+
const detected = await detectProject(absolute)
|
|
540
|
+
|
|
541
|
+
const requestedOutput = typeof input?.outputDir === 'string' ? input.outputDir.trim() : ''
|
|
542
|
+
const auto = !requestedOutput || requestedOutput === 'auto'
|
|
543
|
+
let buildCommand = typeof input?.buildCommand === 'string' ? input.buildCommand.trim() : ''
|
|
544
|
+
let outputDir = auto ? detected.outputDir : requestedOutput
|
|
545
|
+
|
|
546
|
+
const outputMissing = async () => {
|
|
547
|
+
const dir = auto ? detected.outputDir : requestedOutput
|
|
548
|
+
if (!dir) return true
|
|
549
|
+
const info = await pathInfo(path.resolve(absolute, dir))
|
|
550
|
+
return !info?.isDirectory()
|
|
551
|
+
}
|
|
552
|
+
if (!buildCommand && (await outputMissing()) && detected.buildCommand) {
|
|
553
|
+
buildCommand = detected.buildCommand
|
|
554
|
+
onLog({ stream: 'meta', text: `No output folder yet. Building first with "${buildCommand}".` })
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (buildCommand) {
|
|
558
|
+
const needsInstall = detected.packageManager && !detected.hasNodeModules && /\b(npm|pnpm|yarn|bun)\b/.test(buildCommand)
|
|
559
|
+
if (needsInstall) {
|
|
560
|
+
onStep({ id: 'install', label: 'Install dependencies' })
|
|
561
|
+
const install = detected.packageManager === 'npm' ? 'npm install' : `${detected.packageManager} install`
|
|
562
|
+
onLog({ stream: 'meta', text: `node_modules is missing. Running ${install}.` })
|
|
563
|
+
await runBuild(absolute, install, onLog, signal)
|
|
564
|
+
}
|
|
565
|
+
onStep({ id: 'build', label: 'Build' })
|
|
566
|
+
onLog({ stream: 'meta', text: `Running build: ${buildCommand}` })
|
|
567
|
+
await runBuild(absolute, buildCommand, onLog, signal)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (auto) {
|
|
571
|
+
outputDir = (await detectProject(absolute)).outputDir
|
|
572
|
+
if (!outputDir) {
|
|
573
|
+
throw new HttpError(
|
|
574
|
+
400,
|
|
575
|
+
`No output folder found in ${path.basename(absolute)}. Add a build command that creates dist/, build/ or out/, or choose the folder to publish.`,
|
|
576
|
+
'bad_output',
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const targetDir = outputDirFor(absolute, outputDir)
|
|
581
|
+
const info = await pathInfo(targetDir)
|
|
582
|
+
if (!info?.isDirectory()) {
|
|
583
|
+
throw new HttpError(
|
|
584
|
+
400,
|
|
585
|
+
`The output folder "${outputDir}" does not exist${buildCommand ? ' after the build' : '. Add a build command or pick another folder'}.`,
|
|
586
|
+
'bad_output',
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
if (!(await folderHasFiles(targetDir))) {
|
|
590
|
+
throw new HttpError(400, `The output folder "${outputDir}" is empty, so there is nothing to publish.`, 'bad_output')
|
|
591
|
+
}
|
|
592
|
+
if (!(await pathInfo(path.join(targetDir, 'index.html')))) {
|
|
593
|
+
onLog({ stream: 'meta', text: `Note: ${outputDir} has no index.html, so the site root may show a 404.` })
|
|
594
|
+
}
|
|
495
595
|
|
|
596
|
+
onStep({ id: 'publish', label: 'Publish' })
|
|
496
597
|
const common = {
|
|
497
598
|
cwd: absolute,
|
|
498
599
|
targetDir,
|
|
499
|
-
projectName: String(input?.projectName ?? ''),
|
|
600
|
+
projectName: String(input?.projectName ?? '').trim() || detected.name,
|
|
500
601
|
siteId: String(input?.siteId ?? ''),
|
|
501
602
|
onLog,
|
|
502
603
|
signal,
|
package/server/lib/providers.mjs
CHANGED
|
@@ -138,6 +138,7 @@ function trimCatalog(raw) {
|
|
|
138
138
|
temperature: model.temperature !== false,
|
|
139
139
|
modalities: record(model.modalities),
|
|
140
140
|
limit: record(model.limit),
|
|
141
|
+
cost: record(model.cost),
|
|
141
142
|
provider: record(model.provider),
|
|
142
143
|
canonical_model_id: text(model.canonical_model_id),
|
|
143
144
|
}
|
|
@@ -220,7 +221,26 @@ function opencodeKindFromId(modelId, catalogEntry) {
|
|
|
220
221
|
return 'openai-chat'
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
|
|
224
|
+
/** USD per 1M tokens, from a live per-token `pricing` block (OpenRouter style) or models.dev `cost`. */
|
|
225
|
+
function pricingOf(livePricing, catalogCost) {
|
|
226
|
+
const live = record(livePricing)
|
|
227
|
+
const perToken = (value) => {
|
|
228
|
+
const parsed = typeof value === 'string' ? Number.parseFloat(value) : typeof value === 'number' ? value : NaN
|
|
229
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed * 1_000_000 : undefined
|
|
230
|
+
}
|
|
231
|
+
const liveInput = perToken(live.prompt)
|
|
232
|
+
const liveOutput = perToken(live.completion)
|
|
233
|
+
if (liveInput !== undefined && liveOutput !== undefined && (liveInput > 0 || liveOutput > 0)) {
|
|
234
|
+
return { input: liveInput, output: liveOutput }
|
|
235
|
+
}
|
|
236
|
+
const cost = record(catalogCost)
|
|
237
|
+
const input = number(cost.input)
|
|
238
|
+
const output = number(cost.output)
|
|
239
|
+
if (input !== undefined && output !== undefined && (input > 0 || output > 0)) return { input, output }
|
|
240
|
+
return undefined
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function modelInfo({ providerId, id, name, context, maxOutput, vision, catalogEntry, fallbackKind, livePricing }) {
|
|
224
244
|
const catalogDef = catalogEntry ?? {}
|
|
225
245
|
const modalities = record(catalogDef.modalities)
|
|
226
246
|
const limit = record(catalogDef.limit)
|
|
@@ -255,6 +275,7 @@ function modelInfo({ providerId, id, name, context, maxOutput, vision, catalogEn
|
|
|
255
275
|
apiKind,
|
|
256
276
|
free,
|
|
257
277
|
canonicalId: text(catalogDef.canonical_model_id) || undefined,
|
|
278
|
+
pricing: pricingOf(livePricing, catalogDef.cost),
|
|
258
279
|
note:
|
|
259
280
|
providerId === 'opencode' && free
|
|
260
281
|
? 'OpenCode free-tier models only work inside OpenCode. Add credits or use a paid model.'
|
|
@@ -296,6 +317,7 @@ function fromOpenAI(payload, providerId, catalog) {
|
|
|
296
317
|
context: number(item.context_length) ?? number(item.context_window),
|
|
297
318
|
vision: strings(architecture.input_modalities).includes('image'),
|
|
298
319
|
catalogEntry: entry,
|
|
320
|
+
livePricing: item.pricing,
|
|
299
321
|
}),
|
|
300
322
|
)
|
|
301
323
|
}
|