@hienlh/ppm 0.9.80 → 0.9.82
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/.opencode/.env.example +98 -0
- package/.opencode/skills/ads-management/scripts/.env.example +13 -0
- package/.opencode/skills/ai-multimodal/.env.example +230 -0
- package/.opencode/skills/cip-design/.env.example +6 -0
- package/.opencode/skills/devops/.env.example +76 -0
- package/.opencode/skills/docs-seeker/.env.example +15 -0
- package/.opencode/skills/elevenlabs/.env.example +3 -0
- package/.opencode/skills/marketing-dashboard/.env.example +15 -0
- package/.opencode/skills/marketing-dashboard/app/.env.example +2 -0
- package/.opencode/skills/marketing-dashboard/server/.env.example +2 -0
- package/.opencode/skills/mcp-management/scripts/dist/analyze-tools.js +70 -0
- package/.opencode/skills/mcp-management/scripts/dist/cli.js +160 -0
- package/.opencode/skills/mcp-management/scripts/dist/mcp-client.js +183 -0
- package/.opencode/skills/payment-integration/scripts/.env.example +20 -0
- package/.opencode/skills/sequential-thinking/.env.example +8 -0
- package/.repomixignore +22 -0
- package/AGENTS.md +62 -0
- package/CHANGELOG.md +17 -0
- package/CLAUDE.md +12 -0
- package/assets/skills/ppm-guide/SKILL.md +61 -0
- package/bun.lock +9 -1
- package/dist/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/web/assets/chat-tab-bS86TsT5.js +10 -0
- package/dist/web/assets/{code-editor-BFe-hnpF.js → code-editor-BaNaQ33b.js} +1 -1
- package/dist/web/assets/{database-viewer-BeY2V5QI.js → database-viewer-C5MVw8cJ.js} +1 -1
- package/dist/web/assets/{diff-viewer-D6xzs8PP.js → diff-viewer-CUbFMWVo.js} +1 -1
- package/dist/web/assets/{extension-webview-Cd1XYFXO.js → extension-webview-CwGufYEP.js} +1 -1
- package/dist/web/assets/{git-graph-D2XXpiMQ.js → git-graph-BD7A7MLo.js} +1 -1
- package/dist/web/assets/index-BYXjCNlK.css +2 -0
- package/dist/web/assets/index-CpzkPHOC.js +30 -0
- package/dist/web/assets/keybindings-store-DsaANvBz.js +1 -0
- package/dist/web/assets/markdown-renderer-C19IsITh.js +326 -0
- package/dist/web/assets/{port-forwarding-tab-B5rj_I66.js → port-forwarding-tab-BF79F1iL.js} +1 -1
- package/dist/web/assets/{postgres-viewer-DnlqzOnm.js → postgres-viewer-_nYiO_wp.js} +1 -1
- package/dist/web/assets/{settings-tab-CNZpuPD3.js → settings-tab-C1SQMbSu.js} +1 -1
- package/dist/web/assets/{sql-query-editor-Df2kzbPj.js → sql-query-editor-6OFvxxuN.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-Cj1G70z4.js → sqlite-viewer-SNVYFXvB.js} +1 -1
- package/dist/web/assets/{terminal-tab-Dv9A7Xe2.js → terminal-tab-BJEkmrDt.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-CPfIEo8t.js → use-monaco-theme-r8FzlCWr.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +78 -0
- package/docs/project-changelog.md +29 -0
- package/docs/system-architecture.md +2 -0
- package/package.json +5 -2
- package/release-manifest.json +15784 -0
- package/scripts/check-ppm-dir-usage.sh +21 -0
- package/scripts/generate-ppm-guide.ts +92 -0
- package/src/cli/commands/init.ts +2 -1
- package/src/cli/commands/logs.ts +11 -11
- package/src/cli/commands/report.ts +3 -2
- package/src/cli/commands/restart.ts +22 -23
- package/src/cli/commands/skills-cmd.ts +123 -0
- package/src/cli/commands/status.ts +7 -8
- package/src/cli/commands/stop.ts +18 -19
- package/src/index.ts +3 -0
- package/src/lib/account-crypto.ts +12 -7
- package/src/providers/claude-agent-sdk.ts +42 -11
- package/src/server/index.ts +8 -8
- package/src/server/routes/chat.ts +4 -2
- package/src/server/routes/upgrade.ts +3 -5
- package/src/server/ws/chat.ts +31 -0
- package/src/services/cloud-ws.service.ts +6 -3
- package/src/services/cloud.service.ts +20 -19
- package/src/services/cloudflared.service.ts +13 -13
- package/src/services/config.service.ts +5 -7
- package/src/services/db.service.ts +5 -6
- package/src/services/extension-rpc-handlers.ts +2 -2
- package/src/services/extension.service.ts +9 -12
- package/src/services/ppm-dir.ts +14 -0
- package/src/services/slash-discovery/builtin-commands.ts +53 -0
- package/src/services/slash-discovery/builtin-handlers.ts +65 -0
- package/src/services/slash-discovery/definition-source.ts +27 -0
- package/src/services/slash-discovery/discover-skill-roots.ts +128 -0
- package/src/services/slash-discovery/fuzzy-search.ts +76 -0
- package/src/services/slash-discovery/index.ts +42 -0
- package/src/services/slash-discovery/resolve-overrides.ts +41 -0
- package/src/services/slash-discovery/skill-loader.ts +156 -0
- package/src/services/slash-discovery/types.ts +51 -0
- package/src/services/slash-items.service.ts +4 -182
- package/src/services/supervisor-state.ts +14 -15
- package/src/services/supervisor-stopped-page.ts +2 -4
- package/src/services/supervisor.ts +15 -15
- package/src/services/tunnel.service.ts +22 -5
- package/src/services/upgrade.service.ts +2 -3
- package/src/types/chat.ts +3 -1
- package/src/web/components/chat/chat-history-bar.tsx +2 -15
- package/src/web/components/chat/chat-tab.tsx +5 -2
- package/src/web/components/chat/message-input.tsx +48 -6
- package/src/web/components/chat/message-list.tsx +19 -5
- package/src/web/components/chat/slash-command-picker.tsx +21 -12
- package/src/web/components/layout/mobile-nav.tsx +47 -21
- package/src/web/components/layout/panel-layout.tsx +11 -0
- package/src/web/components/layout/upgrade-banner.tsx +48 -2
- package/src/web/components/shared/markdown-renderer.tsx +5 -2
- package/src/web/hooks/use-chat.ts +33 -1
- package/src/web/main.tsx +1 -0
- package/src/web/stores/panel-store.ts +25 -1
- package/src/web/styles/globals.css +14 -0
- package/dist/web/assets/chat-tab-CmSLt4tg.js +0 -10
- package/dist/web/assets/index-BtwsLrdT.css +0 -2
- package/dist/web/assets/index-D6_wwsL_.js +0 -30
- package/dist/web/assets/keybindings-store-C8ryKudw.js +0 -1
- package/dist/web/assets/markdown-renderer-xYMhd9cE.js +0 -69
|
@@ -109,6 +109,7 @@ interface StreamingSession {
|
|
|
109
109
|
*/
|
|
110
110
|
interface PendingApproval {
|
|
111
111
|
resolve: (result: { approved: boolean; data?: unknown }) => void;
|
|
112
|
+
sessionId: string;
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
/**
|
|
@@ -360,7 +361,13 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
360
361
|
this.closeStreamingSession(sessionId);
|
|
361
362
|
this.activeSessions.delete(sessionId);
|
|
362
363
|
this.messageCount.delete(sessionId);
|
|
363
|
-
this
|
|
364
|
+
// Resolve and clean up all pending approvals for this session
|
|
365
|
+
for (const [reqId, pending] of this.pendingApprovals) {
|
|
366
|
+
if (pending.sessionId === sessionId) {
|
|
367
|
+
pending.resolve({ approved: false });
|
|
368
|
+
this.pendingApprovals.delete(reqId);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
364
371
|
this.forkSources.delete(sessionId);
|
|
365
372
|
|
|
366
373
|
// Best-effort: delete JSONL from ~/.claude/projects/
|
|
@@ -472,10 +479,32 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
472
479
|
}
|
|
473
480
|
|
|
474
481
|
async *sendMessage(
|
|
475
|
-
|
|
482
|
+
_sessionId: string,
|
|
476
483
|
message: string,
|
|
477
484
|
opts?: import("./provider.interface.ts").SendMessageOpts & { forkSession?: boolean; priority?: 'now' | 'next' | 'later'; images?: Array<{ data: string; mediaType: string }> },
|
|
478
485
|
): AsyncIterable<ChatEvent> {
|
|
486
|
+
// SDK requires valid UUID session IDs. Short/random IDs can leak from
|
|
487
|
+
// tab derivation or URL parsing — migrate to a real UUID early.
|
|
488
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
489
|
+
let sessionId = _sessionId;
|
|
490
|
+
if (!UUID_RE.test(sessionId)) {
|
|
491
|
+
const newId = crypto.randomUUID();
|
|
492
|
+
console.warn(`[sdk] session=${sessionId} is not a valid UUID — migrating to ${newId}`);
|
|
493
|
+
// Migrate internal maps
|
|
494
|
+
const oldMeta = this.activeSessions.get(sessionId);
|
|
495
|
+
if (oldMeta) {
|
|
496
|
+
this.activeSessions.delete(sessionId);
|
|
497
|
+
oldMeta.id = newId;
|
|
498
|
+
this.activeSessions.set(newId, oldMeta);
|
|
499
|
+
}
|
|
500
|
+
const oldCount = this.messageCount.get(sessionId);
|
|
501
|
+
if (oldCount != null) { this.messageCount.set(newId, oldCount); this.messageCount.delete(sessionId); }
|
|
502
|
+
const oldStream = this.streamingSessions.get(sessionId);
|
|
503
|
+
if (oldStream) { this.streamingSessions.set(newId, oldStream); this.streamingSessions.delete(sessionId); }
|
|
504
|
+
yield { type: "session_migrated" as const, oldSessionId: sessionId, newSessionId: newId };
|
|
505
|
+
sessionId = newId;
|
|
506
|
+
}
|
|
507
|
+
|
|
479
508
|
// Follow-up: push into existing streaming session, yield nothing
|
|
480
509
|
const existingStream = this.streamingSessions.get(sessionId);
|
|
481
510
|
if (existingStream) {
|
|
@@ -543,15 +572,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
543
572
|
*/
|
|
544
573
|
const waitForApproval = (toolName: string, input: unknown): Promise<{ approved: boolean; data?: unknown }> => {
|
|
545
574
|
const requestId = crypto.randomUUID();
|
|
546
|
-
|
|
575
|
+
// No timeout — approval waits indefinitely until user responds or session cleanup resolves it.
|
|
547
576
|
const promise = new Promise<{ approved: boolean; data?: unknown }>((resolve) => {
|
|
548
|
-
this.pendingApprovals.set(requestId, { resolve });
|
|
549
|
-
setTimeout(() => {
|
|
550
|
-
if (this.pendingApprovals.has(requestId)) {
|
|
551
|
-
this.pendingApprovals.delete(requestId);
|
|
552
|
-
resolve({ approved: false });
|
|
553
|
-
}
|
|
554
|
-
}, APPROVAL_TIMEOUT_MS);
|
|
577
|
+
this.pendingApprovals.set(requestId, { resolve, sessionId });
|
|
555
578
|
});
|
|
556
579
|
approvalEvents.push({ type: "approval_request", requestId, tool: toolName, input });
|
|
557
580
|
approvalNotify?.();
|
|
@@ -609,7 +632,6 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
609
632
|
PreToolUse: [{
|
|
610
633
|
matcher: ".*", // Match all tools — our hook checks internally
|
|
611
634
|
hooks: [preToolUseHook],
|
|
612
|
-
timeout: 300, // 5min for user approval
|
|
613
635
|
}],
|
|
614
636
|
};
|
|
615
637
|
|
|
@@ -617,6 +639,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
617
639
|
let resultSubtype: string | undefined;
|
|
618
640
|
let resultNumTurns: number | undefined;
|
|
619
641
|
let resultContextWindowPct: number | undefined;
|
|
642
|
+
let lastAssistantUuid: string | undefined;
|
|
620
643
|
let yieldedDone = false;
|
|
621
644
|
try {
|
|
622
645
|
// Session ID is the canonical ID for both PPM and SDK (no dual-ID mapping).
|
|
@@ -977,6 +1000,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
977
1000
|
// Partial assistant message — streaming text deltas
|
|
978
1001
|
if ((msg as any).type === "partial" || (msg as any).type === "stream_event") {
|
|
979
1002
|
const partial = msg as any;
|
|
1003
|
+
// Track assistant UUID from top-level messages (not subagent children)
|
|
1004
|
+
if (!parentId && partial.uuid) lastAssistantUuid = partial.uuid;
|
|
980
1005
|
// Handle stream_event (raw API events) for text deltas
|
|
981
1006
|
if ((msg as any).type === "stream_event") {
|
|
982
1007
|
const event = partial.event;
|
|
@@ -1014,6 +1039,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1014
1039
|
|
|
1015
1040
|
// Full assistant message
|
|
1016
1041
|
if (msg.type === "assistant") {
|
|
1042
|
+
// Track assistant UUID from top-level messages (not subagent children)
|
|
1043
|
+
if (!parentId && (msg as any).uuid) lastAssistantUuid = (msg as any).uuid;
|
|
1017
1044
|
// SDK assistant messages can carry an error field for auth/billing/rate-limit failures
|
|
1018
1045
|
let assistantError = (msg as any).error as string | undefined;
|
|
1019
1046
|
|
|
@@ -1361,6 +1388,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1361
1388
|
resultSubtype: resultSubtype as any,
|
|
1362
1389
|
numTurns: resultNumTurns,
|
|
1363
1390
|
contextWindowPct: resultContextWindowPct,
|
|
1391
|
+
lastMessageUuid: lastAssistantUuid,
|
|
1364
1392
|
};
|
|
1365
1393
|
|
|
1366
1394
|
// Reset per-turn state for next turn
|
|
@@ -1370,6 +1398,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1370
1398
|
resultSubtype = undefined;
|
|
1371
1399
|
resultNumTurns = undefined;
|
|
1372
1400
|
resultContextWindowPct = undefined;
|
|
1401
|
+
lastAssistantUuid = undefined;
|
|
1373
1402
|
sdkEventCount = 0;
|
|
1374
1403
|
continue; // Wait for next turn from generator
|
|
1375
1404
|
}
|
|
@@ -1438,6 +1467,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1438
1467
|
resultSubtype: resultSubtype as any,
|
|
1439
1468
|
numTurns: resultNumTurns,
|
|
1440
1469
|
contextWindowPct: resultContextWindowPct,
|
|
1470
|
+
lastMessageUuid: lastAssistantUuid,
|
|
1441
1471
|
};
|
|
1442
1472
|
}
|
|
1443
1473
|
}
|
|
@@ -1551,6 +1581,7 @@ function parseSessionMessage(msg: { uuid: string; type: string; message: unknown
|
|
|
1551
1581
|
content: textContent,
|
|
1552
1582
|
events: events.length > 0 ? events : undefined,
|
|
1553
1583
|
timestamp: new Date().toISOString(),
|
|
1584
|
+
sdkUuid: msg.uuid,
|
|
1554
1585
|
};
|
|
1555
1586
|
}
|
|
1556
1587
|
|
package/src/server/index.ts
CHANGED
|
@@ -29,10 +29,10 @@ async function setupLogFile() {
|
|
|
29
29
|
(globalThis as any).__PPM_LOG_SETUP__ = true;
|
|
30
30
|
|
|
31
31
|
const { resolve } = await import("node:path");
|
|
32
|
-
const { homedir } = await import("node:os");
|
|
33
32
|
const { appendFileSync, mkdirSync, existsSync } = await import("node:fs");
|
|
33
|
+
const { getPpmDir } = await import("../services/ppm-dir.ts");
|
|
34
34
|
|
|
35
|
-
const ppmDir =
|
|
35
|
+
const ppmDir = getPpmDir();
|
|
36
36
|
if (!existsSync(ppmDir)) mkdirSync(ppmDir, { recursive: true });
|
|
37
37
|
const logPath = resolve(ppmDir, "ppm.log");
|
|
38
38
|
|
|
@@ -104,9 +104,9 @@ app.get("/api/info", (c) => c.json(ok({
|
|
|
104
104
|
// Public: recent logs for bug reports (last 30 lines)
|
|
105
105
|
app.get("/api/logs/recent", async (c) => {
|
|
106
106
|
const { resolve } = await import("node:path");
|
|
107
|
-
const { homedir } = await import("node:os");
|
|
108
107
|
const { existsSync, readFileSync } = await import("node:fs");
|
|
109
|
-
const
|
|
108
|
+
const { getPpmDir } = await import("../services/ppm-dir.ts");
|
|
109
|
+
const logFile = resolve(getPpmDir(), "ppm.log");
|
|
110
110
|
if (!existsSync(logFile)) return c.json(ok({ logs: "" }));
|
|
111
111
|
const content = readFileSync(logFile, "utf-8");
|
|
112
112
|
const lines = content.split("\n").slice(-30).join("\n").trim();
|
|
@@ -234,12 +234,12 @@ export async function startServer(options: {
|
|
|
234
234
|
|
|
235
235
|
{
|
|
236
236
|
const { resolve } = await import("node:path");
|
|
237
|
-
const { homedir } = await import("node:os");
|
|
238
237
|
const { writeFileSync, readFileSync, mkdirSync, existsSync, openSync } = await import("node:fs");
|
|
239
238
|
const { isCompiledBinary } = await import("../services/autostart-generator.ts");
|
|
240
239
|
const { writeCmd, acquireLock, releaseLock } = await import("../services/supervisor-state.ts");
|
|
240
|
+
const { getPpmDir } = await import("../services/ppm-dir.ts");
|
|
241
241
|
|
|
242
|
-
const ppmDir =
|
|
242
|
+
const ppmDir = getPpmDir();
|
|
243
243
|
if (!existsSync(ppmDir)) mkdirSync(ppmDir, { recursive: true });
|
|
244
244
|
const pidFile = resolve(ppmDir, "ppm.pid");
|
|
245
245
|
const statusFile = resolve(ppmDir, "status.json");
|
|
@@ -490,9 +490,9 @@ if (process.argv.includes("__serve__")) {
|
|
|
490
490
|
// Also write server version to status.json so supervisor heartbeat reports the actual running version.
|
|
491
491
|
try {
|
|
492
492
|
const { resolve: r } = await import("node:path");
|
|
493
|
-
const { homedir: h } = await import("node:os");
|
|
494
493
|
const { readFileSync: rf, writeFileSync: wf, renameSync: rn } = await import("node:fs");
|
|
495
|
-
const
|
|
494
|
+
const { getPpmDir: gd } = await import("../services/ppm-dir.ts");
|
|
495
|
+
const statusFile = r(gd(), "status.json");
|
|
496
496
|
const status = JSON.parse(rf(statusFile, "utf-8"));
|
|
497
497
|
// Write running server version — source of truth for heartbeat
|
|
498
498
|
status.serverVersion = VERSION;
|
|
@@ -5,7 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { chatService } from "../../services/chat.service.ts";
|
|
6
6
|
import { providerRegistry } from "../../providers/registry.ts";
|
|
7
7
|
import { renameSession as sdkRenameSession } from "@anthropic-ai/claude-agent-sdk";
|
|
8
|
-
import { listSlashItems } from "../../services/slash-items.service.ts";
|
|
8
|
+
import { listSlashItems, searchSlashItems } from "../../services/slash-items.service.ts";
|
|
9
9
|
import { getCachedUsage, refreshUsageNow } from "../../services/claude-usage.service.ts";
|
|
10
10
|
import { getSessionLog } from "../../services/session-log.service.ts";
|
|
11
11
|
import { getSessionProjectPath, setSessionMetadata, setSessionTitle, getPinnedSessionIds, pinSession, unpinSession, deleteSessionMapping, deleteSessionMetadata, deleteSessionTitle } from "../../services/db.service.ts";
|
|
@@ -19,7 +19,9 @@ export const chatRoutes = new Hono<Env>();
|
|
|
19
19
|
chatRoutes.get("/slash-items", (c) => {
|
|
20
20
|
try {
|
|
21
21
|
const projectPath = c.get("projectPath");
|
|
22
|
-
const
|
|
22
|
+
const q = c.req.query("q");
|
|
23
|
+
let items = listSlashItems(projectPath);
|
|
24
|
+
if (q) items = searchSlashItems(items, q);
|
|
23
25
|
return c.json(ok(items));
|
|
24
26
|
} catch (e) {
|
|
25
27
|
return c.json(err((e as Error).message), 500);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { readFileSync, existsSync } from "node:fs";
|
|
5
4
|
import { VERSION } from "../../version.ts";
|
|
6
5
|
import {
|
|
@@ -10,8 +9,7 @@ import {
|
|
|
10
9
|
signalSupervisorUpgrade,
|
|
11
10
|
} from "../../services/upgrade.service.ts";
|
|
12
11
|
import { ok, err } from "../../types/api.ts";
|
|
13
|
-
|
|
14
|
-
const STATUS_FILE = resolve(process.env.PPM_HOME || resolve(homedir(), ".ppm"), "status.json");
|
|
12
|
+
import { getPpmDir } from "../../services/ppm-dir.ts";
|
|
15
13
|
|
|
16
14
|
export const upgradeRoutes = new Hono();
|
|
17
15
|
|
|
@@ -19,8 +17,8 @@ export const upgradeRoutes = new Hono();
|
|
|
19
17
|
upgradeRoutes.get("/", (c) => {
|
|
20
18
|
let availableVersion: string | null = null;
|
|
21
19
|
try {
|
|
22
|
-
if (existsSync(
|
|
23
|
-
const data = JSON.parse(readFileSync(
|
|
20
|
+
if (existsSync(resolve(getPpmDir(), "status.json"))) {
|
|
21
|
+
const data = JSON.parse(readFileSync(resolve(getPpmDir(), "status.json"), "utf-8"));
|
|
24
22
|
const candidate = data.availableVersion ?? null;
|
|
25
23
|
// Only report if actually newer than current version
|
|
26
24
|
if (candidate && compareSemver(VERSION, candidate) < 0) {
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -85,6 +85,21 @@ function bufferAndBroadcast(sessionId: string, event: unknown): void {
|
|
|
85
85
|
if (entry.turnEvents.length < MAX_TURN_EVENTS) {
|
|
86
86
|
entry.turnEvents.push({ ...(event as Record<string, unknown>) });
|
|
87
87
|
}
|
|
88
|
+
// Enrich: embed tool_result onto matching tool_use for reconnect reliability.
|
|
89
|
+
// Reconnecting clients may miss separate tool_result events — this ensures
|
|
90
|
+
// the tool_use event itself carries the result as a fallback.
|
|
91
|
+
if (evType === "tool_result") {
|
|
92
|
+
const toolUseId = (event as any)?.toolUseId;
|
|
93
|
+
if (toolUseId) {
|
|
94
|
+
for (let i = entry.turnEvents.length - 1; i >= 0; i--) {
|
|
95
|
+
const buffered = entry.turnEvents[i] as any;
|
|
96
|
+
if (buffered.type === "tool_use" && buffered.toolUseId === toolUseId) {
|
|
97
|
+
buffered.result = { output: (event as any).output, isError: (event as any).isError };
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
88
103
|
}
|
|
89
104
|
broadcast(sessionId, event);
|
|
90
105
|
}
|
|
@@ -567,6 +582,22 @@ export const chatWebSocket = {
|
|
|
567
582
|
entry.permissionMode = parsed.permissionMode;
|
|
568
583
|
}
|
|
569
584
|
|
|
585
|
+
// Intercept PPM-handled built-in commands (e.g. /skills, /version)
|
|
586
|
+
const content = parsed.content.trim();
|
|
587
|
+
const slashMatch = content.match(/^\/(\S+)/);
|
|
588
|
+
if (slashMatch) {
|
|
589
|
+
const { isPpmHandled, executeBuiltin } = await import("../../services/slash-discovery/index.ts");
|
|
590
|
+
const cmdName = slashMatch[1]!;
|
|
591
|
+
if (isPpmHandled(cmdName)) {
|
|
592
|
+
const response = executeBuiltin(cmdName, entry.projectPath ?? "");
|
|
593
|
+
if (response) {
|
|
594
|
+
broadcast(sessionId, { type: "text", content: response });
|
|
595
|
+
broadcast(sessionId, { type: "done", resultSubtype: "builtin", numTurns: 0 });
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
570
601
|
const provider = providerRegistry.get(providerId);
|
|
571
602
|
|
|
572
603
|
if (!entry.isStreamingActive) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { appendFileSync } from "node:fs";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import { getPpmDir } from "./ppm-dir.ts";
|
|
8
8
|
|
|
9
9
|
// ─── Types (must match Cloud's ws-types.ts) ─────────
|
|
10
10
|
interface WsMessage {
|
|
@@ -152,7 +152,9 @@ function doConnect(): void {
|
|
|
152
152
|
sock.onopen = () => {
|
|
153
153
|
if (ws !== sock) return; // stale — newer connection replaced us
|
|
154
154
|
reconnecting = false;
|
|
155
|
-
reconnectAttempt
|
|
155
|
+
// Don't reset reconnectAttempt here — only after auth succeeds.
|
|
156
|
+
// Resetting on open causes tight reconnect loops when the server
|
|
157
|
+
// keeps closing immediately after connect (backoff never builds up).
|
|
156
158
|
log("INFO", "Cloud WS connected, sending auth");
|
|
157
159
|
|
|
158
160
|
// Send auth as first message — server must process this before any other msg
|
|
@@ -170,6 +172,7 @@ function doConnect(): void {
|
|
|
170
172
|
setTimeout(() => {
|
|
171
173
|
if (ws !== sock) return; // replaced during delay
|
|
172
174
|
connected = true;
|
|
175
|
+
reconnectAttempt = 0; // Auth succeeded — reset backoff
|
|
173
176
|
|
|
174
177
|
// Flush queued messages
|
|
175
178
|
while (outboundQueue.length > 0 && connected) {
|
|
@@ -234,6 +237,6 @@ function scheduleReconnect(source = "unknown"): void {
|
|
|
234
237
|
|
|
235
238
|
function log(level: string, msg: string): void {
|
|
236
239
|
const ts = new Date().toISOString();
|
|
237
|
-
const logFile = resolve(
|
|
240
|
+
const logFile = resolve(getPpmDir(), "ppm.log");
|
|
238
241
|
try { appendFileSync(logFile, `[${ts}] [${level}] [cloud-ws] ${msg}\n`); } catch {}
|
|
239
242
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync } from "node:fs";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
import { VERSION } from "../version.ts";
|
|
6
6
|
import { configService } from "./config.service.ts";
|
|
7
|
+
import { getPpmDir } from "./ppm-dir.ts";
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const MACHINE_ID_FILE = resolve(PPM_DIR, "machine-id");
|
|
9
|
+
const authFile = () => resolve(getPpmDir(), "cloud-auth.json");
|
|
10
|
+
const deviceFile = () => resolve(getPpmDir(), "cloud-device.json");
|
|
11
|
+
const machineIdFile = () => resolve(getPpmDir(), "machine-id");
|
|
12
12
|
|
|
13
13
|
const DEFAULT_CLOUD_URL = "https://ppm.hienle.tech";
|
|
14
14
|
const HEARTBEAT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
@@ -47,12 +47,12 @@ interface DeviceInfo {
|
|
|
47
47
|
|
|
48
48
|
/** Get or generate a stable machine ID (random UUID, persists across reboots) */
|
|
49
49
|
export function getMachineId(): string {
|
|
50
|
-
if (existsSync(
|
|
51
|
-
return readFileSync(
|
|
50
|
+
if (existsSync(machineIdFile())) {
|
|
51
|
+
return readFileSync(machineIdFile(), "utf-8").trim();
|
|
52
52
|
}
|
|
53
53
|
const id = randomBytes(16).toString("hex");
|
|
54
54
|
ensurePpmDir();
|
|
55
|
-
writeFileSync(
|
|
55
|
+
writeFileSync(machineIdFile(), id);
|
|
56
56
|
return id;
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -61,8 +61,8 @@ export function getMachineId(): string {
|
|
|
61
61
|
/** Read saved cloud auth credentials */
|
|
62
62
|
export function getCloudAuth(): CloudAuth | null {
|
|
63
63
|
try {
|
|
64
|
-
if (!existsSync(
|
|
65
|
-
return JSON.parse(readFileSync(
|
|
64
|
+
if (!existsSync(authFile())) return null;
|
|
65
|
+
return JSON.parse(readFileSync(authFile(), "utf-8"));
|
|
66
66
|
} catch {
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
@@ -71,13 +71,13 @@ export function getCloudAuth(): CloudAuth | null {
|
|
|
71
71
|
/** Save cloud auth credentials (restricted permissions) */
|
|
72
72
|
export function saveCloudAuth(auth: CloudAuth): void {
|
|
73
73
|
ensurePpmDir();
|
|
74
|
-
writeFileSync(
|
|
75
|
-
try { chmodSync(
|
|
74
|
+
writeFileSync(authFile(), JSON.stringify(auth, null, 2));
|
|
75
|
+
try { chmodSync(authFile(), 0o600); } catch {}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/** Remove cloud auth credentials */
|
|
79
79
|
export function removeCloudAuth(): void {
|
|
80
|
-
try { if (existsSync(
|
|
80
|
+
try { if (existsSync(authFile())) unlinkSync(authFile()); } catch {}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// ─── Device ─────────────────────────────────────────────────────────────
|
|
@@ -85,8 +85,8 @@ export function removeCloudAuth(): void {
|
|
|
85
85
|
/** Read saved cloud device info */
|
|
86
86
|
export function getCloudDevice(): CloudDevice | null {
|
|
87
87
|
try {
|
|
88
|
-
if (!existsSync(
|
|
89
|
-
return JSON.parse(readFileSync(
|
|
88
|
+
if (!existsSync(deviceFile())) return null;
|
|
89
|
+
return JSON.parse(readFileSync(deviceFile(), "utf-8"));
|
|
90
90
|
} catch {
|
|
91
91
|
return null;
|
|
92
92
|
}
|
|
@@ -95,13 +95,13 @@ export function getCloudDevice(): CloudDevice | null {
|
|
|
95
95
|
/** Save cloud device info (restricted permissions) */
|
|
96
96
|
export function saveCloudDevice(device: CloudDevice): void {
|
|
97
97
|
ensurePpmDir();
|
|
98
|
-
writeFileSync(
|
|
99
|
-
try { chmodSync(
|
|
98
|
+
writeFileSync(deviceFile(), JSON.stringify(device, null, 2));
|
|
99
|
+
try { chmodSync(deviceFile(), 0o600); } catch {}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/** Remove cloud device info */
|
|
103
103
|
export function removeCloudDevice(): void {
|
|
104
|
-
try { if (existsSync(
|
|
104
|
+
try { if (existsSync(deviceFile())) unlinkSync(deviceFile()); } catch {}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// ─── API Client ─────────────────────────────────────────────────────────
|
|
@@ -398,7 +398,8 @@ export function stopHeartbeat(): void {
|
|
|
398
398
|
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
399
399
|
|
|
400
400
|
function ensurePpmDir(): void {
|
|
401
|
-
|
|
401
|
+
const dir = getPpmDir();
|
|
402
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
402
403
|
}
|
|
403
404
|
|
|
404
405
|
function openBrowser(url: string): void {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { existsSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { getPpmDir } from "./ppm-dir.ts";
|
|
4
4
|
|
|
5
|
-
const CLOUDFLARED_DIR = resolve(homedir(), ".ppm", "bin");
|
|
6
5
|
const isWindows = process.platform === "win32";
|
|
7
|
-
const
|
|
6
|
+
const cloudflaredDir = () => resolve(getPpmDir(), "bin");
|
|
7
|
+
const cloudflaredPath = () => resolve(cloudflaredDir(), isWindows ? "cloudflared.exe" : "cloudflared");
|
|
8
8
|
|
|
9
9
|
const OS_MAP: Record<string, string> = { darwin: "darwin", linux: "linux", win32: "windows" };
|
|
10
10
|
const ARCH_MAP: Record<string, string> = { x64: "amd64", arm64: "arm64" };
|
|
@@ -67,40 +67,40 @@ async function extractTgz(tgzPath: string, destDir: string): Promise<void> {
|
|
|
67
67
|
* Downloads from GitHub releases if missing. Returns path to binary.
|
|
68
68
|
*/
|
|
69
69
|
export async function ensureCloudflared(): Promise<string> {
|
|
70
|
-
if (existsSync(
|
|
70
|
+
if (existsSync(cloudflaredPath())) return cloudflaredPath();
|
|
71
71
|
|
|
72
|
-
if (!existsSync(
|
|
73
|
-
mkdirSync(
|
|
72
|
+
if (!existsSync(cloudflaredDir())) {
|
|
73
|
+
mkdirSync(cloudflaredDir(), { recursive: true });
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
const url = getDownloadUrl();
|
|
77
77
|
const isTgz = url.endsWith(".tgz");
|
|
78
78
|
const isExe = url.endsWith(".exe");
|
|
79
|
-
const tmpPath = resolve(
|
|
79
|
+
const tmpPath = resolve(cloudflaredDir(), isTgz ? "cloudflared.tgz" : isExe ? "cloudflared.exe.tmp" : "cloudflared.tmp");
|
|
80
80
|
|
|
81
81
|
try {
|
|
82
82
|
const data = await downloadWithProgress(url);
|
|
83
83
|
await Bun.write(tmpPath, data);
|
|
84
84
|
|
|
85
85
|
if (isTgz) {
|
|
86
|
-
await extractTgz(tmpPath,
|
|
86
|
+
await extractTgz(tmpPath, cloudflaredDir());
|
|
87
87
|
unlinkSync(tmpPath);
|
|
88
88
|
} else {
|
|
89
|
-
renameSync(tmpPath,
|
|
89
|
+
renameSync(tmpPath, cloudflaredPath());
|
|
90
90
|
}
|
|
91
91
|
if (!isWindows) {
|
|
92
|
-
chmodSync(
|
|
92
|
+
chmodSync(cloudflaredPath(), 0o755);
|
|
93
93
|
}
|
|
94
94
|
} catch (err) {
|
|
95
95
|
try { unlinkSync(tmpPath); } catch {}
|
|
96
|
-
try { unlinkSync(
|
|
96
|
+
try { unlinkSync(cloudflaredPath()); } catch {}
|
|
97
97
|
throw err;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
return
|
|
100
|
+
return cloudflaredPath();
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/** Get path where cloudflared binary is/will be stored */
|
|
104
104
|
export function getCloudflaredPath(): string {
|
|
105
|
-
return
|
|
105
|
+
return cloudflaredPath();
|
|
106
106
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, renameSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { randomBytes } from "node:crypto";
|
|
5
4
|
import type { PpmConfig, ProjectConfig } from "../types/config.ts";
|
|
6
5
|
import { DEFAULT_CONFIG, sanitizeConfig } from "../types/config.ts";
|
|
@@ -14,8 +13,7 @@ import {
|
|
|
14
13
|
getDb,
|
|
15
14
|
getDbFilePath,
|
|
16
15
|
} from "./db.service.ts";
|
|
17
|
-
|
|
18
|
-
const PPM_DIR = resolve(homedir(), ".ppm");
|
|
16
|
+
import { getPpmDir } from "./ppm-dir.ts";
|
|
19
17
|
|
|
20
18
|
/** Top-level config keys stored in the config table (not projects) */
|
|
21
19
|
const CONFIG_TABLE_KEYS: (keyof PpmConfig)[] = [
|
|
@@ -148,8 +146,8 @@ class ConfigService {
|
|
|
148
146
|
|
|
149
147
|
private migrateYamlIfNeeded(): void {
|
|
150
148
|
const yamlPaths = [
|
|
151
|
-
resolve(
|
|
152
|
-
resolve(
|
|
149
|
+
resolve(getPpmDir(), "config.yaml"),
|
|
150
|
+
resolve(getPpmDir(), "config.dev.yaml"),
|
|
153
151
|
];
|
|
154
152
|
for (const yamlPath of yamlPaths) {
|
|
155
153
|
if (!existsSync(yamlPath)) continue;
|
|
@@ -187,7 +185,7 @@ class ConfigService {
|
|
|
187
185
|
}
|
|
188
186
|
|
|
189
187
|
private migrateSessionMapIfNeeded(): void {
|
|
190
|
-
const mapPath = resolve(
|
|
188
|
+
const mapPath = resolve(getPpmDir(), "session-map.json");
|
|
191
189
|
if (!existsSync(mapPath)) return;
|
|
192
190
|
try {
|
|
193
191
|
const { setSessionMetadata } = require("./db.service.ts");
|
|
@@ -202,7 +200,7 @@ class ConfigService {
|
|
|
202
200
|
}
|
|
203
201
|
|
|
204
202
|
private migratePushSubsIfNeeded(): void {
|
|
205
|
-
const subsPath = resolve(
|
|
203
|
+
const subsPath = resolve(getPpmDir(), "push-subscriptions.json");
|
|
206
204
|
if (!existsSync(subsPath)) return;
|
|
207
205
|
try {
|
|
208
206
|
const { upsertPushSubscription } = require("./db.service.ts");
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { Database, type SQLQueryBindings } from "bun:sqlite";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { mkdirSync, existsSync } from "node:fs";
|
|
5
4
|
import { encrypt, decrypt } from "../lib/account-crypto.ts";
|
|
6
|
-
|
|
7
|
-
const PPM_DIR = process.env.PPM_HOME || resolve(homedir(), ".ppm");
|
|
5
|
+
import { getPpmDir } from "./ppm-dir.ts";
|
|
8
6
|
const CURRENT_SCHEMA_VERSION = 15;
|
|
9
7
|
|
|
10
8
|
let db: Database | null = null;
|
|
@@ -17,14 +15,15 @@ export function setDbProfile(profile: string | null): void {
|
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
function getDbPath(): string {
|
|
20
|
-
if (dbProfile) return resolve(
|
|
21
|
-
return resolve(
|
|
18
|
+
if (dbProfile) return resolve(getPpmDir(), `ppm.${dbProfile}.db`);
|
|
19
|
+
return resolve(getPpmDir(), "ppm.db");
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
/** Get or create the singleton DB instance (lazy init) */
|
|
25
23
|
export function getDb(): Database {
|
|
26
24
|
if (db) return db;
|
|
27
|
-
|
|
25
|
+
const ppmDir = getPpmDir();
|
|
26
|
+
if (!existsSync(ppmDir)) mkdirSync(ppmDir, { recursive: true });
|
|
28
27
|
db = new Database(getDbPath());
|
|
29
28
|
db.exec("PRAGMA journal_mode = WAL");
|
|
30
29
|
db.exec("PRAGMA foreign_keys = ON");
|
|
@@ -156,8 +156,8 @@ export function registerVscodeCompatHandlers(rpc: RpcChannel): void {
|
|
|
156
156
|
const { resolve, relative } = await import("node:path");
|
|
157
157
|
const resolved = resolve(filePath);
|
|
158
158
|
// Allow: CWD (project root) and ~/.ppm/extensions/ (extension storage)
|
|
159
|
-
const {
|
|
160
|
-
const allowedRoots = [resolve(process.cwd()), resolve(
|
|
159
|
+
const { getPpmDir } = await import("./ppm-dir.ts");
|
|
160
|
+
const allowedRoots = [resolve(process.cwd()), resolve(getPpmDir(), "extensions")];
|
|
161
161
|
const isSafe = allowedRoots.some((root) => {
|
|
162
162
|
const rel = relative(root, resolved);
|
|
163
163
|
return !rel.startsWith("..") && !rel.startsWith("/");
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { existsSync } from "node:fs";
|
|
4
3
|
import type { ExtensionManifest, ExtensionInfo, RpcMessage } from "../types/extension.ts";
|
|
5
4
|
import { getExtensions, getExtensionById, insertExtension, getExtensionStorage, setExtensionStorageValue } from "./db.service.ts";
|
|
@@ -8,9 +7,7 @@ import { RpcChannel } from "./extension-rpc.ts";
|
|
|
8
7
|
import { parseManifest, discoverManifests } from "./extension-manifest.ts";
|
|
9
8
|
import { installExtension, removeExtension, devLinkExtension, ensureExtensionsDir } from "./extension-installer.ts";
|
|
10
9
|
import { registerVscodeCompatHandlers } from "./extension-rpc-handlers.ts";
|
|
11
|
-
|
|
12
|
-
const PPM_DIR = process.env.PPM_HOME || resolve(homedir(), ".ppm");
|
|
13
|
-
const EXTENSIONS_DIR = resolve(PPM_DIR, "extensions");
|
|
10
|
+
import { getPpmDir } from "./ppm-dir.ts";
|
|
14
11
|
|
|
15
12
|
class ExtensionService {
|
|
16
13
|
private worker: Worker | null = null;
|
|
@@ -67,15 +64,15 @@ class ExtensionService {
|
|
|
67
64
|
}
|
|
68
65
|
|
|
69
66
|
async discover(): Promise<ExtensionManifest[]> {
|
|
70
|
-
ensureExtensionsDir(
|
|
71
|
-
return discoverManifests(
|
|
67
|
+
ensureExtensionsDir(resolve(getPpmDir(), "extensions"));
|
|
68
|
+
return discoverManifests(resolve(getPpmDir(), "extensions"));
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
async install(name: string): Promise<ExtensionManifest> {
|
|
75
72
|
if (this.installing.has(name)) throw new Error(`Already installing ${name}`);
|
|
76
73
|
this.installing.add(name);
|
|
77
74
|
try {
|
|
78
|
-
return await installExtension(name,
|
|
75
|
+
return await installExtension(name, resolve(getPpmDir(), "extensions"));
|
|
79
76
|
} finally {
|
|
80
77
|
this.installing.delete(name);
|
|
81
78
|
}
|
|
@@ -83,7 +80,7 @@ class ExtensionService {
|
|
|
83
80
|
|
|
84
81
|
async remove(id: string): Promise<void> {
|
|
85
82
|
if (this.activatedIds.has(id)) await this.deactivate(id);
|
|
86
|
-
await removeExtension(id,
|
|
83
|
+
await removeExtension(id, resolve(getPpmDir(), "extensions"));
|
|
87
84
|
contributionRegistry.unregister(id);
|
|
88
85
|
}
|
|
89
86
|
|
|
@@ -95,7 +92,7 @@ class ExtensionService {
|
|
|
95
92
|
if (!row.enabled) throw new Error(`Extension ${id} is disabled`);
|
|
96
93
|
|
|
97
94
|
const manifest: ExtensionManifest = JSON.parse(row.manifest);
|
|
98
|
-
const extDir = resolve(
|
|
95
|
+
const extDir = resolve(resolve(getPpmDir(), "extensions"), "node_modules", id);
|
|
99
96
|
const entryPath = resolve(extDir, manifest.main);
|
|
100
97
|
if (!existsSync(entryPath)) throw new Error(`Entry point not found: ${entryPath}`);
|
|
101
98
|
|
|
@@ -176,7 +173,7 @@ class ExtensionService {
|
|
|
176
173
|
}
|
|
177
174
|
|
|
178
175
|
async devLink(localPath: string): Promise<ExtensionManifest> {
|
|
179
|
-
const manifest = devLinkExtension(localPath,
|
|
176
|
+
const manifest = devLinkExtension(localPath, resolve(getPpmDir(), "extensions"));
|
|
180
177
|
// Auto-activate after dev-link (DB record is created with enabled=1)
|
|
181
178
|
if (!this.activatedIds.has(manifest.id)) {
|
|
182
179
|
try { await this.activate(manifest.id); } catch (e) {
|
|
@@ -187,7 +184,7 @@ class ExtensionService {
|
|
|
187
184
|
}
|
|
188
185
|
|
|
189
186
|
async startup(): Promise<void> {
|
|
190
|
-
ensureExtensionsDir(
|
|
187
|
+
ensureExtensionsDir(resolve(getPpmDir(), "extensions"));
|
|
191
188
|
const manifests = await this.discover();
|
|
192
189
|
for (const m of manifests) {
|
|
193
190
|
if (!getExtensionById(m.id)) {
|
|
@@ -214,7 +211,7 @@ class ExtensionService {
|
|
|
214
211
|
}
|
|
215
212
|
|
|
216
213
|
isActivated(id: string): boolean { return this.activatedIds.has(id); }
|
|
217
|
-
getExtensionsDir(): string { return
|
|
214
|
+
getExtensionsDir(): string { return resolve(getPpmDir(), "extensions"); }
|
|
218
215
|
|
|
219
216
|
/** Push current contributions to all connected browser clients */
|
|
220
217
|
private broadcastContributions(): void {
|