@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 @@
|
|
|
1
|
+
import{i as e}from"./chunk-62oNxeRG.js";import{f as t,m as n,n as r,p as i,r as a,t as o}from"./index-CK_34smc.js";var s=i(),c=t(),l=e(n(),1),u=r(),d=l.forwardRef(({className:e,type:t,...n},r)=>(0,u.jsx)(`input`,{type:t,className:a(`flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50`,e),ref:r,...n}));d.displayName=`Input`;function f({options:e,onResolve:t}){let n=(0,l.useRef)(null),[r,i]=(0,l.useState)(e.defaultValue??``),c=(0,l.useRef)(r);return(0,l.useEffect)(()=>{c.current=r},[r]),(0,l.useEffect)(()=>{n.current?.focus(),n.current?.select()},[]),(0,l.useEffect)(()=>{let e=e=>{e.key===`Escape`&&t(null),e.key===`Enter`&&c.current.trim()&&t(c.current.trim())};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[t]),(0,s.createPortal)((0,u.jsx)(`div`,{className:`fixed inset-0 z-50 flex items-center justify-center bg-black/50`,onClick:e=>{e.target===e.currentTarget&&t(null)},children:(0,u.jsxs)(`div`,{className:a(`w-full max-w-sm rounded-lg border border-border bg-background p-6 shadow-lg`,`mx-4`),children:[(0,u.jsx)(`h2`,{className:`text-base font-semibold text-foreground`,children:e.title}),e.description?(0,u.jsx)(`p`,{className:`mt-2 text-sm text-muted-foreground`,children:e.description}):null,(0,u.jsx)(`div`,{className:`mt-4`,children:(0,u.jsx)(d,{ref:n,value:r,onChange:e=>i(e.target.value),placeholder:e.placeholder,className:`w-full`})}),(0,u.jsxs)(`div`,{className:`mt-5 flex justify-end gap-2`,children:[(0,u.jsx)(o,{variant:`outline`,size:`sm`,onClick:()=>t(null),children:e.cancelLabel??`Cancel`}),(0,u.jsx)(o,{size:`sm`,onClick:()=>r.trim()?t(r.trim()):void 0,disabled:!r.trim(),children:e.confirmLabel??`Save`})]})]})}),document.body)}function p(e){return new Promise(t=>{let n=document.createElement(`div`);document.body.appendChild(n);let r=(0,c.createRoot)(n);function i(){r.unmount(),setTimeout(()=>n.remove(),0)}function a(e){i(),t(e)}r.render((0,u.jsx)(f,{options:e,onResolve:a}))})}export{p as showPrompt};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){return e.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,``)}function t(e,t,n){return{temperature:t?.temperature,maxTokens:t?.maxTokens??(e.maxTokens>0?Math.min(e.maxTokens,32e3):void 0),signal:t?.signal,apiKey:n||t?.apiKey,cacheRetention:t?.cacheRetention,sessionId:t?.sessionId,headers:t?.headers,onPayload:t?.onPayload,onResponse:t?.onResponse,timeoutMs:t?.timeoutMs,maxRetries:t?.maxRetries,maxRetryDelayMs:t?.maxRetryDelayMs,metadata:t?.metadata}}function n(e){return e===`xhigh`?`high`:e}function r(e,t,r,i){let a={minimal:1024,low:2048,medium:8192,high:16384,...i}[n(r)],o=Math.min(e+a,t);return o<=a&&(a=Math.max(0,o-1024)),{maxTokens:o,thinkingBudget:a}}var i=`(image omitted: model does not support images)`,a=`(tool image omitted: model does not support images)`;function o(e,t){let n=[],r=!1;for(let i of e){if(i.type===`image`){r||n.push({type:`text`,text:t}),r=!0;continue}n.push(i),r=i.text===t}return n}function s(e,t){return t.input.includes(`image`)?e:e.map(e=>e.role===`user`&&Array.isArray(e.content)?{...e,content:o(e.content,i)}:e.role===`toolResult`?{...e,content:o(e.content,a)}:e)}function c(e,t,n){let r=new Map,i=s(e,t).map(e=>{if(e.role===`user`)return e;if(e.role===`toolResult`){let t=r.get(e.toolCallId);return t&&t!==e.toolCallId?{...e,toolCallId:t}:e}if(e.role===`assistant`){let i=e,a=i.provider===t.provider&&i.api===t.api&&i.model===t.id,o=i.content.flatMap(e=>{if(e.type===`thinking`)return e.redacted?a?e:[]:a&&e.thinkingSignature?e:!e.thinking||e.thinking.trim()===``?[]:a?e:{type:`text`,text:e.thinking};if(e.type===`text`)return a?e:{type:`text`,text:e.text};if(e.type===`toolCall`){let o=e,s=o;if(!a&&o.thoughtSignature&&(s={...o},delete s.thoughtSignature),!a&&n){let e=n(o.id,t,i);e!==o.id&&(r.set(o.id,e),s={...s,id:e})}return s}return e});return{...i,content:o}}return e}),a=[],o=[],c=new Set,l=()=>{if(o.length>0){for(let e of o)c.has(e.id)||a.push({role:`toolResult`,toolCallId:e.id,toolName:e.name,content:[{type:`text`,text:`No result provided`}],isError:!0,timestamp:Date.now()});o=[],c=new Set}};for(let e=0;e<i.length;e++){let t=i[e];if(t.role===`assistant`){l();let e=t;if(e.stopReason===`error`||e.stopReason===`aborted`)continue;let n=e.content.filter(e=>e.type===`toolCall`);n.length>0&&(o=n,c=new Set),a.push(t)}else t.role===`toolResult`?(c.add(t.toolCallId),a.push(t)):(t.role===`user`&&l(),a.push(t))}return l(),a}export{e as a,n as i,r as n,t as r,c as t};
|
package/dist/favicon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
|
package/dist/index.html
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>速构 QuickForge</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CK_34smc.js"></script>
|
|
9
|
+
<link rel="modulepreload" crossorigin href="/assets/chunk-62oNxeRG.js">
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BQJ8qi1U.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shawnstack/quickforge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI chat application with YOLO-mode local workspace tools. React + Vite + Tailwind CSS frontend, local Node.js storage server.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"chat",
|
|
8
|
+
"llm",
|
|
9
|
+
"claude",
|
|
10
|
+
"openai",
|
|
11
|
+
"anthropic",
|
|
12
|
+
"litellm",
|
|
13
|
+
"react",
|
|
14
|
+
"vite",
|
|
15
|
+
"tailwindcss",
|
|
16
|
+
"yolo",
|
|
17
|
+
"workspace",
|
|
18
|
+
"tools"
|
|
19
|
+
],
|
|
20
|
+
"author": "shawn <1524587436@qq.com>",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/shawnstack/quickforge.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/shawnstack/quickforge/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/shawnstack/quickforge#readme",
|
|
30
|
+
"type": "module",
|
|
31
|
+
"bin": {
|
|
32
|
+
"quickforge": "bin/quickforge.mjs",
|
|
33
|
+
"qf": "bin/quickforge.mjs"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"bin",
|
|
37
|
+
"server",
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"package.json"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"dev": "node server/index.mjs --dev",
|
|
45
|
+
"dev:web": "vite --host 127.0.0.1 --port 5176 --strictPort",
|
|
46
|
+
"build": "tsc -b && vite build",
|
|
47
|
+
"lint": "eslint .",
|
|
48
|
+
"start": "node server/index.mjs",
|
|
49
|
+
"preview": "node server/index.mjs"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@mariozechner/mini-lit": "^0.2.1",
|
|
53
|
+
"@mariozechner/pi-agent-core": "^0.70.5",
|
|
54
|
+
"@mariozechner/pi-ai": "^0.70.5",
|
|
55
|
+
"@mariozechner/pi-web-ui": "^0.70.5",
|
|
56
|
+
"@tailwindcss/vite": "^4.2.4",
|
|
57
|
+
"class-variance-authority": "^0.7.1",
|
|
58
|
+
"clsx": "^2.1.1",
|
|
59
|
+
"lit": "^3.3.2",
|
|
60
|
+
"lucide-react": "^1.11.0",
|
|
61
|
+
"react": "^19.2.5",
|
|
62
|
+
"react-dom": "^19.2.5",
|
|
63
|
+
"tailwind-merge": "^3.5.0",
|
|
64
|
+
"tailwindcss": "^4.2.4"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@eslint/js": "^10.0.1",
|
|
68
|
+
"@types/node": "^24.12.2",
|
|
69
|
+
"@types/react": "^19.2.14",
|
|
70
|
+
"@types/react-dom": "^19.2.3",
|
|
71
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
72
|
+
"eslint": "^10.2.1",
|
|
73
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
74
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
75
|
+
"globals": "^17.5.0",
|
|
76
|
+
"typescript": "~6.0.2",
|
|
77
|
+
"typescript-eslint": "^8.58.2",
|
|
78
|
+
"vite": "^8.0.10"
|
|
79
|
+
}
|
|
80
|
+
}
|
package/server/index.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createServer } from 'node:http'
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { sendJson, sendError } from './utils/response.mjs'
|
|
7
|
+
import { openBrowser } from './utils/platform.mjs'
|
|
8
|
+
import { migrateLegacyDataDirs, ensureStorage, dataDir, storageDir } from './storage.mjs'
|
|
9
|
+
import { setDefaultWorkspaceRoot, initializeActiveProject, readProjectConfig, getActiveProject } from './project-config.mjs'
|
|
10
|
+
import { getWorkspaceRoot } from './utils/workspace.mjs'
|
|
11
|
+
import { handleStorageApi } from './routes/storage.mjs'
|
|
12
|
+
import { handleProjectApi } from './routes/project.mjs'
|
|
13
|
+
import { handleFilesystemApi } from './routes/filesystem.mjs'
|
|
14
|
+
import { handleToolApi } from './routes/tools.mjs'
|
|
15
|
+
import { handleInstructionsApi } from './routes/instructions.mjs'
|
|
16
|
+
import { serveStatic } from './routes/static.mjs'
|
|
17
|
+
import { setActiveWorkspaceRootForFilesystem } from './routes/filesystem.mjs'
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
20
|
+
const __dirname = path.dirname(__filename)
|
|
21
|
+
const projectRoot = path.resolve(__dirname, '..')
|
|
22
|
+
|
|
23
|
+
const isDev = process.argv.includes('--dev')
|
|
24
|
+
const host = process.env.QUICKFORGE_HOST || process.env.FASTCODE_HOST || '127.0.0.1'
|
|
25
|
+
const port = Number(process.env.QUICKFORGE_PORT || process.env.FASTCODE_PORT || (isDev ? 32176 : 5176))
|
|
26
|
+
const vitePort = Number(process.env.QUICKFORGE_VITE_PORT || process.env.FASTCODE_VITE_PORT || 5176)
|
|
27
|
+
|
|
28
|
+
setDefaultWorkspaceRoot(process.env.QUICKFORGE_WORKSPACE_DIR || process.env.FASTCODE_WORKSPACE_DIR || projectRoot)
|
|
29
|
+
|
|
30
|
+
// --- Route dispatching ---
|
|
31
|
+
async function handleApi(req, res, url) {
|
|
32
|
+
const pathname = url.pathname
|
|
33
|
+
const parts = pathname.split('/').filter(Boolean)
|
|
34
|
+
|
|
35
|
+
// Health check
|
|
36
|
+
if (req.method === 'GET' && pathname === '/api/health') {
|
|
37
|
+
const config = await readProjectConfig()
|
|
38
|
+
sendJson(res, 200, {
|
|
39
|
+
ok: true,
|
|
40
|
+
mode: isDev ? 'development' : 'production',
|
|
41
|
+
dataDir,
|
|
42
|
+
storageDir,
|
|
43
|
+
workspaceRoot: getWorkspaceRoot(),
|
|
44
|
+
project: getActiveProject(config),
|
|
45
|
+
})
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Instructions
|
|
50
|
+
if (req.method === 'GET' && pathname === '/api/instructions') {
|
|
51
|
+
await handleInstructionsApi(req, res, url)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Project routes
|
|
56
|
+
if (pathname === '/api/project' || pathname.startsWith('/api/project/')) {
|
|
57
|
+
await handleProjectApi(req, res, url)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Filesystem routes
|
|
62
|
+
if (pathname === '/api/filesystem' || pathname.startsWith('/api/filesystem/')) {
|
|
63
|
+
await handleFilesystemApi(req, res, url)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Tool routes
|
|
68
|
+
if (pathname.startsWith('/api/tools/') || (parts[0] === 'api' && parts[1] === 'projects' && parts[3] === 'tools')) {
|
|
69
|
+
await handleToolApi(req, res, url)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Storage routes (catch-all)
|
|
74
|
+
if (parts[0] === 'api' && parts[1] === 'storage') {
|
|
75
|
+
await handleStorageApi(req, res, url)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const error = new Error('Not found')
|
|
80
|
+
error.statusCode = 404
|
|
81
|
+
throw error
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- Vite dev server ---
|
|
85
|
+
function startVite() {
|
|
86
|
+
const viteCli = path.join(projectRoot, 'node_modules', 'vite', 'bin', 'vite.js')
|
|
87
|
+
const child = spawn(process.execPath, [viteCli, '--host', '127.0.0.1', '--port', String(vitePort), '--strictPort'], {
|
|
88
|
+
cwd: projectRoot,
|
|
89
|
+
stdio: 'inherit',
|
|
90
|
+
shell: false,
|
|
91
|
+
env: { ...process.env, QUICKFORGE_SERVER_PORT: String(port) },
|
|
92
|
+
})
|
|
93
|
+
child.on('exit', (code) => {
|
|
94
|
+
if (code && code !== 0) process.exitCode = code
|
|
95
|
+
})
|
|
96
|
+
process.on('exit', () => child.kill())
|
|
97
|
+
process.on('SIGINT', () => {
|
|
98
|
+
child.kill('SIGINT')
|
|
99
|
+
process.exit(0)
|
|
100
|
+
})
|
|
101
|
+
process.on('SIGTERM', () => {
|
|
102
|
+
child.kill('SIGTERM')
|
|
103
|
+
process.exit(0)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Bootstrap ---
|
|
108
|
+
const server = createServer(async (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || `${host}:${port}`}`)
|
|
111
|
+
if (url.pathname.startsWith('/api/')) {
|
|
112
|
+
await handleApi(req, res, url)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (isDev) {
|
|
117
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })
|
|
118
|
+
res.end('QuickForge local API server is running. Open the Vite app at http://127.0.0.1:5176')
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await serveStatic(req, res, url)
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(error)
|
|
125
|
+
sendError(res, error)
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
await migrateLegacyDataDirs()
|
|
130
|
+
await ensureStorage()
|
|
131
|
+
await initializeActiveProject()
|
|
132
|
+
setActiveWorkspaceRootForFilesystem(getWorkspaceRoot())
|
|
133
|
+
|
|
134
|
+
server.listen(port, host, () => {
|
|
135
|
+
console.log(`QuickForge local API: http://${host}:${port}`)
|
|
136
|
+
console.log(`QuickForge data dir: ${dataDir}`)
|
|
137
|
+
console.log(`QuickForge project: ${getWorkspaceRoot()}`)
|
|
138
|
+
|
|
139
|
+
if (isDev) {
|
|
140
|
+
startVite()
|
|
141
|
+
setTimeout(() => openBrowser(`http://localhost:${vitePort}`), 1000)
|
|
142
|
+
} else {
|
|
143
|
+
openBrowser(`http://localhost:${port}`)
|
|
144
|
+
}
|
|
145
|
+
})
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { ensureStorage, projectConfigFile, dataDir } from './storage.mjs'
|
|
4
|
+
import { promises as fs } from 'node:fs'
|
|
5
|
+
import { setWorkspaceRoot, getWorkspaceRoot, assertDirectory } from './utils/workspace.mjs'
|
|
6
|
+
|
|
7
|
+
let defaultWorkspaceRoot = ''
|
|
8
|
+
|
|
9
|
+
export function setDefaultWorkspaceRoot(root) {
|
|
10
|
+
defaultWorkspaceRoot = path.resolve(root)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function projectNameFromPath(dir) {
|
|
14
|
+
return path.basename(dir) || dir
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function defaultProjectConfig() {
|
|
18
|
+
const now = new Date().toISOString()
|
|
19
|
+
const id = 'default'
|
|
20
|
+
return {
|
|
21
|
+
activeProjectId: id,
|
|
22
|
+
projects: [
|
|
23
|
+
{
|
|
24
|
+
id,
|
|
25
|
+
name: projectNameFromPath(defaultWorkspaceRoot),
|
|
26
|
+
path: defaultWorkspaceRoot,
|
|
27
|
+
lastOpenedAt: now,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function readProjectConfig() {
|
|
34
|
+
await ensureStorage()
|
|
35
|
+
const file = projectConfigFile()
|
|
36
|
+
try {
|
|
37
|
+
const text = await fs.readFile(file, 'utf8')
|
|
38
|
+
const parsed = text.trim() ? JSON.parse(text) : defaultProjectConfig()
|
|
39
|
+
if (!Array.isArray(parsed.projects) || parsed.projects.length === 0) return defaultProjectConfig()
|
|
40
|
+
return parsed
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error?.code === 'ENOENT') return defaultProjectConfig()
|
|
43
|
+
throw error
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function writeProjectConfig(config) {
|
|
48
|
+
await ensureStorage()
|
|
49
|
+
const file = projectConfigFile()
|
|
50
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`
|
|
51
|
+
await fs.writeFile(tmp, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
52
|
+
await fs.rename(tmp, file)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getActiveProject(config) {
|
|
56
|
+
return config.projects.find((project) => project.id === config.activeProjectId) || config.projects[0]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function setActiveProjectPath(inputPath) {
|
|
60
|
+
const resolved = path.resolve(String(inputPath || ''))
|
|
61
|
+
await assertDirectory(resolved)
|
|
62
|
+
|
|
63
|
+
const config = await readProjectConfig()
|
|
64
|
+
const now = new Date().toISOString()
|
|
65
|
+
let project = config.projects.find((item) => path.resolve(item.path) === resolved)
|
|
66
|
+
if (!project) {
|
|
67
|
+
project = {
|
|
68
|
+
id: randomUUID(),
|
|
69
|
+
name: projectNameFromPath(resolved),
|
|
70
|
+
path: resolved,
|
|
71
|
+
lastOpenedAt: now,
|
|
72
|
+
}
|
|
73
|
+
config.projects.unshift(project)
|
|
74
|
+
} else {
|
|
75
|
+
project.name = projectNameFromPath(resolved)
|
|
76
|
+
project.path = resolved
|
|
77
|
+
project.lastOpenedAt = now
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
config.activeProjectId = project.id
|
|
81
|
+
config.projects = [project, ...config.projects.filter((item) => item.id !== project.id)].slice(0, 20)
|
|
82
|
+
await writeProjectConfig(config)
|
|
83
|
+
setWorkspaceRoot(resolved)
|
|
84
|
+
return { project, projects: config.projects }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function initializeActiveProject() {
|
|
88
|
+
const config = await readProjectConfig()
|
|
89
|
+
const activeProject = getActiveProject(config)
|
|
90
|
+
if (activeProject?.path) {
|
|
91
|
+
try {
|
|
92
|
+
await assertDirectory(activeProject.path)
|
|
93
|
+
setWorkspaceRoot(path.resolve(activeProject.path))
|
|
94
|
+
return
|
|
95
|
+
} catch {
|
|
96
|
+
// Fall back to the app project if the stored project was removed.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const fallback = await setActiveProjectPath(defaultWorkspaceRoot)
|
|
101
|
+
setWorkspaceRoot(path.resolve(fallback.project.path))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function projectContextFromId(projectId) {
|
|
105
|
+
const config = await readProjectConfig()
|
|
106
|
+
const project = config.projects.find((item) => item.id === projectId)
|
|
107
|
+
if (!project) {
|
|
108
|
+
const error = new Error('Unknown project')
|
|
109
|
+
error.statusCode = 404
|
|
110
|
+
throw error
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await assertDirectory(project.path)
|
|
114
|
+
return { project, workspaceRoot: path.resolve(project.path) }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function readInstructionsFile(filePath) {
|
|
118
|
+
try {
|
|
119
|
+
const content = await fs.readFile(filePath, 'utf8')
|
|
120
|
+
const trimmed = content.trim()
|
|
121
|
+
return trimmed || null
|
|
122
|
+
} catch {
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { promises as fs } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { sendJson } from '../utils/response.mjs'
|
|
6
|
+
import { pathExists, assertDirectory } from '../utils/workspace.mjs'
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
9
|
+
const projectRoot = path.resolve(path.dirname(__filename), '../..')
|
|
10
|
+
let _activeWorkspaceRoot = projectRoot
|
|
11
|
+
|
|
12
|
+
export function setActiveWorkspaceRootForFilesystem(root) {
|
|
13
|
+
_activeWorkspaceRoot = path.resolve(root)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function getFilesystemRoots() {
|
|
17
|
+
const roots = []
|
|
18
|
+
const addRoot = async (name, rootPath) => {
|
|
19
|
+
if (!rootPath) return
|
|
20
|
+
const resolved = path.resolve(rootPath)
|
|
21
|
+
if (!(await pathExists(resolved))) return
|
|
22
|
+
if (roots.some((entry) => path.resolve(entry.path) === resolved)) return
|
|
23
|
+
roots.push({ name, path: resolved })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const home = os.homedir()
|
|
27
|
+
await addRoot('Home', home)
|
|
28
|
+
await addRoot('Desktop', path.join(home, 'Desktop'))
|
|
29
|
+
await addRoot('Documents', path.join(home, 'Documents'))
|
|
30
|
+
await addRoot('QuickForge', projectRoot)
|
|
31
|
+
await addRoot('Current project', _activeWorkspaceRoot)
|
|
32
|
+
|
|
33
|
+
if (process.platform === 'win32') {
|
|
34
|
+
for (let code = 65; code <= 90; code += 1) {
|
|
35
|
+
const drive = `${String.fromCharCode(code)}:\\`
|
|
36
|
+
await addRoot(drive, drive)
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
await addRoot('Filesystem', '/')
|
|
40
|
+
if (process.platform === 'darwin' && (await pathExists('/Volumes'))) {
|
|
41
|
+
const volumes = await fs.readdir('/Volumes', { withFileTypes: true }).catch(() => [])
|
|
42
|
+
for (const volume of volumes) {
|
|
43
|
+
if (volume.isDirectory() || volume.isSymbolicLink()) {
|
|
44
|
+
await addRoot(volume.name, path.join('/Volumes', volume.name))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return roots
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function listFilesystemDirectories(inputPath) {
|
|
54
|
+
const requestedPath = String(inputPath || os.homedir())
|
|
55
|
+
const resolved = path.resolve(requestedPath)
|
|
56
|
+
await assertDirectory(resolved)
|
|
57
|
+
|
|
58
|
+
const entries = await fs.readdir(resolved, { withFileTypes: true }).catch((error) => {
|
|
59
|
+
error.statusCode = error?.code === 'EACCES' || error?.code === 'EPERM' ? 403 : 400
|
|
60
|
+
throw error
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const directories = entries
|
|
64
|
+
.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
|
|
65
|
+
.map((entry) => ({ name: entry.name, path: path.join(resolved, entry.name) }))
|
|
66
|
+
.sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }))
|
|
67
|
+
|
|
68
|
+
const parsed = path.parse(resolved)
|
|
69
|
+
const parent = resolved === parsed.root ? null : path.dirname(resolved)
|
|
70
|
+
return { path: resolved, parent, directories }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function handleFilesystemApi(req, res, url) {
|
|
74
|
+
if (req.method === 'GET' && url.pathname === '/api/filesystem/roots') {
|
|
75
|
+
sendJson(res, 200, { roots: await getFilesystemRoots() })
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (req.method === 'GET' && url.pathname === '/api/filesystem/directories') {
|
|
80
|
+
sendJson(res, 200, await listFilesystemDirectories(url.searchParams.get('path')))
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const error = new Error('Not found')
|
|
85
|
+
error.statusCode = 404
|
|
86
|
+
throw error
|
|
87
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { sendJson } from '../utils/response.mjs'
|
|
3
|
+
import { readInstructionsFile, projectContextFromId } from '../project-config.mjs'
|
|
4
|
+
import { dataDir } from '../storage.mjs'
|
|
5
|
+
|
|
6
|
+
export async function handleInstructionsApi(req, res, url) {
|
|
7
|
+
if (req.method !== 'GET') {
|
|
8
|
+
const error = new Error('Method not allowed')
|
|
9
|
+
error.statusCode = 405
|
|
10
|
+
throw error
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const projectId = url.searchParams.get('projectId')
|
|
14
|
+
let projectInstructions = null
|
|
15
|
+
|
|
16
|
+
if (projectId) {
|
|
17
|
+
try {
|
|
18
|
+
const { workspaceRoot } = await projectContextFromId(projectId)
|
|
19
|
+
projectInstructions = await readInstructionsFile(path.join(workspaceRoot, 'AGENTS.md'))
|
|
20
|
+
} catch {
|
|
21
|
+
// project not found or inaccessible — leave projectInstructions null
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const globalInstructions = await readInstructionsFile(path.join(dataDir, 'AGENTS.md'))
|
|
26
|
+
|
|
27
|
+
sendJson(res, 200, {
|
|
28
|
+
global: globalInstructions,
|
|
29
|
+
project: projectInstructions,
|
|
30
|
+
})
|
|
31
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
|
|
2
|
+
import { getActiveProject, setActiveProjectPath, readProjectConfig, writeProjectConfig } from '../project-config.mjs'
|
|
3
|
+
import { getWorkspaceRoot, setWorkspaceRoot } from '../utils/workspace.mjs'
|
|
4
|
+
import { selectDirectoryDialog } from '../utils/platform.mjs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
|
|
7
|
+
export async function handleProjectApi(req, res, url) {
|
|
8
|
+
const config = await readProjectConfig()
|
|
9
|
+
|
|
10
|
+
if (req.method === 'GET' && url.pathname === '/api/project') {
|
|
11
|
+
sendJson(res, 200, { project: getActiveProject(config), projects: config.projects, workspaceRoot: getWorkspaceRoot() })
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (req.method === 'POST' && url.pathname === '/api/project/select-directory') {
|
|
16
|
+
console.log('[project] Opening directory picker dialog...')
|
|
17
|
+
const selectedPath = await selectDirectoryDialog()
|
|
18
|
+
console.log('[project] Directory picker result:', selectedPath ? `"${selectedPath}"` : '(cancelled/empty)')
|
|
19
|
+
if (!selectedPath) {
|
|
20
|
+
sendJson(res, 200, { cancelled: true, project: getActiveProject(config), projects: config.projects })
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
const result = await setActiveProjectPath(selectedPath)
|
|
24
|
+
sendJson(res, 200, { cancelled: false, ...result, workspaceRoot: getWorkspaceRoot() })
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (req.method === 'POST' && url.pathname === '/api/project/path') {
|
|
29
|
+
const body = await readJsonBody(req)
|
|
30
|
+
const result = await setActiveProjectPath(body?.path)
|
|
31
|
+
sendJson(res, 200, { ...result, workspaceRoot: getWorkspaceRoot() })
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (req.method === 'POST' && url.pathname === '/api/project/active') {
|
|
36
|
+
const body = await readJsonBody(req)
|
|
37
|
+
const selected = config.projects.find((project) => project.id === body?.id)
|
|
38
|
+
if (!selected) {
|
|
39
|
+
const error = new Error('Unknown project')
|
|
40
|
+
error.statusCode = 404
|
|
41
|
+
throw error
|
|
42
|
+
}
|
|
43
|
+
const result = await setActiveProjectPath(selected.path)
|
|
44
|
+
sendJson(res, 200, { ...result, workspaceRoot: getWorkspaceRoot() })
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (req.method === 'DELETE' && url.pathname.startsWith('/api/project/')) {
|
|
49
|
+
const id = decodeSegment(url.pathname.split('/').filter(Boolean)[2])
|
|
50
|
+
const nextProjects = config.projects.filter((project) => project.id !== id)
|
|
51
|
+
if (nextProjects.length === config.projects.length) {
|
|
52
|
+
const error = new Error('Unknown project')
|
|
53
|
+
error.statusCode = 404
|
|
54
|
+
throw error
|
|
55
|
+
}
|
|
56
|
+
config.projects = nextProjects.length ? nextProjects : defaultProjectConfigFallback().projects
|
|
57
|
+
if (config.activeProjectId === id) config.activeProjectId = config.projects[0].id
|
|
58
|
+
await writeProjectConfig(config)
|
|
59
|
+
const active = getActiveProject(config)
|
|
60
|
+
setWorkspaceRoot(path.resolve(active.path))
|
|
61
|
+
sendJson(res, 200, { project: active, projects: config.projects, workspaceRoot: getWorkspaceRoot() })
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const error = new Error('Not found')
|
|
66
|
+
error.statusCode = 404
|
|
67
|
+
throw error
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function defaultProjectConfigFallback() {
|
|
71
|
+
const fallbackPath = getWorkspaceRoot()
|
|
72
|
+
return {
|
|
73
|
+
activeProjectId: 'default',
|
|
74
|
+
projects: [{ id: 'default', name: path.basename(fallbackPath) || 'QuickForge', path: fallbackPath, lastOpenedAt: new Date().toISOString() }],
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { promises as fs } from 'node:fs'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
6
|
+
const projectRoot = path.resolve(path.dirname(__filename), '../..')
|
|
7
|
+
|
|
8
|
+
function getContentType(filePath) {
|
|
9
|
+
const extension = path.extname(filePath).toLowerCase()
|
|
10
|
+
return {
|
|
11
|
+
'.html': 'text/html; charset=utf-8',
|
|
12
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
13
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
14
|
+
'.css': 'text/css; charset=utf-8',
|
|
15
|
+
'.json': 'application/json; charset=utf-8',
|
|
16
|
+
'.svg': 'image/svg+xml',
|
|
17
|
+
'.png': 'image/png',
|
|
18
|
+
'.jpg': 'image/jpeg',
|
|
19
|
+
'.jpeg': 'image/jpeg',
|
|
20
|
+
'.webp': 'image/webp',
|
|
21
|
+
'.woff': 'font/woff',
|
|
22
|
+
'.woff2': 'font/woff2',
|
|
23
|
+
'.ttf': 'font/ttf',
|
|
24
|
+
}[extension] || 'application/octet-stream'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function serveStatic(req, res, url) {
|
|
28
|
+
const distDir = path.join(projectRoot, 'dist')
|
|
29
|
+
const requested = decodeURIComponent(url.pathname === '/' ? '/index.html' : url.pathname)
|
|
30
|
+
const normalized = path.normalize(requested).replace(/^([.][.][\/])+/, '')
|
|
31
|
+
let filePath = path.join(distDir, normalized)
|
|
32
|
+
|
|
33
|
+
if (!filePath.startsWith(distDir)) {
|
|
34
|
+
res.writeHead(403)
|
|
35
|
+
res.end('Forbidden')
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const stat = await fs.stat(filePath)
|
|
41
|
+
if (stat.isDirectory()) filePath = path.join(filePath, 'index.html')
|
|
42
|
+
} catch {
|
|
43
|
+
filePath = path.join(distDir, 'index.html')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const data = await fs.readFile(filePath)
|
|
48
|
+
res.writeHead(200, {
|
|
49
|
+
'content-type': getContentType(filePath),
|
|
50
|
+
'cache-control': filePath.endsWith('index.html') ? 'no-cache' : 'public, max-age=31536000, immutable',
|
|
51
|
+
})
|
|
52
|
+
res.end(data)
|
|
53
|
+
} catch {
|
|
54
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })
|
|
55
|
+
res.end('Build output not found. Run npm run build first.')
|
|
56
|
+
}
|
|
57
|
+
}
|