@shawnstack/quickforge 1.0.0
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/LICENSE +21 -0
- package/README.md +131 -0
- package/bin/quickforge.mjs +172 -0
- package/dist/assets/anthropic-u1nbNXhV.js +39 -0
- package/dist/assets/azure-openai-responses-DQ6xSOmb.js +1 -0
- package/dist/assets/chunk-62oNxeRG.js +1 -0
- package/dist/assets/confirm-dialog-DSmrqQ60.js +1 -0
- package/dist/assets/github-copilot-headers-C0toI16e.js +1 -0
- package/dist/assets/google-OeyKMN12.js +1 -0
- package/dist/assets/google-gemini-cli-SnPixyBu.js +2 -0
- package/dist/assets/google-shared-CXUHW-9O.js +11 -0
- package/dist/assets/google-vertex-y0o2eCZV.js +1 -0
- package/dist/assets/hash-fDQBJsbb.js +1 -0
- package/dist/assets/headers-Drkm68SQ.js +1 -0
- package/dist/assets/index-BQJ8qi1U.css +3 -0
- package/dist/assets/index-CK_34smc.js +3048 -0
- package/dist/assets/mistral-DzE_jn-B.js +44 -0
- package/dist/assets/openai-CuiHR4mv.js +16 -0
- package/dist/assets/openai-codex-responses-MtFRvp_b.js +7 -0
- package/dist/assets/openai-completions-C2dhwzO8.js +5 -0
- package/dist/assets/openai-responses-C4n0VhzY.js +1 -0
- package/dist/assets/openai-responses-shared-D2RkRvTj.js +10 -0
- package/dist/assets/pdf.worker.min-Cpi8b8z3.mjs +28 -0
- package/dist/assets/prompt-dialog-B4BD09Oc.js +1 -0
- package/dist/assets/transform-messages-BFwlToJ0.js +1 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +15 -0
- package/package.json +80 -0
- package/server/index.mjs +145 -0
- package/server/project-config.mjs +125 -0
- package/server/routes/filesystem.mjs +87 -0
- package/server/routes/instructions.mjs +31 -0
- package/server/routes/project.mjs +76 -0
- package/server/routes/static.mjs +57 -0
- package/server/routes/storage.mjs +97 -0
- package/server/routes/tools.mjs +31 -0
- package/server/storage.mjs +217 -0
- package/server/tools/index.mjs +236 -0
- package/server/utils/platform.mjs +131 -0
- package/server/utils/response.mjs +35 -0
- package/server/utils/workspace.mjs +135 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
|
|
5
|
+
export function spawnCollect(command, args, options = {}) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn(command, args, {
|
|
8
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9
|
+
windowsHide: options.windowsHide ?? true,
|
|
10
|
+
shell: false,
|
|
11
|
+
})
|
|
12
|
+
let stdout = ''
|
|
13
|
+
let stderr = ''
|
|
14
|
+
let timedOut = false
|
|
15
|
+
const timer = options.timeoutMs
|
|
16
|
+
? setTimeout(() => {
|
|
17
|
+
timedOut = true
|
|
18
|
+
child.kill('SIGTERM')
|
|
19
|
+
}, options.timeoutMs)
|
|
20
|
+
: undefined
|
|
21
|
+
child.stdout.on('data', (chunk) => {
|
|
22
|
+
stdout += chunk.toString()
|
|
23
|
+
})
|
|
24
|
+
child.stderr.on('data', (chunk) => {
|
|
25
|
+
stderr += chunk.toString()
|
|
26
|
+
})
|
|
27
|
+
child.on('error', (error) => {
|
|
28
|
+
if (timer) clearTimeout(timer)
|
|
29
|
+
reject(error)
|
|
30
|
+
})
|
|
31
|
+
child.on('close', (code) => {
|
|
32
|
+
if (timer) clearTimeout(timer)
|
|
33
|
+
resolve({ code, stdout, stderr, timedOut })
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function selectDirectoryDialog() {
|
|
39
|
+
if (process.platform === 'win32') {
|
|
40
|
+
const script = `
|
|
41
|
+
$ErrorActionPreference = 'Stop'
|
|
42
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
43
|
+
Add-Type -AssemblyName System.Drawing
|
|
44
|
+
|
|
45
|
+
$form = New-Object System.Windows.Forms.Form
|
|
46
|
+
$form.Text = 'QuickForge'
|
|
47
|
+
$form.StartPosition = 'CenterScreen'
|
|
48
|
+
$form.Size = New-Object System.Drawing.Size(1, 1)
|
|
49
|
+
$form.ShowInTaskbar = $false
|
|
50
|
+
$form.TopMost = $true
|
|
51
|
+
$form.Opacity = 0.01
|
|
52
|
+
|
|
53
|
+
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
54
|
+
$dialog.Description = 'Select QuickForge project folder'
|
|
55
|
+
$dialog.ShowNewFolderButton = $true
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
[void]$form.Show()
|
|
59
|
+
[void]$form.Activate()
|
|
60
|
+
$result = $dialog.ShowDialog($form)
|
|
61
|
+
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
62
|
+
[Console]::Out.Write($dialog.SelectedPath)
|
|
63
|
+
}
|
|
64
|
+
exit 0
|
|
65
|
+
} finally {
|
|
66
|
+
if ($dialog) { $dialog.Dispose() }
|
|
67
|
+
if ($form) { $form.Dispose() }
|
|
68
|
+
}
|
|
69
|
+
`
|
|
70
|
+
let result = await spawnCollect(
|
|
71
|
+
'powershell.exe',
|
|
72
|
+
['-NoProfile', '-STA', '-ExecutionPolicy', 'Bypass', '-Command', script],
|
|
73
|
+
{ windowsHide: false, timeoutMs: 10 * 60 * 1000 },
|
|
74
|
+
).catch(() => null)
|
|
75
|
+
if (!result) {
|
|
76
|
+
result = await spawnCollect(
|
|
77
|
+
'pwsh.exe',
|
|
78
|
+
['-NoProfile', '-STA', '-ExecutionPolicy', 'Bypass', '-Command', script],
|
|
79
|
+
{ windowsHide: false, timeoutMs: 10 * 60 * 1000 },
|
|
80
|
+
).catch(() => null)
|
|
81
|
+
}
|
|
82
|
+
if (!result) {
|
|
83
|
+
const error = new Error('PowerShell was not found. Please install PowerShell or enable Windows PowerShell.')
|
|
84
|
+
error.statusCode = 500
|
|
85
|
+
throw error
|
|
86
|
+
}
|
|
87
|
+
if (result.timedOut) {
|
|
88
|
+
const error = new Error('Folder picker timed out. It may have been blocked by Windows or opened on another desktop.')
|
|
89
|
+
error.statusCode = 504
|
|
90
|
+
throw error
|
|
91
|
+
}
|
|
92
|
+
if (result.code === 0) return result.stdout.trim()
|
|
93
|
+
const error = new Error(result.stderr.trim() || 'Failed to open folder picker')
|
|
94
|
+
error.statusCode = 500
|
|
95
|
+
throw error
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (process.platform === 'darwin') {
|
|
99
|
+
const result = await spawnCollect('osascript', ['-e', 'POSIX path of (choose folder with prompt "Select QuickForge project folder")'])
|
|
100
|
+
if (result.code === 0) return result.stdout.trim()
|
|
101
|
+
if (/User canceled/i.test(result.stderr)) return ''
|
|
102
|
+
const error = new Error(result.stderr.trim() || 'Failed to open folder picker')
|
|
103
|
+
error.statusCode = 500
|
|
104
|
+
throw error
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const zenity = await spawnCollect('zenity', ['--file-selection', '--directory', '--title=Select QuickForge project folder']).catch(() => null)
|
|
108
|
+
if (zenity) {
|
|
109
|
+
if (zenity.code === 0) return zenity.stdout.trim()
|
|
110
|
+
if (zenity.code === 1) return ''
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const kdialog = await spawnCollect('kdialog', ['--getexistingdirectory', os.homedir(), 'Select QuickForge project folder']).catch(() => null)
|
|
114
|
+
if (kdialog) {
|
|
115
|
+
if (kdialog.code === 0) return kdialog.stdout.trim()
|
|
116
|
+
if (kdialog.code === 1) return ''
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const error = new Error('No supported folder picker found. Install zenity or kdialog on Linux.')
|
|
120
|
+
error.statusCode = 501
|
|
121
|
+
throw error
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function openBrowser(url) {
|
|
125
|
+
if (process.env.QUICKFORGE_NO_OPEN === '1' || process.env.FASTCODE_NO_OPEN === '1') return
|
|
126
|
+
|
|
127
|
+
const command = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open'
|
|
128
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '""', url] : [url]
|
|
129
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore', shell: false })
|
|
130
|
+
child.unref()
|
|
131
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const DEFAULT_MAX_BODY_BYTES = Number(process.env.QUICKFORGE_MAX_BODY_BYTES || process.env.FASTCODE_MAX_BODY_BYTES || 50 * 1024 * 1024)
|
|
2
|
+
|
|
3
|
+
export function sendJson(res, status, value) {
|
|
4
|
+
const body = JSON.stringify(value)
|
|
5
|
+
res.writeHead(status, {
|
|
6
|
+
'content-type': 'application/json; charset=utf-8',
|
|
7
|
+
'cache-control': 'no-store',
|
|
8
|
+
})
|
|
9
|
+
res.end(body)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function sendError(res, error) {
|
|
13
|
+
const status = error?.statusCode || 500
|
|
14
|
+
sendJson(res, status, { error: error?.message || 'Internal server error' })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function readJsonBody(req, maxBodyBytes = DEFAULT_MAX_BODY_BYTES) {
|
|
18
|
+
const chunks = []
|
|
19
|
+
let size = 0
|
|
20
|
+
for await (const chunk of req) {
|
|
21
|
+
size += chunk.length
|
|
22
|
+
if (size > maxBodyBytes) {
|
|
23
|
+
const error = new Error('Request body is too large')
|
|
24
|
+
error.statusCode = 413
|
|
25
|
+
throw error
|
|
26
|
+
}
|
|
27
|
+
chunks.push(chunk)
|
|
28
|
+
}
|
|
29
|
+
const text = Buffer.concat(chunks).toString('utf8')
|
|
30
|
+
return text ? JSON.parse(text) : null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function decodeSegment(value) {
|
|
34
|
+
return decodeURIComponent(value || '')
|
|
35
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
let _workspaceRoot = ''
|
|
5
|
+
|
|
6
|
+
export function setWorkspaceRoot(root) {
|
|
7
|
+
_workspaceRoot = path.resolve(root)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getWorkspaceRoot() {
|
|
11
|
+
return _workspaceRoot
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getToolWorkspaceRoot(context) {
|
|
15
|
+
return context?.workspaceRoot || getWorkspaceRoot()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isInside(parent, child) {
|
|
19
|
+
const relative = path.relative(parent, child)
|
|
20
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveWorkspacePath(input = '.', context) {
|
|
24
|
+
const workspaceRoot = getToolWorkspaceRoot(context)
|
|
25
|
+
const candidate = path.isAbsolute(input)
|
|
26
|
+
? path.resolve(input)
|
|
27
|
+
: path.resolve(workspaceRoot, input)
|
|
28
|
+
|
|
29
|
+
if (!isInside(workspaceRoot, candidate)) {
|
|
30
|
+
const error = new Error(`Path is outside the selected project: ${input}`)
|
|
31
|
+
error.statusCode = 403
|
|
32
|
+
throw error
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return candidate
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function toWorkspaceRelative(fullPath, context) {
|
|
39
|
+
return path.relative(getToolWorkspaceRoot(context), fullPath).replace(/\\/g, '/') || '.'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isSensitiveWorkspacePath(fullPath, context) {
|
|
43
|
+
const relative = toWorkspaceRelative(fullPath, context)
|
|
44
|
+
const parts = relative.split('/')
|
|
45
|
+
const name = parts.at(-1) || ''
|
|
46
|
+
return (
|
|
47
|
+
parts.includes('.git') ||
|
|
48
|
+
name === '.env' ||
|
|
49
|
+
name.startsWith('.env.') ||
|
|
50
|
+
name.endsWith('.pem') ||
|
|
51
|
+
name.endsWith('.key') ||
|
|
52
|
+
name.endsWith('.p12') ||
|
|
53
|
+
name.endsWith('.pfx') ||
|
|
54
|
+
name.endsWith('.crt') ||
|
|
55
|
+
name.endsWith('.cer') ||
|
|
56
|
+
name.endsWith('.token') ||
|
|
57
|
+
name === 'credentials.json' ||
|
|
58
|
+
name === 'secrets.json' ||
|
|
59
|
+
name === 'id_rsa' ||
|
|
60
|
+
name === 'id_ed25519'
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function assertSafeWorkspacePath(fullPath, context) {
|
|
65
|
+
if (isSensitiveWorkspacePath(fullPath, context)) {
|
|
66
|
+
const error = new Error(`Access to sensitive path is blocked: ${toWorkspaceRelative(fullPath, context)}`)
|
|
67
|
+
error.statusCode = 403
|
|
68
|
+
throw error
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function truncateText(text, maxChars = 50000) {
|
|
73
|
+
if (text.length <= maxChars) return text
|
|
74
|
+
return `${text.slice(0, maxChars)}\n\n[truncated ${text.length - maxChars} characters]`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function splitLines(text) {
|
|
78
|
+
return text.split(/\r?\n/)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function pathExists(dir) {
|
|
82
|
+
try {
|
|
83
|
+
await fs.access(dir)
|
|
84
|
+
return true
|
|
85
|
+
} catch {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function assertDirectory(dir) {
|
|
91
|
+
const stat = await fs.stat(dir).catch(() => null)
|
|
92
|
+
if (!stat || !stat.isDirectory()) {
|
|
93
|
+
const error = new Error(`Project directory does not exist: ${dir}`)
|
|
94
|
+
error.statusCode = 400
|
|
95
|
+
throw error
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function directorySize(dir) {
|
|
100
|
+
try {
|
|
101
|
+
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
102
|
+
const sizes = await Promise.all(entries.map(async (entry) => {
|
|
103
|
+
const full = path.join(dir, entry.name)
|
|
104
|
+
if (entry.isDirectory()) return directorySize(full)
|
|
105
|
+
const stat = await fs.stat(full)
|
|
106
|
+
return stat.size
|
|
107
|
+
}))
|
|
108
|
+
return sizes.reduce((sum, value) => sum + value, 0)
|
|
109
|
+
} catch {
|
|
110
|
+
return 0
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function shouldSkipSearchDir(name) {
|
|
115
|
+
return ['.git', 'node_modules', 'dist', 'dist-ssr', '.vite'].includes(name)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function shouldSearchFile(name) {
|
|
119
|
+
const lower = name.toLowerCase()
|
|
120
|
+
const blocked = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.pdf', '.zip', '.gz', '.7z', '.exe', '.dll', '.woff', '.woff2', '.ttf']
|
|
121
|
+
return !blocked.some((extension) => lower.endsWith(extension))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function walkFiles(root, files = [], context) {
|
|
125
|
+
const entries = await fs.readdir(root, { withFileTypes: true })
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
const fullPath = path.join(root, entry.name)
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
if (!shouldSkipSearchDir(entry.name)) await walkFiles(fullPath, files, context)
|
|
130
|
+
} else if (entry.isFile() && shouldSearchFile(entry.name) && !isSensitiveWorkspacePath(fullPath, context)) {
|
|
131
|
+
files.push(fullPath)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return files
|
|
135
|
+
}
|