@hienlh/ppm 0.1.6 → 0.2.1

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 (47) hide show
  1. package/bun.lock +52 -0
  2. package/dist/web/assets/{button-KIZetva8.js → button-CvHWF07y.js} +1 -1
  3. package/dist/web/assets/{chat-tab-CNXjLOhI.js → chat-tab-C4ovA2w4.js} +3 -3
  4. package/dist/web/assets/{code-editor-tGMPwYNs.js → code-editor-BgiyQO-M.js} +1 -1
  5. package/dist/web/assets/{dialog-D8ulRTfX.js → dialog-f3IZM-6v.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-B4A8pPbo.js → diff-viewer-8_asmBRZ.js} +1 -1
  7. package/dist/web/assets/{dist-C4W3AGh3.js → dist-CCBctnax.js} +1 -1
  8. package/dist/web/assets/{git-graph-ODjrGZOQ.js → git-graph-BiyTIbCz.js} +1 -1
  9. package/dist/web/assets/{git-status-panel-B0Im1hrU.js → git-status-panel-BifyO31N.js} +1 -1
  10. package/dist/web/assets/index-DILaVO6p.css +2 -0
  11. package/dist/web/assets/index-DasstYgw.js +11 -0
  12. package/dist/web/assets/project-list-C7L3hZct.js +1 -0
  13. package/dist/web/assets/settings-tab-Cn5Ja0_J.js +1 -0
  14. package/dist/web/assets/{terminal-tab-DDf6S-Tu.js → terminal-tab-CyjhG4Ao.js} +1 -1
  15. package/dist/web/index.html +8 -8
  16. package/dist/web/sw.js +1 -1
  17. package/docs/code-standards.md +74 -0
  18. package/docs/codebase-summary.md +11 -7
  19. package/docs/deployment-guide.md +81 -11
  20. package/docs/project-overview-pdr.md +62 -2
  21. package/docs/system-architecture.md +24 -8
  22. package/package.json +5 -2
  23. package/src/cli/commands/init.ts +196 -43
  24. package/src/cli/commands/logs.ts +58 -0
  25. package/src/cli/commands/report.ts +60 -0
  26. package/src/cli/commands/status.ts +73 -0
  27. package/src/cli/commands/stop.ts +24 -10
  28. package/src/index.ts +48 -6
  29. package/src/server/index.ts +184 -17
  30. package/src/services/cloudflared.service.ts +99 -0
  31. package/src/services/tunnel.service.ts +100 -0
  32. package/src/types/config.ts +2 -0
  33. package/src/web/app.tsx +10 -2
  34. package/src/web/components/auth/login-screen.tsx +9 -2
  35. package/src/web/components/layout/sidebar.tsx +15 -0
  36. package/src/web/components/ui/sonner.tsx +9 -6
  37. package/src/web/hooks/use-health-check.ts +95 -0
  38. package/src/web/stores/settings-store.ts +19 -0
  39. package/src/web/stores/tab-store.ts +28 -8
  40. package/src/web/styles/globals.css +4 -3
  41. package/dist/web/assets/index-BePIZMuy.css +0 -2
  42. package/dist/web/assets/index-D2STBl88.js +0 -10
  43. package/dist/web/assets/project-list-VjQQcU3X.js +0 -1
  44. package/dist/web/assets/settings-tab-ChhdL0EG.js +0 -1
  45. /package/dist/web/assets/{dist-PA84y4Ga.js → dist-B6sG2GPc.js} +0 -0
  46. /package/dist/web/assets/{react-BSLFEYu8.js → react-gOPBns57.js} +0 -0
  47. /package/dist/web/assets/{utils-DpJF9mAi.js → utils-61GRB9Cb.js} +0 -0
@@ -1,57 +1,210 @@
1
1
  import { resolve } from "node:path";
2
2
  import { homedir } from "node:os";
3
3
  import { existsSync } from "node:fs";
4
+ import { input, confirm, select, password } from "@inquirer/prompts";
4
5
  import { configService } from "../../services/config.service.ts";
5
6
  import { projectService } from "../../services/project.service.ts";
6
7
 
