@hienlh/ppm 0.12.11 → 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 (64) hide show
  1. package/CHANGELOG.md +18 -0
  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/bun.lock +2062 -0
  8. package/bunfig.toml +2 -0
  9. package/dist/web/assets/{audio-preview-DnQmf9fu.js → audio-preview-J5neETTY.js} +1 -1
  10. package/dist/web/assets/chat-tab-sVHRa1Fz.js +12 -0
  11. package/dist/web/assets/{code-editor-B-lU1fz3.js → code-editor-tMfcFaQ5.js} +2 -2
  12. package/dist/web/assets/{conflict-editor-BYzf3LuW.js → conflict-editor-FydCxWTC.js} +1 -1
  13. package/dist/web/assets/{database-viewer-DjvnIn8p.js → database-viewer-Celi1puH.js} +1 -1
  14. package/dist/web/assets/diff-viewer-NgDJLTk9.js +4 -0
  15. package/dist/web/assets/{extension-webview-4xMREn_x.js → extension-webview-xWAdCj3q.js} +1 -1
  16. package/dist/web/assets/{image-preview-CkS2PVdQ.js → image-preview-C6bFkdZD.js} +1 -1
  17. package/dist/web/assets/index-BMhiElt6.css +2 -0
  18. package/dist/web/assets/{index-FGlF8IWZ.js → index-DtbAoxyy.js} +2 -2
  19. package/dist/web/assets/{markdown-renderer-Bj2B05Km.js → markdown-renderer-BAnnk1pI.js} +1 -1
  20. package/dist/web/assets/{pdf-preview-CCyw5cuH.js → pdf-preview-BNuFTSOL.js} +1 -1
  21. package/dist/web/assets/{port-forwarding-tab-Cebb5Eix.js → port-forwarding-tab-BbDlGxAs.js} +1 -1
  22. package/dist/web/assets/{postgres-viewer-BrOiliEv.js → postgres-viewer-Cman1YRO.js} +1 -1
  23. package/dist/web/assets/{settings-tab-D0XjupJm.js → settings-tab-n5X_Dbu4.js} +1 -1
  24. package/dist/web/assets/{sqlite-viewer-OEVq_-Po.js → sqlite-viewer-D6JT11uu.js} +1 -1
  25. package/dist/web/assets/{terminal-tab-MjmJaQyA.js → terminal-tab-B4kMthYo.js} +1 -1
  26. package/dist/web/assets/{video-preview-B819qvlp.js → video-preview-BftQOOzF.js} +1 -1
  27. package/dist/web/index.html +2 -2
  28. package/dist/web/sw.js +1 -1
  29. package/docs/project-changelog.md +15 -1
  30. package/package.json +3 -3
  31. package/scripts/generate-ppm-skill.ts +23 -0
  32. package/scripts/lib/generate-cli-reference.ts +81 -0
  33. package/scripts/lib/generate-common-tasks.ts +14 -0
  34. package/scripts/lib/generate-http-api.ts +145 -0
  35. package/scripts/lib/generate-skill-md.ts +28 -0
  36. package/scripts/lib/write-output.ts +17 -0
  37. package/src/cli/commands/export-cmd.ts +85 -0
  38. package/src/index.ts +167 -153
  39. package/src/providers/claude-agent-sdk.ts +1 -135
  40. package/src/server/index.ts +2 -1
  41. package/src/server/routes/chat.ts +18 -0
  42. package/src/server/routes/git.ts +16 -0
  43. package/src/services/git.service.ts +34 -0
  44. package/src/services/jsonl-transcript-parser.ts +216 -0
  45. package/src/services/skill-export/backup-existing.ts +33 -0
  46. package/src/services/skill-export/copy-bundled-skill.ts +36 -0
  47. package/src/services/skill-export/generate-db-schema.ts +66 -0
  48. package/src/services/skill-export/index.ts +6 -0
  49. package/src/services/skill-export/resolve-assets-dir.ts +31 -0
  50. package/src/services/skill-export/resolve-target-dir.ts +17 -0
  51. package/src/services/supervisor.ts +2 -1
  52. package/src/web/components/chat/chat-tab.tsx +6 -1
  53. package/src/web/components/chat/message-list.tsx +101 -9
  54. package/src/web/components/chat/pre-compact-button.tsx +50 -0
  55. package/src/web/components/editor/diff-viewer.tsx +21 -5
  56. package/src/web/hooks/use-chat.ts +37 -1
  57. package/src/web/lib/flatten-expansions.ts +36 -0
  58. package/templates/skill/SKILL.md.tmpl +74 -0
  59. package/templates/skill/common-tasks.md +139 -0
  60. package/assets/skills/ppm-guide/SKILL.md +0 -61
  61. package/dist/web/assets/chat-tab-Cf6T3mGO.js +0 -12
  62. package/dist/web/assets/diff-viewer-CP2jcR5J.js +0 -4
  63. package/dist/web/assets/index-BTjuH4fn.css +0 -2
  64. package/scripts/generate-ppm-guide.ts +0 -92
