@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.
Files changed (162) 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/.repomixignore +22 -0
  17. package/AGENTS.md +62 -0
  18. package/CHANGELOG.md +17 -0
  19. package/CLAUDE.md +12 -0
  20. package/assets/skills/ppm-guide/SKILL.md +61 -0
  21. package/bun.lock +9 -1
  22. package/dist/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  23. package/dist/web/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  24. package/dist/web/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  25. package/dist/web/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  26. package/dist/web/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  27. package/dist/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  28. package/dist/web/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  29. package/dist/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  30. package/dist/web/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  31. package/dist/web/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  32. package/dist/web/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  33. package/dist/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  34. package/dist/web/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  35. package/dist/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  36. package/dist/web/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  37. package/dist/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  38. package/dist/web/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  39. package/dist/web/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  40. package/dist/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  41. package/dist/web/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  42. package/dist/web/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  43. package/dist/web/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  44. package/dist/web/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  45. package/dist/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  46. package/dist/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  47. package/dist/web/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  48. package/dist/web/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  49. package/dist/web/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  50. package/dist/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  51. package/dist/web/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  52. package/dist/web/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  53. package/dist/web/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  54. package/dist/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  55. package/dist/web/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  56. package/dist/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  57. package/dist/web/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  58. package/dist/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  59. package/dist/web/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  60. package/dist/web/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  61. package/dist/web/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  62. package/dist/web/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  63. package/dist/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  64. package/dist/web/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  65. package/dist/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  66. package/dist/web/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  67. package/dist/web/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  68. package/dist/web/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  69. package/dist/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  70. package/dist/web/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  71. package/dist/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  72. package/dist/web/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  73. package/dist/web/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  74. package/dist/web/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  75. package/dist/web/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  76. package/dist/web/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  77. package/dist/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  78. package/dist/web/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  79. package/dist/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  80. package/dist/web/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  81. package/dist/web/assets/chat-tab-bS86TsT5.js +10 -0
  82. package/dist/web/assets/{code-editor-BFe-hnpF.js → code-editor-BaNaQ33b.js} +1 -1
  83. package/dist/web/assets/{database-viewer-BeY2V5QI.js → database-viewer-C5MVw8cJ.js} +1 -1
  84. package/dist/web/assets/{diff-viewer-D6xzs8PP.js → diff-viewer-CUbFMWVo.js} +1 -1
  85. package/dist/web/assets/{extension-webview-Cd1XYFXO.js → extension-webview-CwGufYEP.js} +1 -1
  86. package/dist/web/assets/{git-graph-D2XXpiMQ.js → git-graph-BD7A7MLo.js} +1 -1
  87. package/dist/web/assets/index-BYXjCNlK.css +2 -0
  88. package/dist/web/assets/index-CpzkPHOC.js +30 -0
  89. package/dist/web/assets/keybindings-store-DsaANvBz.js +1 -0
  90. package/dist/web/assets/markdown-renderer-C19IsITh.js +326 -0
  91. package/dist/web/assets/{port-forwarding-tab-B5rj_I66.js → port-forwarding-tab-BF79F1iL.js} +1 -1
  92. package/dist/web/assets/{postgres-viewer-DnlqzOnm.js → postgres-viewer-_nYiO_wp.js} +1 -1
  93. package/dist/web/assets/{settings-tab-CNZpuPD3.js → settings-tab-C1SQMbSu.js} +1 -1
  94. package/dist/web/assets/{sql-query-editor-Df2kzbPj.js → sql-query-editor-6OFvxxuN.js} +1 -1
  95. package/dist/web/assets/{sqlite-viewer-Cj1G70z4.js → sqlite-viewer-SNVYFXvB.js} +1 -1
  96. package/dist/web/assets/{terminal-tab-Dv9A7Xe2.js → terminal-tab-BJEkmrDt.js} +1 -1
  97. package/dist/web/assets/{use-monaco-theme-CPfIEo8t.js → use-monaco-theme-r8FzlCWr.js} +1 -1
  98. package/dist/web/index.html +2 -2
  99. package/dist/web/sw.js +1 -1
  100. package/docs/codebase-summary.md +78 -0
  101. package/docs/project-changelog.md +29 -0
  102. package/docs/system-architecture.md +2 -0
  103. package/package.json +5 -2
  104. package/release-manifest.json +15784 -0
  105. package/scripts/check-ppm-dir-usage.sh +21 -0
  106. package/scripts/generate-ppm-guide.ts +92 -0
  107. package/src/cli/commands/init.ts +2 -1
  108. package/src/cli/commands/logs.ts +11 -11
  109. package/src/cli/commands/report.ts +3 -2
  110. package/src/cli/commands/restart.ts +22 -23
  111. package/src/cli/commands/skills-cmd.ts +123 -0
  112. package/src/cli/commands/status.ts +7 -8
  113. package/src/cli/commands/stop.ts +18 -19
  114. package/src/index.ts +3 -0
  115. package/src/lib/account-crypto.ts +12 -7
  116. package/src/providers/claude-agent-sdk.ts +42 -11
  117. package/src/server/index.ts +8 -8
  118. package/src/server/routes/chat.ts +4 -2
  119. package/src/server/routes/upgrade.ts +3 -5
  120. package/src/server/ws/chat.ts +31 -0
  121. package/src/services/cloud-ws.service.ts +6 -3
  122. package/src/services/cloud.service.ts +20 -19
  123. package/src/services/cloudflared.service.ts +13 -13
  124. package/src/services/config.service.ts +5 -7
  125. package/src/services/db.service.ts +5 -6
  126. package/src/services/extension-rpc-handlers.ts +2 -2
  127. package/src/services/extension.service.ts +9 -12
  128. package/src/services/ppm-dir.ts +14 -0
  129. package/src/services/slash-discovery/builtin-commands.ts +53 -0
  130. package/src/services/slash-discovery/builtin-handlers.ts +65 -0
  131. package/src/services/slash-discovery/definition-source.ts +27 -0
  132. package/src/services/slash-discovery/discover-skill-roots.ts +128 -0
  133. package/src/services/slash-discovery/fuzzy-search.ts +76 -0
  134. package/src/services/slash-discovery/index.ts +42 -0
  135. package/src/services/slash-discovery/resolve-overrides.ts +41 -0
  136. package/src/services/slash-discovery/skill-loader.ts +156 -0
  137. package/src/services/slash-discovery/types.ts +51 -0
  138. package/src/services/slash-items.service.ts +4 -182
  139. package/src/services/supervisor-state.ts +14 -15
  140. package/src/services/supervisor-stopped-page.ts +2 -4
  141. package/src/services/supervisor.ts +15 -15
  142. package/src/services/tunnel.service.ts +22 -5
  143. package/src/services/upgrade.service.ts +2 -3
  144. package/src/types/chat.ts +3 -1
  145. package/src/web/components/chat/chat-history-bar.tsx +2 -15
  146. package/src/web/components/chat/chat-tab.tsx +5 -2
  147. package/src/web/components/chat/message-input.tsx +48 -6
  148. package/src/web/components/chat/message-list.tsx +19 -5
  149. package/src/web/components/chat/slash-command-picker.tsx +21 -12
  150. package/src/web/components/layout/mobile-nav.tsx +47 -21
  151. package/src/web/components/layout/panel-layout.tsx +11 -0
  152. package/src/web/components/layout/upgrade-banner.tsx +48 -2
  153. package/src/web/components/shared/markdown-renderer.tsx +5 -2
  154. package/src/web/hooks/use-chat.ts +33 -1
  155. package/src/web/main.tsx +1 -0
  156. package/src/web/stores/panel-store.ts +25 -1
  157. package/src/web/styles/globals.css +14 -0
  158. package/dist/web/assets/chat-tab-CmSLt4tg.js +0 -10
  159. package/dist/web/assets/index-BtwsLrdT.css +0 -2
  160. package/dist/web/assets/index-D6_wwsL_.js +0 -30
  161. package/dist/web/assets/keybindings-store-C8ryKudw.js +0 -1
  162. package/dist/web/assets/markdown-renderer-xYMhd9cE.js +0 -69
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+ # Fail if any service file uses hardcoded homedir + .ppm (should use getPpmDir())
3
+ # Allowed exceptions: ppm-dir.ts itself, autostart, ppmbot, fs-browse, git-dirs, claude-usage, slash-discovery
4
+
5
+ VIOLATIONS=$(grep -rn -E '(resolve|join)\(homedir\(\).*\.ppm' src/ \
6
+ --include='*.ts' \
7
+ | grep -v 'ppm-dir.ts' \
8
+ | grep -v 'autostart-' \
9
+ | grep -v 'ppmbot/' \
10
+ | grep -v 'bot-cmd' \
11
+ | grep -v 'claude-usage' \
12
+ | grep -v 'fs-browse' \
13
+ | grep -v 'git-dirs' \
14
+ | grep -v 'discover-skill-roots')
15
+
16
+ if [ -n "$VIOLATIONS" ]; then
17
+ echo "ERROR: Direct homedir() + .ppm usage found. Use getPpmDir() from src/services/ppm-dir.ts instead:"
18
+ echo "$VIOLATIONS"
19
+ exit 1
20
+ fi
21
+ echo "OK: All services use getPpmDir()"
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Generate the bundled PPM guide skill from docs/ and CLAUDE.md.
4
+ * Output: assets/skills/ppm-guide/SKILL.md
5
+ *
6
+ * Usage: bun scripts/generate-ppm-guide.ts
7
+ */
8
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
9
+ import { resolve, dirname } from "node:path";
10
+
11
+ const ROOT = resolve(dirname(import.meta.path), "..");
12
+ const OUT_DIR = resolve(ROOT, "assets/skills/ppm-guide");
13
+ const OUT_FILE = resolve(OUT_DIR, "SKILL.md");
14
+ const MAX_LINES = 150;
15
+
16
+ /** Read a file safely, return empty string if missing */
17
+ function read(rel: string): string {
18
+ const p = resolve(ROOT, rel);
19
+ return existsSync(p) ? readFileSync(p, "utf-8") : "";
20
+ }
21
+
22
+ /** Extract first N non-empty lines from content */
23
+ function firstLines(content: string, n: number): string {
24
+ return content.split("\n").filter((l) => l.trim()).slice(0, n).join("\n");
25
+ }
26
+
27
+ /** Extract a markdown section by heading (## heading) */
28
+ function extractSection(content: string, heading: string, maxLines = 15): string {
29
+ const regex = new RegExp(`^##\\s+${heading}.*$`, "mi");
30
+ const match = content.match(regex);
31
+ if (!match) return "";
32
+ const start = content.indexOf(match[0]) + match[0].length;
33
+ const rest = content.slice(start);
34
+ const nextHeading = rest.search(/^##\s/m);
35
+ const section = nextHeading > 0 ? rest.slice(0, nextHeading) : rest;
36
+ return section.split("\n").slice(0, maxLines).join("\n").trim();
37
+ }
38
+
39
+ // --- Build guide content ---
40
+ const overview = firstLines(read("docs/project-overview-pdr.md"), 8);
41
+ const claudeMd = read("CLAUDE.md");
42
+ const commands = extractSection(claudeMd, "Commands", 12);
43
+ const devConfig = extractSection(claudeMd, "Dev Config", 10);
44
+ const architecture = extractSection(claudeMd, "Architecture", 8);
45
+ const codeStandards = firstLines(read("docs/code-standards.md"), 10);
46
+
47
+ const sections = [
48
+ "---",
49
+ "name: ppm-guide",
50
+ "description: PPM project structure, commands, config, and development workflow reference",
51
+ 'argument-hint: "[topic]"',
52
+ "---",
53
+ "",
54
+ "# PPM Guide",
55
+ "",
56
+ "## Overview",
57
+ overview,
58
+ "",
59
+ "## CLI Commands",
60
+ commands || "See `ppm --help` for available commands.",
61
+ "",
62
+ "## Dev Config",
63
+ devConfig || "Config stored in SQLite (~/.ppm/ppm.db). Dev uses ~/.ppm/ppm.dev.db on port 8081.",
64
+ "",
65
+ "## Architecture",
66
+ architecture || "Hono (HTTP) + Bun WebSocket backend, React + Vite frontend, Claude Agent SDK for AI.",
67
+ "",
68
+ "## Code Standards",
69
+ codeStandards || "See docs/code-standards.md for full conventions.",
70
+ "",
71
+ "## Slash Commands",
72
+ "Use `/skills` to list all available skills and commands.",
73
+ "Use `/help` for session help, `/status` for context usage, `/compact` to reduce context.",
74
+ "",
75
+ "## Dev Workflow",
76
+ "1. `bun dev:server` — Start backend (port 8081, dev DB)",
77
+ "2. `bun dev:web` — Start Vite frontend (port 5173)",
78
+ "3. `bun test` — Run all tests",
79
+ "4. `bun run typecheck` — TypeScript type checking",
80
+ ];
81
+
82
+ // Enforce line cap
83
+ let output = sections.join("\n");
84
+ const lines = output.split("\n");
85
+ if (lines.length > MAX_LINES) {
86
+ output = lines.slice(0, MAX_LINES).join("\n") + "\n";
87
+ }
88
+
89
+ // Write output
90
+ if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true });
91
+ writeFileSync(OUT_FILE, output, "utf-8");
92
+ console.log(`✓ Generated ${OUT_FILE} (${output.split("\n").length} lines)`);
@@ -1,6 +1,7 @@
1
1
  import { resolve, basename } from "node:path";
