@rexvane/piweb 0.3.13 → 0.3.15

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.
Files changed (103) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +6 -6
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/react-loadable-manifest.json +1 -1
  6. package/.next/required-server-files.js +3 -1
  7. package/.next/required-server-files.json +3 -1
  8. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/server/app/_global-error.html +1 -1
  10. package/.next/server/app/_global-error.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/server/app/_not-found.html +1 -1
  16. package/.next/server/app/_not-found.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  20. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  21. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  22. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  23. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  24. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  25. package/.next/server/app/api/models/route.js.nft.json +1 -1
  26. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  27. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  28. package/.next/server/app/api/security/route.js.nft.json +1 -1
  29. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  30. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  31. package/.next/server/app/api/sessions/import/route.js +3 -3
  32. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  33. package/.next/server/app/api/skills/route.js +2 -2
  34. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  35. package/.next/server/app/api/update/route.js +1 -1
  36. package/.next/server/app/api/update/route.js.nft.json +1 -1
  37. package/.next/server/app/api/version/route.js +1 -1
  38. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  39. package/.next/server/app/index.html +1 -1
  40. package/.next/server/app/index.rsc +3 -3
  41. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  42. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  43. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  44. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  45. package/.next/server/app/login.html +1 -1
  46. package/.next/server/app/login.rsc +2 -2
  47. package/.next/server/app/login.segments/_full.segment.rsc +2 -2
  48. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  49. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  50. package/.next/server/app/page_client-reference-manifest.js +1 -1
  51. package/.next/server/app-paths-manifest.json +6 -6
  52. package/.next/server/chunks/496.js +1 -1
  53. package/.next/server/chunks/686.js +1 -1
  54. package/.next/server/middleware-build-manifest.js +1 -1
  55. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  56. package/.next/server/pages/404.html +1 -1
  57. package/.next/server/pages/500.html +1 -1
  58. package/.next/server/server-reference-manifest.json +1 -1
  59. package/.next/static/chunks/4632.0ed92ae42905ce40.js +1 -0
  60. package/.next/static/chunks/4686-7d694def920ff093.js +1 -0
  61. package/.next/static/chunks/{webpack-b1a572e319353f5d.js → webpack-42c5aae79b0e5d8c.js} +1 -1
  62. package/README.md +52 -8
  63. package/assets/archive.png +0 -0
  64. package/assets/growth-1.png +0 -0
  65. package/assets/growth-2.png +0 -0
  66. package/assets/import.png +0 -0
  67. package/assets/main.png +0 -0
  68. package/assets/outline.png +0 -0
  69. package/assets/prompts-1.png +0 -0
  70. package/assets/prompts-2.png +0 -0
  71. package/assets/providers-1.png +0 -0
  72. package/assets/providers-2.png +0 -0
  73. package/assets/providers-3.png +0 -0
  74. package/assets/trace-1.png +0 -0
  75. package/assets/trace-2.png +0 -0
  76. package/assets/trace-3.png +0 -0
  77. package/assets/trace-4.png +0 -0
  78. package/assets/viewer-1.png +0 -0
  79. package/assets/viewer-2.png +0 -0
  80. package/next.config.ts +6 -2
  81. package/package.json +7 -3
  82. package/scripts/launcher.mjs +1 -1
  83. package/scripts/managed-niubash-assets.mjs +146 -0
  84. package/scripts/prepare-niubash-packages.mjs +182 -0
  85. package/scripts/smoke-managed-niubash.mjs +62 -0
  86. package/scripts/verify-package.mjs +53 -0
  87. package/src/components/SessionImportSection.tsx +76 -23
  88. package/src/i18n.tsx +2 -0
  89. package/src/lib/agent-manager.ts +3 -0
  90. package/src/lib/managed-niubash.ts +227 -0
  91. package/src/lib/session-import/claude.ts +23 -2
  92. package/src/lib/session-import/codex.ts +61 -8
  93. package/src/lib/session-import/dsh.ts +12 -1
  94. package/src/lib/session-import/files.ts +14 -0
  95. package/src/lib/session-import/grok.ts +56 -23
  96. package/src/lib/session-import/types.ts +12 -0
  97. package/src/lib/skills-service.ts +51 -14
  98. package/.next/static/chunks/4632.d574c210ad78d065.js +0 -1
  99. package/.next/static/chunks/4686-0a4155f8488314b8.js +0 -1
  100. package/assets/showcase.png +0 -0
  101. package/bin/pi.js +0 -50
  102. /package/.next/static/{olV7jxKRN5VhfEK012WhV → DdBwPYoHBKtaJ97QIr6Co}/_buildManifest.js +0 -0
  103. /package/.next/static/{olV7jxKRN5VhfEK012WhV → DdBwPYoHBKtaJ97QIr6Co}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rexvane/piweb",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Modern web UI for the pi coding agent, driven by the official pi SDK without modifying pi",
@@ -28,7 +28,7 @@
28
28
  "src",
29
29
  "scripts",
30
30
  "public",
31
- "assets/showcase.png",
31
+ "assets",
32
32
  "next.config.ts",
33
33
  "tsconfig.json",
34
34
  "postcss.config.mjs",
@@ -39,9 +39,9 @@
39
39
  "build": "node --max-old-space-size=8192 node_modules/next/dist/bin/next build --webpack",
40
40
  "build:release": "node scripts/release.mjs",
41
41
  "postinstall": "node scripts/postinstall.mjs",
42
+ "prepare:niubash": "node scripts/prepare-niubash-packages.mjs",
42
43
  "prepublishOnly": "npm run typecheck && npm run test",
43
44
  "start": "node bin/piweb.js",
44
- "web": "node bin/pi.js web",
45
45
  "typecheck": "tsc --noEmit",
46
46
  "verify:package": "node scripts/verify-package.mjs",
47
47
  "test": "vitest run",
@@ -73,6 +73,10 @@
73
73
  "tailwindcss": "^4.2.2",
74
74
  "typescript": "^7"
75
75
  },
76
+ "optionalDependencies": {
77
+ "@rexvane/piweb-niubash-win32-arm64": "1.1.4-piweb.0",
78
+ "@rexvane/piweb-niubash-win32-x64": "1.1.4-piweb.0"
79
+ },
76
80
  "devDependencies": {
77
81
  "@testing-library/dom": "^10.4.2",
78
82
  "@testing-library/react": "^16.3.3",
@@ -169,7 +169,7 @@ export async function runLauncher({ args = process.argv.slice(2), env = process.
169
169
  const options = parseLauncherArgs(args, env);
170
170
  const log = dependencies.log ?? console.log;
171
171
  if (options.help) {
172
- log("Usage: pi web [-p, --port <port>] [-H, --hostname <host>] [--dev] [--no-open]");
172
+ log("Usage: piweb [-p, --port <port>] [-H, --hostname <host>] [--dev] [--no-open]");
173
173
  log("默认绑定 127.0.0.1:30141;端口被占自动 +1。开发模式仅允许本机监听。");
174
174
  log("对外监听必须使用生产构建与 PI_WEB_PASSWORD。构建/恢复发布:npm run build:release。");
175
175
  return 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
+ }
@@ -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
+ }
@@ -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;