@hienlh/ppm 0.12.12 → 0.13.2

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 (119) hide show
  1. package/CHANGELOG.md +31 -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--hRMnXRZ.js} +1 -1
  11. package/dist/web/assets/chat-tab-4kL3DNxf.js +12 -0
  12. package/dist/web/assets/{code-editor-BtspASkW.js → code-editor-Caq5_BaF.js} +4 -4
  13. package/dist/web/assets/{conflict-editor-Dgsu6fmj.js → conflict-editor-Dlo25nmt.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-DcBl6OkV.js} +2 -2
  16. package/dist/web/assets/{diff-viewer-2pPy97Tl.js → diff-viewer-CCzPq1o-.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-D7bGVSEd.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-CfkqnhXJ.js} +1 -1
  23. package/dist/web/assets/index-BGFG66Gh.js +27 -0
  24. package/dist/web/assets/index-Bce0weeW.css +2 -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-DyAm7zuA.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-CZPcuy5c.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-3RNozlZ5.js} +1 -1
  35. package/dist/web/assets/{postgres-viewer-BgBJAJ9q.js → postgres-viewer-CXJv4TXc.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-Cnav4g2u.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-C8WUEFhA.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-CaEsMxp8.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-Dfz71RGb.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/server/index.ts +12 -4
  62. package/src/services/autostart-generator.ts +3 -1
  63. package/src/services/autostart-register.ts +17 -0
  64. package/src/services/sd-notify.ts +27 -0
  65. package/src/services/skill-export/backup-existing.ts +33 -0
  66. package/src/services/skill-export/copy-bundled-skill.ts +36 -0
  67. package/src/services/skill-export/generate-db-schema.ts +66 -0
  68. package/src/services/skill-export/index.ts +6 -0
  69. package/src/services/skill-export/resolve-assets-dir.ts +31 -0
  70. package/src/services/skill-export/resolve-target-dir.ts +17 -0
  71. package/src/services/supervisor.ts +31 -5
  72. package/src/web/components/chat/chat-history-bar.tsx +2 -9
  73. package/src/web/components/chat/chat-history-panel.tsx +2 -9
  74. package/src/web/components/chat/chat-tab.tsx +6 -1
  75. package/src/web/components/chat/chat-welcome.tsx +1 -18
  76. package/src/web/components/chat/message-list.tsx +96 -43
  77. package/src/web/components/layout/draggable-tab.tsx +12 -5
  78. package/src/web/hooks/use-chat.ts +37 -1
  79. package/src/web/hooks/use-notification-badge.ts +7 -7
  80. package/src/web/lib/favicon.ts +37 -15
  81. package/src/web/lib/flatten-expansions.ts +36 -0
  82. package/src/web/lib/format-date.ts +21 -0
  83. package/src/web/styles/globals.css +12 -0
  84. package/templates/skill/SKILL.md.tmpl +74 -0
  85. package/templates/skill/common-tasks.md +139 -0
  86. package/assets/skills/ppm-guide/SKILL.md +0 -61
  87. package/bun.lock +0 -2062
  88. package/bunfig.toml +0 -2
  89. package/dist/web/assets/ai-settings-section-NNWp6nw7.js +0 -1
  90. package/dist/web/assets/architecture-PBZL5I3N-DDuzYaUV.js +0 -1
  91. package/dist/web/assets/chat-tab-BZlP1qjX.js +0 -12
  92. package/dist/web/assets/chevron-up-BWBvMZkp.js +0 -1
  93. package/dist/web/assets/gitGraph-HDMCJU4V-BURAevTc.js +0 -1
  94. package/dist/web/assets/index-BWSRKVZn.js +0 -23
  95. package/dist/web/assets/index-b6tIZImC.css +0 -2
  96. package/dist/web/assets/info-3K5VOQVL-tSD4Fpi3.js +0 -1
  97. package/dist/web/assets/keybindings-store-BvdUoEC7.js +0 -1
  98. package/dist/web/assets/packet-RMMSAZCW-DmDLZUrV.js +0 -1
  99. package/dist/web/assets/pie-UPGHQEXC-w03Pc9ZR.js +0 -1
  100. package/dist/web/assets/pre-compact-button-Dp7Hs49L.js +0 -1
  101. package/dist/web/assets/pre-compact-section-DnM5fGSR.js +0 -1
  102. package/dist/web/assets/radar-KQ55EAFF-C9XQvoey.js +0 -1
  103. package/dist/web/assets/settings-tab-zYWKTq5z.js +0 -1
  104. package/dist/web/assets/treemap-KZPCXAKY-lmftxSky.js +0 -1
  105. package/scripts/generate-ppm-guide.ts +0 -92
  106. package/src/web/components/chat/pre-compact-section.tsx +0 -69
  107. /package/dist/web/assets/{api-client-DIhJ5qVW.js → api-client-Dvzcc_EO.js} +0 -0
  108. /package/dist/web/assets/{csv-parser-B5QW8pZ6.js → csv-parser--2WJNgS7.js} +0 -0
  109. /package/dist/web/assets/{dist-GtkSekuX.js → dist-im4ynINo.js} +0 -0
  110. /package/dist/web/assets/{katex-C3cZrCvP.js → katex-CKoArbIw.js} +0 -0
  111. /package/dist/web/assets/{lib-Bu71-TFS.js → lib-DQHnkzGy.js} +0 -0
  112. /package/dist/web/assets/{react-DMIOAtcX.js → react-GqWghJ-L.js} +0 -0
  113. /package/dist/web/assets/{refresh-cw-BjrAbUJe.js → refresh-cw-LlbZDJpO.js} +0 -0
  114. /package/dist/web/assets/{sql-completion-provider-CULTsCqR.js → sql-completion-provider-C3cq9j99.js} +0 -0
  115. /package/dist/web/assets/{table-tf7pRkME.js → table-Dq575bPF.js} +0 -0
  116. /package/dist/web/assets/{text-wrap-BV-R4Vvy.js → text-wrap-Cn6BNQfq.js} +0 -0
  117. /package/dist/web/assets/{trash-2-DjQOpgUV.js → trash-2-CJYoLw7Q.js} +0 -0
  118. /package/dist/web/assets/{utils-CQux7CsO.js → utils-CTg5uAYR.js} +0 -0
  119. /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
+ }
@@ -499,11 +499,15 @@ export async function startServer(options: {
499
499
  qr.generate(shareUrl, { small: true });
500
500
  }
501
501
 
502
- // Auto-enable system service (systemd/launchd) for boot resilience
502
+ // Auto-enable system service (systemd/launchd) for boot resilience.
503
+ // Also regenerates a stale unit file (missing Type=notify) so users who
504
+ // upgrade past the v0.13 → v0.14 systemd cgroup fix get the new unit
505
+ // picked up on next `systemctl --user restart ppm.service` / reboot.
503
506
  try {
504
- const { getAutoStartStatus, enableAutoStart } = await import("../services/autostart-register.ts");
507
+ const { getAutoStartStatus, enableAutoStart, isAutoStartUnitStale } = await import("../services/autostart-register.ts");
505
508
  const status = getAutoStartStatus();
506
- if (!status.enabled) {
509
+ const stale = status.enabled && isAutoStartUnitStale();
510
+ if (!status.enabled || stale) {
507
511
  const autoConfig = {
508
512
  port, host,
509
513
  share: !!options.share,
@@ -511,7 +515,11 @@ export async function startServer(options: {
511
515
  };
512
516
  // skipStart: supervisor is already running from direct spawn above
513
517
  await enableAutoStart(autoConfig, { skipStart: true });
514
- console.log(` ✓ Auto-restart enabled (${status.platform}). Disable: ppm autostart disable`);
518
+ if (stale) {
519
+ console.log(` ↻ Auto-restart config migrated (Type=notify). Run 'systemctl --user restart ppm.service' to apply.`);
520
+ } else {
521
+ console.log(` ✓ Auto-restart enabled (${status.platform}). Disable: ppm autostart disable`);
522
+ }
515
523
  }
516
524
  } catch {}
517
525
 
@@ -125,10 +125,12 @@ After=network-online.target
125
125
  Wants=network-online.target
126
126
 
127
127
  [Service]
128
- Type=simple
128
+ Type=notify
129
+ NotifyAccess=all
129
130
  ExecStart=${execStart}
130
131
  Restart=on-failure
131
132
  RestartSec=5
133
+ TimeoutStartSec=60
132
134
  TimeoutStopSec=10
133
135
  KillMode=mixed
134
136
  ${envPath}
@@ -350,4 +350,21 @@ export function getAutoStartStatus(): AutoStartStatus {
350
350
  };
351
351
  }
352
352
 
353
+ /**
354
+ * Detect whether an existing systemd unit file is outdated and needs
355
+ * regeneration. Currently flags units missing Type=notify (introduced to fix
356
+ * the WSL/systemd upgrade-kill bug). Linux-only; returns false elsewhere.
357
+ */
358
+ export function isAutoStartUnitStale(): boolean {
359
+ if (process.platform !== "linux") return false;
360
+ try {
361
+ const path = getServicePath();
362
+ if (!existsSync(path)) return false;
363
+ const content = readFileSync(path, "utf-8");
364
+ return !content.includes("Type=notify");
365
+ } catch {
366
+ return false;
367
+ }
368
+ }
369
+
353
370
  export { loadMetadata, METADATA_FILE };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * sd_notify helper — forwards messages to systemd via the `systemd-notify` binary.
3
+ * No-op on non-systemd platforms (NOTIFY_SOCKET unset).
4
+ *
5
+ * Usage:
6
+ * await sdNotify("READY=1"); // mark unit active (Type=notify)
7
+ * await sdNotify(`MAINPID=${newPid}`); // handoff main process (NotifyAccess=all)
8
+ *
9
+ * Shelling out to `systemd-notify` avoids implementing AF_UNIX SOCK_DGRAM
10
+ * transport in Node/Bun (not supported by node:dgram). The binary ships with
11
+ * systemd itself, so availability matches systemd availability.
12
+ */
13
+ export async function sdNotify(state: string): Promise<void> {
14
+ if (!process.env.NOTIFY_SOCKET) return; // not running under systemd
15
+ try {
16
+ const proc = Bun.spawn({
17
+ cmd: ["systemd-notify", state],
18
+ stdio: ["ignore", "ignore", "ignore"],
19
+ env: process.env,
20
+ });
21
+ await proc.exited;
22
+ } catch {
23
+ // best-effort: if systemd-notify is missing, startup still proceeds
24
+ // (Type=notify units without READY=1 will time out, but that's already
25
+ // the failure mode — this helper doesn't make it worse).
26
+ }
27
+ }
@@ -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
+ }