@pi-chamber/web 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/bin/cli.test.js +6 -8
  2. package/bin/lib/cli-tunnel-capabilities.js +0 -2
  3. package/dist/assets/{CommitSection-DPjTv8fA.js → CommitSection-BpbMC0et.js} +1 -1
  4. package/dist/assets/{DiagramEditor-ChOor9l9.js → DiagramEditor-dFTMKHuO.js} +1 -1
  5. package/dist/assets/{DiagramView-3hAT86v_.js → DiagramView-DVw_8Px1.js} +1 -1
  6. package/dist/assets/{DiffView-D1dyOiw2.js → DiffView-CkoY1oAM.js} +1 -1
  7. package/dist/assets/{FilesView-BCzghrc7.js → FilesView-C0hyg2pa.js} +6 -6
  8. package/dist/assets/{GitView-CYF1E1p4.js → GitView-D36hpDsb.js} +4 -4
  9. package/dist/assets/{JsonTreeView-CrJO6LNu.js → JsonTreeView-BF8S-bPN.js} +1 -1
  10. package/dist/assets/{MarkdownRendererImpl-BmKjrkGD.js → MarkdownRendererImpl-BVNChdig.js} +1 -1
  11. package/dist/assets/{PatchDiff-DgvPLx6x.js → PatchDiff-B--OaNB1.js} +1 -1
  12. package/dist/assets/{PierreDiffViewer-InnT0JVA.js → PierreDiffViewer-0Qkp9ICt.js} +1 -1
  13. package/dist/assets/{SessionSwitcherDropdown-C0OdvirK.js → SessionSwitcherDropdown-cFWgy1td.js} +101 -102
  14. package/dist/assets/SettingsView-D3ZZyjU3.js +4 -0
  15. package/dist/assets/SettingsWindow-BuJCvYoR.js +1 -0
  16. package/dist/assets/{ToolOutputDialog-gzwcb8nI.js → ToolOutputDialog-Bi9BoEe7.js} +4 -4
  17. package/dist/assets/{ToolPartDiffPreview-C45V5bhy.js → ToolPartDiffPreview-DGX7N4zr.js} +1 -1
  18. package/dist/assets/{VirtualizedFileDiff-uKfnkQWb.js → VirtualizedFileDiff-CxraYoDZ.js} +1 -1
  19. package/dist/assets/{collapsible-Bq8sDiKS.js → collapsible-d4-HChej.js} +1 -1
  20. package/dist/assets/{computeVirtualFileMetrics-BPgZ_RSe.js → computeVirtualFileMetrics-BuD14qFL.js} +2 -2
  21. package/dist/assets/{contextFileOpenGuard-CydO7PkI.js → contextFileOpenGuard-DirV-hCs.js} +1 -1
  22. package/dist/assets/{index-BSuNNtBh.js → index-BWmmf5yJ.js} +1 -1
  23. package/dist/assets/{index-CH7o4-_q.js → index-DEYblXCW.js} +7 -7
  24. package/dist/assets/index-DLKBsLKY.css +1 -0
  25. package/dist/assets/{isDiffPlainText-f9b4cKNo.js → isDiffPlainText-LoMmNzmX.js} +1 -1
  26. package/dist/assets/{isFilePlainText-BjESb-gU.js → isFilePlainText-3Hj9i02t.js} +1 -1
  27. package/dist/assets/{main-B508FBXr.js → main-BsszlAdQ.js} +2 -2
  28. package/dist/assets/main-CUvZIN16.js +133 -0
  29. package/dist/assets/{markdownCore-BFmf8a-O.js → markdownCore-sGxk8jrc.js} +2 -2
  30. package/dist/assets/{miniChat-BTlz8IPL.js → miniChat-C6SDQL4G.js} +2 -2
  31. package/dist/assets/{mobile-DBkrRt80.js → mobile-DBW3XDmZ.js} +2 -2
  32. package/dist/assets/renderElectronMiniChatApp-B1_fTdN-.js +1 -0
  33. package/dist/assets/renderMobileApp-CFxZc_1a.js +14 -0
  34. package/dist/assets/{runtimeSurface-Bec4O8FD.js → runtimeSurface-9aJdtQbh.js} +1 -1
  35. package/dist/assets/{useInlineCommentController-CHmIm0Rr.js → useInlineCommentController-Cl--s6vE.js} +2 -2
  36. package/dist/assets/usePanelSlide-D5ffPTGM.js +56 -0
  37. package/dist/assets/{useWindowTitle-CE5N_NeU.js → useWindowTitle-C-AZD713.js} +3 -3
  38. package/dist/assets/{vendor-base-ui-DwfTg5wD.js → vendor-base-ui-DL8r2fgQ.js} +1 -1
  39. package/dist/assets/{vendor-codemirror-language-data-q1yHU1R9.js → vendor-codemirror-language-data-HC9JT4I3.js} +2 -2
  40. package/dist/assets/{web-components-CLirSfwt.js → web-components-skJ_VpnT.js} +4 -4
  41. package/dist/index.html +4 -4
  42. package/dist/mini-chat.html +3 -3
  43. package/dist/mobile.html +3 -3
  44. package/package.json +1 -1
  45. package/server/lib/package-manager.js +0 -34
  46. package/server/lib/pi/routes.js +32 -2
  47. package/server/lib/pi/session-daemon/DOCUMENTATION.md +1 -1
  48. package/server/lib/pi/session-daemon/runtime-registry.js +3 -0
  49. package/server/lib/pi/session-daemon/session-daemon.js +150 -10
  50. package/server/lib/pi/session-daemon/session-daemon.test.js +22 -0
  51. package/server/lib/tunnels/DOCUMENTATION.md +6 -7
  52. package/server/lib/tunnels/executable-search.test.js +4 -4
  53. package/server/lib/tunnels/install-help.js +0 -10
  54. package/server/lib/tunnels/install-help.test.js +0 -9
  55. package/server/lib/tunnels/types.js +0 -1
  56. package/dist/assets/SettingsView-JAfC_cBL.js +0 -3
  57. package/dist/assets/SettingsWindow-BBEsjcJE.js +0 -1
  58. package/dist/assets/index-o4Eea60G.css +0 -1
  59. package/dist/assets/main-nG4uFyAy.js +0 -133
  60. package/dist/assets/renderElectronMiniChatApp-DW3fSR5w.js +0 -1
  61. package/dist/assets/renderMobileApp-9qkroKty.js +0 -14
  62. package/dist/assets/usePanelSlide-DCX8qQ4K.js +0 -56
  63. package/server/lib/ngrok-tunnel.js +0 -343
  64. package/server/lib/ngrok-tunnel.test.js +0 -50
  65. package/server/lib/tunnels/providers/ngrok.js +0 -32