2
2
  import { homedir } from "node:os";
3
3
  import { existsSync } from "node:fs";
4
+ import { getPpmDir } from "../../services/ppm-dir.ts";
4
5
  import { input, confirm, select, password } from "@inquirer/prompts";
5
6
  import { configService } from "../../services/config.service.ts";
6
7
  import { projectService } from "../../services/project.service.ts";
@@ -25,7 +26,7 @@ export function hasConfig(): boolean {
25
26
  const dbConfig = getAllConfig();
26
27
  if (Object.keys(dbConfig).length > 0) return true;
27
28
  } catch {}
28
- const globalConfig = resolve(homedir(), ".ppm", "config.yaml");
29
+ const globalConfig = resolve(getPpmDir(), "config.yaml");
29
30
  return existsSync(globalConfig);
30
31
  }
31
32
 
@@ -1,24 +1,24 @@
1
1
  import { resolve } from "node:path";
2
- import { homedir } from "node:os";
3
2
  import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import { getPpmDir } from "../../services/ppm-dir.ts";
4
4
 
5
- const LOG_FILE = resolve(homedir(), ".ppm", "ppm.log");
5
+ const logFile = () => resolve(getPpmDir(), "ppm.log");
6
6
 
7
7
  export async function showLogs(options: { tail?: string; follow?: boolean; clear?: boolean }) {
8
8
  if (options.clear) {
9
9
  const { writeFileSync } = await import("node:fs");
10
- writeFileSync(LOG_FILE, "");
10
+ writeFileSync(logFile(), "");
11
11
  console.log("Logs cleared.");
12
12
  return;
13
13
  }
14
14
 
15
- if (!existsSync(LOG_FILE)) {
15
+ if (!existsSync(logFile())) {
16
16
  console.log("No log file found. Start PPM daemon first.");
17
17
  return;
18
18
  }
19
19
 
20
20
  const lines = parseInt(options.tail ?? "50", 10);
21
- const content = readFileSync(LOG_FILE, "utf-8");
21
+ const content = readFileSync(logFile(), "utf-8");
22
22
  const allLines = content.split("\n");
23
23
  const lastN = allLines.slice(-lines).join("\n");
24
24
 
@@ -32,14 +32,14 @@ export async function showLogs(options: { tail?: string; follow?: boolean; clear
32
32
  if (options.follow) {
33
33
  // Tail -f behavior
34
34
  const { watch } = await import("node:fs");
35
- let lastSize = statSync(LOG_FILE).size;
35
+ let lastSize = statSync(logFile()).size;
36
36
  console.log("\n--- Following logs (Ctrl+C to stop) ---\n");
37
37
 
38
- watch(LOG_FILE, () => {
38
+ watch(logFile(), () => {
39
39
  try {
40
- const newSize = statSync(LOG_FILE).size;
40
+ const newSize = statSync(logFile()).size;
41
41
  if (newSize > lastSize) {
42
- const fd = Bun.file(LOG_FILE);
42
+ const fd = Bun.file(logFile());
43
43
  fd.slice(lastSize, newSize).text().then((text) => {
44
44
  process.stdout.write(text);
45
45
  });
@@ -52,7 +52,7 @@ export async function showLogs(options: { tail?: string; follow?: boolean; clear
52
52
 
53
53
  /** Get last N lines of log for bug reports */
54
54
  export function getRecentLogs(lines = 30): string {
55
- if (!existsSync(LOG_FILE)) return "(no logs)";
56
- const content = readFileSync(LOG_FILE, "utf-8");
55
+ if (!existsSync(logFile())) return "(no logs)";
56
+ const content = readFileSync(logFile(), "utf-8");
57
57
  return content.split("\n").slice(-lines).join("\n").trim() || "(empty)";
58
58
  }
@@ -1,14 +1,15 @@
1
- import { homedir, platform, arch, release } from "node:os";
1
+ import { platform, arch, release } from "node:os";
2
2
  import { resolve } from "node:path";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { getRecentLogs } from "./logs.ts";
5
+ import { getPpmDir } from "../../services/ppm-dir.ts";
5
6
 
6
7
  const REPO = "hienlh/ppm";
7
8
 
8
9
  export async function reportBug() {
9
10
  const { VERSION: version } = await import("../../version.ts");
10
11
  const logs = getRecentLogs(30);
11
- const statusFile = resolve(homedir(), ".ppm", "status.json");
12
+ const statusFile = resolve(getPpmDir(), "status.json");
12
13
  let statusInfo = "(not running)";
13
14
  if (existsSync(statusFile)) {
14
15
  try { statusInfo = readFileSync(statusFile, "utf-8"); } catch {}
@@ -1,26 +1,25 @@
1
1
  import { resolve } from "node:path";
2
- import { homedir } from "node:os";
3
2
  import { readFileSync, writeFileSync, existsSync, openSync, unlinkSync, renameSync } from "node:fs";
3
+ import { getPpmDir } from "../../services/ppm-dir.ts";
4
4
 
5
- const PPM_DIR = resolve(homedir(), ".ppm");
6
- const STATUS_FILE = resolve(PPM_DIR, "status.json");
7
- const PID_FILE = resolve(PPM_DIR, "ppm.pid");
8
- const RESTARTING_FLAG = resolve(PPM_DIR, ".restarting");
9
- const RESTART_RESULT = resolve(PPM_DIR, ".restart-result");
5
+ const statusFile = () => resolve(getPpmDir(), "status.json");
6
+ const pidFile = () => resolve(getPpmDir(), "ppm.pid");
7
+ const restartingFlag = () => resolve(getPpmDir(), ".restarting");
8
+ const restartResult = () => resolve(getPpmDir(), ".restart-result");
10
9
 
11
10
  /** Restart only the server process, keeping the tunnel alive */
12
11
  export async function restartServer(options: { config?: string; force?: boolean }) {
13
12
  // Ignore SIGHUP so this process survives when PPM terminal dies
14
13
  process.on("SIGHUP", () => {});
15
14
 
16
- if (!existsSync(STATUS_FILE)) {
15
+ if (!existsSync(statusFile())) {
17
16
  console.log("No PPM daemon running. Use 'ppm start' instead.");
18
17
  process.exit(1);
19
18
  }
20
19
 
21
20
  let status: Record<string, unknown>;
22
21
  try {
23
- status = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
22
+ status = JSON.parse(readFileSync(statusFile(), "utf-8"));
24
23
  } catch {
25
24
  console.log("Corrupt status file. Use 'ppm stop && ppm start' instead.");
26
25
  process.exit(1);
@@ -45,7 +44,7 @@ export async function restartServer(options: { config?: string; force?: boolean
45
44
  // Stopped state: treat restart as resume (send resume command)
46
45
  if (state === "stopped") {
47
46
  console.log("\n Server is stopped. Resuming via supervisor...\n");
48
- const cmdFile = resolve(PPM_DIR, ".supervisor-cmd");
47
+ const cmdFile = resolve(getPpmDir(), ".supervisor-cmd");
49
48
  writeFileSync(cmdFile, JSON.stringify({ action: "resume" }));
50
49
  // Signal supervisor (Windows: polling picks up command file)
51
50
  if (process.platform !== "win32") {
@@ -59,7 +58,7 @@ export async function restartServer(options: { config?: string; force?: boolean
59
58
  while (Date.now() - rStart < 15_000) {
60
59
  await Bun.sleep(500);
61
60
  try {
62
- const newStatus = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
61
+ const newStatus = JSON.parse(readFileSync(statusFile(), "utf-8"));
63
62
  if (newStatus.state === "running" && newStatus.pid) {
64
63
  console.log(` ✓ Server resumed (PID: ${newStatus.pid})`);
65
64
  if (newStatus.shareUrl) console.log(` ➜ Share: ${newStatus.shareUrl}`);
@@ -85,7 +84,7 @@ export async function restartServer(options: { config?: string; force?: boolean
85
84
  while (Date.now() - start < 15_000) {
86
85
  await Bun.sleep(500);
87
86
  try {
88
- const newStatus = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
87
+ const newStatus = JSON.parse(readFileSync(statusFile(), "utf-8"));
89
88
  const newPid = newStatus.pid as number | undefined;
90
89
  if (newPid && newPid !== oldServerPid) {
91
90
  // Verify it's alive
@@ -120,10 +119,10 @@ export async function restartServer(options: { config?: string; force?: boolean
120
119
  const host = status.host as string ?? configService.get("host");
121
120
 
122
121
  // Write restarting flag so tunnel cleanup handler skips killing cloudflared
123
- writeFileSync(RESTARTING_FLAG, "");
122
+ writeFileSync(restartingFlag(), "");
124
123
 
125
124
  // Clear previous result
126
- try { unlinkSync(RESTART_RESULT); } catch {}
125
+ try { unlinkSync(restartResult()); } catch {}
127
126
 
128
127
  // Pre-restart message — user sees this before terminal dies (if running inside PPM)
129
128
  console.log("\n Restarting PPM server...");
@@ -135,14 +134,14 @@ export async function restartServer(options: { config?: string; force?: boolean
135
134
  const params = JSON.stringify({
136
135
  serverPid, port, host, serverScript,
137
136
  config: options.config ?? "",
138
- statusFile: STATUS_FILE,
139
- pidFile: PID_FILE,
140
- restartingFlag: RESTARTING_FLAG,
141
- resultFile: RESTART_RESULT,
142
- ppmDir: PPM_DIR,
137
+ statusFile: statusFile(),
138
+ pidFile: pidFile(),
139
+ restartingFlag: restartingFlag(),
140
+ resultFile: restartResult(),
141
+ ppmDir: getPpmDir(),
143
142
  });
144
143
 
145
- const workerPath = resolve(PPM_DIR, ".restart-worker.ts");
144
+ const workerPath = resolve(getPpmDir(), ".restart-worker.ts");
146
145
  writeFileSync(workerPath, `
147
146
  import { readFileSync, writeFileSync, openSync, unlinkSync, appendFileSync } from "node:fs";
148
147
  import { createServer } from "node:net";
@@ -299,7 +298,7 @@ main();
299
298
  `);
300
299
 
301
300
  // Spawn worker as a fully detached process
302
- const logFile = resolve(PPM_DIR, "ppm.log");
301
+ const logFile = resolve(getPpmDir(), "ppm.log");
303
302
  const logFd = openSync(logFile, "a");
304
303
  const worker = Bun.spawn({
305
304
  cmd: [process.execPath, "run", workerPath],
@@ -313,15 +312,15 @@ main();
313
312
  const pollStart = Date.now();
314
313
  while (Date.now() - pollStart < 20000) {
315
314
  await Bun.sleep(500);
316
- if (existsSync(RESTART_RESULT)) {
315
+ if (existsSync(restartResult())) {
317
316
  try {
318
- const result = JSON.parse(readFileSync(RESTART_RESULT, "utf-8"));
317
+ const result = JSON.parse(readFileSync(restartResult(), "utf-8"));
319
318
  if (result.ok) {
320
319
  console.log(` ✓ ${result.message}`);
321
320
  } else {
322
321
  console.error(` ✗ ${result.message}`);
323
322
  }
324
- unlinkSync(RESTART_RESULT);
323
+ unlinkSync(restartResult());
325
324
  } catch {}
326
325
  process.exit(0);
327
326
  }
@@ -0,0 +1,123 @@
1
+ import { Command } from "commander";
2
+
3
+ const C = {
4
+ reset: "\x1b[0m",
5
+ bold: "\x1b[1m",
6
+ green: "\x1b[32m",
7
+ yellow: "\x1b[33m",
8
+ cyan: "\x1b[36m",
9
+ dim: "\x1b[2m",
10
+ red: "\x1b[31m",
11
+ };
12
+
13
+ export function registerSkillsCommands(program: Command): void {
14
+ const skills = program
15
+ .command("skills")
16
+ .description("Manage and inspect discovered skills & commands")
17
+ .option("--project <path>", "Project path", process.cwd());
18
+
19
+ // Default action (no subcommand) → list
20
+ skills.action(async (opts) => { await listAction(opts); });
21
+
22
+ skills
23
+ .command("list")
24
+ .description("List all discovered skills and commands")
25
+ .option("--json", "JSON output")
26
+ .option("--project <path>", "Project path", process.cwd())
27
+ .action(async (opts) => { await listAction({ ...skills.opts(), ...opts }); });
28
+
29
+ skills
30
+ .command("search <query>")
31
+ .description("Fuzzy search skills and commands")
32
+ .option("--json", "JSON output")
33
+ .option("--project <path>", "Project path", process.cwd())
34
+ .action(async (query: string, opts) => {
35
+ const merged = { ...skills.opts(), ...opts };
36
+ const { listSlashItems, searchSlashItems } = await import("../../services/slash-discovery/index.ts");
37
+ const items = listSlashItems(merged.project ?? process.cwd());
38
+ const results = searchSlashItems(items, query);
39
+ if (merged.json) { console.log(JSON.stringify(results, null, 2)); return; }
40
+ if (results.length === 0) {
41
+ console.log(`${C.yellow}No matches for "${query}"${C.reset}`);
42
+ return;
43
+ }
44
+ console.log(`\n${C.bold}Search results for "${query}"${C.reset} (${results.length} matches)\n`);
45
+ for (const item of results) {
46
+ const typeLabel = item.type === "builtin" ? `${C.green}builtin${C.reset}` : item.type === "skill" ? `${C.cyan}skill${C.reset}` : `${C.yellow}command${C.reset}`;
47
+ console.log(` ${C.bold}/${item.name}${C.reset} [${typeLabel}] ${C.dim}${item.description || ""}${C.reset}`);
48
+ }
49
+ console.log();
50
+ });
51
+
52
+ skills
53
+ .command("info <name>")
54
+ .description("Show detailed info for a specific skill")
55
+ .option("--json", "JSON output")
56
+ .option("--project <path>", "Project path", process.cwd())
57
+ .action(async (name: string, opts) => {
58
+ const merged = { ...skills.opts(), ...opts };
59
+ const { listSlashItemsDetailed } = await import("../../services/slash-discovery/index.ts");
60
+ const result = listSlashItemsDetailed(merged.project ?? process.cwd());
61
+ const item = result.active.find((i) => i.name === name)
62
+ ?? result.shadowed.find((i) => i.name === name);
63
+ if (!item) {
64
+ console.error(`${C.red}✗${C.reset} Skill "${name}" not found`);
65
+ process.exit(1);
66
+ }
67
+ if (merged.json) { console.log(JSON.stringify(item, null, 2)); return; }
68
+ console.log(`\n${C.bold}/${item.name}${C.reset}`);
69
+ console.log(` Type: ${item.type}`);
70
+ console.log(` Source: ${item.source}`);
71
+ console.log(` Scope: ${item.scope}`);
72
+ console.log(` Path: ${item.filePath || "(built-in)"}`);
73
+ console.log(` Description: ${item.description || "(none)"}`);
74
+ if ("shadowedBy" in item) {
75
+ console.log(` ${C.yellow}Shadowed by:${C.reset} ${(item as any).shadowedBy.source}`);
76
+ }
77
+ console.log();
78
+ });
79
+ }
80
+
81
+ async function listAction(opts: any): Promise<void> {
82
+ const { listSlashItemsDetailed } = await import("../../services/slash-discovery/index.ts");
83
+ const result = listSlashItemsDetailed(opts.project ?? process.cwd());
84
+
85
+ if (opts.json) { console.log(JSON.stringify(result, null, 2)); return; }
86
+
87
+ // Roots section
88
+ if (result.roots.length > 0) {
89
+ console.log(`\n${C.bold}Skill Roots:${C.reset}`);
90
+ for (const root of result.roots) {
91
+ console.log(` ${root.path} ${C.dim}(${root.source})${C.reset}`);
92
+ }
93
+ }
94
+
95
+ const skills = result.active.filter((i) => i.type === "skill");
96
+ const commands = result.active.filter((i) => i.type === "command");
97
+ const builtins = result.active.filter((i) => i.type === "builtin");
98
+ console.log(`\n${skills.length} skills, ${commands.length} commands, ${builtins.length} built-in (${result.shadowed.length} shadowed)\n`);
99
+
100
+ // Active items table
101
+ const nonBuiltin = result.active.filter((i) => i.type !== "builtin");
102
+ if (nonBuiltin.length > 0) {
103
+ const nameW = Math.max(4, ...nonBuiltin.map((i) => i.name.length));
104
+ const typeW = 7;
105
+ const srcW = Math.max(6, ...nonBuiltin.map((i) => i.source.length));
106
+ const header = ` ${"Name".padEnd(nameW)} ${"Type".padEnd(typeW)} ${"Source".padEnd(srcW)} Description`;
107
+ console.log(`${C.bold}${header}${C.reset}`);
108
+ console.log(` ${"-".repeat(nameW)} ${"-".repeat(typeW)} ${"-".repeat(srcW)} ${"-".repeat(20)}`);
109
+ for (const item of nonBuiltin) {
110
+ const typeLabel = item.type === "skill" ? "skill" : "command";
111
+ console.log(` ${item.name.padEnd(nameW)} ${typeLabel.padEnd(typeW)} ${item.source.padEnd(srcW)} ${item.description || ""}`);
112
+ }
113
+ }
114
+
115
+ // Shadowed items
116
+ if (result.shadowed.length > 0) {
117
+ console.log(`\n${C.yellow}Shadowed:${C.reset}`);
118
+ for (const item of result.shadowed) {
119
+ console.log(` ${item.name} [${item.type}] ${item.source} ${C.dim}← shadowed by ${item.shadowedBy.source}${C.reset}`);
120
+ }
121
+ }
122
+ console.log();
123
+ }
@@ -1,9 +1,6 @@
1
1
  import { resolve } from "node:path";
2
- import { homedir } from "node:os";
3
2
  import { readFileSync, existsSync } from "node:fs";
4
-
5
- const STATUS_FILE = resolve(homedir(), ".ppm", "status.json");
6
- const PID_FILE = resolve(homedir(), ".ppm", "ppm.pid");
3
+ import { getPpmDir } from "../../services/ppm-dir.ts";
7
4
 
8
5
  interface DaemonStatus {
9
6
  running: boolean;
@@ -33,9 +30,10 @@ function getDaemonStatus(): DaemonStatus {
33
30
  state: null, pausedAt: null, pauseReason: null, lastCrashError: null,
34
31
  };
35
32
 
36
- if (existsSync(STATUS_FILE)) {
33
+ const statusFile = resolve(getPpmDir(), "status.json");
34
+ if (existsSync(statusFile)) {
37
35
  try {
38
- const data = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
36
+ const data = JSON.parse(readFileSync(statusFile, "utf-8"));
39
37
  const pid = data.pid as number;
40
38
  const tunnelPid = (data.tunnelPid as number) ?? null;
41
39
  const tunnelAlive = tunnelPid ? isAlive(tunnelPid) : false;
@@ -59,9 +57,10 @@ function getDaemonStatus(): DaemonStatus {
59
57
  } catch { return dead; }
60
58
  }
61
59
 
62
- if (existsSync(PID_FILE)) {
60
+ const pidFile = resolve(getPpmDir(), "ppm.pid");
61
+ if (existsSync(pidFile)) {
63
62
  try {
64
- const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
63
+ const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
65
64
  return { ...dead, running: isAlive(pid), pid };
66
65
  } catch { return dead; }
67
66
  }
@@ -1,11 +1,10 @@
1
1
  import { resolve } from "node:path";
2
- import { homedir } from "node:os";
3
2
  import { readFileSync, writeFileSync, unlinkSync, existsSync } from "node:fs";
3
+ import { getPpmDir } from "../../services/ppm-dir.ts";
4
4
 
5
- const PPM_DIR = process.env.PPM_HOME || resolve(homedir(), ".ppm");
6
- const PID_FILE = resolve(PPM_DIR, "ppm.pid");
7
- const STATUS_FILE = resolve(PPM_DIR, "status.json");
8
- const CMD_FILE = resolve(PPM_DIR, ".supervisor-cmd");
5
+ const pidFile = () => resolve(getPpmDir(), "ppm.pid");
6
+ const statusFile = () => resolve(getPpmDir(), "status.json");
7
+ const cmdFile = () => resolve(getPpmDir(), ".supervisor-cmd");
9
8
 
10
9
  function killPid(pid: number, label: string): boolean {
11
10
  try {
@@ -57,18 +56,18 @@ export async function stopServer(options?: { all?: boolean; kill?: boolean }) {
57
56
  console.log(" Stopping all PPM and cloudflared processes...\n");
58
57
  const cfKilled = killAllByName("cloudflared");
59
58
  let killed = 0;
60
- if (existsSync(STATUS_FILE)) {
59
+ if (existsSync(statusFile())) {
61
60
  try {
62
- const data = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
61
+ const data = JSON.parse(readFileSync(statusFile(), "utf-8"));
63
62
  // Kill supervisor first (cascades to server + tunnel children)
64
63
  if (data.supervisorPid) { killPid(data.supervisorPid, "supervisor"); killed++; }
65
64
  if (data.pid) { killPid(data.pid, "server"); killed++; }
66
65
  if (data.tunnelPid) { killPid(data.tunnelPid, "tunnel"); killed++; }
67
66
  } catch {}
68
67
  }
69
- if (existsSync(PID_FILE)) {
68
+ if (existsSync(pidFile())) {
70
69
  try {
71
- const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
70
+ const pid = parseInt(readFileSync(pidFile(), "utf-8").trim(), 10);
72
71
  if (!isNaN(pid)) { killPid(pid, "supervisor/server (pidfile)"); killed++; }
73
72
  } catch {}
74
73
  }
@@ -89,8 +88,8 @@ export async function stopServer(options?: { all?: boolean; kill?: boolean }) {
89
88
  /** Soft stop: write command file + signal supervisor → kills server only */
90
89
  async function softStopCmd() {
91
90
  let status: Record<string, unknown> | null = null;
92
- if (existsSync(STATUS_FILE)) {
93
- try { status = JSON.parse(readFileSync(STATUS_FILE, "utf-8")); } catch {}
91
+ if (existsSync(statusFile())) {
92
+ try { status = JSON.parse(readFileSync(statusFile(), "utf-8")); } catch {}
94
93
  }
95
94
 
96
95
  const supervisorPid = (status?.supervisorPid as number) ?? null;
@@ -115,7 +114,7 @@ async function softStopCmd() {
115
114
  }
116
115
 
117
116
  // Write soft stop command file + signal supervisor (Windows: polling picks it up)
118
- writeFileSync(CMD_FILE, JSON.stringify({ action: "soft_stop" }));
117
+ writeFileSync(cmdFile(), JSON.stringify({ action: "soft_stop" }));
119
118
  if (process.platform !== "win32") {
120
119
  try { process.kill(supervisorPid, "SIGUSR2"); } catch (e) {
121
120
  console.error(` Failed to signal supervisor: ${e}`);
@@ -128,7 +127,7 @@ async function softStopCmd() {
128
127
  while (Date.now() - start < 5000) {
129
128
  await Bun.sleep(500);
130
129
  try {
131
- const data = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
130
+ const data = JSON.parse(readFileSync(statusFile(), "utf-8"));
132
131
  if (data.state === "stopped") {
133
132
  console.log("PPM server stopped. Supervisor still alive (Cloud WS + tunnel).");
134
133
  console.log("Use 'ppm start' to restart or 'ppm stop --kill' to fully shut down.");
@@ -143,12 +142,12 @@ async function softStopCmd() {
143
142
  async function hardStop() {
144
143
  let status: { pid?: number; tunnelPid?: number; supervisorPid?: number } | null = null;
145
144
 
146
- if (existsSync(STATUS_FILE)) {
147
- try { status = JSON.parse(readFileSync(STATUS_FILE, "utf-8")); } catch {}
145
+ if (existsSync(statusFile())) {
146
+ try { status = JSON.parse(readFileSync(statusFile(), "utf-8")); } catch {}
148
147
  }
149
148
 
150
- const pidFromFile = existsSync(PID_FILE)
151
- ? parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10)
149
+ const pidFromFile = existsSync(pidFile())
150
+ ? parseInt(readFileSync(pidFile(), "utf-8").trim(), 10)
152
151
  : NaN;
153
152
 
154
153
  const supervisorPid = status?.supervisorPid ?? null;
@@ -191,6 +190,6 @@ async function hardStop() {
191
190
  }
192
191
 
193
192
  function cleanup() {
194
- if (existsSync(STATUS_FILE)) unlinkSync(STATUS_FILE);
195
- if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
193
+ if (existsSync(statusFile())) unlinkSync(statusFile());
194
+ if (existsSync(pidFile())) unlinkSync(pidFile());
196
195
  }
package/src/index.ts CHANGED
@@ -145,6 +145,9 @@ registerAutoStartCommands(program);
145
145
  const { registerCloudCommands } = await import("./cli/commands/cloud.ts");
146
146
  registerCloudCommands(program);
147
147
 
148
+ const { registerSkillsCommands } = await import("./cli/commands/skills-cmd.ts");
149
+ registerSkillsCommands(program);
150
+
148
151
  const { registerExtCommands } = await import("./cli/commands/ext-cmd.ts");
149
152
  registerExtCommands(program);
150
153
 
@@ -1,25 +1,30 @@
1
1
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
2
2
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
- import { homedir } from "node:os";
4
+ import { getPpmDir } from "../services/ppm-dir.ts";
5
5
 
6
6
  const ALGO = "aes-256-gcm";
7
7
 
8
- let keyPath = resolve(homedir(), ".ppm", "account.key");
8
+ let _keyPathOverride: string | null = null;
9
+
10
+ function getKeyPath(): string {
11
+ return _keyPathOverride ?? resolve(getPpmDir(), "account.key");
12
+ }
9
13
 
10
14
  /** Override key path (for tests) */
11
15
  export function setKeyPath(path: string): void {
12
- keyPath = path;
16
+ _keyPathOverride = path;
13
17
  _key = null; // invalidate cached key
14
18
  }
15
19
 
16
20
  function loadOrCreateKey(): Buffer {
17
- if (existsSync(keyPath)) {
18
- return Buffer.from(readFileSync(keyPath, "utf-8").trim(), "hex");
21
+ const kp = getKeyPath();
22
+ if (existsSync(kp)) {
23
+ return Buffer.from(readFileSync(kp, "utf-8").trim(), "hex");
19
24
  }
20
25
  const key = randomBytes(32);
21
- mkdirSync(resolve(keyPath, ".."), { recursive: true });
22
- writeFileSync(keyPath, key.toString("hex"), { mode: 0o600 });
26
+ mkdirSync(resolve(kp, ".."), { recursive: true });
27
+ writeFileSync(kp, key.toString("hex"), { mode: 0o600 });
23
28
  return key;
24
29
  }
25
30