7
- export async function initProject() {
8
- const ppmDir = resolve(homedir(), ".ppm");
9
- const globalConfig = resolve(ppmDir, "config.yaml");
8
+ const DEFAULT_PORT = 3210;
10
9
 
11
- // Load existing or create default
12
- configService.load();
13
- console.log(`Config: ${configService.getConfigPath()}`);
14
-
15
- // Scan CWD for git repos
16
- const cwd = process.cwd();
17
- console.log(`\nScanning ${cwd} for git repositories...`);
18
- const repos = projectService.scanForGitRepos(cwd);
19
-
20
- if (repos.length === 0) {
21
- console.log("No git repositories found.");
22
- } else {
23
- console.log(`Found ${repos.length} git repo(s):\n`);
24
- const existing = configService.get("projects");
25
-
26
- let added = 0;
27
- for (const repoPath of repos) {
28
- const name = repoPath.split("/").pop() ?? "unknown";
29
- const alreadyRegistered = existing.some(
30
- (p) => resolve(p.path) === repoPath || p.name === name,
31
- );
32
-
33
- if (alreadyRegistered) {
34
- console.log(` [skip] ${name} (${repoPath}) already registered`);
35
- continue;
36
- }
37
-
38
- try {
39
- projectService.add(repoPath, name);
40
- console.log(` [added] ${name} (${repoPath})`);
41
- added++;
42
- } catch (e) {
43
- console.log(` [error] ${name}: ${(e as Error).message}`);
44
- }
10
+ export interface InitOptions {
11
+ port?: string;
12
+ scan?: string;
13
+ auth?: boolean;
14
+ password?: string;
15
+ share?: boolean;
16
+ /** Skip prompts, use defaults + flags only (for VPS/scripts) */
17
+ yes?: boolean;
18
+ }
19
+
20
+ /** Check if config already exists */
21
+ export function hasConfig(): boolean {
22
+ const globalConfig = resolve(homedir(), ".ppm", "config.yaml");
23
+ const localConfig = resolve(process.cwd(), "ppm.yaml");
24
+ return existsSync(globalConfig) || existsSync(localConfig);
25
+ }
26
+
27
+ export async function initProject(options: InitOptions = {}) {
28
+ const nonInteractive = options.yes ?? false;
29
+
30
+ // Check if already initialized
31
+ if (hasConfig() && !nonInteractive) {
32
+ const overwrite = await confirm({
33
+ message: "PPM is already configured. Re-initialize? (this will overwrite your config)",
34
+ default: false,
35
+ });
36
+ if (!overwrite) {
37
+ console.log(" Cancelled.");
38
+ return;
39
+ }
40
+ }
41
+
42
+ console.log("\n 🔧 PPM Setup\n");
43
+
44
+ // 1. Device name
45
+ const defaultHostname = (await import("node:os")).hostname();
46
+ const deviceName = nonInteractive
47
+ ? (options as any).deviceName ?? defaultHostname
48
+ : await input({
49
+ message: "Device name (shown in UI to identify this machine):",
50
+ default: defaultHostname,
51
+ });
52
+
53
+ // 2. Port
54
+ const portValue = options.port
55
+ ? parseInt(options.port, 10)
56
+ : nonInteractive
57
+ ? DEFAULT_PORT
58
+ : parseInt(await input({
59
+ message: "Port:",
60
+ default: String(DEFAULT_PORT),
61
+ validate: (v) => /^\d+$/.test(v) && +v > 0 && +v < 65536 ? true : "Enter valid port (1-65535)",
62
+ }), 10);
63
+
64
+ // 2. Scan directory
65
+ const scanDir = options.scan
66
+ ?? (nonInteractive
67
+ ? homedir()
68
+ : await input({
69
+ message: "Projects directory to scan:",
70
+ default: homedir(),
71
+ }));
72
+
73
+ // 3. Auth
74
+ const authEnabled = options.auth
75
+ ?? (nonInteractive
76
+ ? true
77
+ : await confirm({
78
+ message: "Enable authentication?",
79
+ default: true,
80
+ }));
81
+
82
+ // 4. Password (if auth enabled)
83
+ let authToken = "";
84
+ if (authEnabled) {
85
+ authToken = options.password
86
+ ?? (nonInteractive
87
+ ? generateToken()
88
+ : await password({
89
+ message: "Set access password (leave empty to auto-generate):",
90
+ }));
91
+ if (!authToken) authToken = generateToken();
92
+ }
93
+
94
+ // 5. Share (install cloudflared)
95
+ const wantShare = options.share
96
+ ?? (nonInteractive
97
+ ? false
98
+ : await confirm({
99
+ message: "Install cloudflared for public sharing (--share)?",
100
+ default: false,
101
+ }));
102
+
103
+ // 6. Advanced settings
104
+ let aiModel = "claude-sonnet-4-6";
105
+ let aiEffort: "low" | "medium" | "high" | "max" = "high";
106
+ let aiMaxTurns = 100;
107
+ let aiApiKeyEnv = "ANTHROPIC_API_KEY";
108
+
109
+ if (!nonInteractive) {
110
+ const wantAdvanced = await confirm({
111
+ message: "Configure advanced AI settings?",
112
+ default: false,
113
+ });
114
+
115
+ if (wantAdvanced) {
116
+ aiModel = await select({
117
+ message: "AI model:",
118
+ choices: [
119
+ { value: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (fast, recommended)" },
120
+ { value: "claude-opus-4-6", name: "Claude Opus 4.6 (powerful)" },
121
+ { value: "claude-haiku-4-5", name: "Claude Haiku 4.5 (cheap)" },
122
+ ],
123
+ default: "claude-sonnet-4-6",
124
+ });
125
+
126
+ aiEffort = await select({
127
+ message: "Thinking effort:",
128
+ choices: [
129
+ { value: "low" as const, name: "Low" },
130
+ { value: "medium" as const, name: "Medium" },
131
+ { value: "high" as const, name: "High (recommended)" },
132
+ { value: "max" as const, name: "Max" },
133
+ ],
134
+ default: "high" as const,
135
+ });
136
+
137
+ aiMaxTurns = parseInt(await input({
138
+ message: "Max turns per chat:",
139
+ default: "100",
140
+ validate: (v) => /^\d+$/.test(v) && +v >= 1 && +v <= 500 ? true : "Enter 1-500",
141
+ }), 10);
142
+
143
+ aiApiKeyEnv = await input({
144
+ message: "API key env variable:",
145
+ default: "ANTHROPIC_API_KEY",
146
+ });
45
147
  }
148
+ }
46
149
 
47
- console.log(`\nAdded ${added} project(s).`);
150
+ // Apply config
151
+ configService.load();
152
+ configService.set("device_name", deviceName);
153
+ configService.set("port", portValue);
154
+ configService.set("auth", { enabled: authEnabled, token: authToken });
155
+ configService.set("ai", {
156
+ default_provider: "claude",
157
+ providers: {
158
+ claude: {
159
+ type: "agent-sdk",
160
+ api_key_env: aiApiKeyEnv,
161
+ model: aiModel,
162
+ effort: aiEffort,
163
+ max_turns: aiMaxTurns,
164
+ },
165
+ },
166
+ });
167
+ configService.save();
168
+
169
+ // Scan for projects
170
+ console.log(`\n Scanning ${scanDir} for git repositories...`);
171
+ const repos = projectService.scanForGitRepos(scanDir);
172
+ const existing = configService.get("projects");
173
+ let added = 0;
174
+
175
+ for (const repoPath of repos) {
176
+ const name = repoPath.split("/").pop() ?? "unknown";
177
+ if (existing.some((p) => resolve(p.path) === repoPath || p.name === name)) continue;
178
+ try {
179
+ projectService.add(repoPath, name);
180
+ added++;
181
+ } catch {}
48
182
  }
183
+ console.log(` Found ${repos.length} repo(s), added ${added} new project(s).`);
49
184
 
50
- const auth = configService.get("auth");
51
- console.log(`\nAuth: ${auth.enabled ? "enabled" : "disabled"}`);
52
- if (auth.enabled) {
53
- console.log(`Token: ${auth.token}`);
185
+ // Install cloudflared if requested
186
+ if (wantShare) {
187
+ console.log("\n Installing cloudflared...");
188
+ const { ensureCloudflared } = await import("../../services/cloudflared.service.ts");
189
+ await ensureCloudflared();
190
+ console.log(" ✓ cloudflared ready");
54
191
  }
55
192
 
56
- console.log(`\nRun "ppm start" to start the server.`);
193
+ // 8. Next steps
194
+ console.log(`\n ✓ Config saved to ${configService.getConfigPath()}\n`);
195
+ console.log(" Next steps:");
196
+ console.log(` ppm start # Start (daemon, port ${portValue})`);
197
+ console.log(` ppm start -f # Start in foreground`);
198
+ if (wantShare) {
199
+ console.log(` ppm start --share # Start + public URL`);
200
+ }
201
+ if (authEnabled) {
202
+ console.log(`\n Access password: ${authToken}`);
203
+ }
204
+ console.log();
205
+ }
206
+
207
+ function generateToken(): string {
208
+ const { randomBytes } = require("node:crypto");
209
+ return randomBytes(16).toString("hex");
57
210
  }
@@ -0,0 +1,58 @@
1
+ import { resolve } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { existsSync, readFileSync, statSync } from "node:fs";
4
+
5
+ const LOG_FILE = resolve(homedir(), ".ppm", "ppm.log");
6
+
7
+ export async function showLogs(options: { tail?: string; follow?: boolean; clear?: boolean }) {
8
+ if (options.clear) {
9
+ const { writeFileSync } = await import("node:fs");
10
+ writeFileSync(LOG_FILE, "");
11
+ console.log("Logs cleared.");
12
+ return;
13
+ }
14
+
15
+ if (!existsSync(LOG_FILE)) {
16
+ console.log("No log file found. Start PPM daemon first.");
17
+ return;
18
+ }
19
+
20
+ const lines = parseInt(options.tail ?? "50", 10);
21
+ const content = readFileSync(LOG_FILE, "utf-8");
22
+ const allLines = content.split("\n");
23
+ const lastN = allLines.slice(-lines).join("\n");
24
+
25
+ if (!lastN.trim()) {
26
+ console.log("Log file is empty.");
27
+ return;
28
+ }
29
+
30
+ console.log(lastN);
31
+
32
+ if (options.follow) {
33
+ // Tail -f behavior
34
+ const { watch } = await import("node:fs");
35
+ let lastSize = statSync(LOG_FILE).size;
36
+ console.log("\n--- Following logs (Ctrl+C to stop) ---\n");
37
+
38
+ watch(LOG_FILE, () => {
39
+ try {
40
+ const newSize = statSync(LOG_FILE).size;
41
+ if (newSize > lastSize) {
42
+ const fd = Bun.file(LOG_FILE);
43
+ fd.slice(lastSize, newSize).text().then((text) => {
44
+ process.stdout.write(text);
45
+ });
46
+ lastSize = newSize;
47
+ }
48
+ } catch {}
49
+ });
50
+ }
51
+ }
52
+
53
+ /** Get last N lines of log for bug reports */
54
+ export function getRecentLogs(lines = 30): string {
55
+ if (!existsSync(LOG_FILE)) return "(no logs)";
56
+ const content = readFileSync(LOG_FILE, "utf-8");
57
+ return content.split("\n").slice(-lines).join("\n").trim() || "(empty)";
58
+ }
@@ -0,0 +1,60 @@
1
+ import { homedir, platform, arch, release } from "node:os";
2
+ import { resolve } from "node:path";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { getRecentLogs } from "./logs.ts";
5
+
6
+ const REPO = "hienlh/ppm";
7
+
8
+ export async function reportBug() {
9
+ const version = "0.2.1";
10
+ const logs = getRecentLogs(30);
11
+ const statusFile = resolve(homedir(), ".ppm", "status.json");
12
+ let statusInfo = "(not running)";
13
+ if (existsSync(statusFile)) {
14
+ try { statusInfo = readFileSync(statusFile, "utf-8"); } catch {}
15
+ }
16
+
17
+ const body = [
18
+ "## Environment",
19
+ `- PPM: v${version}`,
20
+ `- OS: ${platform()} ${arch()} ${release()}`,
21
+ `- Bun: ${Bun.version}`,
22
+ "",
23
+ "## Description",
24
+ "<!-- Describe the bug -->",
25
+ "",
26
+ "## Steps to Reproduce",
27
+ "1. ",
28
+ "",
29
+ "## Expected Behavior",
30
+ "",
31
+ "## Daemon Status",
32
+ "```json",
33
+ statusInfo,
34
+ "```",
35
+ "",
36
+ "## Recent Logs (last 30 lines)",
37
+ "```",
38
+ logs,
39
+ "```",
40
+ ].join("\n");
41
+
42
+ const title = encodeURIComponent("bug: ");
43
+ const encodedBody = encodeURIComponent(body);
44
+ const url = `https://github.com/${REPO}/issues/new?title=${title}&body=${encodedBody}`;
45
+
46
+ console.log(" Opening GitHub issue form in browser...\n");
47
+ console.log(" Environment info and recent logs will be pre-filled.\n");
48
+
49
+ const { $ } = await import("bun");
50
+ try {
51
+ if (platform() === "darwin") {
52
+ await $`open ${url}`.quiet();
53
+ } else {
54
+ await $`xdg-open ${url}`.quiet();
55
+ }
56
+ } catch {
57
+ console.log(" Could not open browser. Copy this URL:\n");
58
+ console.log(` ${url}\n`);
59
+ }
60
+ }
@@ -0,0 +1,73 @@
1
+ import { resolve } from "node:path";
2
+ import { homedir } from "node:os";
3
+ 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");
7
+
8
+ interface DaemonStatus {
9
+ running: boolean;
10
+ pid: number | null;
11
+ port: number | null;
12
+ host: string | null;
13
+ shareUrl: string | null;
14
+ }
15
+
16
+ function getDaemonStatus(): DaemonStatus {
17
+ const notRunning: DaemonStatus = { running: false, pid: null, port: null, host: null, shareUrl: null };
18
+
19
+ // Try status.json first
20
+ if (existsSync(STATUS_FILE)) {
21
+ try {
22
+ const data = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
23
+ const pid = data.pid as number;
24
+ // Check if process is actually alive
25
+ try {
26
+ process.kill(pid, 0); // signal 0 = check existence
27
+ return { running: true, pid, port: data.port, host: data.host, shareUrl: data.shareUrl ?? null };
28
+ } catch {
29
+ return notRunning; // stale status file
30
+ }
31
+ } catch {
32
+ return notRunning;
33
+ }
34
+ }
35
+
36
+ // Fallback to ppm.pid
37
+ if (existsSync(PID_FILE)) {
38
+ try {
39
+ const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
40
+ process.kill(pid, 0);
41
+ return { running: true, pid, port: null, host: null, shareUrl: null };
42
+ } catch {
43
+ return notRunning;
44
+ }
45
+ }
46
+
47
+ return notRunning;
48
+ }
49
+
50
+ export async function showStatus(options: { json?: boolean }) {
51
+ const status = getDaemonStatus();
52
+
53
+ if (options.json) {
54
+ console.log(JSON.stringify(status));
55
+ return;
56
+ }
57
+
58
+ if (!status.running) {
59
+ console.log(" PPM is not running.");
60
+ return;
61
+ }
62
+
63
+ console.log(`\n PPM daemon is running\n`);
64
+ console.log(` PID: ${status.pid}`);
65
+ if (status.port) console.log(` Local: http://localhost:${status.port}/`);
66
+ if (status.shareUrl) {
67
+ console.log(` Share: ${status.shareUrl}`);
68
+ const qr = await import("qrcode-terminal");
69
+ console.log();
70
+ qr.generate(status.shareUrl, { small: true });
71
+ }
72
+ console.log();
73
+ }
@@ -3,29 +3,43 @@ import { homedir } from "node:os";
3
3
  import { readFileSync, unlinkSync, existsSync } from "node:fs";
4
4
 
5
5
  const PID_FILE = resolve(homedir(), ".ppm", "ppm.pid");
6
+ const STATUS_FILE = resolve(homedir(), ".ppm", "status.json");
6
7
 
7
8
  export async function stopServer() {
8
- if (!existsSync(PID_FILE)) {
9
- console.log("No PPM daemon running (PID file not found).");
10
- return;
9
+ let pid: number | null = null;
10
+
11
+ // Try status.json first (new format)
12
+ if (existsSync(STATUS_FILE)) {
13
+ try {
14
+ const status = JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
15
+ pid = status.pid;
16
+ } catch {}
17
+ }
18
+
19
+ // Fallback to ppm.pid (compat)
20
+ if (!pid && existsSync(PID_FILE)) {
21
+ pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
11
22
  }
12
23
 
13
- const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
14
- if (isNaN(pid)) {
15
- console.log("Invalid PID file. Removing it.");
16
- unlinkSync(PID_FILE);
24
+ if (!pid || isNaN(pid)) {
25
+ console.log("No PPM daemon running.");
26
+ // Cleanup stale files
27
+ if (existsSync(STATUS_FILE)) unlinkSync(STATUS_FILE);
28
+ if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
17
29
  return;
18
30
  }
19
31
 
20
32
  try {
21
33
  process.kill(pid);
22
- unlinkSync(PID_FILE);
34
+ if (existsSync(STATUS_FILE)) unlinkSync(STATUS_FILE);
35
+ if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
23
36
  console.log(`PPM daemon stopped (PID: ${pid}).`);
24
37
  } catch (e) {
25
38
  const error = e as NodeJS.ErrnoException;
26
39
  if (error.code === "ESRCH") {
27
- console.log(`Process ${pid} not found. Cleaning up PID file.`);
28
- unlinkSync(PID_FILE);
40
+ console.log(`Process ${pid} not found. Cleaning up stale files.`);
41
+ if (existsSync(STATUS_FILE)) unlinkSync(STATUS_FILE);
42
+ if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
29
43
  } else {
30
44
  console.error(`Failed to stop process ${pid}: ${error.message}`);
31
45
  }
package/src/index.ts CHANGED
@@ -6,15 +6,22 @@ const program = new Command();
6
6
  program
7
7
  .name("ppm")
8
8
  .description("Personal Project Manager — mobile-first web IDE")
9
- .version("0.1.0");
9
+ .version("0.2.1");
10
10
 
11
11
  program
12
12
  .command("start")
13
- .description("Start the PPM server")
13
+ .description("Start the PPM server (background by default)")
14
14
  .option("-p, --port <port>", "Port to listen on")
15
- .option("-d, --daemon", "Run as background daemon")
15
+ .option("-f, --foreground", "Run in foreground (default: background daemon)")
16
+ .option("-d, --daemon", "Run as background daemon (default, kept for compat)")
17
+ .option("-s, --share", "Share via public URL (Cloudflare tunnel)")
16
18
  .option("-c, --config <path>", "Path to config file")
17
19
  .action(async (options) => {
20
+ // Auto-init on first run
21
+ const { hasConfig, initProject } = await import("./cli/commands/init.ts");
22
+ if (!hasConfig()) {
23
+ await initProject();
24
+ }
18
25
  const { startServer } = await import("./server/index.ts");
19
26
  await startServer(options);
20
27
  });
@@ -27,6 +34,15 @@ program
27
34
  await stopServer();
28
35
  });
29
36
 
37
+ program
38
+ .command("status")
39
+ .description("Show PPM daemon status")
40
+ .option("--json", "Output as JSON")
41
+ .action(async (options) => {
42
+ const { showStatus } = await import("./cli/commands/status.ts");
43
+ await showStatus(options);
44
+ });
45
+
30
46
  program
31
47
  .command("open")
32
48
  .description("Open PPM in browser")
@@ -37,11 +53,37 @@ program
37
53
  });
38
54
 
39
55
  program
40
- .command("init")
41
- .description("Initialize PPM configuration — scan for git repos, create config")
56
+ .command("logs")
57
+ .description("View PPM daemon logs")
58
+ .option("-n, --tail <lines>", "Number of lines to show", "50")
59
+ .option("-f, --follow", "Follow log output")
60
+ .option("--clear", "Clear log file")
61
+ .action(async (options) => {
62
+ const { showLogs } = await import("./cli/commands/logs.ts");
63
+ await showLogs(options);
64
+ });
65
+
66
+ program
67
+ .command("report")
68
+ .description("Report a bug on GitHub (pre-fills env info + logs)")
42
69
  .action(async () => {
70
+ const { reportBug } = await import("./cli/commands/report.ts");
71
+ await reportBug();
72
+ });
73
+
74
+ program
75
+ .command("init")
76
+ .description("Initialize PPM configuration (interactive or via flags)")
77
+ .option("-p, --port <port>", "Port to listen on")
78
+ .option("--scan <path>", "Directory to scan for git repos")
79
+ .option("--auth", "Enable authentication")
80
+ .option("--no-auth", "Disable authentication")
81
+ .option("--password <pw>", "Set access password")
82
+ .option("--share", "Pre-install cloudflared for sharing")
83
+ .option("-y, --yes", "Non-interactive mode (use defaults + flags)")
84
+ .action(async (options) => {
43
85
  const { initProject } = await import("./cli/commands/init.ts");
44
- await initProject();
86
+ await initProject(options);
45
87
  });
46
88
 
47
89
  const { registerProjectsCommands } = await import("./cli/commands/projects.ts");