@hienlh/ppm 0.12.12 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +11 -0
  3. package/assets/skills/ppm/SKILL.md +74 -0
  4. package/assets/skills/ppm/references/cli-reference.md +728 -0
  5. package/assets/skills/ppm/references/common-tasks.md +139 -0
  6. package/assets/skills/ppm/references/http-api.md +204 -0
  7. package/dist/web/assets/ai-settings-section-QE6nBNgN.js +1 -0
  8. package/dist/web/assets/{api-settings-C3T95dWg.js → api-settings-DAk7D-NP.js} +1 -1
  9. package/dist/web/assets/architecture-PBZL5I3N-DvZbltvY.js +1 -0
  10. package/dist/web/assets/{audio-preview-BkbgGtDH.js → audio-preview-J5neETTY.js} +1 -1
  11. package/dist/web/assets/chat-tab-sVHRa1Fz.js +12 -0
  12. package/dist/web/assets/{code-editor-BtspASkW.js → code-editor-tMfcFaQ5.js} +4 -4
  13. package/dist/web/assets/{conflict-editor-Dgsu6fmj.js → conflict-editor-FydCxWTC.js} +1 -1
  14. package/dist/web/assets/{csv-preview-DcWCjQkZ.js → csv-preview-HMSavgBb.js} +1 -1
  15. package/dist/web/assets/{database-viewer-C85RxdMV.js → database-viewer-Celi1puH.js} +2 -2
  16. package/dist/web/assets/{diff-viewer-2pPy97Tl.js → diff-viewer-NgDJLTk9.js} +1 -1
  17. package/dist/web/assets/{esm-_CLpyLJ_.js → esm-K1XIK4vc.js} +1 -1
  18. package/dist/web/assets/{extension-store-BZDZ9QRc.js → extension-store-3yZYn07W.js} +1 -1
  19. package/dist/web/assets/{extension-webview-U1lMYZ0p.js → extension-webview-xWAdCj3q.js} +1 -1
  20. package/dist/web/assets/{file-store-4BpOJthN.js → file-store-BrbCNyLm.js} +1 -1
  21. package/dist/web/assets/gitGraph-HDMCJU4V-BxhdxFgj.js +1 -0
  22. package/dist/web/assets/{image-preview-BcT1SbY2.js → image-preview-C6bFkdZD.js} +1 -1
  23. package/dist/web/assets/index-BMhiElt6.css +2 -0
  24. package/dist/web/assets/index-DtbAoxyy.js +23 -0
  25. package/dist/web/assets/info-3K5VOQVL-BwAZ2zd8.js +1 -0
  26. package/dist/web/assets/{input-2eDVjcRZ.js → input-Dk49gO8E.js} +1 -1
  27. package/dist/web/assets/{keybindings-store-BOG1yviy.js → keybindings-store-B-zET-0o.js} +1 -1
  28. package/dist/web/assets/keybindings-store-DaBV6qhz.js +1 -0
  29. package/dist/web/assets/{markdown-renderer-Dbam_-04.js → markdown-renderer-BAnnk1pI.js} +3 -3
  30. package/dist/web/assets/packet-RMMSAZCW-tx2n5Qry.js +1 -0
  31. package/dist/web/assets/{pdf-preview-BmHVGx32.js → pdf-preview-BNuFTSOL.js} +1 -1
  32. package/dist/web/assets/pie-UPGHQEXC-D6S2MqVT.js +1 -0
  33. package/dist/web/assets/plus-51UQ45rf.js +1 -0
  34. package/dist/web/assets/{port-forwarding-tab-Dkq1upWC.js → port-forwarding-tab-BbDlGxAs.js} +1 -1
  35. package/dist/web/assets/{postgres-viewer-BgBJAJ9q.js → postgres-viewer-Cman1YRO.js} +3 -3
  36. package/dist/web/assets/radar-KQ55EAFF-BviZcL-b.js +1 -0
  37. package/dist/web/assets/{scroll-area-CdxNNnN-.js → scroll-area-BEllam7_.js} +1 -1
  38. package/dist/web/assets/{settings-store-CMAssqyb.js → settings-store-BLLR7ed8.js} +2 -2
  39. package/dist/web/assets/settings-tab-n5X_Dbu4.js +1 -0
  40. package/dist/web/assets/{sql-query-editor-b7zJ8XPp.js → sql-query-editor-CVAnRFbi.js} +1 -1
  41. package/dist/web/assets/{sqlite-viewer-4lLAz1es.js → sqlite-viewer-D6JT11uu.js} +1 -1
  42. package/dist/web/assets/{tab-store-DNBsLdPn.js → tab-store-B3M9hjho.js} +1 -1
  43. package/dist/web/assets/{terminal-tab-BtnqkN1H.js → terminal-tab-B4kMthYo.js} +1 -1
  44. package/dist/web/assets/treemap-KZPCXAKY-CM54VdaB.js +1 -0
  45. package/dist/web/assets/{use-blob-url-QX-XajU8.js → use-blob-url-e9uTXjv5.js} +1 -1
  46. package/dist/web/assets/{use-monaco-theme-D68oX3XU.js → use-monaco-theme-BkZDwoVd.js} +1 -1
  47. package/dist/web/assets/{vendor-mermaid-sQS4C_iL.js → vendor-mermaid-Dx86tuVP.js} +2 -2
  48. package/dist/web/assets/{video-preview-CkOKvVLt.js → video-preview-BftQOOzF.js} +1 -1
  49. package/dist/web/index.html +18 -18
  50. package/dist/web/sw.js +1 -1
  51. package/docs/project-changelog.md +15 -1
  52. package/package.json +3 -3
  53. package/scripts/generate-ppm-skill.ts +23 -0
  54. package/scripts/lib/generate-cli-reference.ts +81 -0
  55. package/scripts/lib/generate-common-tasks.ts +14 -0
  56. package/scripts/lib/generate-http-api.ts +145 -0
  57. package/scripts/lib/generate-skill-md.ts +28 -0
  58. package/scripts/lib/write-output.ts +17 -0
  59. package/src/cli/commands/export-cmd.ts +85 -0
  60. package/src/index.ts +167 -153
  61. package/src/services/skill-export/backup-existing.ts +33 -0
  62. package/src/services/skill-export/copy-bundled-skill.ts +36 -0
  63. package/src/services/skill-export/generate-db-schema.ts +66 -0
  64. package/src/services/skill-export/index.ts +6 -0
  65. package/src/services/skill-export/resolve-assets-dir.ts +31 -0
  66. package/src/services/skill-export/resolve-target-dir.ts +17 -0
  67. package/src/web/components/chat/chat-tab.tsx +6 -1
  68. package/src/web/components/chat/message-list.tsx +96 -43
  69. package/src/web/hooks/use-chat.ts +37 -1
  70. package/src/web/lib/flatten-expansions.ts +36 -0
  71. package/templates/skill/SKILL.md.tmpl +74 -0
  72. package/templates/skill/common-tasks.md +139 -0
  73. package/assets/skills/ppm-guide/SKILL.md +0 -61
  74. package/dist/web/assets/ai-settings-section-NNWp6nw7.js +0 -1
  75. package/dist/web/assets/architecture-PBZL5I3N-DDuzYaUV.js +0 -1
  76. package/dist/web/assets/chat-tab-BZlP1qjX.js +0 -12
  77. package/dist/web/assets/chevron-up-BWBvMZkp.js +0 -1
  78. package/dist/web/assets/gitGraph-HDMCJU4V-BURAevTc.js +0 -1
  79. package/dist/web/assets/index-BWSRKVZn.js +0 -23
  80. package/dist/web/assets/index-b6tIZImC.css +0 -2
  81. package/dist/web/assets/info-3K5VOQVL-tSD4Fpi3.js +0 -1
  82. package/dist/web/assets/keybindings-store-BvdUoEC7.js +0 -1
  83. package/dist/web/assets/packet-RMMSAZCW-DmDLZUrV.js +0 -1
  84. package/dist/web/assets/pie-UPGHQEXC-w03Pc9ZR.js +0 -1
  85. package/dist/web/assets/pre-compact-button-Dp7Hs49L.js +0 -1
  86. package/dist/web/assets/pre-compact-section-DnM5fGSR.js +0 -1
  87. package/dist/web/assets/radar-KQ55EAFF-C9XQvoey.js +0 -1
  88. package/dist/web/assets/settings-tab-zYWKTq5z.js +0 -1
  89. package/dist/web/assets/treemap-KZPCXAKY-lmftxSky.js +0 -1
  90. package/scripts/generate-ppm-guide.ts +0 -92
  91. package/src/web/components/chat/pre-compact-section.tsx +0 -69
  92. /package/dist/web/assets/{api-client-DIhJ5qVW.js → api-client-Dvzcc_EO.js} +0 -0
  93. /package/dist/web/assets/{csv-parser-B5QW8pZ6.js → csv-parser--2WJNgS7.js} +0 -0
  94. /package/dist/web/assets/{dist-GtkSekuX.js → dist-im4ynINo.js} +0 -0
  95. /package/dist/web/assets/{katex-C3cZrCvP.js → katex-CKoArbIw.js} +0 -0
  96. /package/dist/web/assets/{lib-Bu71-TFS.js → lib-DQHnkzGy.js} +0 -0
  97. /package/dist/web/assets/{react-DMIOAtcX.js → react-GqWghJ-L.js} +0 -0
  98. /package/dist/web/assets/{refresh-cw-BjrAbUJe.js → refresh-cw-LlbZDJpO.js} +0 -0
  99. /package/dist/web/assets/{sql-completion-provider-CULTsCqR.js → sql-completion-provider-C3cq9j99.js} +0 -0
  100. /package/dist/web/assets/{table-tf7pRkME.js → table-Dq575bPF.js} +0 -0
  101. /package/dist/web/assets/{text-wrap-BV-R4Vvy.js → text-wrap-Cn6BNQfq.js} +0 -0
  102. /package/dist/web/assets/{trash-2-DjQOpgUV.js → trash-2-CJYoLw7Q.js} +0 -0
  103. /package/dist/web/assets/{utils-CQux7CsO.js → utils-CTg5uAYR.js} +0 -0
  104. /package/dist/web/assets/{vendor-xterm-K3_Xwigj.js → vendor-xterm-CU2c3f0A.js} +0 -0