@@ -0,0 +1,81 @@
1
+ // Walk the Commander tree produced by buildProgram() and emit a markdown reference.
2
+ // No side effects — buildProgram() assembles commands but does not invoke any `action` callback.
3
+ import type { Command, Option } from "commander";
4
+ import { buildProgram } from "../../src/index.ts";
5
+ import type { OutputFile } from "./write-output.ts";
6
+
7
+ export async function generateCliReference(_root: string): Promise<OutputFile[]> {
8
+ const program = await buildProgram();
9
+ const header = "# PPM CLI Reference\n\n_Auto-generated. Do not edit._\n\nRoot binary: `ppm`. Run `ppm <command> --help` for full usage.\n";
10
+
11
+ // Global options section (root-level options only, excluding implicit -V/-h).
12
+ const globalOptsMd = renderRootOptions(program);
13
+
14
+ // Child commands sorted alphabetically for deterministic output.
15
+ const children = [...program.commands].filter((c) => !isHidden(c)).sort((a, b) => a.name().localeCompare(b.name()));
16
+ const sections = children.map((c) => renderCommand(c, 2)).join("\n");
17
+
18
+ const content = `${header}\n${globalOptsMd}\n## Commands\n\n${sections}`;
19
+ return [{ relPath: "references/cli-reference.md", content }];
20
+ }
21
+
22
+ function isHidden(cmd: Command): boolean {
23
+ // Commander's internal flag. Public typings omit `_hidden`; access defensively.
24
+ return Boolean((cmd as unknown as { _hidden?: boolean })._hidden);
25
+ }
26
+
27
+ function renderRootOptions(program: Command): string {
28
+ const opts = program.options.filter((o) => !o.hidden);
29
+ if (opts.length === 0) return "";
30
+ const rows = opts.map((o) => `- \`${o.flags}\` — ${o.description || "_(no description)_"}`).join("\n");
31
+ return `## Global Options\n\n${rows}\n`;
32
+ }
33
+
34
+ function renderCommand(cmd: Command, depth: number): string {
35
+ const heading = "#".repeat(Math.min(depth, 6));
36
+ const pathName = commandPath(cmd);
37
+ const desc = cmd.description() || "_(no description)_";
38
+
39
+ const parts: string[] = [];
40
+ parts.push(`${heading} \`ppm ${pathName}\``);
41
+ parts.push("");
42
+ parts.push(desc);
43
+
44
+ const opts = cmd.options.filter((o: Option) => !o.hidden);
45
+ if (opts.length > 0) {
46
+ parts.push("");
47
+ parts.push("**Options:**");
48
+ for (const o of opts) {
49
+ const d = o.description || "_(no description)_";
50
+ const def = o.defaultValue !== undefined ? ` (default: \`${JSON.stringify(o.defaultValue)}\`)` : "";
51
+ parts.push(`- \`${o.flags}\` — ${d}${def}`);
52
+ }
53
+ }
54
+
55
+ const usage = cmd.usage();
56
+ if (usage && usage !== "[options]") {
57
+ parts.push("");
58
+ parts.push(`**Usage:** \`ppm ${pathName} ${usage}\``);
59
+ }
60
+
61
+ parts.push("");
62
+
63
+ // Recurse into subcommands (preserve registration order per phase 2 spec).
64
+ const subs = cmd.commands.filter((c) => !isHidden(c));
65
+ for (const sub of subs) {
66
+ parts.push(renderCommand(sub, depth + 1));
67
+ }
68
+
69
+ return parts.join("\n");
70
+ }
71
+
72
+ function commandPath(cmd: Command): string {
73
+ // Walk up parents to construct e.g. `db list` instead of just `list`.
74
+ const chain: string[] = [];
75
+ let cur: Command | null = cmd;
76
+ while (cur && cur.parent) {
77
+ chain.unshift(cur.name());
78
+ cur = cur.parent;
79
+ }
80
+ return chain.join(" ");
81
+ }
@@ -0,0 +1,14 @@
1
+ // Phase 4 populates. Phase 1 stub.
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import type { OutputFile } from "./write-output.ts";
5
+
6
+ export function generateCommonTasks(root: string): OutputFile[] {
7
+ const tmplPath = resolve(root, "templates/skill/common-tasks.md");
8
+ if (existsSync(tmplPath)) {
9
+ const content = readFileSync(tmplPath, "utf-8");
10
+ return [{ relPath: "references/common-tasks.md", content }];
11
+ }
12
+ const stub = "# PPM Common Tasks\n\n_TODO: populated in phase 4._\n";
13
+ return [{ relPath: "references/common-tasks.md", content: stub }];
14
+ }
@@ -0,0 +1,145 @@
1
+ // Static scan of Hono route definitions. Two-pass:
2
+ // 1. Parse src/server/index.ts → mount map { mountPath: routerIdent } + import { routerIdent: relFile }.
3
+ // 2. For each mounted route file, regex-scan HTTP method calls, group by mount prefix.
4
+ // No code execution — all data comes from string parsing.
5
+ import { readFileSync, existsSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import type { OutputFile } from "./write-output.ts";
8
+
9
+ const METHOD_RE = /\.(get|post|put|patch|delete|all)\s*\(\s*["'`]([^"'`]+)["'`]/g;
10
+ const MOUNT_RE = /\.route\s*\(\s*["'`]([^"'`]+)["'`]\s*,\s*(\w+)\s*\)/g;
11
+ const IMPORT_RE = /import\s*\{\s*([\w,\s]+)\s*\}\s*from\s*["'`]([^"'`]+)["'`]/g;
12
+
13
+ interface RouteEntry {
14
+ method: string;
15
+ path: string;
16
+ desc?: string;
17
+ }
18
+
19
+ export function generateHttpApi(root: string): OutputFile[] {
20
+ const serverIndexPath = resolve(root, "src/server/index.ts");
21
+ if (!existsSync(serverIndexPath)) {
22
+ return [{ relPath: "references/http-api.md", content: "# PPM HTTP API\n\n_Server index not found._\n" }];
23
+ }
24
+
25
+ const serverSrc = readFileSync(serverIndexPath, "utf-8");
26
+ const importMap = parseImports(serverSrc);
27
+ const mounts = parseMounts(serverSrc);
28
+
29
+ // Group routes by mount prefix
30
+ const grouped: Record<string, RouteEntry[]> = {};
31
+ const warnings: string[] = [];
32
+
33
+ for (const [prefix, routerIdent] of mounts) {
34
+ const rel = importMap.get(routerIdent);
35
+ if (!rel) {
36
+ warnings.push(`Unresolved import for router '${routerIdent}' (mount: ${prefix})`);
37
+ continue;
38
+ }
39
+ const routeFile = resolveImport(serverIndexPath, rel);
40
+ if (!existsSync(routeFile)) {
41
+ warnings.push(`Route file not found: ${routeFile} (router: ${routerIdent})`);
42
+ continue;
43
+ }
44
+ const entries = scanRoutes(readFileSync(routeFile, "utf-8"));
45
+ if (!grouped[prefix]) grouped[prefix] = [];
46
+ grouped[prefix].push(...entries);
47
+ }
48
+
49
+ // Build markdown
50
+ const parts: string[] = [];
51
+ parts.push("# PPM HTTP API");
52
+ parts.push("");
53
+ parts.push("_Auto-generated. Do not edit._");
54
+ parts.push("");
55
+ parts.push("_Base URL: `http://localhost:8080` (default; override via `ppm config set port <n>`)._");
56
+ parts.push("");
57
+
58
+ const sortedPrefixes = Object.keys(grouped).sort();
59
+ for (const prefix of sortedPrefixes) {
60
+ const routes = grouped[prefix];
61
+ if (!routes || routes.length === 0) continue;
62
+ parts.push(`## ${prefix || "/"}`);
63
+ parts.push("");
64
+ for (const r of routes) {
65
+ const fullPath = joinPath(prefix, r.path);
66
+ const method = r.method.toUpperCase().padEnd(6);
67
+ parts.push(`- \`${method} ${fullPath}\`${r.desc ? ` — ${r.desc}` : ""}`);
68
+ }
69
+ parts.push("");
70
+ }
71
+
72
+ parts.push("## WebSocket");
73
+ parts.push("");
74
+ parts.push("- `ws://<host>/ws/chat` — AI chat stream (Claude Agent SDK)");
75
+ parts.push("- `ws://<host>/ws/terminal` — PTY terminal multiplexer");
76
+ parts.push("- `ws://<host>/ws/extensions` — extension host channel");
77
+ parts.push("");
78
+
79
+ const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf-8")) as { version: string };
80
+ parts.push(`<!-- Generated from src/server/routes/ for PPM v${pkg.version} -->`);
81
+
82
+ if (warnings.length > 0) {
83
+ parts.push("");
84
+ parts.push("<!--");
85
+ parts.push("Scanner warnings (build-time):");
86
+ for (const w of warnings) parts.push(` - ${w}`);
87
+ parts.push("-->");
88
+ }
89
+
90
+ return [{ relPath: "references/http-api.md", content: parts.join("\n") + "\n" }];
91
+ }
92
+
93
+ function parseImports(src: string): Map<string, string> {
94
+ const map = new Map<string, string>();
95
+ let m: RegExpExecArray | null;
96
+ const re = new RegExp(IMPORT_RE);
97
+ while ((m = re.exec(src)) !== null) {
98
+ const ids = (m[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
99
+ const path = m[2] ?? "";
100
+ for (const id of ids) map.set(id, path);
101
+ }
102
+ return map;
103
+ }
104
+
105
+ function parseMounts(src: string): Array<[string, string]> {
106
+ const out: Array<[string, string]> = [];
107
+ let m: RegExpExecArray | null;
108
+ const re = new RegExp(MOUNT_RE);
109
+ while ((m = re.exec(src)) !== null) {
110
+ out.push([m[1] ?? "", m[2] ?? ""]);
111
+ }
112
+ return out;
113
+ }
114
+
115
+ function resolveImport(fromFile: string, spec: string): string {
116
+ // Resolve relative import from a source file. Preserve `.ts` suffix if present.
117
+ const fromDir = resolve(fromFile, "..");
118
+ let abs = resolve(fromDir, spec);
119
+ if (!abs.endsWith(".ts") && !abs.endsWith(".js")) {
120
+ abs += ".ts";
121
+ }
122
+ return abs;
123
+ }
124
+
125
+ function scanRoutes(src: string): RouteEntry[] {
126
+ const entries: RouteEntry[] = [];
127
+ let m: RegExpExecArray | null;
128
+ const re = new RegExp(METHOD_RE);
129
+ while ((m = re.exec(src)) !== null) {
130
+ const method = m[1] ?? "";
131
+ const path = m[2] ?? "";
132
+ // Skip obviously-bogus matches (e.g. "http://" in URLs — regex already avoids these since path can't start with http:).
133
+ if (path.startsWith("http:") || path.startsWith("https:")) continue;
134
+ entries.push({ method, path });
135
+ }
136
+ return entries;
137
+ }
138
+
139
+ function joinPath(prefix: string, routePath: string): string {
140
+ if (!prefix || prefix === "/") return routePath;
141
+ if (routePath === "/" || routePath === "") return prefix;
142
+ const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
143
+ const r = routePath.startsWith("/") ? routePath : `/${routePath}`;
144
+ return p + r;
145
+ }
@@ -0,0 +1,28 @@
1
+ // Phase 4 populates. Phase 1 stub returns a placeholder SKILL.md.
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import type { OutputFile } from "./write-output.ts";
5
+
6
+ export function generateSkillMd(root: string): OutputFile[] {
7
+ const tmplPath = resolve(root, "templates/skill/SKILL.md.tmpl");
8
+ if (existsSync(tmplPath)) {
9
+ const tmpl = readFileSync(tmplPath, "utf-8");
10
+ const pkgJson = JSON.parse(readFileSync(resolve(root, "package.json"), "utf-8")) as { version: string };
11
+ const footer = `<!-- Generated for PPM v${pkgJson.version} at build time. Re-run \`ppm export skill --install\` to refresh. -->`;
12
+ const content = tmpl.replace("<!-- AUTO:version_footer -->", footer);
13
+ return [{ relPath: "SKILL.md", content }];
14
+ }
15
+ // Stub fallback when template not yet authored (used during phase 1)
16
+ const stub = [
17
+ "---",
18
+ "name: ppm",
19
+ "description: Control PPM via CLI, HTTP API, and SQLite config DB.",
20
+ "---",
21
+ "",
22
+ "# PPM Skill",
23
+ "",
24
+ "_Stub — template pending (phase 4)._",
25
+ "",
26
+ ].join("\n");
27
+ return [{ relPath: "SKILL.md", content: stub }];
28
+ }
@@ -0,0 +1,17 @@
1
+ // Shared helper: recursively create dirs then write files. Used by skill package generator.
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+
5
+ export interface OutputFile {
6
+ relPath: string;
7
+ content: string;
8
+ }
9
+
10
+ export function writeFiles(rootDir: string, files: OutputFile[]): void {
11
+ for (const f of files) {
12
+ const abs = resolve(rootDir, f.relPath);
13
+ mkdirSync(dirname(abs), { recursive: true });
14
+ writeFileSync(abs, f.content, "utf-8");
15
+ }
16
+ console.log(`[generate-ppm-skill] wrote ${files.length} files to ${rootDir}`);
17
+ }
@@ -0,0 +1,85 @@
1
+ // `ppm export skill` — install bundled skill package to ~/.claude/skills/ppm/ (or custom path)
2
+ // so external AI tools (Claude Code, compatible agents) can control PPM.
3
+ import type { Command } from "commander";
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+ import {
7
+ resolveTargetDir,
8
+ resolveAssetsDir,
9
+ backupExisting,
10
+ copyBundledSkill,
11
+ generateDbSchemaMarkdown,
12
+ type SkillScope,
13
+ } from "../../services/skill-export/index.ts";
14
+
15
+ interface ExportSkillOpts {
16
+ install?: boolean;
17
+ scope: SkillScope;
18
+ output?: string;
19
+ format: string;
20
+ }
21
+
22
+ export function registerExportCommands(program: Command): void {
23
+ const exp = program
24
+ .command("export")
25
+ .description("Export PPM metadata for external tools (AI agents, editors)");
26
+
27
+ exp
28
+ .command("skill")
29
+ .description("Export Claude Code skill for controlling PPM from external AI tools")
30
+ .option("--install", "Install to target dir (default scope=user → ~/.claude/skills/ppm/)")
31
+ .option("--scope <scope>", "Install scope: user | project", "user")
32
+ .option("--output <dir>", "Custom output directory (overrides --scope)")
33
+ .option("--format <fmt>", "Output format", "claude-code")
34
+ .action(async (opts: ExportSkillOpts) => {
35
+ if (opts.format !== "claude-code") {
36
+ console.error(`Unsupported format: ${opts.format}. Only 'claude-code' is supported in v1.`);
37
+ process.exit(1);
38
+ }
39
+ if (opts.scope && opts.scope !== "user" && opts.scope !== "project") {
40
+ console.error(`Invalid scope: ${opts.scope}. Use 'user' or 'project'.`);
41
+ process.exit(1);
42
+ }
43
+
44
+ let assetsDir: string;
45
+ try {
46
+ assetsDir = resolveAssetsDir();
47
+ } catch (e) {
48
+ console.error(e instanceof Error ? e.message : String(e));
49
+ process.exit(2);
50
+ }
51
+
52
+ // Preview mode: print merged SKILL.md to stdout.
53
+ if (!opts.install && !opts.output) {
54
+ const skillPath = resolve(assetsDir, "SKILL.md");
55
+ process.stdout.write(readFileSync(skillPath, "utf-8"));
56
+ return;
57
+ }
58
+
59
+ const target = resolveTargetDir({ scope: opts.scope, output: opts.output });
60
+
61
+ try {
62
+ const backedUp = backupExisting(target);
63
+ mkdirSync(target, { recursive: true });
64
+ copyBundledSkill(assetsDir, target);
65
+
66
+ // Runtime DB schema (reads ~/.ppm/ppm.db readonly)
67
+ const refsDir = resolve(target, "references");
68
+ mkdirSync(refsDir, { recursive: true });
69
+ writeFileSync(resolve(refsDir, "db-schema.md"), generateDbSchemaMarkdown(), "utf-8");
70
+
71
+ console.log(`✓ Installed PPM skill → ${target}`);
72
+ if (backedUp.length > 0) {
73
+ console.log(` Backed up ${backedUp.length} existing file(s) with .bak-<timestamp> suffix.`);
74
+ console.log(` Safe to delete those backups if not needed.`);
75
+ }
76
+ if (!existsSync(resolve(target, "SKILL.md"))) {
77
+ console.error("Post-install verification failed: SKILL.md missing in target.");
78
+ process.exit(1);
79
+ }
80
+ } catch (e) {
81
+ console.error(`Install failed: ${e instanceof Error ? e.message : String(e)}`);
82
+ process.exit(1);
83
+ }
84
+ });
85
+ }