@firstpick/pi-package-webui 0.4.0 → 0.4.2
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/README.md +24 -9
- package/bin/pi-webui.mjs +415 -29
- package/index.ts +16 -1
- package/lib/trust-boundaries.mjs +1 -0
- package/package.json +1 -1
- package/public/app.js +766 -47
- package/public/index.html +44 -1
- package/public/styles.css +516 -4
- package/tests/http-endpoints-harness.test.mjs +97 -1
- package/tests/mobile-static.test.mjs +21 -10
- package/tests/session-auth-harness.test.mjs +4 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import { chmod, mkdtemp, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { chmod, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { networkInterfaces, tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -154,6 +154,47 @@ try {
|
|
|
154
154
|
assert.equal(gitRemote.status, 200);
|
|
155
155
|
assert.equal(gitRemote.body?.ok, true, "remote endpoint should add origin without pushing");
|
|
156
156
|
assert.equal(gitRemote.body?.data?.remoteUrl, "https://github.com/Firstp1ck/pi-webui-http-harness.git");
|
|
157
|
+
|
|
158
|
+
await writeFile(path.join(cwd, "single.txt"), "created\n");
|
|
159
|
+
const gitAddCreated = await request("127.0.0.1", "/api/git-workflow/add", { method: "POST", body: { tab: tabId } });
|
|
160
|
+
assert.equal(gitAddCreated.status, 200);
|
|
161
|
+
assert.equal(gitAddCreated.body?.ok, true, "git add endpoint should stage a new single file");
|
|
162
|
+
const createdDefault = await request("127.0.0.1", `/api/git-workflow/default-commit-message?tab=${encodeURIComponent(tabId)}`);
|
|
163
|
+
assert.equal(createdDefault.status, 200);
|
|
164
|
+
assert.equal(createdDefault.body?.ok, true, "default commit message endpoint should return ok for a staged single file");
|
|
165
|
+
assert.equal(createdDefault.body?.data?.message, "created single.txt");
|
|
166
|
+
const createdCommit = await request("127.0.0.1", "/api/git-workflow/commit", { method: "POST", body: { variant: "input", message: createdDefault.body?.data?.message, tab: tabId } });
|
|
167
|
+
assert.equal(createdCommit.status, 200);
|
|
168
|
+
assert.equal(createdCommit.body?.ok, true, "input commit endpoint should accept the generated single-file default");
|
|
169
|
+
|
|
170
|
+
await writeFile(path.join(cwd, "single.txt"), "updated\n");
|
|
171
|
+
const gitAddUpdated = await request("127.0.0.1", "/api/git-workflow/add", { method: "POST", body: { tab: tabId } });
|
|
172
|
+
assert.equal(gitAddUpdated.status, 200);
|
|
173
|
+
assert.equal(gitAddUpdated.body?.ok, true, "git add endpoint should stage a single-file update");
|
|
174
|
+
const updatedDefault = await request("127.0.0.1", `/api/git-workflow/default-commit-message?tab=${encodeURIComponent(tabId)}`);
|
|
175
|
+
assert.equal(updatedDefault.status, 200);
|
|
176
|
+
assert.equal(updatedDefault.body?.data?.message, "updated single.txt");
|
|
177
|
+
const updatedCommit = await request("127.0.0.1", "/api/git-workflow/commit", { method: "POST", body: { variant: "input", message: updatedDefault.body?.data?.message, tab: tabId } });
|
|
178
|
+
assert.equal(updatedCommit.status, 200);
|
|
179
|
+
assert.equal(updatedCommit.body?.ok, true, "input commit endpoint should accept the update default");
|
|
180
|
+
|
|
181
|
+
await rm(path.join(cwd, "single.txt"));
|
|
182
|
+
const gitAddDeleted = await request("127.0.0.1", "/api/git-workflow/add", { method: "POST", body: { tab: tabId } });
|
|
183
|
+
assert.equal(gitAddDeleted.status, 200);
|
|
184
|
+
assert.equal(gitAddDeleted.body?.ok, true, "git add endpoint should stage a single-file deletion");
|
|
185
|
+
const deletedDefault = await request("127.0.0.1", `/api/git-workflow/default-commit-message?tab=${encodeURIComponent(tabId)}`);
|
|
186
|
+
assert.equal(deletedDefault.status, 200);
|
|
187
|
+
assert.equal(deletedDefault.body?.data?.message, "deleted single.txt");
|
|
188
|
+
|
|
189
|
+
await writeFile(path.join(cwd, "multi-a.txt"), "a\n");
|
|
190
|
+
await writeFile(path.join(cwd, "multi-b.txt"), "b\n");
|
|
191
|
+
const gitAddMultiple = await request("127.0.0.1", "/api/git-workflow/add", { method: "POST", body: { tab: tabId } });
|
|
192
|
+
assert.equal(gitAddMultiple.status, 200);
|
|
193
|
+
assert.equal(gitAddMultiple.body?.ok, true, "git add endpoint should stage multiple files");
|
|
194
|
+
const multipleDefault = await request("127.0.0.1", `/api/git-workflow/default-commit-message?tab=${encodeURIComponent(tabId)}`);
|
|
195
|
+
assert.equal(multipleDefault.status, 200);
|
|
196
|
+
assert.equal(multipleDefault.body?.ok, true, "default commit message endpoint should still return ok when no default is available");
|
|
197
|
+
assert.equal(multipleDefault.body?.data?.message, "", "multiple staged files should not get a default commit message");
|
|
157
198
|
} else {
|
|
158
199
|
console.log("http-endpoints-harness: git not available; skipping git init workflow endpoint checks");
|
|
159
200
|
}
|
|
@@ -204,8 +245,15 @@ try {
|
|
|
204
245
|
assert.equal(traversalDelete.status, 403, "session delete outside the session dir must return 403");
|
|
205
246
|
assert.match(String(traversalDelete.body?.error || ""), /session directory/i);
|
|
206
247
|
|
|
248
|
+
const initialAuth = await request("127.0.0.1", "/api/remote-auth");
|
|
249
|
+
assert.equal(initialAuth.status, 200);
|
|
250
|
+
assert.equal(initialAuth.body?.data?.auth?.enabled, false, "remote PIN auth should be off by default");
|
|
251
|
+
|
|
207
252
|
const lan = lanAddress();
|
|
208
253
|
if (lan) {
|
|
254
|
+
const remoteHealthBeforeAuth = await request(lan, "/api/health");
|
|
255
|
+
assert.equal(remoteHealthBeforeAuth.status, 200, "LAN clients should connect without a PIN while auth is off");
|
|
256
|
+
|
|
209
257
|
const remoteDelete = await request(lan, "/api/session-delete", {
|
|
210
258
|
method: "POST",
|
|
211
259
|
body: { sessionPath: path.join(cwd, "outside.jsonl"), confirmed: true, tab: tabId },
|
|
@@ -221,7 +269,55 @@ try {
|
|
|
221
269
|
|
|
222
270
|
const remoteClose = await request(lan, "/api/network/close", { method: "POST" });
|
|
223
271
|
assert.equal(remoteClose.status, 403, "network close must be localhost-only");
|
|
272
|
+
|
|
273
|
+
const enableAuth = await request("127.0.0.1", "/api/remote-auth/settings", { method: "POST", body: { enabled: true } });
|
|
274
|
+
assert.equal(enableAuth.status, 200, "localhost can enable remote PIN auth");
|
|
275
|
+
const pin = enableAuth.body?.data?.auth?.pin;
|
|
276
|
+
assert.match(pin, /^\d{4}$/, "enabling remote auth should generate a 4-digit PIN");
|
|
277
|
+
|
|
278
|
+
const remoteHealthWithAuth = await request(lan, "/api/health");
|
|
279
|
+
assert.equal(remoteHealthWithAuth.status, 401, "unauthenticated LAN clients should be challenged while remote auth is on");
|
|
280
|
+
|
|
281
|
+
const wrongPin = pin === "0000" ? "0001" : "0000";
|
|
282
|
+
const badLogin = await request(lan, "/api/remote-auth", { method: "POST", body: { pin: wrongPin } });
|
|
283
|
+
assert.equal(badLogin.status, 403, "wrong remote PIN should be rejected");
|
|
284
|
+
|
|
285
|
+
const loginResponse = await fetch(`http://${lan}:${port}/api/remote-auth`, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: { "content-type": "application/json" },
|
|
288
|
+
body: JSON.stringify({ pin }),
|
|
289
|
+
signal: AbortSignal.timeout(5_000),
|
|
290
|
+
});
|
|
291
|
+
assert.equal(loginResponse.status, 200, "correct remote PIN should be accepted");
|
|
292
|
+
const authCookie = loginResponse.headers.get("set-cookie")?.split(";", 1)[0];
|
|
293
|
+
assert.ok(authCookie, "remote auth login should set an auth cookie");
|
|
294
|
+
|
|
295
|
+
const authedHealth = await fetch(`http://${lan}:${port}/api/health`, {
|
|
296
|
+
headers: { cookie: authCookie },
|
|
297
|
+
signal: AbortSignal.timeout(5_000),
|
|
298
|
+
});
|
|
299
|
+
assert.equal(authedHealth.status, 200, "authenticated LAN client should reach guarded APIs");
|
|
300
|
+
await authedHealth.json();
|
|
301
|
+
|
|
302
|
+
const remoteSettings = await fetch(`http://${lan}:${port}/api/remote-auth/settings`, {
|
|
303
|
+
method: "POST",
|
|
304
|
+
headers: { "content-type": "application/json", cookie: authCookie },
|
|
305
|
+
body: JSON.stringify({ enabled: false }),
|
|
306
|
+
signal: AbortSignal.timeout(5_000),
|
|
307
|
+
});
|
|
308
|
+
assert.equal(remoteSettings.status, 403, "remote clients must not toggle remote PIN auth settings");
|
|
309
|
+
await remoteSettings.json().catch(() => undefined);
|
|
310
|
+
|
|
311
|
+
const disableAuth = await request("127.0.0.1", "/api/remote-auth/settings", { method: "POST", body: { enabled: false } });
|
|
312
|
+
assert.equal(disableAuth.status, 200, "localhost can disable remote PIN auth");
|
|
313
|
+
const remoteHealthAfterDisable = await request(lan, "/api/health");
|
|
314
|
+
assert.equal(remoteHealthAfterDisable.status, 200, "LAN clients should reconnect without a PIN after auth is disabled");
|
|
224
315
|
} else {
|
|
316
|
+
const enableAuth = await request("127.0.0.1", "/api/remote-auth/settings", { method: "POST", body: { enabled: true } });
|
|
317
|
+
assert.equal(enableAuth.status, 200, "localhost can enable remote PIN auth");
|
|
318
|
+
assert.match(enableAuth.body?.data?.auth?.pin, /^\d{4}$/);
|
|
319
|
+
const disableAuth = await request("127.0.0.1", "/api/remote-auth/settings", { method: "POST", body: { enabled: false } });
|
|
320
|
+
assert.equal(disableAuth.status, 200, "localhost can disable remote PIN auth");
|
|
225
321
|
console.log("http-endpoints-harness: no LAN address detected; skipping remote-client checks");
|
|
226
322
|
}
|
|
227
323
|
|
|
@@ -267,7 +267,7 @@ assert.match(css, /\.composer-publish-menu:hover > \.composer-publish-button\[da
|
|
|
267
267
|
assert.match(css, /\.composer-publish-menu-panel \{[\s\S]*?display:\s*none;[\s\S]*?flex-direction:\s*column/, "Publish workflow menu should hide when closed and expand like grouped tabs");
|
|
268
268
|
assert.match(css, /\.composer-publish-menu:hover \.composer-publish-menu-panel,[\s\S]*?\.composer-publish-menu:focus-within \.composer-publish-menu-panel,[\s\S]*?\.composer-publish-menu\.open \.composer-publish-menu-panel \{\n\s+display:\s*flex;/, "Publish workflow menu should open on hover, focus, or explicit open state");
|
|
269
269
|
assert.match(css, /\.composer-native-command-button \{[\s\S]*?color:\s*var\(--ctp-mauve\)/, "skills/tools command menu should have a distinct slash-command button style");
|
|
270
|
-
assert.match(css, /\.composer-options-menu-panel \{[\s\S]*?max-height:\s*min\(
|
|
270
|
+
assert.match(css, /\.composer-options-menu-panel \{[\s\S]*?max-height:\s*min\(calc\(var\(--visual-viewport-height, 100dvh\) - 2rem\), 44rem\)/, "Options menu should be tall enough for common commands without scrolling on normal viewports");
|
|
271
271
|
assert.match(css, /\.composer-native-command-menu-item \{[\s\S]*?color:\s*var\(--ctp-mauve\)/, "skills/tools command menu items should be styled separately from publish actions");
|
|
272
272
|
assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?grid-column: span 1/, "Publish and command menu buttons should fit beside Git workflow in mobile actions");
|
|
273
273
|
assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
|
|
@@ -399,6 +399,8 @@ assert.match(app, /initializeCustomBackground\(\)\.catch/, "startup should resto
|
|
|
399
399
|
assert.match(app, /Restart Web UI to load themes/, "frontend should explain when a stale server cannot serve the themes endpoint");
|
|
400
400
|
assert.match(app, /themeSelect\.addEventListener\("change"/, "side-panel theme selector should switch themes immediately");
|
|
401
401
|
assert.match(app, /open \? "Close for network" : "Open to network"/, "network button should toggle from open to close action");
|
|
402
|
+
assert.match(app, /remoteAuthToggle: \$\("#remoteAuthToggle"\)/, "Controls should expose the remote PIN auth toggle");
|
|
403
|
+
assert.match(app, /api\("\/api\/remote-auth\/settings", \{ method: "POST"/, "remote PIN auth toggle should call the settings endpoint");
|
|
402
404
|
assert.match(app, /api\("\/api\/network\/close", \{ method: "POST"/, "network close action should call the close endpoint");
|
|
403
405
|
assert.match(app, /webuiVersionBadge: \$\("#webuiVersionBadge"\)/, "frontend should bind the Control Deck version badge");
|
|
404
406
|
assert.match(app, /webuiDevBadge: \$\("#webuiDevBadge"\)/, "frontend should bind the Control Deck dev badge");
|
|
@@ -561,7 +563,10 @@ assert.match(app, /\["skills", "tuiSkillsCommand"\][\s\S]*\["tools", "tuiToolsCo
|
|
|
561
563
|
assert.match(app, /function setNativeCommandMenuOpen\(open\)/, "frontend should track the skills/tools command menu open state separately from Publish");
|
|
562
564
|
assert.match(app, /nativeSkillsButton\.hidden = !isOptionalFeatureEnabled\("tuiSkillsCommand"\)[\s\S]*nativeToolsButton\.hidden = !isOptionalFeatureEnabled\("tuiToolsCommand"\)/, "skills/tools menu items should be hidden by their optional feature toggles");
|
|
563
565
|
assert.match(app, /function renderCommands\(\)/, "side-panel commands should be re-renderable from current optional feature state");
|
|
564
|
-
assert.match(app, /function installOptionalFeature\(featureId\)/, "optional features should expose
|
|
566
|
+
assert.match(app, /function installOptionalFeature\(featureId, \{ update = false \} = \{\}\)/, "optional features should expose install and update actions");
|
|
567
|
+
assert.match(app, /api\("\/api\/optional-features"/, "optional feature panel should fetch package install/update status from the backend");
|
|
568
|
+
assert.match(app, /packageStatus\?\.updateAvailable[\s\S]*action\.textContent = "Update…"/, "optional feature package drift should turn the install action into an update action");
|
|
569
|
+
assert.match(app, /optionalFeatureInstallMessages\.set\(featureId[\s\S]*waiting for package-manager output/, "optional feature installs should show running feedback while npm is active");
|
|
565
570
|
assert.match(app, /api\("\/api\/optional-feature-install"/, "optional feature install action should call the backend installer endpoint");
|
|
566
571
|
assert.match(app, /id: "safetyGuard"[\s\S]*?@firstpick\/pi-extension-safety-guard/, "optional features should include the safety guard companion");
|
|
567
572
|
assert.match(app, /id: "tuiSkillsCommand"[\s\S]*?@firstpick\/pi-extension-setup-skills/, "optional features should include the TUI skills command companion");
|
|
@@ -858,7 +863,7 @@ assert.match(css, /\.composer-actions-panel > \.composer-publish-menu:hover::aft
|
|
|
858
863
|
assert.match(css, /\.composer-actions-panel > \.composer-publish-menu \.composer-publish-menu-panel \{[\s\S]*?position:\s*absolute;[\s\S]*?inset:\s*auto auto calc\(100% \+ 0\.38rem\) 0;[\s\S]*?max-height:\s*min\(34dvh, 18rem\);[\s\S]*?overflow:\s*auto;/, "opened mobile Actions dropdown panels should float upward over the Actions controls with their own scrollbar");
|
|
859
864
|
assert.match(css, /\.composer-actions-panel > \.composer-publish-menu \.composer-publish-menu-panel \{[\s\S]*?width:\s*100%;[\s\S]*?min-width:\s*0;[\s\S]*?max-width:\s*100%;/, "mobile Actions dropdown panels should align to the width of their trigger buttons");
|
|
860
865
|
assert.match(css, /\.composer-actions-panel > \.composer-publish-menu \.composer-publish-menu-item \{[\s\S]*?width:\s*100%;[\s\S]*?min-width:\s*0;[\s\S]*?white-space:\s*normal;/, "mobile Actions dropdown option buttons should not keep desktop min-widths that misalign with triggers");
|
|
861
|
-
assert.match(css, /\.composer-actions-panel > \.composer-options-menu \.composer-publish-menu-panel \{[\s\S]*?inset-inline:\s*auto 0;[\s\S]*?max-height:\s*min\(
|
|
866
|
+
assert.match(css, /\.composer-actions-panel > \.composer-options-menu \.composer-publish-menu-panel \{[\s\S]*?inset-inline:\s*auto 0;[\s\S]*?max-height:\s*min\(calc\(var\(--visual-viewport-height, 100dvh\) - 2rem\), 44rem\);/, "mobile Options dropdown should be tall enough to avoid scrolling for the standard option list");
|
|
862
867
|
assert.match(app, /function setMobileTabsExpanded\(/, "mobile tab strip should be JS-toggleable");
|
|
863
868
|
assert.match(css, /@media \(max-width: 720px\), \(max-device-width: 720px\), \(pointer: coarse\) and \(hover: none\) \{[\s\S]*?\.terminal-tab-group \{\n\s+display:\s*grid;\n\s+grid-template-columns:\s*minmax\(0, 1fr\) auto;/, "mobile terminal tab groups should use a stable grid row for the tab and close button when expanded");
|
|
864
869
|
assert.match(css, /@media \(max-width: 720px\), \(max-device-width: 720px\), \(pointer: coarse\) and \(hover: none\) \{[\s\S]*?\.terminal-tab-group-menu \{[\s\S]*?grid-column:\s*1 \/ -1;[\s\S]*?margin:\s*0\.34rem 0 0;/, "mobile terminal tab group menus should not add horizontal margins that overflow and distort the tab card");
|
|
@@ -956,7 +961,7 @@ assert.match(server, /WEBUI_CONTROLLED_PACKAGES = new Set\(\[WEBUI_PACKAGE, \.\.
|
|
|
956
961
|
assert.match(server, /const args = \["--mode", "rpc", "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-themes"\]/, "Web UI tabs should disable implicit resource loading before adding curated resource paths");
|
|
957
962
|
assert.match(server, /normalPiResourcePathsForTab[\s\S]*WEBUI_CONTROLLED_PACKAGES\.has\(packageName\)[\s\S]*continue/, "Web UI tab resource resolution should exclude separately installed Web UI feature packages");
|
|
958
963
|
assert.match(server, /startedWebuiResourcePaths\(resourceType\)/, "Web UI tabs should load feature resources from the started Web UI package");
|
|
959
|
-
assert.match(server, /
|
|
964
|
+
assert.match(server, /resolveInstalledPackageSubpath\(nodeModulesRef\.packageName, nodeModulesRef\.subpath\)/, "Web UI should prefer workspace/global/package-root installed packages for node_modules manifest entries");
|
|
960
965
|
assert.match(server, /const CODEX_TOKEN_REFRESH_SKEW_MS = 5 \* 60 \* 1000/, "server should refresh Codex OAuth tokens before they expire");
|
|
961
966
|
assert.match(server, /url\.pathname === "\/api\/codex-usage" && req\.method === "GET"/, "server should expose a sanitized Codex usage endpoint");
|
|
962
967
|
assert.match(server, /OPENAI_CODEX_USAGE_ENDPOINT/, "server should query Codex usage from the backend, not the browser");
|
|
@@ -1072,12 +1077,16 @@ assert.match(server, /const OPTIONAL_FEATURE_PACKAGES = new Map/, "server should
|
|
|
1072
1077
|
assert.match(server, /\["safetyGuard", "@firstpick\/pi-extension-safety-guard"\]/, "server should allow installing the safety guard optional feature");
|
|
1073
1078
|
assert.match(server, /\["tuiSkillsCommand", "@firstpick\/pi-extension-setup-skills"\]/, "server should allow installing the TUI skills optional feature");
|
|
1074
1079
|
assert.match(server, /\["tuiToolsCommand", "@firstpick\/pi-extension-tools"\]/, "server should allow installing the TUI tools optional feature");
|
|
1080
|
+
assert.match(server, /function optionalFeaturePackageStatus\(featureId\)/, "server should report optional feature package install/update status");
|
|
1075
1081
|
assert.match(server, /function installOptionalFeaturePackage\(featureId\)/, "server should provide optional feature package installation helper");
|
|
1076
1082
|
assert.match(server, /PI_WEBUI_OPTIONAL_FEATURE_INSTALL_ROOT/, "optional feature installs should support an explicit package-manager root override");
|
|
1077
|
-
assert.match(server, /function configuredAgentNpmRoot\(\)/, "global Web UI launches should
|
|
1078
|
-
assert.match(server, /installRootDeclaresPackage\(.*?@firstpick\/pi-package-webui/s, "optional feature installs should
|
|
1083
|
+
assert.match(server, /function configuredAgentNpmRoot\(\)/, "global Web UI launches should consider Pi's agent npm root for optional packages");
|
|
1084
|
+
assert.match(server, /installRootDeclaresPackage\(.*?@firstpick\/pi-package-webui/s, "optional feature installs should reuse a node_modules parent that declares the Web UI package dependency");
|
|
1085
|
+
assert.match(server, /installRootContainsPackage\(.*?@firstpick\/pi-package-webui/s, "global npm Web UI launches should also accept the prefix containing the Web UI package folder");
|
|
1086
|
+
assert.match(server, /resolveInstalledPackageSubpath\(nodeModulesRef\.packageName, nodeModulesRef\.subpath\)/, "started Web UI resource resolution should fall back to globally installed sibling optional packages");
|
|
1079
1087
|
assert.match(server, /if \(webuiDevServer\) return installRoot/, "source-checkout Web UI launches should still use the checkout root for optional feature installs");
|
|
1080
|
-
assert.match(server, /Could not determine a safe optional feature install root/, "optional feature installs should fail closed when no
|
|
1088
|
+
assert.match(server, /Could not determine a safe optional feature install root/, "optional feature installs should fail closed when no safe package root can be found");
|
|
1089
|
+
assert.match(server, /url\.pathname === "\/api\/optional-features" && req\.method === "GET"/, "server should expose optional feature package status endpoint");
|
|
1081
1090
|
assert.match(server, /url\.pathname === "\/api\/optional-feature-install" && req\.method === "POST"/, "server should expose optional feature install endpoint");
|
|
1082
1091
|
assert.match(server, /requireLocalhostRoute\(req, url\.pathname\)/, "optional feature install endpoint should use shared localhost trust policy");
|
|
1083
1092
|
assert.match(server, /url\.pathname === "\/api\/skill-file" && req\.method === "GET"[\s\S]*?getSkillFileData/, "server should expose GET /api/skill-file for editable skill content");
|
|
@@ -1102,6 +1111,7 @@ assert.match(readme, /Feedback reactions \(`👍`, `👎`, `\?`\) on final assis
|
|
|
1102
1111
|
assert.match(readme, /POST \/api\/action-feedback\?tab=<tabId>/, "README should document the action-feedback endpoint");
|
|
1103
1112
|
assert.match(readme, /`@` file\/path references with live suggestions/, "README should describe @ file/path reference autocomplete");
|
|
1104
1113
|
assert.match(readme, /GET \/api\/path-suggestions\?tab=<tabId>&query=<path>/, "README should document the path-suggestions endpoint");
|
|
1114
|
+
assert.match(readme, /GET \/api\/optional-features/, "README should document optional feature status endpoint");
|
|
1105
1115
|
assert.match(readme, /POST \/api\/optional-feature-install/, "README should document optional feature install endpoint");
|
|
1106
1116
|
assert.match(readme, /server-persisted fast picks/, "README should describe server-persisted fast picks");
|
|
1107
1117
|
assert.match(readme, /browser notifications when a tab needs an extension UI response and an optional side-panel toggle for agent-done notifications/, "README should describe blocked-tab and agent-done notifications");
|
|
@@ -1109,10 +1119,11 @@ assert.match(readme, /blocked-tab browser notifications, and optional agent-done
|
|
|
1109
1119
|
assert.match(readme, /Side-panel theme picker backed by optional `@firstpick\/pi-themes-bundle` themes when loaded/, "README should describe optional theme selection");
|
|
1110
1120
|
assert.match(readme, /## Optional companion packages/, "README should document optional Web UI companion packages");
|
|
1111
1121
|
assert.match(readme, /curates Pi resources from the Web UI package that started the server/, "README should document started-package-based Web UI feature loading");
|
|
1112
|
-
assert.match(readme, /
|
|
1122
|
+
assert.match(readme, /Companion packages installed as global\/npm-prefix siblings/, "README should document global sibling companion discovery");
|
|
1123
|
+
assert.match(readme, /avoiding duplicate loads while keeping global `pi-webui` launches working/, "README should document duplicate companion suppression");
|
|
1113
1124
|
assert.match(readme, /checks loaded Pi capabilities directly through RPC-visible commands and live widget events/, "README should document capability-based startup checks");
|
|
1114
|
-
assert.match(readme, /side panel shows each optional feature as enabled, disabled, or install-needed/, "README should document optional feature side-panel controls");
|
|
1115
|
-
assert.match(readme, /Installing a
|
|
1125
|
+
assert.match(readme, /side panel shows each optional feature as enabled, disabled, installed-but-not-loaded, update-available, or install-needed/, "README should document optional feature side-panel controls");
|
|
1126
|
+
assert.match(readme, /Installing or updating a feature is an explicit, warned action with running\/failure feedback/, "README should document optional feature install and update warning behavior");
|
|
1116
1127
|
assert.match(readme, /\.\/start-webui\.sh --dev --cwd \/path\/to\/project/, "README should document the dev helper launcher");
|
|
1117
1128
|
assert.match(readme, /sync-pi-package-symlinks\.sh[\s\S]*only one copy is loaded/, "README should document dev companion symlink setup");
|
|
1118
1129
|
assert.match(startScript, /--dev\)/, "start-webui.sh should accept a --dev flag");
|
|
@@ -132,6 +132,10 @@ try {
|
|
|
132
132
|
LOCALHOST_ONLY_POST_ROUTES.has("/api/network/close"),
|
|
133
133
|
"closing network access must be localhost-only like opening it",
|
|
134
134
|
);
|
|
135
|
+
assert.ok(
|
|
136
|
+
LOCALHOST_ONLY_POST_ROUTES.has("/api/remote-auth/settings"),
|
|
137
|
+
"remote PIN auth settings must be localhost-only",
|
|
138
|
+
);
|
|
135
139
|
} finally {
|
|
136
140
|
await rm(tempDir, { recursive: true, force: true });
|
|
137
141
|
await rm(outsideDir, { recursive: true, force: true });
|