@hienlh/ppm 0.9.94 → 0.9.95

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 (39) hide show
  1. package/.opencode/.env.example +98 -0
  2. package/.opencode/skills/ads-management/scripts/.env.example +13 -0
  3. package/.opencode/skills/ai-multimodal/.env.example +230 -0
  4. package/.opencode/skills/cip-design/.env.example +6 -0
  5. package/.opencode/skills/devops/.env.example +76 -0
  6. package/.opencode/skills/docs-seeker/.env.example +15 -0
  7. package/.opencode/skills/elevenlabs/.env.example +3 -0
  8. package/.opencode/skills/marketing-dashboard/.env.example +15 -0
  9. package/.opencode/skills/marketing-dashboard/app/.env.example +2 -0
  10. package/.opencode/skills/marketing-dashboard/server/.env.example +2 -0
  11. package/.opencode/skills/mcp-management/scripts/dist/analyze-tools.js +70 -0
  12. package/.opencode/skills/mcp-management/scripts/dist/cli.js +160 -0
  13. package/.opencode/skills/mcp-management/scripts/dist/mcp-client.js +183 -0
  14. package/.opencode/skills/payment-integration/scripts/.env.example +20 -0
  15. package/.opencode/skills/sequential-thinking/.env.example +8 -0
  16. package/CHANGELOG.md +5 -0
  17. package/bun.lock +6 -0
  18. package/dist/web/assets/{chat-tab-DQNdrUvL.js → chat-tab-BRM81W0L.js} +3 -3
  19. package/dist/web/assets/{code-editor-B4XNYHnl.js → code-editor-lxeFhLDX.js} +2 -2
  20. package/dist/web/assets/{conflict-editor-BcsRDSCw.js → conflict-editor-CO6NyEYJ.js} +1 -1
  21. package/dist/web/assets/{database-viewer-CfzAAtm3.js → database-viewer-CU2X8VC-.js} +1 -1
  22. package/dist/web/assets/{diff-viewer-DgA9z9Ux.js → diff-viewer-EybMrfw9.js} +1 -1
  23. package/dist/web/assets/extension-webview-DkacDy3f.js +3 -0
  24. package/dist/web/assets/{index-B4mGNywE.js → index-D7PJ14mf.js} +2 -2
  25. package/dist/web/assets/{markdown-renderer-sIjU5LtB.js → markdown-renderer-ocvtw_4F.js} +1 -1
  26. package/dist/web/assets/{port-forwarding-tab-BMXnuRuI.js → port-forwarding-tab-Chz3t_rM.js} +1 -1
  27. package/dist/web/assets/{postgres-viewer-B6Wj5xiN.js → postgres-viewer-cf8Xbssy.js} +1 -1
  28. package/dist/web/assets/{settings-tab-BKQo79HU.js → settings-tab-1jARRAlz.js} +1 -1
  29. package/dist/web/assets/{sqlite-viewer-CytNesG3.js → sqlite-viewer-CveDk6KG.js} +1 -1
  30. package/dist/web/assets/{terminal-tab-DjfxKMSB.js → terminal-tab-RvacNDWY.js} +1 -1
  31. package/dist/web/index.html +1 -1
  32. package/dist/web/sw.js +1 -1
  33. package/package.json +1 -1
  34. package/src/cli/commands/stop.ts +31 -0
  35. package/src/server/index.ts +68 -17
  36. package/src/services/autostart-generator.ts +1 -1
  37. package/src/services/autostart-register.ts +34 -29
  38. package/src/web/components/extensions/extension-webview.tsx +16 -30
  39. package/dist/web/assets/extension-webview-gHGB2Nw2.js +0 -3