@@ -1,343 +0,0 @@
1
- import { spawn, spawnSync } from 'child_process';
2
- import {
3
- createExecutableSearchEnv,
4
- resolveExecutableLaunchTarget,
5
- } from './tunnels/executable-search.js';
6
- import { getTunnelDependencyInstallInfo } from './tunnels/install-help.js';
7
- import { TUNNEL_PROVIDER_NGROK } from './tunnels/types.js';
8
-
9
- const DEFAULT_STARTUP_TIMEOUT_MS = 30000;
10
- const NGROK_API_URL = 'http://127.0.0.1:4040/api/tunnels';
11
- const NGROK_PUBLIC_URL_REGEX = /https:\/\/[^\s"']+/i;
12
- const NGROK_AUTHTOKEN_HELP = 'Run: ngrok config add-authtoken <your-ngrok-token>';
13
- const getNgrokInstallInfo = () => getTunnelDependencyInstallInfo(TUNNEL_PROVIDER_NGROK);
14
-
15
- export async function checkNgrokAvailable() {
16
- const target = resolveExecutableLaunchTarget('ngrok');
17
- if (target) {
18
- try {
19
- const result = spawnSync(target.command, ['version'], {
20
- encoding: 'utf8',
21
- stdio: ['pipe', 'pipe', 'pipe'],
22
- windowsHide: true,
23
- env: target.env,
24
- });
25
- if (result.status === 0) {
26
- return { available: true, path: target.command, version: result.stdout.trim() || result.stderr.trim() };
27
- }
28
- } catch {
29
- // Ignore and report unavailable below.
30
- }
31
- }
32
- return { available: false, path: null, version: null };
33
- }
34
-
35
- export async function checkNgrokAuthtokenConfigured(ngrokPath = null) {
36
- if (typeof process.env.NGROK_AUTHTOKEN === 'string' && process.env.NGROK_AUTHTOKEN.trim().length > 0) {
37
- return { configured: true, detail: 'NGROK_AUTHTOKEN is set.' };
38
- }
39
-
40
- const target = ngrokPath
41
- ? { command: ngrokPath, env: createExecutableSearchEnv() }
42
- : resolveExecutableLaunchTarget('ngrok');
43
- if (!target) {
44
- return { configured: false, detail: getNgrokInstallInfo().message };
45
- }
46
-
47
- try {
48
- const result = spawnSync(target.command, ['config', 'check'], {
49
- encoding: 'utf8',
50
- stdio: ['pipe', 'pipe', 'pipe'],
51
- windowsHide: true,
52
- env: target.env,
53
- });
54
- const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
55
- if (result.status === 0) {
56
- return { configured: true, detail: output || 'ngrok config is valid.' };
57
- }
58
- return { configured: false, detail: output || NGROK_AUTHTOKEN_HELP };
59
- } catch (error) {
60
- return {
61
- configured: false,
62
- detail: error instanceof Error ? error.message : String(error),
63
- };
64
- }
65
- }
66
-
67
- export async function checkNgrokApiReachability({ fetchImpl = globalThis.fetch, timeoutMs = 5000 } = {}) {
68
- if (typeof fetchImpl !== 'function') {
69
- return { reachable: false, status: null, error: 'Fetch API is unavailable in this runtime.' };
70
- }
71
-
72
- const controller = new AbortController();
73
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
74
- try {
75
- const response = await fetchImpl('https://api.ngrok.com/', {
76
- method: 'GET',
77
- signal: controller.signal,
78
- });
79
- return { reachable: true, status: response.status, error: null };
80
- } catch (error) {
81
- return {
82
- reachable: false,
83
- status: null,
84
- error: error instanceof Error ? error.message : String(error),
85
- };
86
- } finally {
87
- clearTimeout(timeout);
88
- }
89
- }
90
-
91
- const spawnNgrok = (args, resolvedBinaryPath = 'ngrok') => spawn(resolvedBinaryPath, args, {
92
- stdio: ['ignore', 'pipe', 'pipe'],
93
- windowsHide: true,
94
- env: createExecutableSearchEnv(),
95
- killSignal: 'SIGINT',
96
- });
97
-
98
- const normalizeNgrokPublicUrl = (value) => {
99
- if (typeof value !== 'string' || value.trim().length === 0) {
100
- return null;
101
- }
102
- const trimmed = value.trim();
103
- try {
104
- const parsed = new URL(trimmed);
105
- if (parsed.protocol === 'https:' && parsed.hostname.includes('ngrok')) {
106
- return parsed.toString().replace(/\/$/, '');
107
- }
108
- } catch {
109
- return null;
110
- }
111
- return null;
112
- };
113
-
114
- export function extractNgrokPublicUrlFromText(text) {
115
- if (typeof text !== 'string' || text.trim().length === 0) {
116
- return null;
117
- }
118
-
119
- const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
120
- for (const line of lines) {
121
- try {
122
- const parsed = JSON.parse(line);
123
- const parsedUrl = normalizeNgrokPublicUrl(parsed?.url) || normalizeNgrokPublicUrl(parsed?.public_url);
124
- if (parsedUrl) {
125
- return parsedUrl;
126
- }
127
- } catch {
128
- // ngrok may emit non-JSON diagnostics even when log-format=json.
129
- }
130
-
131
- const match = line.match(NGROK_PUBLIC_URL_REGEX);
132
- const matchedUrl = normalizeNgrokPublicUrl(match?.[0]);
133
- if (matchedUrl) {
134
- return matchedUrl;
135
- }
136
- }
137
-
138
- return null;
139
- }
140
-
141
- const normalizeNgrokDiagnosticText = (value) => {
142
- if (typeof value !== 'string') {
143
- return '';
144
- }
145
- return value
146
- .replace(/\r/g, '')
147
- .split('\n')
148
- .map((line) => line.trim())
149
- .filter(Boolean)
150
- .join(' ')
151
- .replace(/\s+/g, ' ')
152
- .trim();
153
- };
154
-
155
- export const summarizeNgrokOutput = (lines) => {
156
- const nonEmptyLines = Array.isArray(lines)
157
- ? lines.map((line) => String(line || '').trim()).filter(Boolean)
158
- : [];
159
- if (nonEmptyLines.length === 0) {
160
- return '';
161
- }
162
-
163
- for (const line of [...nonEmptyLines].reverse()) {
164
- try {
165
- const parsed = JSON.parse(line);
166
- const level = typeof parsed?.lvl === 'string' ? parsed.lvl.toLowerCase() : '';
167
- if (level !== 'eror' && level !== 'error' && level !== 'crit') {
168
- continue;
169
- }
170
- const err = normalizeNgrokDiagnosticText(parsed?.err);
171
- if (err && err !== '<nil>') {
172
- return err;
173
- }
174
- } catch {
175
- // Not a JSON ngrok log line.
176
- }
177
- }
178
-
179
- for (const line of [...nonEmptyLines].reverse()) {
180
- try {
181
- const parsed = JSON.parse(line);
182
- const err = normalizeNgrokDiagnosticText(parsed?.err);
183
- if (err && err !== '<nil>' && !/context canceled/i.test(err)) {
184
- return err;
185
- }
186
- const msg = normalizeNgrokDiagnosticText(parsed?.msg);
187
- if (msg && /failed|error|invalid|auth/i.test(msg)) {
188
- return msg;
189
- }
190
- } catch {
191
- // Not a JSON ngrok log line.
192
- }
193
- }
194
-
195
- const errorLines = nonEmptyLines
196
- .filter((line) => /^ERROR:/i.test(line))
197
- .map((line) => normalizeNgrokDiagnosticText(line.replace(/^ERROR:\s*/i, '')))
198
- .filter(Boolean);
199
- if (errorLines.length > 0) {
200
- return errorLines.slice(0, 4).join(' ');
201
- }
202
-
203
- const lastLine = [...nonEmptyLines].reverse().find((line) => line.trim().length > 0);
204
- if (!lastLine) {
205
- return '';
206
- }
207
- try {
208
- const parsed = JSON.parse(lastLine);
209
- if (typeof parsed?.err === 'string' && parsed.err.trim().length > 0) {
210
- return normalizeNgrokDiagnosticText(parsed.err);
211
- }
212
- if (typeof parsed?.msg === 'string' && parsed.msg.trim().length > 0) {
213
- return normalizeNgrokDiagnosticText(parsed.msg);
214
- }
215
- } catch {
216
- // Fall through to plain text output.
217
- }
218
- return normalizeNgrokDiagnosticText(lastLine);
219
- };
220
-
221
- const appendNgrokOutputSummary = (message, lines) => {
222
- const summary = summarizeNgrokOutput(lines);
223
- return summary ? `${message}: ${summary}` : message;
224
- };
225
-
226
- async function fetchNgrokPublicUrl(fetchImpl = globalThis.fetch) {
227
- if (typeof fetchImpl !== 'function') {
228
- return null;
229
- }
230
- try {
231
- const response = await fetchImpl(NGROK_API_URL, { method: 'GET' });
232
- if (!response.ok) {
233
- return null;
234
- }
235
- const payload = await response.json();
236
- const tunnels = Array.isArray(payload?.tunnels) ? payload.tunnels : [];
237
- const httpsTunnel = tunnels.find((entry) => entry?.proto === 'https' && normalizeNgrokPublicUrl(entry?.public_url));
238
- const fallbackTunnel = tunnels.find((entry) => normalizeNgrokPublicUrl(entry?.public_url));
239
- return normalizeNgrokPublicUrl(httpsTunnel?.public_url) || normalizeNgrokPublicUrl(fallbackTunnel?.public_url);
240
- } catch {
241
- return null;
242
- }
243
- }
244
-
245
- export async function startNgrokQuickTunnel({ port }) {
246
- const ngrokCheck = await checkNgrokAvailable();
247
- if (!ngrokCheck.available) {
248
- throw new Error(getNgrokInstallInfo().message);
249
- }
250
-
251
- const authtokenCheck = await checkNgrokAuthtokenConfigured(ngrokCheck.path);
252
- if (!authtokenCheck.configured) {
253
- throw new Error(`ngrok authtoken is not configured. ${authtokenCheck.detail || NGROK_AUTHTOKEN_HELP}`);
254
- }
255
-
256
- if (!Number.isFinite(port)) {
257
- throw new Error('A local port is required to start an ngrok tunnel');
258
- }
259
-
260
- const child = spawnNgrok(['http', '--log=stdout', '--log-format=json', `127.0.0.1:${port}`], ngrokCheck.path);
261
- let publicUrl = null;
262
- const recentOutput = [];
263
-
264
- const captureOutput = (chunk) => {
265
- const text = chunk.toString('utf8');
266
- const parsedUrl = extractNgrokPublicUrlFromText(text);
267
- if (parsedUrl) {
268
- publicUrl = parsedUrl;
269
- }
270
-
271
- for (const line of text.split(/\r?\n/)) {
272
- const trimmed = line.trim();
273
- if (!trimmed) {
274
- continue;
275
- }
276
- recentOutput.push(trimmed);
277
- if (recentOutput.length > 200) {
278
- recentOutput.shift();
279
- }
280
- }
281
- return text;
282
- };
283
-
284
- child.stdout.on('data', (chunk) => {
285
- captureOutput(chunk);
286
- });
287
-
288
- child.stderr.on('data', (chunk) => {
289
- const text = captureOutput(chunk);
290
- process.stderr.write(text);
291
- });
292
-
293
- await new Promise((resolve, reject) => {
294
- let settled = false;
295
- const finish = (handler, value) => {
296
- if (settled) {
297
- return;
298
- }
299
- settled = true;
300
- clearTimeout(timeout);
301
- clearInterval(checkReady);
302
- child.off('error', onError);
303
- child.off('exit', onExit);
304
- handler(value);
305
- };
306
-
307
- const timeout = setTimeout(() => {
308
- try { child.kill('SIGINT'); } catch { /* ignore */ }
309
- finish(reject, new Error(appendNgrokOutputSummary('Ngrok tunnel URL not received within 30 seconds', recentOutput)));
310
- }, DEFAULT_STARTUP_TIMEOUT_MS);
311
-
312
- const checkReady = setInterval(async () => {
313
- publicUrl = publicUrl || await fetchNgrokPublicUrl();
314
- if (publicUrl) {
315
- finish(resolve, null);
316
- }
317
- }, 250);
318
-
319
- const onError = (error) => {
320
- finish(reject, new Error(`Ngrok failed to start: ${error.message}`));
321
- };
322
-
323
- const onExit = (code) => {
324
- finish(reject, new Error(appendNgrokOutputSummary(`Ngrok exited while starting (code ${code ?? 'unknown'})`, recentOutput)));
325
- };
326
-
327
- child.once('error', onError);
328
- child.once('exit', onExit);
329
- });
330
-
331
- return {
332
- mode: 'quick',
333
- stop: () => {
334
- try {
335
- child.kill('SIGINT');
336
- } catch {
337
- // Ignore.
338
- }
339
- },
340
- process: child,
341
- getPublicUrl: () => publicUrl,
342
- };
343
- }
@@ -1,50 +0,0 @@
1
- import { describe, expect, it } from 'bun:test';
2
-
3
- import {
4
- extractNgrokPublicUrlFromText,
5
- summarizeNgrokOutput,
6
- } from './ngrok-tunnel.js';
7
-
8
- describe('extractNgrokPublicUrlFromText', () => {
9
- it('extracts public URL from ngrok JSON logs', () => {
10
- const url = extractNgrokPublicUrlFromText('{"lvl":"info","msg":"started tunnel","url":"https://demo.ngrok-free.app"}\n');
11
-
12
- expect(url).toBe('https://demo.ngrok-free.app');
13
- });
14
-
15
- it('extracts public URL from ngrok text output', () => {
16
- const url = extractNgrokPublicUrlFromText('Forwarding https://demo.ngrok-free.app -> http://127.0.0.1:3000');
17
-
18
- expect(url).toBe('https://demo.ngrok-free.app');
19
- });
20
-
21
- it('ignores non-ngrok URLs', () => {
22
- const url = extractNgrokPublicUrlFromText('{"url":"https://example.com"}\n');
23
-
24
- expect(url).toBeNull();
25
- });
26
- });
27
-
28
- describe('summarizeNgrokOutput', () => {
29
- it('prefers actionable JSON error details over trailing help text', () => {
30
- const summary = summarizeNgrokOutput([
31
- '{"err":"authentication failed: Your ngrok-agent version \\"3.3.1\\" is too old. The minimum supported agent version for your account is \\"3.20.0\\".\\r\\n\\r\\nERR_NGROK_121\\r\\n","lvl":"crit","msg":"command failed"}',
32
- 'NAME:',
33
- ' http - start an HTTP tunnel',
34
- ]);
35
-
36
- expect(summary).toContain('Your ngrok-agent version "3.3.1" is too old');
37
- expect(summary).toContain('ERR_NGROK_121');
38
- });
39
-
40
- it('skips bare ERROR lines and keeps the useful error text', () => {
41
- const summary = summarizeNgrokOutput([
42
- 'ERROR:',
43
- 'ERROR: authentication failed',
44
- 'ERROR:',
45
- 'ERROR: ERR_NGROK_121',
46
- ]);
47
-
48
- expect(summary).toBe('authentication failed ERR_NGROK_121');
49
- });
50
- });
@@ -1,32 +0,0 @@
1
- import {
2
- checkNgrokApiReachability,
3
- checkNgrokAuthtokenConfigured,
4
- checkNgrokAvailable,
5
- startNgrokQuickTunnel,
6
- } from '../../ngrok-tunnel.js';
7
-
8
- import {
9
- TUNNEL_INTENT_EPHEMERAL_PUBLIC,
10
- TUNNEL_MODE_QUICK,
11
- TUNNEL_PROVIDER_NGROK,
12
- TunnelServiceError,
13
- } from '../types.js';
14
- import { getTunnelDependencyInstallInfo } from '../install-help.js';
15
-
16
- export const ngrokTunnelProviderCapabilities = {
17
- provider: TUNNEL_PROVIDER_NGROK,
18
- defaults: {
19
- mode: TUNNEL_MODE_QUICK,
20
- optionDefaults: {},
21
- },
22
- modes: [
23
- {
24
- key: TUNNEL_MODE_QUICK,
25
- label: 'Quick Tunnel',
26
- intent: TUNNEL_INTENT_EPHEMERAL_PUBLIC,
27
- requires: [],
28
- supports: ['sessionTTL'],
29
- stability: 'beta',
30
- },
31
- ],
32
- };