package/src/index.ts CHANGED
@@ -3,156 +3,170 @@
3
3
  import { Command } from "commander";
4
4
  import { VERSION } from "./version.ts";
5
5
 
6
- const program = new Command();
7
-
8
- program
9
- .name("ppm")
10
- .description("Personal Project Manager mobile-first web IDE")
11
- .version(VERSION)
12
- .hook("preAction", () => {
13
- console.log(` PPM v${VERSION}\n`);
14
- });
15
-
16
- program
17
- .command("start")
18
- .description("Start the PPM server (background by default)")
19
- .option("-p, --port <port>", "Port to listen on")
20
- .option("-s, --share", "(deprecated) Tunnel is now always enabled")
21
- .option("--profile <name>", "DB profile name (e.g. 'dev' → ppm.dev.db)")
22
- .action(async (options) => {
23
- // Set DB profile before any DB access
24
- const { setDbProfile } = await import("./services/db.service.ts");
25
- if (options.profile) {
26
- setDbProfile(options.profile);
27
- }
28
- // Auto-init on first run
29
- const { hasConfig, initProject } = await import("./cli/commands/init.ts");
30
- if (!hasConfig()) {
31
- await initProject();
32
- }
33
- const { startServer } = await import("./server/index.ts");
34
- await startServer(options);
35
- });
36
-
37
- program
38
- .command("stop")
39
- .description("Stop the PPM server (supervisor stays alive)")
40
- .option("-a, --all", "Kill all PPM and cloudflared processes (including untracked)")
41
- .option("--kill", "Full shutdown (kills supervisor too)")
42
- .action(async (options) => {
43
- const { stopServer } = await import("./cli/commands/stop.ts");
44
- await stopServer(options);
45
- });
46
-
47
- program
48
- .command("down")
49
- .description("Fully shut down PPM (supervisor + server + tunnel)")
50
- .action(async () => {
51
- const { stopServer } = await import("./cli/commands/stop.ts");
52
- await stopServer({ kill: true });
53
- });
54
-
55
- program
56
- .command("restart")
57
- .description("Restart the server (keeps tunnel alive)")
58
- .option("--force", "Force resume from paused state")
59
- .action(async (options) => {
60
- const { restartServer } = await import("./cli/commands/restart.ts");
61
- await restartServer(options);
62
- });
63
-
64
- program
65
- .command("status")
66
- .description("Show PPM daemon status")
67
- .option("-a, --all", "Show all PPM and cloudflared processes (including untracked)")
68
- .option("--json", "Output as JSON")
69
- .action(async (options) => {
70
- const { showStatus } = await import("./cli/commands/status.ts");
71
- await showStatus(options);
72
- });
73
-
74
- program
75
- .command("open")
76
- .description("Open PPM in browser")
77
- .action(async () => {
78
- const { openBrowser } = await import("./cli/commands/open.ts");
79
- await openBrowser();
80
- });
81
-
82
- program
83
- .command("logs")
84
- .description("View PPM daemon logs")
85
- .option("-n, --tail <lines>", "Number of lines to show", "50")
86
- .option("-f, --follow", "Follow log output")
87
- .option("--clear", "Clear log file")
88
- .action(async (options) => {
89
- const { showLogs } = await import("./cli/commands/logs.ts");
90
- await showLogs(options);
91
- });
92
-
93
- program
94
- .command("report")
95
- .description("Report a bug on GitHub (pre-fills env info + logs)")
96
- .action(async () => {
97
- const { reportBug } = await import("./cli/commands/report.ts");
98
- await reportBug();
99
- });
100
-
101
- program
102
- .command("init")
103
- .description("Initialize PPM configuration (interactive or via flags)")
104
- .option("-p, --port <port>", "Port to listen on")
105
- .option("--scan <path>", "Directory to scan for git repos")
106
- .option("--auth", "Enable authentication")
107
- .option("--no-auth", "Disable authentication")
108
- .option("--password <pw>", "Set access password")
109
- .option("--share", "Pre-install cloudflared for sharing")
110
- .option("-y, --yes", "Non-interactive mode (use defaults + flags)")
111
- .action(async (options) => {
112
- const { initProject } = await import("./cli/commands/init.ts");
113
- await initProject(options);
114
- });
115
-
116
- const { registerProjectsCommands } = await import("./cli/commands/projects.ts");
117
- registerProjectsCommands(program);
118
-
119
- const { registerConfigCommands } = await import("./cli/commands/config-cmd.ts");
120
- registerConfigCommands(program);
121
-
122
- const { registerGitCommands } = await import("./cli/commands/git-cmd.ts");
123
- registerGitCommands(program);
124
-
125
- const { registerChatCommands } = await import("./cli/commands/chat-cmd.ts");
126
- registerChatCommands(program);
127
-
128
- program
129
- .command("upgrade")
130
- .description("Check for and install PPM updates")
131
- .option("--check", "Only check for updates, don't install")
132
- .action(async (options) => {
133
- const { upgradeCmd } = await import("./cli/commands/upgrade.ts");
134
- await upgradeCmd(options);
135
- });
136
-
137
- const { registerAutoStartCommands } = await import("./cli/commands/autostart.ts");
138
- registerAutoStartCommands(program);
139
-
140
- const { registerCloudCommands } = await import("./cli/commands/cloud.ts");
141
- registerCloudCommands(program);
142
-
143
- const { registerSkillsCommands } = await import("./cli/commands/skills-cmd.ts");
144
- registerSkillsCommands(program);
145
-
146
- const { registerExtCommands } = await import("./cli/commands/ext-cmd.ts");
147
- registerExtCommands(program);
148
-
149
- const { registerDbCommands } = await import("./cli/commands/db-cmd.ts");
150
- registerDbCommands(program);
151
-
152
- const { registerBotCommands } = await import("./cli/commands/bot-cmd.ts");
153
- registerBotCommands(program);
154
-
155
- const { registerJiraCommands } = await import("./cli/commands/jira-cmd.ts");
156
- await registerJiraCommands(program);
157
-
158
- program.parse();
6
+ /**
7
+ * Assemble the CLI program without parsing argv. Exported so build-time tools
8
+ * (e.g. scripts/generate-ppm-skill.ts) can introspect the Commander tree for
9
+ * auto-generated documentation. `preAction` hooks and action callbacks are
10
+ * registered but not invoked until `.parseAsync()` runs.
11
+ */
12
+ export async function buildProgram(): Promise<Command> {
13
+ const program = new Command();
14
+
15
+ program
16
+ .name("ppm")
17
+ .description("Personal Project Manager — mobile-first web IDE")
18
+ .version(VERSION)
19
+ .hook("preAction", () => {
20
+ console.log(` PPM v${VERSION}\n`);
21
+ });
22
+
23
+ program
24
+ .command("start")
25
+ .description("Start the PPM server (background by default)")
26
+ .option("-p, --port <port>", "Port to listen on")
27
+ .option("-s, --share", "(deprecated) Tunnel is now always enabled")
28
+ .option("--profile <name>", "DB profile name (e.g. 'dev' → ppm.dev.db)")
29
+ .action(async (options) => {
30
+ const { setDbProfile } = await import("./services/db.service.ts");
31
+ if (options.profile) {
32
+ setDbProfile(options.profile);
33
+ }
34
+ const { hasConfig, initProject } = await import("./cli/commands/init.ts");
35
+ if (!hasConfig()) {
36
+ await initProject();
37
+ }
38
+ const { startServer } = await import("./server/index.ts");
39
+ await startServer(options);
40
+ });
41
+
42
+ program
43
+ .command("stop")
44
+ .description("Stop the PPM server (supervisor stays alive)")
45
+ .option("-a, --all", "Kill all PPM and cloudflared processes (including untracked)")
46
+ .option("--kill", "Full shutdown (kills supervisor too)")
47
+ .action(async (options) => {
48
+ const { stopServer } = await import("./cli/commands/stop.ts");
49
+ await stopServer(options);
50
+ });
51
+
52
+ program
53
+ .command("down")
54
+ .description("Fully shut down PPM (supervisor + server + tunnel)")
55
+ .action(async () => {
56
+ const { stopServer } = await import("./cli/commands/stop.ts");
57
+ await stopServer({ kill: true });
58
+ });
59
+
60
+ program
61
+ .command("restart")
62
+ .description("Restart the server (keeps tunnel alive)")
63
+ .option("--force", "Force resume from paused state")
64
+ .action(async (options) => {
65
+ const { restartServer } = await import("./cli/commands/restart.ts");
66
+ await restartServer(options);
67
+ });
68
+
69
+ program
70
+ .command("status")
71
+ .description("Show PPM daemon status")
72
+ .option("-a, --all", "Show all PPM and cloudflared processes (including untracked)")
73
+ .option("--json", "Output as JSON")
74
+ .action(async (options) => {
75
+ const { showStatus } = await import("./cli/commands/status.ts");
76
+ await showStatus(options);
77
+ });
78
+
79
+ program
80
+ .command("open")
81
+ .description("Open PPM in browser")
82
+ .action(async () => {
83
+ const { openBrowser } = await import("./cli/commands/open.ts");
84
+ await openBrowser();
85
+ });
86
+
87
+ program
88
+ .command("logs")
89
+ .description("View PPM daemon logs")
90
+ .option("-n, --tail <lines>", "Number of lines to show", "50")
91
+ .option("-f, --follow", "Follow log output")
92
+ .option("--clear", "Clear log file")
93
+ .action(async (options) => {
94
+ const { showLogs } = await import("./cli/commands/logs.ts");
95
+ await showLogs(options);
96
+ });
97
+
98
+ program
99
+ .command("report")
100
+ .description("Report a bug on GitHub (pre-fills env info + logs)")
101
+ .action(async () => {
102
+ const { reportBug } = await import("./cli/commands/report.ts");
103
+ await reportBug();
104
+ });
105
+
106
+ program
107
+ .command("init")
108
+ .description("Initialize PPM configuration (interactive or via flags)")
109
+ .option("-p, --port <port>", "Port to listen on")
110
+ .option("--scan <path>", "Directory to scan for git repos")
111
+ .option("--auth", "Enable authentication")
112
+ .option("--no-auth", "Disable authentication")
113
+ .option("--password <pw>", "Set access password")
114
+ .option("--share", "Pre-install cloudflared for sharing")
115
+ .option("-y, --yes", "Non-interactive mode (use defaults + flags)")
116
+ .action(async (options) => {
117
+ const { initProject } = await import("./cli/commands/init.ts");
118
+ await initProject(options);
119
+ });
120
+
121
+ const { registerProjectsCommands } = await import("./cli/commands/projects.ts");
122
+ registerProjectsCommands(program);
123
+
124
+ const { registerConfigCommands } = await import("./cli/commands/config-cmd.ts");
125
+ registerConfigCommands(program);
126
+
127
+ const { registerGitCommands } = await import("./cli/commands/git-cmd.ts");
128
+ registerGitCommands(program);
129
+
130
+ const { registerChatCommands } = await import("./cli/commands/chat-cmd.ts");
131
+ registerChatCommands(program);
132
+
133
+ program
134
+ .command("upgrade")
135
+ .description("Check for and install PPM updates")
136
+ .option("--check", "Only check for updates, don't install")
137
+ .action(async (options) => {
138
+ const { upgradeCmd } = await import("./cli/commands/upgrade.ts");
139
+ await upgradeCmd(options);
140
+ });
141
+
142
+ const { registerAutoStartCommands } = await import("./cli/commands/autostart.ts");
143
+ registerAutoStartCommands(program);
144
+
145
+ const { registerCloudCommands } = await import("./cli/commands/cloud.ts");
146
+ registerCloudCommands(program);
147
+
148
+ const { registerSkillsCommands } = await import("./cli/commands/skills-cmd.ts");
149
+ registerSkillsCommands(program);
150
+
151
+ const { registerExtCommands } = await import("./cli/commands/ext-cmd.ts");
152
+ registerExtCommands(program);
153
+
154
+ const { registerDbCommands } = await import("./cli/commands/db-cmd.ts");
155
+ registerDbCommands(program);
156
+
157
+ const { registerBotCommands } = await import("./cli/commands/bot-cmd.ts");
158
+ registerBotCommands(program);
159
+
160
+ const { registerJiraCommands } = await import("./cli/commands/jira-cmd.ts");
161
+ await registerJiraCommands(program);
162
+
163
+ const { registerExportCommands } = await import("./cli/commands/export-cmd.ts");
164
+ registerExportCommands(program);
165
+
166
+ return program;
167
+ }
168
+
169
+ if (import.meta.main) {
170
+ const program = await buildProgram();
171
+ program.parse();
172
+ }
@@ -0,0 +1,33 @@
1
+ // Rename existing skill files to `.bak-<timestamp>` before overwriting.
2
+ // Preserves earlier backups by embedding a UTC timestamp in the suffix.
3
+ import { existsSync, renameSync, readdirSync, statSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+
6
+ /** Produce a compact UTC timestamp like `202604211733` (YYYYMMDDHHmm). */
7
+ export function makeTimestamp(d: Date = new Date()): string {
8
+ return d.toISOString().replace(/[-:T]/g, "").slice(0, 12);
9
+ }
10
+
11
+ export function backupExisting(targetDir: string, ts: string = makeTimestamp()): string[] {
12
+ if (!existsSync(targetDir)) return [];
13
+ const backedUp: string[] = [];
14
+ walkAndBackup(targetDir, ts, backedUp);
15
+ return backedUp;
16
+ }
17
+
18
+ function walkAndBackup(dir: string, ts: string, collected: string[]): void {
19
+ const entries = readdirSync(dir);
20
+ for (const name of entries) {
21
+ // Skip already-backed-up files to avoid `.md.bak-X.bak-Y` chains.
22
+ if (name.includes(".bak-")) continue;
23
+ const abs = resolve(dir, name);
24
+ const st = statSync(abs);
25
+ if (st.isDirectory()) {
26
+ walkAndBackup(abs, ts, collected);
27
+ } else if (st.isFile() && name.endsWith(".md")) {
28
+ const dest = `${abs}.bak-${ts}`;
29
+ renameSync(abs, dest);
30
+ collected.push(dest);
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,36 @@
1
+ // Recursive copy of the bundled skill package to a target directory.
2
+ // Skips any stale `.bak-*` files in source (defensive, should not occur in bundle).
3
+ import { cpSync, mkdirSync, existsSync, readdirSync, statSync, copyFileSync } from "node:fs";
4
+ import { resolve, relative } from "node:path";
5
+
6
+ export function copyBundledSkill(sourceDir: string, targetDir: string): string[] {
7
+ if (!existsSync(sourceDir)) {
8
+ throw new Error(`Source skill dir not found: ${sourceDir}`);
9
+ }
10
+ mkdirSync(targetDir, { recursive: true });
11
+ const copied: string[] = [];
12
+ walk(sourceDir, sourceDir, targetDir, copied);
13
+ return copied;
14
+ }
15
+
16
+ function walk(rootSrc: string, dir: string, targetRoot: string, collected: string[]): void {
17
+ for (const name of readdirSync(dir)) {
18
+ if (name.includes(".bak-")) continue;
19
+ const abs = resolve(dir, name);
20
+ const rel = relative(rootSrc, abs);
21
+ const dest = resolve(targetRoot, rel);
22
+ const st = statSync(abs);
23
+ if (st.isDirectory()) {
24
+ mkdirSync(dest, { recursive: true });
25
+ walk(rootSrc, abs, targetRoot, collected);
26
+ } else if (st.isFile()) {
27
+ copyFileSync(abs, dest);
28
+ collected.push(dest);
29
+ }
30
+ }
31
+ }
32
+
33
+ // Fallback single-shot copy using Node 16.7+ cpSync (kept in case walk is bypassed).
34
+ export function copyTree(src: string, dest: string): void {
35
+ cpSync(src, dest, { recursive: true });
36
+ }
@@ -0,0 +1,66 @@
1
+ // Runtime: read the user's PPM SQLite config DB (readonly) and render a markdown schema doc.
2
+ // Never opens the DB read-write. Gracefully handles missing DB.
3
+ import { existsSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { Database } from "bun:sqlite";
6
+ import { getPpmDir } from "../ppm-dir.ts";
7
+
8
+ interface ColumnInfo {
9
+ cid: number;
10
+ name: string;
11
+ type: string;
12
+ notnull: number;
13
+ dflt_value: string | null;
14
+ pk: number;
15
+ }
16
+
17
+ interface TableRow {
18
+ name: string;
19
+ }
20
+
21
+ export function generateDbSchemaMarkdown(dbPath?: string): string {
22
+ const path = dbPath ?? resolve(getPpmDir(), "ppm.db");
23
+ const header = "# PPM Database Schema\n\n_Auto-generated at install time from your local config DB._\n";
24
+
25
+ if (!existsSync(path)) {
26
+ return `${header}\n_Database not found at \`${path}\`. Run \`ppm init\` to create it._\n`;
27
+ }
28
+
29
+ let db: Database | null = null;
30
+ try {
31
+ db = new Database(path, { readonly: true });
32
+ const tables = db
33
+ .query<TableRow, []>("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
34
+ .all();
35
+
36
+ if (tables.length === 0) {
37
+ return `${header}\n_No tables found in \`${path}\`._\n`;
38
+ }
39
+
40
+ const parts: string[] = [header, ""];
41
+ parts.push(`Source: \`${path}\``);
42
+ parts.push("");
43
+
44
+ for (const t of tables) {
45
+ parts.push(`## ${t.name}`);
46
+ parts.push("");
47
+ const cols = db.query<ColumnInfo, []>(`PRAGMA table_info("${t.name}")`).all();
48
+ parts.push("| Column | Type | Nullable | PK | Default |");
49
+ parts.push("|---|---|---|---|---|");
50
+ for (const c of cols) {
51
+ const nullable = c.notnull === 0 ? "yes" : "no";
52
+ const pk = c.pk > 0 ? "yes" : "";
53
+ const def = c.dflt_value !== null ? `\`${c.dflt_value}\`` : "";
54
+ parts.push(`| \`${c.name}\` | \`${c.type || "—"}\` | ${nullable} | ${pk} | ${def} |`);
55
+ }
56
+ parts.push("");
57
+ }
58
+
59
+ return parts.join("\n") + "\n";
60
+ } catch (e) {
61
+ const msg = e instanceof Error ? e.message : String(e);
62
+ return `${header}\n_Failed to read database at \`${path}\`: ${msg}_\n`;
63
+ } finally {
64
+ db?.close();
65
+ }
66
+ }
@@ -0,0 +1,6 @@
1
+ // Barrel re-exports for the skill-export service.
2
+ export { resolveTargetDir, type SkillScope, type ResolveTargetOpts } from "./resolve-target-dir.ts";
3
+ export { resolveAssetsDir } from "./resolve-assets-dir.ts";
4
+ export { backupExisting, makeTimestamp } from "./backup-existing.ts";
5
+ export { copyBundledSkill } from "./copy-bundled-skill.ts";
6
+ export { generateDbSchemaMarkdown } from "./generate-db-schema.ts";
@@ -0,0 +1,31 @@
1
+ // Resolve the bundled skill assets dir. Works both in dev (`bun src/index.ts`)
2
+ // and installed npm package. Throws a clear error if assets are missing.
3
+ import { fileURLToPath } from "node:url";
4
+ import { resolve, dirname } from "node:path";
5
+ import { existsSync } from "node:fs";
6
+
7
+ const ASSETS_REL = "assets/skills/ppm";
8
+
9
+ /**
10
+ * Walks up from this file to find the repo/package root that contains
11
+ * `assets/skills/ppm/SKILL.md`. Covers both:
12
+ * - dev: src/services/skill-export/*.ts → repo root is ../../..
13
+ * - bundled: compiled binary walks up similarly when `bun build --compile` preserves structure
14
+ */
15
+ export function resolveAssetsDir(): string {
16
+ const here = dirname(fileURLToPath(import.meta.url));
17
+ const candidates = [
18
+ resolve(here, "../../..", ASSETS_REL),
19
+ resolve(here, "../..", ASSETS_REL),
20
+ resolve(here, "..", ASSETS_REL),
21
+ resolve(process.cwd(), ASSETS_REL),
22
+ ];
23
+ for (const c of candidates) {
24
+ if (existsSync(resolve(c, "SKILL.md"))) return c;
25
+ }
26
+ throw new Error(
27
+ `Bundled PPM skill assets not found. Searched:\n ${candidates.join(
28
+ "\n ",
29
+ )}\nRun \`bun run generate:skill\` (dev) or reinstall PPM.`,
30
+ );
31
+ }
@@ -0,0 +1,17 @@
1
+ // Resolve the install target directory based on scope/output flags.
2
+ // Precedence: --output > --scope project > --scope user (default).
3
+ import { resolve } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ export type SkillScope = "user" | "project";
7
+
8
+ export interface ResolveTargetOpts {
9
+ scope?: SkillScope;
10
+ output?: string;
11
+ }
12
+
13
+ export function resolveTargetDir(opts: ResolveTargetOpts): string {
14
+ if (opts.output) return resolve(opts.output);
15
+ if (opts.scope === "project") return resolve(process.cwd(), ".claude/skills/ppm");
16
+ return resolve(homedir(), ".claude/skills/ppm");
17
+ }
@@ -88,6 +88,9 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
88
88
 
89
89
  const {
90
90
  messages,
91
+ renderedMessages,
92
+ expandCompact,
93
+ isCompactExpanded,
91
94
  messagesLoading,
92
95
  isStreaming,
93
96
  phase,
@@ -384,7 +387,9 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
384
387
 
385
388
  {/* Messages */}
386
389
  <MessageList
387
- messages={messages}
390
+ messages={renderedMessages}
391
+ onExpandCompact={expandCompact}
392
+ isCompactExpanded={isCompactExpanded}
388
393
  messagesLoading={messagesLoading}
389
394
  pendingApproval={pendingApproval}
390
395
  onApprovalResponse={respondToApproval}