@@ -347,23 +347,46 @@ export async function startServer(options: {
347
347
  await ensureCloudflared();
348
348
  }
349
349
 
350
- // Spawn supervisor process (manages server + tunnel children)
350
+ // ── Try starting via system service manager (crash recovery) ────────
351
+ // If autostart was previously enabled, start via systemd/launchd so the OS
352
+ // monitors the supervisor and restarts it on crash (Restart=always).
353
+ let startedViaService = false;
354
+ {
355
+ const { getAutoStartStatus } = await import("../services/autostart-register.ts");
356
+ const autoStatus = getAutoStartStatus();
357
+ if (autoStatus.enabled && !autoStatus.running) {
358
+ if (process.platform === "linux") {
359
+ // Update service file in case config changed (port, share, etc.)
360
+ const { enableAutoStart } = await import("../services/autostart-register.ts");
361
+ await enableAutoStart({ port, host, share: !!options.share, configPath: options.config, profile: options.profile });
362
+ startedViaService = true;
363
+ } else if (process.platform === "darwin") {
364
+ const { enableAutoStart } = await import("../services/autostart-register.ts");
365
+ await enableAutoStart({ port, host, share: !!options.share, configPath: options.config, profile: options.profile });
366
+ startedViaService = true;
367
+ }
368
+ }
369
+ }
370
+
371
+ // ── Spawn supervisor directly (fallback or first run) ────────────────
351
372
  const isCompiledBin = isCompiledBinary();
352
373
  const logFile = resolve(ppmDir, "ppm.log");
353
374
  const logFd = openSync(logFile, "a");
354
375
  const supervisorScript = resolve(import.meta.dir, "..", "services", "supervisor.ts");
355
376
 
356
- const superviseArgs = [
357
- "__supervise__", String(port), host,
358
- options.config ?? "", options.profile ?? "",
359
- ];
360
- if (options.share) superviseArgs.push("--share");
361
- // Strip trailing empty args (before --share flag)
362
- while (superviseArgs.length > 1 && superviseArgs[superviseArgs.length - 1] === "") superviseArgs.pop();
363
-
364
377
  let supervisorPid: number;
365
378
 
366
- if (process.platform === "win32") {
379
+ if (startedViaService) {
380
+ // Supervisor was started by systemd/launchd — read PID from status.json
381
+ supervisorPid = 0; // will be read from status.json below
382
+ } else if (process.platform === "win32") {
383
+ const superviseArgs = [
384
+ "__supervise__", String(port), host,
385
+ options.config ?? "", options.profile ?? "",
386
+ ];
387
+ if (options.share) superviseArgs.push("--share");
388
+ while (superviseArgs.length > 1 && superviseArgs[superviseArgs.length - 1] === "") superviseArgs.pop();
389
+
367
390
  const bunExe = process.execPath.replace(/\\/g, "\\\\");
368
391
  const logEscaped = logFile.replace(/\\/g, "\\\\");
369
392
  const errLog = logFile.replace(/\.log$/, ".err.log").replace(/\\/g, "\\\\");
@@ -388,6 +411,13 @@ export async function startServer(options: {
388
411
  process.exit(1);
389
412
  }
390
413
  } else {
414
+ const superviseArgs = [
415
+ "__supervise__", String(port), host,
416
+ options.config ?? "", options.profile ?? "",
417
+ ];
418
+ if (options.share) superviseArgs.push("--share");
419
+ while (superviseArgs.length > 1 && superviseArgs[superviseArgs.length - 1] === "") superviseArgs.pop();
420
+
391
421
  const cmd = isCompiledBin
392
422
  ? [process.execPath, ...superviseArgs]
393
423
  : [process.execPath, "run", supervisorScript, ...superviseArgs];
@@ -405,26 +435,30 @@ export async function startServer(options: {
405
435
  let serverPid: number | null = null;
406
436
  while (Date.now() - startWait < 10_000) {
407
437
  await Bun.sleep(500);
408
- // Check supervisor is still alive
409
- try { process.kill(supervisorPid, 0); } catch {
410
- console.error(" ✗ Supervisor exited immediately after start.");
411
- console.error(" Check logs: ppm logs");
412
- process.exit(1);
413
- }
414
438
  // Check if server PID appeared in status.json
415
439
  try {
416
440
  const data = JSON.parse(readFileSync(statusFile, "utf-8"));
417
441
  if (data.pid && data.supervisorPid) {
442
+ // Update supervisorPid if started via service (was 0 initially)
443
+ if (!supervisorPid) supervisorPid = data.supervisorPid;
418
444
  serverPid = data.pid;
419
445
  break;
420
446
  }
421
447
  } catch {}
448
+ // Check supervisor is still alive (skip if PID unknown from service start)
449
+ if (supervisorPid) {
450
+ try { process.kill(supervisorPid, 0); } catch {
451
+ console.error(" ✗ Supervisor exited immediately after start.");
452
+ console.error(" Check logs: ppm logs");
453
+ process.exit(1);
454
+ }
455
+ }
422
456
  }
423
457
 
424
458
  if (!serverPid) {
425
459
  console.error(" ✗ Server did not start within 10 seconds.");
426
460
  console.error(" Check logs: ppm logs");
427
- try { process.kill(supervisorPid); } catch {}
461
+ if (supervisorPid) { try { process.kill(supervisorPid); } catch {} }
428
462
  process.exit(1);
429
463
  }
430
464
 
@@ -456,6 +490,23 @@ export async function startServer(options: {
456
490
  qr.generate(shareUrl, { small: true });
457
491
  }
458
492
 
493
+ // Auto-enable system service (systemd/launchd) for boot resilience
494
+ try {
495
+ const { getAutoStartStatus, enableAutoStart } = await import("../services/autostart-register.ts");
496
+ const status = getAutoStartStatus();
497
+ if (!status.enabled) {
498
+ const autoConfig = {
499
+ port, host,
500
+ share: !!options.share,
501
+ configPath: options.config,
502
+ profile: options.profile,
503
+ };
504
+ // skipStart: supervisor is already running from direct spawn above
505
+ await enableAutoStart(autoConfig, { skipStart: true });
506
+ console.log(` ✓ Auto-restart enabled (${status.platform}). Disable: ppm autostart disable`);
507
+ }
508
+ } catch {}
509
+
459
510
  console.log(` Commands:`);
460
511
  console.log(` ppm restart Reload config (keeps tunnel URL)`);
461
512
  console.log(` ppm stop Stop server & tunnel`);
@@ -132,7 +132,7 @@ Wants=network-online.target
132
132
  [Service]
133
133
  Type=simple
134
134
  ExecStart=${execStart}
135
- Restart=always
135
+ Restart=on-failure
136
136
  RestartSec=5
137
137
  ${envPath}
138
138
  WorkingDirectory=${homedir()}/.ppm
@@ -60,34 +60,37 @@ function removeMetadata(): void {
60
60
 
61
61
  // ─── macOS ──────────────────────────────────────────────────────────────
62
62
 
63
- async function enableMacOS(config: AutoStartConfig): Promise<string> {
63
+ async function enableMacOS(config: AutoStartConfig, opts?: { skipStart?: boolean }): Promise<string> {
64
64
  const plistPath = getPlistPath();
65
65
  const plistDir = dirname(plistPath);
66
66
 
67
67
  if (!existsSync(plistDir)) mkdirSync(plistDir, { recursive: true });
68
68
  writeFileSync(plistPath, generatePlist(config));
69
69
 
70
- // Unload first if already loaded (ignore errors)
71
- Bun.spawnSync({
72
- cmd: ["launchctl", "bootout", `gui/${process.getuid!()}`, plistPath],
73
- stdout: "ignore", stderr: "ignore",
74
- });
75
-
76
- // Load the agent
77
- const result = Bun.spawnSync({
78
- cmd: ["launchctl", "bootstrap", `gui/${process.getuid!()}`, plistPath],
79
- stdout: "pipe", stderr: "pipe",
80
- });
70
+ // Skip loading if supervisor is already running from direct spawn
71
+ if (!opts?.skipStart) {
72
+ // Unload first if already loaded (ignore errors)
73
+ Bun.spawnSync({
74
+ cmd: ["launchctl", "bootout", `gui/${process.getuid!()}`, plistPath],
75
+ stdout: "ignore", stderr: "ignore",
76
+ });
81
77
 
82
- if (result.exitCode !== 0) {
83
- // Fallback to legacy syntax
84
- const legacy = Bun.spawnSync({
85
- cmd: ["launchctl", "load", plistPath],
78
+ // Load the agent
79
+ const result = Bun.spawnSync({
80
+ cmd: ["launchctl", "bootstrap", `gui/${process.getuid!()}`, plistPath],
86
81
  stdout: "pipe", stderr: "pipe",
87
82
  });
88
- if (legacy.exitCode !== 0) {
89
- const err = legacy.stderr.toString().trim();
90
- throw new Error(`launchctl load failed: ${err}`);
83
+
84
+ if (result.exitCode !== 0) {
85
+ // Fallback to legacy syntax
86
+ const legacy = Bun.spawnSync({
87
+ cmd: ["launchctl", "load", plistPath],
88
+ stdout: "pipe", stderr: "pipe",
89
+ });
90
+ if (legacy.exitCode !== 0) {
91
+ const err = legacy.stderr.toString().trim();
92
+ throw new Error(`launchctl load failed: ${err}`);
93
+ }
91
94
  }
92
95
  }
93
96
 
@@ -146,7 +149,7 @@ function statusMacOS(): AutoStartStatus {
146
149
 
147
150
  // ─── Linux ──────────────────────────────────────────────────────────────
148
151
 
149
- async function enableLinux(config: AutoStartConfig): Promise<string> {
152
+ async function enableLinux(config: AutoStartConfig, opts?: { skipStart?: boolean }): Promise<string> {
150
153
  const servicePath = getServicePath();
151
154
  const serviceDir = dirname(servicePath);
152
155
 
@@ -171,11 +174,13 @@ async function enableLinux(config: AutoStartConfig): Promise<string> {
171
174
  throw new Error(`systemctl enable failed: ${enable.stderr.toString().trim()}`);
172
175
  }
173
176
 
174
- // Start
175
- Bun.spawnSync({
176
- cmd: ["systemctl", "--user", "start", "ppm.service"],
177
- stdout: "ignore", stderr: "ignore",
178
- });
177
+ // Start (skip if supervisor is already running from direct spawn)
178
+ if (!opts?.skipStart) {
179
+ Bun.spawnSync({
180
+ cmd: ["systemctl", "--user", "start", "ppm.service"],
181
+ stdout: "ignore", stderr: "ignore",
182
+ });
183
+ }
179
184
 
180
185
  // Enable lingering so service runs at boot without login
181
186
  Bun.spawnSync({
@@ -312,11 +317,11 @@ function statusWindows(): AutoStartStatus {
312
317
 
313
318
  // ─── Public API ─────────────────────────────────────────────────────────
314
319
 
315
- /** Enable auto-start for the current platform */
316
- export async function enableAutoStart(config: AutoStartConfig): Promise<string> {
320
+ /** Enable auto-start for the current platform. skipStart=true registers without starting (when supervisor is already running). */
321
+ export async function enableAutoStart(config: AutoStartConfig, opts?: { skipStart?: boolean }): Promise<string> {
317
322
  const platform = process.platform;
318
- if (platform === "darwin") return enableMacOS(config);
319
- if (platform === "linux") return enableLinux(config);
323
+ if (platform === "darwin") return enableMacOS(config, opts);
324
+ if (platform === "linux") return enableLinux(config, opts);
320
325
  if (platform === "win32") return enableWindows(config);
321
326
  throw new Error(`Auto-start not supported on ${platform}`);
322
327
  }
@@ -54,47 +54,33 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
54
54
  const rawHtml = panel?.html ?? "";
55
55
  const html = injectVscodeApiShim(rawHtml);
56
56
 
57
- // On reload: resolve project path, then dispatch command with retry
58
- // Retry needed because WS connection may not be ready on first attempt
57
+ // On reload: resolve project path and dispatch command once
58
+ // No retry if it fails, user closes tab and reopens to retry
59
59
  useEffect(() => {
60
60
  if (panel || !viewType) return;
61
- // Mark project as "dispatched" so project-sync effect doesn't double-dispatch
62
61
  if (projectName) prevProjectRef.current = projectName;
63
62
  const command = viewType.includes(".") ? viewType : `${viewType}.view`;
64
63
  let cancelled = false;
65
- let resolvedArgs: unknown[] | null = null;
66
64
 
67
- async function resolveArgs(): Promise<unknown[]> {
68
- if (resolvedArgs) return resolvedArgs;
69
- if (!projectName) return [];
70
- try {
71
- const res = await fetch("/api/projects");
72
- const json = await res.json() as { ok: boolean; data?: { name: string; path: string }[] };
73
- const match = json.data?.find((p) => p.name === projectName);
74
- resolvedArgs = match ? [match.path] : [];
75
- } catch {
76
- resolvedArgs = [];
65
+ async function dispatch() {
66
+ let args: unknown[] = [];
67
+ if (projectName) {
68
+ try {
69
+ const res = await fetch("/api/projects");
70
+ const json = await res.json() as { ok: boolean; data?: { name: string; path: string }[] };
71
+ const match = json.data?.find((p) => p.name === projectName);
72
+ if (match) args = [match.path];
73
+ } catch {}
77
74
  }
78
- return resolvedArgs;
79
- }
80
-
81
- async function attempt() {
82
- const args = await resolveArgs();
83
75
  if (cancelled) return;
84
76
  window.dispatchEvent(new CustomEvent("ext:command:execute", {
85
77
  detail: { command, args },
86
78
  }));
87
79
  }
88
80
 
89
- // First attempt after short delay (let WS connect), then retry every 2s
90
- const initialTimer = setTimeout(() => {
91
- if (!cancelled) attempt();
92
- }, 500);
93
- const retryTimer = setInterval(() => {
94
- if (!cancelled) attempt();
95
- }, 2_000);
96
-
97
- return () => { cancelled = true; clearTimeout(initialTimer); clearInterval(retryTimer); };
81
+ // Short delay to let WS connect after page load
82
+ const timer = setTimeout(dispatch, 500);
83
+ return () => { cancelled = true; clearTimeout(timer); };
98
84
  }, [panel, viewType, projectName]);
99
85
 
100
86
  // When panel exists, ensure correct project is loaded.
@@ -168,10 +154,10 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
168
154
  };
169
155
  }, []);
170
156
 
171
- // Timeout: if panel doesn't appear within 10s, show error
157
+ // Timeout: if panel doesn't appear within 5s, show error
172
158
  useEffect(() => {
173
159
  if (panel) { setTimedOut(false); return; }
174
- const timer = setTimeout(() => setTimedOut(true), 10_000);
160
+ const timer = setTimeout(() => setTimedOut(true), 5_000);
175
161
  return () => clearTimeout(timer);
176
162
  }, [panel]);
177
163
 
@@ -1,3 +0,0 @@
1
- import{o as e}from"./rolldown-runtime-FhOqtrmT.js";import{b as t,x as n}from"./vendor-markdown-0Mxgxy0L.js";import{t as r}from"./tab-store-DZbiYk7y.js";import{t as i}from"./extension-store-3yZYn07W.js";import{A as a}from"./index-B4mGNywE.js";var o=e(n(),1),s=t(),c=`<script>
2
- function acquireVsCodeApi(){return{postMessage:function(m){window.parent.postMessage(m,"*")},getState:function(){try{return JSON.parse(sessionStorage.getItem("vscode-state")||"null")}catch{return null}},setState:function(s){sessionStorage.setItem("vscode-state",JSON.stringify(s));return s}}}
3
- <\/script>`;function l(e){if(!e)return e;let t=e.indexOf(`<head>`);return t===-1?c+e:e.slice(0,t+6)+c+e.slice(t+6)}function u({metadata:e}){let t=e?.panelId,n=e?.viewType,c=r(e=>e.currentProject)||e?.projectName||void 0,[u,d]=(0,o.useState)(!1),f=i(e=>{if(t&&e.webviewPanels[t])return e.webviewPanels[t];if(n){let t=n.includes(`.`)?n:`${n}.view`;return Object.values(e.webviewPanels).find(e=>e.viewType===n||e.viewType===t)}}),p=f?.id??t,m=(0,o.useRef)(null),h=f?.html??``,g=l(h);(0,o.useEffect)(()=>{if(f||!n)return;c&&(_.current=c);let e=n.includes(`.`)?n:`${n}.view`,t=!1,r=null;async function i(){if(r)return r;if(!c)return[];try{let e=(await(await fetch(`/api/projects`)).json()).data?.find(e=>e.name===c);r=e?[e.path]:[]}catch{r=[]}return r}async function a(){let n=await i();t||window.dispatchEvent(new CustomEvent(`ext:command:execute`,{detail:{command:e,args:n}}))}let o=setTimeout(()=>{t||a()},500),s=setInterval(()=>{t||a()},2e3);return()=>{t=!0,clearTimeout(o),clearInterval(s)}},[f,n,c]);let _=(0,o.useRef)(null);(0,o.useEffect)(()=>{if(!f||!n||!c||c===_.current)return;_.current=c;let e=n.includes(`.`)?n:`${n}.view`;(async()=>{try{let t=(await(await fetch(`/api/projects`)).json()).data?.find(e=>e.name===c);t&&window.dispatchEvent(new CustomEvent(`ext:command:execute`,{detail:{command:e,args:[t.path]}}))}catch{}})()},[f,n,c]);let v=e?.extensionId,y=i(e=>{if(v&&e.activationErrors[v])return e.activationErrors[v];if(n){for(let[t,r]of Object.entries(e.activationErrors))if(t===`ext-${n}`)return r}}),b=(0,o.useCallback)(()=>{if(d(!1),!n)return;let e=n.includes(`.`)?n:`${n}.view`;(async()=>{try{let t=(await(await fetch(`/api/projects`)).json()).data?.find(e=>e.name===c),n=t?[t.path]:[];window.dispatchEvent(new CustomEvent(`ext:command:execute`,{detail:{command:e,args:n}}))}catch{}})()},[n,c]),x=(0,o.useRef)(null);return(0,o.useEffect)(()=>{x.current=p??null},[p]),(0,o.useEffect)(()=>()=>{let e=x.current;e&&(i.getState().removeWebviewPanel(e),window.dispatchEvent(new CustomEvent(`ext:webview:close`,{detail:{panelId:e}})))},[]),(0,o.useEffect)(()=>{if(f){d(!1);return}let e=setTimeout(()=>d(!0),1e4);return()=>clearTimeout(e)},[f]),(0,o.useEffect)(()=>{if(!p)return;let e=e=>{m.current&&e.source===m.current.contentWindow&&window.dispatchEvent(new CustomEvent(`ext:webview:send`,{detail:{panelId:p,message:e.data}}))};return window.addEventListener(`message`,e),()=>window.removeEventListener(`message`,e)},[p]),(0,o.useEffect)(()=>{if(!p)return;let e=e=>{let{panelId:t,message:n}=e.detail;t===p&&m.current?.contentWindow?.postMessage(n,`*`)};return window.addEventListener(`ext:webview:message`,e),()=>window.removeEventListener(`ext:webview:message`,e)},[p]),!f||!h?(0,s.jsx)(`div`,{className:`flex flex-col items-center justify-center h-full gap-3 text-sm text-text-subtle`,children:u?(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(`span`,{className:`text-destructive font-medium`,children:`Extension failed to load`}),y&&(0,s.jsx)(`span`,{className:`text-xs text-muted-foreground max-w-md text-center`,children:y}),(0,s.jsx)(`button`,{onClick:b,className:`text-xs text-primary hover:underline`,children:`Retry`})]}):(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(a,{className:`size-5 animate-spin`}),(0,s.jsx)(`span`,{children:`Loading extension...`})]})}):(0,s.jsx)(`div`,{className:`h-full w-full relative`,children:(0,s.jsx)(`iframe`,{ref:m,srcDoc:g,sandbox:`allow-scripts`,className:`w-full h-full border-0 bg-white dark:bg-zinc-900`,title:f.title},p)})}export{u as ExtensionWebview};