@rexvane/piweb 0.3.14 → 0.3.16
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +12 -9
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +6 -6
- package/.next/react-loadable-manifest.json +16 -4
- package/.next/required-server-files.js +3 -1
- package/.next/required-server-files.json +3 -1
- package/.next/routes-manifest.json +20 -0
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +7 -4
- package/.next/server/app/_not-found.segments/_full.segment.rsc +7 -4
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -2
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +5 -2
- package/.next/server/app/api/agent/[id]/events/route.js +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/server/app/api/git/route.js +2 -2
- package/.next/server/app/api/git/route.js.nft.json +1 -1
- package/.next/server/app/api/growth/route.js +1 -1
- package/.next/server/app/api/growth/route.js.nft.json +1 -1
- package/.next/server/app/api/health/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/models/route.js.nft.json +1 -1
- package/.next/server/app/api/pi-settings/route.js +1 -1
- package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/plugins/route.js.nft.json +1 -1
- package/.next/server/app/api/prompts/route.js +1 -1
- package/.next/server/app/api/prompts/route.js.nft.json +1 -1
- package/.next/server/app/api/security/route.js +1 -1
- package/.next/server/app/api/security/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/import/route.js +3 -3
- package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +2 -2
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/swarm/[id]/route.js +1 -0
- package/.next/server/app/api/swarm/[id]/route.js.nft.json +1 -0
- package/.next/server/app/api/swarm/[id]/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/swarm/route.js +1 -0
- package/.next/server/app/api/swarm/route.js.nft.json +1 -0
- package/.next/server/app/api/swarm/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/tests/route.js +2 -0
- package/.next/server/app/api/tests/route.js.nft.json +1 -0
- package/.next/server/app/api/tests/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/update/route.js +2 -2
- package/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/api/version/route.js.nft.json +1 -1
- package/.next/server/app/api/web-auth/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +9 -6
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +7 -4
- package/.next/server/app/index.segments/_full.segment.rsc +9 -6
- package/.next/server/app/index.segments/_tree.segment.rsc +5 -2
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +8 -5
- package/.next/server/app/login.segments/_full.segment.rsc +8 -5
- package/.next/server/app/login.segments/_tree.segment.rsc +5 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +6 -3
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +12 -9
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/532.js +1 -1
- package/.next/server/chunks/686.js +6 -2
- package/.next/server/chunks/757.js +1 -0
- package/.next/server/chunks/775.js +3 -0
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/0t1jBUAuVaTFTXxRyIo3q/_buildManifest.js +1 -0
- package/.next/static/chunks/2608.43c58236c9cce8e6.js +1 -0
- package/.next/static/chunks/{2716.a9e808190a4e2424.js → 2716.4ec42df1d51354c9.js} +1 -1
- package/.next/static/chunks/4632.62139b565b657aac.js +1 -0
- package/.next/static/chunks/{4673.d3d0a7f2599080fb.js → 4673.7796104395deb8d7.js} +1 -1
- package/.next/static/chunks/4686-17f21a006739a596.js +1 -0
- package/.next/static/chunks/7577.10af9bb661652da8.js +1 -0
- package/.next/static/chunks/8866.38ec4ac4ee5a1b74.js +1 -0
- package/.next/static/chunks/app/_global-error/page-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/config/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/files/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/git/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/growth/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/health/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/models/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/security/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/skills/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/swarm/[id]/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/swarm/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/tests/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/update/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/version/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/page-14379b8060d5cf07.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/webpack-d1a69ad9edb38b11.js +1 -0
- package/.next/static/css/d0a0162cb481abe8.css +1 -0
- package/.next/types/app/api/swarm/[id]/route.ts +353 -0
- package/.next/types/app/api/swarm/route.ts +353 -0
- package/.next/types/app/api/tests/route.ts +353 -0
- package/.next/types/routes.d.ts +4 -1
- package/.next/types/validator.ts +27 -0
- package/README.md +61 -7
- package/assets/archive.png +0 -0
- package/assets/growth-1.png +0 -0
- package/assets/growth-2.png +0 -0
- package/assets/import.png +0 -0
- package/assets/main.png +0 -0
- package/assets/outline.png +0 -0
- package/assets/prompts-1.png +0 -0
- package/assets/prompts-2.png +0 -0
- package/assets/providers-1.png +0 -0
- package/assets/providers-2.png +0 -0
- package/assets/providers-3.png +0 -0
- package/assets/trace-1.png +0 -0
- package/assets/trace-2.png +0 -0
- package/assets/trace-3.png +0 -0
- package/assets/trace-4.png +0 -0
- package/assets/viewer-1.png +0 -0
- package/assets/viewer-2.png +0 -0
- package/next.config.ts +6 -2
- package/package.json +10 -5
- package/scripts/launcher.mjs +1 -1
- package/scripts/managed-niubash-assets.mjs +146 -0
- package/scripts/pick-folder.ps1 +11 -2
- package/scripts/prepare-niubash-packages.mjs +182 -0
- package/scripts/release.mjs +11 -31
- package/scripts/run-test-file.mjs +34 -0
- package/scripts/smoke-managed-niubash.mjs +62 -0
- package/scripts/verify-package.mjs +53 -0
- package/scripts/verify-release-config.mjs +24 -0
- package/src/app/api/config/route.ts +4 -1
- package/src/app/api/security/route.ts +8 -3
- package/src/app/api/swarm/[id]/route.ts +28 -0
- package/src/app/api/swarm/route.ts +24 -0
- package/src/app/api/tests/route.ts +27 -0
- package/src/app/api/update/route.ts +8 -2
- package/src/app/globals.css +777 -0
- package/src/app/layout.tsx +13 -6
- package/src/components/AppShell.tsx +505 -199
- package/src/components/ChatInput.tsx +153 -26
- package/src/components/ChatWindow.tsx +46 -40
- package/src/components/ContextMeter.tsx +11 -17
- package/src/components/FileViewer.tsx +1 -1
- package/src/components/GitPanel.tsx +13 -11
- package/src/components/ModelSelector.tsx +19 -6
- package/src/components/PiMark.tsx +5 -38
- package/src/components/ProjectPanel.tsx +1 -0
- package/src/components/PromptPanel.tsx +132 -51
- package/src/components/SessionImportSection.tsx +53 -37
- package/src/components/SessionSidebar.tsx +159 -57
- package/src/components/SettingsPanel.tsx +330 -193
- package/src/components/SwarmCoordinatorPod.tsx +1021 -0
- package/src/components/TestInspectorPod.tsx +549 -0
- package/src/components/icons.tsx +23 -0
- package/src/hooks/usePiWeb.ts +112 -8
- package/src/i18n.tsx +59 -3
- package/src/lib/agent-manager.ts +182 -11
- package/src/lib/command-validation.ts +9 -1
- package/src/lib/delegation.ts +93 -0
- package/src/lib/managed-niubash.ts +231 -0
- package/src/lib/pi.ts +3 -2
- package/src/lib/runtime-activity.ts +27 -0
- package/src/lib/session-import/claude.ts +13 -2
- package/src/lib/session-import/codex.ts +44 -8
- package/src/lib/session-import/files.ts +14 -0
- package/src/lib/session-import/grok.ts +54 -24
- package/src/lib/session-import/index.ts +5 -0
- package/src/lib/session-import/lock.ts +25 -0
- package/src/lib/session-import/types.ts +12 -0
- package/src/lib/session-import/writer.ts +8 -3
- package/src/lib/skills-service.ts +16 -9
- package/src/lib/swarm-service.ts +371 -0
- package/src/lib/test-run-service.ts +122 -0
- package/src/lib/theme.ts +68 -1
- package/src/lib/trajectory.ts +1 -0
- package/src/lib/types.ts +14 -0
- package/src/lib/workflow.ts +55 -0
- package/.next/static/LOFl9bqqMV2goBV8erESO/_buildManifest.js +0 -1
- package/.next/static/chunks/2608.ec8ff1f5767c9056.js +0 -1
- package/.next/static/chunks/4632.112d2e893d48d188.js +0 -1
- package/.next/static/chunks/4686-7d694def920ff093.js +0 -1
- package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/page-9a5440047c5a415c.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/webpack-d28e132e1b8c100c.js +0 -1
- package/.next/static/css/b2d35abc828d4713.css +0 -1
- package/assets/showcase.png +0 -0
- package/bin/pi.js +0 -50
- /package/.next/static/{LOFl9bqqMV2goBV8erESO → 0t1jBUAuVaTFTXxRyIo3q}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const NIUBASH_VERSION = "1.1.4";
|
|
6
|
+
export const NIUBASH_TAG = `v${NIUBASH_VERSION}`;
|
|
7
|
+
export const NIUBASH_DISTRIBUTION_VERSION = `${NIUBASH_VERSION}-piweb.0`;
|
|
8
|
+
export const NIUBASH_REPOSITORY = "https://github.com/unixwin/niubash";
|
|
9
|
+
|
|
10
|
+
export const MANAGED_NIUBASH_ASSETS = Object.freeze({
|
|
11
|
+
x64: Object.freeze({
|
|
12
|
+
arch: "x64",
|
|
13
|
+
machine: 0x8664,
|
|
14
|
+
packageName: "@rexvane/piweb-niubash-win32-x64",
|
|
15
|
+
directory: "piweb-niubash-win32-x64",
|
|
16
|
+
asset: "niubash-v1.1.4-win-x64.zip",
|
|
17
|
+
sha256: "9f543634288a316f7ed8e5a76ff794eca402cb21e74b8f8a551daffafde162f5",
|
|
18
|
+
}),
|
|
19
|
+
arm64: Object.freeze({
|
|
20
|
+
arch: "arm64",
|
|
21
|
+
machine: 0xaa64,
|
|
22
|
+
packageName: "@rexvane/piweb-niubash-win32-arm64",
|
|
23
|
+
directory: "piweb-niubash-win32-arm64",
|
|
24
|
+
asset: "niubash-v1.1.4-win-arm64.zip",
|
|
25
|
+
sha256: "77e98ba8d7f6be70c35388bcc28bdbc883c06bcaafffbe186a88788ad32ccf00",
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const NIUBASH_LICENSES = Object.freeze([
|
|
30
|
+
Object.freeze({
|
|
31
|
+
filename: "NIUBASH.txt",
|
|
32
|
+
url: "https://raw.githubusercontent.com/unixwin/niubash/v1.1.4/LICENSE",
|
|
33
|
+
sha256: "1290aae99e71239ee3154e7847830e49e7a36c758fa6d659d73fa752dc9f81c8",
|
|
34
|
+
}),
|
|
35
|
+
Object.freeze({
|
|
36
|
+
filename: "RUBASH.txt",
|
|
37
|
+
url: "https://raw.githubusercontent.com/unixwin/rubash/8b81c7501646/LICENSE",
|
|
38
|
+
sha256: "e426a75a20633f3088da22aef61a5a44d06f1f884c600f6011d00702c3989079",
|
|
39
|
+
}),
|
|
40
|
+
Object.freeze({
|
|
41
|
+
filename: "WINUXCMD.txt",
|
|
42
|
+
url: "https://raw.githubusercontent.com/unixwin/WinuxCmd/v1.0.8/LICENSE",
|
|
43
|
+
sha256: "8ffc4ed56f5e6ab8183a7ece2e3aa6ea4800b0a4e8170d11bd60856f2dd760f3",
|
|
44
|
+
}),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export function niubashAssetUrl(asset) {
|
|
48
|
+
return `${NIUBASH_REPOSITORY}/releases/download/${NIUBASH_TAG}/${asset.asset}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sha256(data) {
|
|
52
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function sha256File(filename) {
|
|
56
|
+
return sha256(fs.readFileSync(filename));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function listFiles(root) {
|
|
60
|
+
const files = [];
|
|
61
|
+
const visit = (directory, prefix = "") => {
|
|
62
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
63
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
64
|
+
const absolute = path.join(directory, entry.name);
|
|
65
|
+
if (entry.isDirectory()) visit(absolute, relative);
|
|
66
|
+
else if (entry.isFile()) files.push(relative);
|
|
67
|
+
else throw new Error(`managed niubash runtime contains an unsupported entry: ${relative}`);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
visit(root);
|
|
71
|
+
return files;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function readPeMachine(filename) {
|
|
75
|
+
const data = fs.readFileSync(filename);
|
|
76
|
+
if (data.length < 64 || data[0] !== 0x4d || data[1] !== 0x5a) throw new Error(`${filename} is not a PE executable`);
|
|
77
|
+
const peOffset = data.readUInt32LE(0x3c);
|
|
78
|
+
if (peOffset + 6 > data.length || data.toString("ascii", peOffset, peOffset + 4) !== "PE\0\0") {
|
|
79
|
+
throw new Error(`${filename} has an invalid PE header`);
|
|
80
|
+
}
|
|
81
|
+
return data.readUInt16LE(peOffset + 4);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readJson(filename) {
|
|
85
|
+
return JSON.parse(fs.readFileSync(filename, "utf8"));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function assertEqual(actual, expected, label) {
|
|
89
|
+
if (actual !== expected) throw new Error(`${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Validate a prepared source package or an unpacked npm tarball. */
|
|
93
|
+
export function validateManagedNiubashPackage(packageRoot, asset, { allowExtraRuntimeFiles = true } = {}) {
|
|
94
|
+
const packageInfo = readJson(path.join(packageRoot, "package.json"));
|
|
95
|
+
assertEqual(packageInfo.name, asset.packageName, "managed niubash package name");
|
|
96
|
+
assertEqual(packageInfo.version, NIUBASH_DISTRIBUTION_VERSION, "managed niubash package version");
|
|
97
|
+
if (!Array.isArray(packageInfo.os) || packageInfo.os.length !== 1 || packageInfo.os[0] !== "win32") {
|
|
98
|
+
throw new Error(`${asset.packageName} must be restricted to os=win32`);
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(packageInfo.cpu) || packageInfo.cpu.length !== 1 || packageInfo.cpu[0] !== asset.arch) {
|
|
101
|
+
throw new Error(`${asset.packageName} must be restricted to cpu=${asset.arch}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const runtime = path.join(packageRoot, "runtime");
|
|
105
|
+
if (!fs.existsSync(path.join(runtime, "niu.exe"))) {
|
|
106
|
+
throw new Error(`${asset.packageName} has no prepared runtime; run \`node scripts/prepare-niubash-packages.mjs\` before packing/publishing`);
|
|
107
|
+
}
|
|
108
|
+
const manifest = readJson(path.join(packageRoot, "runtime-manifest.json"));
|
|
109
|
+
assertEqual(manifest.schema, 1, "managed niubash manifest schema");
|
|
110
|
+
assertEqual(manifest.source?.repository, NIUBASH_REPOSITORY, "managed niubash source repository");
|
|
111
|
+
assertEqual(manifest.source?.tag, NIUBASH_TAG, "managed niubash source tag");
|
|
112
|
+
assertEqual(manifest.source?.asset, asset.asset, "managed niubash source asset");
|
|
113
|
+
assertEqual(manifest.source?.sha256, asset.sha256, "managed niubash source SHA-256");
|
|
114
|
+
if (!manifest.files || typeof manifest.files !== "object" || Array.isArray(manifest.files)) {
|
|
115
|
+
throw new Error(`${asset.packageName} runtime manifest has no file map`);
|
|
116
|
+
}
|
|
117
|
+
const expectedFiles = Object.keys(manifest.files);
|
|
118
|
+
if (expectedFiles.length < 300) throw new Error(`${asset.packageName} runtime is incomplete (${expectedFiles.length} files)`);
|
|
119
|
+
for (const relative of expectedFiles) {
|
|
120
|
+
if (!relative || relative.includes("\\") || path.posix.isAbsolute(relative) || relative.split("/").includes("..")) {
|
|
121
|
+
throw new Error(`${asset.packageName} runtime manifest contains an unsafe path: ${relative}`);
|
|
122
|
+
}
|
|
123
|
+
const filename = path.join(runtime, ...relative.split("/"));
|
|
124
|
+
const stat = fs.statSync(filename);
|
|
125
|
+
if (!stat.isFile()) throw new Error(`${asset.packageName} runtime entry is not a file: ${relative}`);
|
|
126
|
+
assertEqual(sha256File(filename), manifest.files[relative], `${asset.packageName} hash for ${relative}`);
|
|
127
|
+
}
|
|
128
|
+
const actualFiles = listFiles(runtime);
|
|
129
|
+
const extra = actualFiles.filter((file) => !(file in manifest.files));
|
|
130
|
+
if (!allowExtraRuntimeFiles && extra.length > 0) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`${asset.packageName} runtime has ${extra.length} files not in the signed manifest (niubash self-activates command links on first run). Re-run \`node scripts/prepare-niubash-packages.mjs\` before packing. First extra: ${extra[0]}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const required of ["niu.exe", "winuxcmd/usr/bin/winuxcmd.exe", "bundles/oh-my-niu/bundle.toml"]) {
|
|
137
|
+
if (!expectedFiles.includes(required)) throw new Error(`${asset.packageName} is missing ${required}`);
|
|
138
|
+
}
|
|
139
|
+
for (const executable of ["niu.exe", "winuxcmd/usr/bin/winuxcmd.exe"]) {
|
|
140
|
+
assertEqual(readPeMachine(path.join(runtime, ...executable.split("/"))), asset.machine, `${asset.packageName} PE machine for ${executable}`);
|
|
141
|
+
}
|
|
142
|
+
for (const license of NIUBASH_LICENSES) {
|
|
143
|
+
assertEqual(sha256File(path.join(packageRoot, "licenses", license.filename)), license.sha256, `${asset.packageName} license ${license.filename}`);
|
|
144
|
+
}
|
|
145
|
+
return { files: expectedFiles.length, runtime };
|
|
146
|
+
}
|
package/scripts/pick-folder.ps1
CHANGED
|
@@ -82,6 +82,10 @@ public class PiFolderPicker {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
public static bool ShouldRetryWithoutOwner(int hr, IntPtr hwnd) {
|
|
86
|
+
return hr == unchecked((int)0x80070578) && hwnd != IntPtr.Zero;
|
|
87
|
+
}
|
|
88
|
+
|
|
85
89
|
public static string Pick(string title) {
|
|
86
90
|
try {
|
|
87
91
|
EnableDpiAwareness();
|
|
@@ -90,8 +94,13 @@ public class PiFolderPicker {
|
|
|
90
94
|
dlg.GetOptions(out options);
|
|
91
95
|
dlg.SetOptions(options | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);
|
|
92
96
|
if (!string.IsNullOrEmpty(title)) dlg.SetTitle(title);
|
|
93
|
-
|
|
94
|
-
int hr = dlg.Show(
|
|
97
|
+
IntPtr hwnd = GetForegroundWindow();
|
|
98
|
+
int hr = dlg.Show(hwnd);
|
|
99
|
+
if (hr == unchecked((int)0x800704C7)) return "";
|
|
100
|
+
// Retry only an invalid owner handle, never a user cancellation or arbitrary failure.
|
|
101
|
+
if (ShouldRetryWithoutOwner(hr, hwnd)) {
|
|
102
|
+
hr = dlg.Show(IntPtr.Zero);
|
|
103
|
+
}
|
|
95
104
|
if (hr != 0) return "";
|
|
96
105
|
IShellItem item;
|
|
97
106
|
dlg.GetResult(out item);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build the Windows-only npm packages from pinned official niubash portable
|
|
4
|
+
* archives. Nothing is downloaded on macOS/Linux user installs: this runs only
|
|
5
|
+
* in PiWeb's release workflow (or explicitly by a maintainer).
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import {
|
|
13
|
+
MANAGED_NIUBASH_ASSETS,
|
|
14
|
+
NIUBASH_LICENSES,
|
|
15
|
+
NIUBASH_REPOSITORY,
|
|
16
|
+
NIUBASH_TAG,
|
|
17
|
+
listFiles,
|
|
18
|
+
niubashAssetUrl,
|
|
19
|
+
sha256,
|
|
20
|
+
sha256File,
|
|
21
|
+
validateManagedNiubashPackage,
|
|
22
|
+
} from "./managed-niubash-assets.mjs";
|
|
23
|
+
import { isMainModule } from "./entrypoint.mjs";
|
|
24
|
+
|
|
25
|
+
const DEFAULT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
26
|
+
|
|
27
|
+
async function download(url, expectedSha256) {
|
|
28
|
+
let lastError;
|
|
29
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch(url, {
|
|
32
|
+
headers: { "user-agent": "PiWeb managed-niubash packager" },
|
|
33
|
+
signal: AbortSignal.timeout(120_000),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
36
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
37
|
+
const actual = sha256(data);
|
|
38
|
+
if (actual !== expectedSha256) throw new Error(`SHA-256 mismatch: expected ${expectedSha256}, got ${actual}`);
|
|
39
|
+
return data;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
lastError = error;
|
|
42
|
+
if (attempt < 3) await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`failed to download ${url}: ${lastError instanceof Error ? lastError.message : lastError}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractZip(archive, destination) {
|
|
49
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
50
|
+
let result;
|
|
51
|
+
if (process.platform === "win32") {
|
|
52
|
+
result = spawnSync("powershell.exe", [
|
|
53
|
+
"-NoProfile",
|
|
54
|
+
"-NonInteractive",
|
|
55
|
+
"-Command",
|
|
56
|
+
"Expand-Archive -LiteralPath $env:PIWEB_NIUBASH_ARCHIVE -DestinationPath $env:PIWEB_NIUBASH_DESTINATION -Force",
|
|
57
|
+
], {
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
env: { ...process.env, PIWEB_NIUBASH_ARCHIVE: archive, PIWEB_NIUBASH_DESTINATION: destination },
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
result = spawnSync("unzip", ["-q", "-o", archive, "-d", destination], { encoding: "utf8" });
|
|
63
|
+
if (result.error?.code === "ENOENT") {
|
|
64
|
+
result = spawnSync("python3", ["-m", "zipfile", "-e", archive, destination], { encoding: "utf8" });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (result.error) throw new Error(`cannot extract ${path.basename(archive)}: ${result.error.message}`);
|
|
68
|
+
if (result.status !== 0) throw new Error(`cannot extract ${path.basename(archive)}: ${result.stderr || result.stdout}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function replaceDirectory(source, destination) {
|
|
72
|
+
fs.rmSync(destination, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
73
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
74
|
+
try {
|
|
75
|
+
fs.renameSync(source, destination);
|
|
76
|
+
return;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// Windows:刚写入几百个文件时,Defender/索引器会短暂持有句柄,目录 rename 报 EPERM。
|
|
79
|
+
// 先退避重试,仍不行就退化成复制(读通常是被允许的)。
|
|
80
|
+
if (!["EPERM", "EBUSY", "EACCES"].includes(error?.code)) throw error;
|
|
81
|
+
if (attempt >= 5) {
|
|
82
|
+
fs.cpSync(source, destination, { recursive: true });
|
|
83
|
+
fs.rmSync(source, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 250));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function extractedPortableRoot(destination) {
|
|
92
|
+
const entries = fs.readdirSync(destination, { withFileTypes: true }).filter((entry) => entry.name !== "__MACOSX");
|
|
93
|
+
if (entries.length !== 1 || !entries[0].isDirectory()) throw new Error("niubash archive must contain exactly one top-level directory");
|
|
94
|
+
return path.join(destination, entries[0].name);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function prepareLicenses(root, packageRoot) {
|
|
98
|
+
const destination = path.join(packageRoot, "licenses");
|
|
99
|
+
const staging = path.join(packageRoot, `.licenses-${process.pid}`);
|
|
100
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
101
|
+
fs.mkdirSync(staging, { recursive: true });
|
|
102
|
+
try {
|
|
103
|
+
for (const license of NIUBASH_LICENSES) {
|
|
104
|
+
const source = path.join(root, "third-party", "managed-niubash", "licenses", license.filename);
|
|
105
|
+
if (sha256File(source) !== license.sha256) throw new Error(`license source failed integrity check: ${license.filename}`);
|
|
106
|
+
fs.copyFileSync(source, path.join(staging, license.filename));
|
|
107
|
+
}
|
|
108
|
+
await replaceDirectory(staging, destination);
|
|
109
|
+
} finally {
|
|
110
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function prepareAsset(root, asset, log) {
|
|
115
|
+
const packageRoot = path.join(root, "platform-packages", asset.directory);
|
|
116
|
+
if (!fs.existsSync(path.join(packageRoot, "package.json"))) throw new Error(`missing platform package skeleton: ${packageRoot}`);
|
|
117
|
+
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), `piweb-niubash-${asset.arch}-`));
|
|
118
|
+
const archive = path.join(temporary, asset.asset);
|
|
119
|
+
const extracted = path.join(temporary, "extracted");
|
|
120
|
+
const staging = path.join(packageRoot, `.runtime-${process.pid}`);
|
|
121
|
+
try {
|
|
122
|
+
log(`[piweb] Downloading ${asset.asset} ...`);
|
|
123
|
+
fs.writeFileSync(archive, await download(niubashAssetUrl(asset), asset.sha256));
|
|
124
|
+
extractZip(archive, extracted);
|
|
125
|
+
const source = extractedPortableRoot(extracted);
|
|
126
|
+
for (const required of ["niu.exe", "winuxcmd", "bundles"]) {
|
|
127
|
+
if (!fs.existsSync(path.join(source, required))) throw new Error(`${asset.asset} is missing ${required}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
131
|
+
fs.cpSync(source, staging, { recursive: true });
|
|
132
|
+
await replaceDirectory(staging, path.join(packageRoot, "runtime"));
|
|
133
|
+
const runtime = path.join(packageRoot, "runtime");
|
|
134
|
+
|
|
135
|
+
const files = {};
|
|
136
|
+
for (const relative of listFiles(runtime)) files[relative] = sha256File(path.join(runtime, ...relative.split("/")));
|
|
137
|
+
fs.writeFileSync(path.join(packageRoot, "runtime-manifest.json"), `${JSON.stringify({
|
|
138
|
+
schema: 1,
|
|
139
|
+
source: { repository: NIUBASH_REPOSITORY, tag: NIUBASH_TAG, asset: asset.asset, sha256: asset.sha256 },
|
|
140
|
+
files,
|
|
141
|
+
}, null, 2)}\n`);
|
|
142
|
+
await prepareLicenses(root, packageRoot);
|
|
143
|
+
const checked = validateManagedNiubashPackage(packageRoot, asset, { allowExtraRuntimeFiles: false });
|
|
144
|
+
log(`[piweb] Prepared ${asset.packageName}: ${checked.files} runtime files.`);
|
|
145
|
+
return checked;
|
|
146
|
+
} finally {
|
|
147
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
148
|
+
fs.rmSync(temporary, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function selectAssets(args) {
|
|
153
|
+
if (args.length === 0) return Object.values(MANAGED_NIUBASH_ASSETS);
|
|
154
|
+
if (args.length === 1 && args[0] === "--current") {
|
|
155
|
+
if (process.platform !== "win32" || !(process.arch in MANAGED_NIUBASH_ASSETS)) {
|
|
156
|
+
throw new Error(`--current requires Windows x64/ARM64 (got ${process.platform}/${process.arch})`);
|
|
157
|
+
}
|
|
158
|
+
return [MANAGED_NIUBASH_ASSETS[process.arch]];
|
|
159
|
+
}
|
|
160
|
+
if (args.length === 1 && args[0].startsWith("--arch=")) {
|
|
161
|
+
const arch = args[0].slice("--arch=".length);
|
|
162
|
+
const asset = MANAGED_NIUBASH_ASSETS[arch];
|
|
163
|
+
if (!asset) throw new Error(`unsupported niubash architecture: ${arch}`);
|
|
164
|
+
return [asset];
|
|
165
|
+
}
|
|
166
|
+
throw new Error("Usage: node scripts/prepare-niubash-packages.mjs [--current|--arch=x64|--arch=arm64]");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function prepareManagedNiubashPackages(root = DEFAULT_ROOT, { args = [], log = console.log } = {}) {
|
|
170
|
+
const results = [];
|
|
171
|
+
for (const asset of selectAssets(args)) results.push(await prepareAsset(root, asset, log));
|
|
172
|
+
return results;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (isMainModule(import.meta.url)) {
|
|
176
|
+
try {
|
|
177
|
+
await prepareManagedNiubashPackages(DEFAULT_ROOT, { args: process.argv.slice(2) });
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`[piweb] ${error instanceof Error ? error.message : error}`);
|
|
180
|
+
process.exitCode = 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
package/scripts/release.mjs
CHANGED
|
@@ -2,8 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import {
|
|
6
|
-
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
7
6
|
import { assertInstallation, assertLocalPath, readProductionBuild, resolveBuildOutput, RELEASES_DIR, RELEASE_MANIFEST } from "./build-output.mjs";
|
|
8
7
|
import { runCommand } from "./process-runner.mjs";
|
|
9
8
|
import { isMainModule } from "./entrypoint.mjs";
|
|
@@ -77,21 +76,6 @@ async function writeReleaseTypeConfig(root, buildDir) {
|
|
|
77
76
|
});
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
/** Run in a fresh process, after install/pull, so cached config/Next modules cannot lie. */
|
|
81
|
-
export async function verifyReleaseConfig(root = process.cwd(), env = process.env) {
|
|
82
|
-
const installation = assertInstallation(root);
|
|
83
|
-
const buildDir = resolveBuildOutput(installation, env.PIWEB_BUILD_DIR);
|
|
84
|
-
if (!buildDir.startsWith(`${RELEASES_DIR}/`)) throw new Error("release requires an isolated build output");
|
|
85
|
-
const require = createRequire(path.join(installation, "package.json"));
|
|
86
|
-
const module = await import(pathToFileURL(require.resolve("next/dist/server/config.js")).href);
|
|
87
|
-
const loadConfig = typeof module.default === "function" ? module.default : module.default.default;
|
|
88
|
-
const { PHASE_PRODUCTION_BUILD } = require("next/constants");
|
|
89
|
-
const config = await loadConfig(PHASE_PRODUCTION_BUILD, installation, { silent: true });
|
|
90
|
-
if (config.distDir !== buildDir || config.typescript?.tsconfigPath !== CONFIG_PATH || config.typescript?.ignoreBuildErrors) {
|
|
91
|
-
throw new Error("Next config does not honor the isolated release directory/typecheck; refusing to build over the serving output");
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
79
|
export const PI_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
96
80
|
const PI_AI_PACKAGE = "@earendil-works/pi-ai";
|
|
97
81
|
|
|
@@ -150,7 +134,7 @@ export async function prepareRelease({ root = ROOT, env = process.env, run = run
|
|
|
150
134
|
signal?.throwIfAborted();
|
|
151
135
|
assertInstallation(installation);
|
|
152
136
|
await writeReleaseTypeConfig(installation, buildDir);
|
|
153
|
-
await run(process.execPath, [path.join(installation, "scripts", "release.mjs")
|
|
137
|
+
await run(process.execPath, [path.join(installation, "scripts", "verify-release-config.mjs")], {
|
|
154
138
|
cwd: installation, env: childEnv, timeoutMs: 60_000, signal,
|
|
155
139
|
});
|
|
156
140
|
await run("npm", ["run", "check"], { cwd: installation, env: childEnv, timeoutMs: 600_000, signal });
|
|
@@ -175,19 +159,15 @@ if (isMainModule(import.meta.url)) {
|
|
|
175
159
|
process.once("SIGINT", cancel);
|
|
176
160
|
process.once("SIGTERM", cancel);
|
|
177
161
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
prepare: updatePi ? async (context) => { await installPiUpdate(context); } : undefined,
|
|
188
|
-
});
|
|
189
|
-
console.log(`[piweb] Release ready: ${release.buildDir} (${release.buildId}). Restart PiWeb manually when idle to use it.`);
|
|
190
|
-
}
|
|
162
|
+
const updatePi = process.argv.length === 3 && process.argv[2] === "--update-pi";
|
|
163
|
+
if (process.argv.length > 2 && !updatePi) throw new Error("Usage: node scripts/release.mjs [--update-pi]");
|
|
164
|
+
console.log("[piweb] Checking and building an isolated release; the existing server is not restarted.");
|
|
165
|
+
if (updatePi) console.log("[piweb] Pi dependency update: use a maintenance window with no active sessions; node_modules is shared.");
|
|
166
|
+
const release = await prepareRelease({
|
|
167
|
+
signal: controller.signal,
|
|
168
|
+
prepare: updatePi ? async (context) => { await installPiUpdate(context); } : undefined,
|
|
169
|
+
});
|
|
170
|
+
console.log(`[piweb] Release ready: ${release.buildDir} (${release.buildId}). Restart PiWeb manually when idle to use it.`);
|
|
191
171
|
} catch (error) {
|
|
192
172
|
console.error(`[piweb] Release failed: ${error instanceof Error ? error.message : error}`);
|
|
193
173
|
process.exitCode = 1;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
const [cwd, selected, reportFile] = process.argv.slice(2);
|
|
6
|
+
let runner;
|
|
7
|
+
try {
|
|
8
|
+
if (!cwd || !selected || !reportFile) throw new Error("expected workspace, test file and report path");
|
|
9
|
+
const api = await import(pathToFileURL(path.join(cwd, "node_modules/vitest/dist/node.js")).href);
|
|
10
|
+
const options = { root: cwd, run: true, watch: false, reporters: ["json"], outputFile: reportFile };
|
|
11
|
+
runner = Number(api.version.split(".")[0]) >= 5
|
|
12
|
+
? await api.createVitest(options)
|
|
13
|
+
: await api.createVitest("test", options);
|
|
14
|
+
if (typeof runner.globTestSpecifications !== "function" || typeof runner.runTestSpecifications !== "function") {
|
|
15
|
+
throw new Error("this Vitest version does not support exact test specifications");
|
|
16
|
+
}
|
|
17
|
+
const canonical = (file) => process.platform === "win32" ? file.toLowerCase() : file;
|
|
18
|
+
const target = canonical(await fs.realpath(selected));
|
|
19
|
+
const specs = await runner.globTestSpecifications();
|
|
20
|
+
const exact = [];
|
|
21
|
+
for (const spec of specs) {
|
|
22
|
+
if (canonical(await fs.realpath(spec.moduleId)) === target) exact.push(spec);
|
|
23
|
+
}
|
|
24
|
+
if (!exact.length) throw new Error("selected file is excluded by the workspace Vitest configuration");
|
|
25
|
+
// Discovery is read-only; only exact specifications are handed to the executor.
|
|
26
|
+
if (typeof runner.standalone === "function") await runner.standalone();
|
|
27
|
+
else await runner.init();
|
|
28
|
+
await runner.runTestSpecifications(exact);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
} finally {
|
|
33
|
+
await runner?.close();
|
|
34
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Native smoke test. Run on both windows-latest (x64) and windows-11-arm. */
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { MANAGED_NIUBASH_ASSETS, validateManagedNiubashPackage } from "./managed-niubash-assets.mjs";
|
|
10
|
+
|
|
11
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
if (process.platform !== "win32" || !(process.arch in MANAGED_NIUBASH_ASSETS)) {
|
|
13
|
+
throw new Error(`managed niubash smoke test requires Windows x64/ARM64 (got ${process.platform}/${process.arch})`);
|
|
14
|
+
}
|
|
15
|
+
const asset = MANAGED_NIUBASH_ASSETS[process.arch];
|
|
16
|
+
const packageRoot = path.join(root, "platform-packages", asset.directory);
|
|
17
|
+
validateManagedNiubashPackage(packageRoot, asset, { allowExtraRuntimeFiles: false });
|
|
18
|
+
const require = createRequire(path.join(root, "package.json"));
|
|
19
|
+
const runtime = require(path.join(packageRoot, "index.cjs"));
|
|
20
|
+
assert.equal(runtime.packageName, asset.packageName);
|
|
21
|
+
assert.equal(runtime.arch, process.arch);
|
|
22
|
+
|
|
23
|
+
const version = spawnSync(runtime.shellPath, ["--version"], { encoding: "utf8", windowsHide: true });
|
|
24
|
+
assert.equal(version.status, 0, version.stderr);
|
|
25
|
+
assert.match(version.stdout, /Niubash 1\.1\.4/);
|
|
26
|
+
assert.match(version.stdout, /rubash\s+git 8b81c7501646/);
|
|
27
|
+
assert.match(version.stdout, /winuxcmd WinuxCmd 1\.0\.8/);
|
|
28
|
+
|
|
29
|
+
const operations = createLocalBashOperations({ shellPath: runtime.shellPath });
|
|
30
|
+
async function execute(command, options = {}) {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
const result = await operations.exec(command, process.cwd(), {
|
|
33
|
+
onData: (data) => chunks.push(data),
|
|
34
|
+
...options,
|
|
35
|
+
});
|
|
36
|
+
return { exitCode: result.exitCode, output: Buffer.concat(chunks).toString("utf8") };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const empty = await execute("");
|
|
40
|
+
assert.equal(empty.exitCode, 0);
|
|
41
|
+
assert.equal(empty.output, "");
|
|
42
|
+
|
|
43
|
+
const pwd = await execute("pwd");
|
|
44
|
+
assert.equal(pwd.exitCode, 0);
|
|
45
|
+
assert.equal(path.resolve(pwd.output.trim().replaceAll("/", path.sep)).toLowerCase(), path.resolve(process.cwd()).toLowerCase());
|
|
46
|
+
|
|
47
|
+
const quoted = await execute(`node -e "console.log(JSON.stringify(process.argv.slice(1)))" 'a b' '' 'c"d' 'e\\f'`);
|
|
48
|
+
assert.equal(quoted.exitCode, 0, quoted.output);
|
|
49
|
+
assert.deepEqual(JSON.parse(quoted.output.trim()), ["a b", "", "c\"d", "e\\f"]);
|
|
50
|
+
|
|
51
|
+
const shellQuotedCwd = process.cwd().replaceAll("'", `'"'"'`);
|
|
52
|
+
const windowsPath = await execute(`cd '${shellQuotedCwd}' && pwd`);
|
|
53
|
+
assert.equal(windowsPath.exitCode, 0, windowsPath.output);
|
|
54
|
+
assert.equal(path.resolve(windowsPath.output.trim().replaceAll("/", path.sep)).toLowerCase(), path.resolve(process.cwd()).toLowerCase());
|
|
55
|
+
|
|
56
|
+
await assert.rejects(() => execute("sleep 5", { timeout: 0.1 }), /timeout/);
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const aborted = execute("sleep 5", { signal: controller.signal });
|
|
59
|
+
setTimeout(() => controller.abort(), 100);
|
|
60
|
+
await assert.rejects(() => aborted, /aborted/);
|
|
61
|
+
|
|
62
|
+
console.log(`[piweb] Managed niubash ${process.arch} smoke test passed (${version.stdout.split(/\r?\n/, 1)[0]}).`);
|
|
@@ -13,9 +13,15 @@ import fs from "node:fs";
|
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
16
17
|
import { fileURLToPath } from "node:url";
|
|
17
18
|
import { readProductionBuild } from "./build-output.mjs";
|
|
18
19
|
import { isMainModule } from "./entrypoint.mjs";
|
|
20
|
+
import {
|
|
21
|
+
MANAGED_NIUBASH_ASSETS,
|
|
22
|
+
NIUBASH_DISTRIBUTION_VERSION,
|
|
23
|
+
validateManagedNiubashPackage,
|
|
24
|
+
} from "./managed-niubash-assets.mjs";
|
|
19
25
|
|
|
20
26
|
const DEFAULT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
27
|
|
|
@@ -66,9 +72,56 @@ export function verifyPackedPackage(root, { log = console.log } = {}) {
|
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
/** Pack and inspect one actual OS/CPU package, including every runtime hash and license. */
|
|
76
|
+
export function verifyPackedManagedNiubashPackage(packageRoot, asset, { log = console.log } = {}) {
|
|
77
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `piweb-niubash-pack-${asset.arch}-`));
|
|
78
|
+
try {
|
|
79
|
+
const packed = JSON.parse(runNpm(["pack", "--json", "--pack-destination", tmp], { cwd: packageRoot }));
|
|
80
|
+
const entry = Array.isArray(packed) ? packed[0] : packed;
|
|
81
|
+
if (!entry?.filename) throw new Error(`npm pack produced no tarball for ${asset.packageName}`);
|
|
82
|
+
const files = (entry.files ?? []).map((file) => String(file.path).replaceAll("\\", "/").replace(/^package\//, ""));
|
|
83
|
+
for (const required of [
|
|
84
|
+
"package.json", "index.cjs", "runtime-manifest.json", "runtime/niu.exe",
|
|
85
|
+
"runtime/winuxcmd/usr/bin/winuxcmd.exe", "runtime/bundles/oh-my-niu/bundle.toml",
|
|
86
|
+
"licenses/NIUBASH.txt", "licenses/RUBASH.txt", "licenses/WINUXCMD.txt",
|
|
87
|
+
]) {
|
|
88
|
+
if (!files.includes(required)) throw new Error(`${asset.packageName} tarball is missing ${required}`);
|
|
89
|
+
}
|
|
90
|
+
const unpacked = path.join(tmp, "unpacked");
|
|
91
|
+
fs.mkdirSync(unpacked, { recursive: true });
|
|
92
|
+
run("tar", ["-xzf", entry.filename, "-C", "unpacked"], { cwd: tmp });
|
|
93
|
+
const installedRoot = path.join(unpacked, "package");
|
|
94
|
+
const checked = validateManagedNiubashPackage(installedRoot, asset, { allowExtraRuntimeFiles: false });
|
|
95
|
+
const require = createRequire(path.join(installedRoot, "package.json"));
|
|
96
|
+
const exported = require(path.join(installedRoot, "index.cjs"));
|
|
97
|
+
if (exported.packageName !== asset.packageName || exported.arch !== asset.arch
|
|
98
|
+
|| exported.shellPath !== path.join(installedRoot, "runtime", "niu.exe")) {
|
|
99
|
+
throw new Error(`${asset.packageName} has an invalid runtime export`);
|
|
100
|
+
}
|
|
101
|
+
const bytes = fs.statSync(path.join(tmp, entry.filename)).size;
|
|
102
|
+
log(`[piweb] ${entry.filename}: ${checked.files} runtime files, ${(bytes / 1024 / 1024).toFixed(1)} MB.`);
|
|
103
|
+
return { filename: entry.filename, files, bytes, runtimeFiles: checked.files };
|
|
104
|
+
} finally {
|
|
105
|
+
fs.rmSync(tmp, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function verifyManagedNiubashPackages(root, { log = console.log } = {}) {
|
|
110
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
111
|
+
const results = [];
|
|
112
|
+
for (const asset of Object.values(MANAGED_NIUBASH_ASSETS)) {
|
|
113
|
+
if (manifest.optionalDependencies?.[asset.packageName] !== NIUBASH_DISTRIBUTION_VERSION) {
|
|
114
|
+
throw new Error(`PiWeb must use exact optional dependency ${asset.packageName}@${NIUBASH_DISTRIBUTION_VERSION}`);
|
|
115
|
+
}
|
|
116
|
+
results.push(verifyPackedManagedNiubashPackage(path.join(root, "platform-packages", asset.directory), asset, { log }));
|
|
117
|
+
}
|
|
118
|
+
return results;
|
|
119
|
+
}
|
|
120
|
+
|
|
69
121
|
if (isMainModule(import.meta.url)) {
|
|
70
122
|
try {
|
|
71
123
|
verifyPackedPackage(DEFAULT_ROOT);
|
|
124
|
+
verifyManagedNiubashPackages(DEFAULT_ROOT);
|
|
72
125
|
} catch (error) {
|
|
73
126
|
console.error(`[piweb] ${error instanceof Error ? error.message : error}`);
|
|
74
127
|
process.exitCode = 1;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { assertInstallation, resolveBuildOutput, RELEASES_DIR } from "./build-output.mjs";
|
|
5
|
+
import { isMainModule } from "./entrypoint.mjs";
|
|
6
|
+
|
|
7
|
+
/** Run in a fresh process after install/pull so cached Next config cannot be reused. */
|
|
8
|
+
export async function verifyReleaseConfig(root = process.cwd(), env = process.env) {
|
|
9
|
+
const installation = assertInstallation(root);
|
|
10
|
+
const buildDir = resolveBuildOutput(installation, env.PIWEB_BUILD_DIR);
|
|
11
|
+
if (!buildDir.startsWith(`${RELEASES_DIR}/`)) throw new Error("release requires an isolated build output");
|
|
12
|
+
const requireFromInstall = createRequire(path.join(installation, "package.json"));
|
|
13
|
+
const module = await import(pathToFileURL(requireFromInstall.resolve("next/dist/server/config.js")).href);
|
|
14
|
+
const loadConfig = typeof module.default === "function" ? module.default : module.default.default;
|
|
15
|
+
const { PHASE_PRODUCTION_BUILD } = requireFromInstall("next/constants");
|
|
16
|
+
const config = await loadConfig(PHASE_PRODUCTION_BUILD, installation, { silent: true });
|
|
17
|
+
if (config.distDir !== buildDir || config.typescript?.tsconfigPath !== `${RELEASES_DIR}/tsconfig.json` || config.typescript?.ignoreBuildErrors) {
|
|
18
|
+
throw new Error("Next config does not honor the isolated release directory/typecheck; refusing to build over the serving output");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (isMainModule(import.meta.url)) {
|
|
23
|
+
await verifyReleaseConfig();
|
|
24
|
+
}
|
|
@@ -9,7 +9,10 @@ export const dynamic = "force-dynamic";
|
|
|
9
9
|
export async function GET() {
|
|
10
10
|
try {
|
|
11
11
|
const file = path.join(getAgentDir(), "settings.json");
|
|
12
|
-
const content = await fs.readFile(file, "utf8")
|
|
12
|
+
const content = await fs.readFile(file, "utf8").catch((error: NodeJS.ErrnoException) => {
|
|
13
|
+
if (error.code === "ENOENT") return "{}\n";
|
|
14
|
+
throw error;
|
|
15
|
+
});
|
|
13
16
|
return new Response(content, {
|
|
14
17
|
headers: {
|
|
15
18
|
"Content-Type": "application/json; charset=utf-8",
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import type { ProjectTrust } from "@/lib/security-service";
|
|
3
3
|
import { getSecurity, setProjectTrust } from "@/lib/security-service";
|
|
4
|
-
import { reloadLoader, setProjectTrust as setProjectTrustDecision } from "@/lib/pi";
|
|
4
|
+
import { reloadLoader, resolveProjectTrust, setProjectTrust as setProjectTrustDecision } from "@/lib/pi";
|
|
5
5
|
import { reloadSessionsForCwd } from "@/lib/agent-manager";
|
|
6
6
|
import { BoundaryError, resolveWorkspacePath } from "@/lib/path-security";
|
|
7
7
|
|
|
8
8
|
export const dynamic = "force-dynamic";
|
|
9
9
|
|
|
10
|
-
export async function GET() {
|
|
10
|
+
export async function GET(req: Request) {
|
|
11
11
|
try {
|
|
12
|
+
const params = new URL(req.url).searchParams;
|
|
13
|
+
if (params.has("cwd")) {
|
|
14
|
+
const cwd = await resolveWorkspacePath(params.get("cwd"));
|
|
15
|
+
return NextResponse.json({ success: true, data: resolveProjectTrust(cwd) });
|
|
16
|
+
}
|
|
12
17
|
return NextResponse.json({ success: true, data: await getSecurity() });
|
|
13
18
|
} catch (err: any) {
|
|
14
|
-
return NextResponse.json({ success: false, error: String(err?.message ?? err) }, { status: 500 });
|
|
19
|
+
return NextResponse.json({ success: false, error: String(err?.message ?? err) }, { status: err instanceof BoundaryError ? 400 : 500 });
|
|
15
20
|
}
|
|
16
21
|
}
|
|
17
22
|
|