@hienlh/ppm 0.1.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.
- package/.claude/agent-memory/tester/MEMORY.md +3 -0
- package/.claude/agent-memory/tester/project-ppm-test-conventions.md +32 -0
- package/.env.example +1 -0
- package/.github/workflows/release.yml +46 -0
- package/README.md +349 -0
- package/bun.lock +1217 -0
- package/components.json +21 -0
- package/docs/code-standards.md +574 -0
- package/docs/codebase-summary.md +294 -0
- package/docs/deployment-guide.md +631 -0
- package/docs/design-guidelines.md +661 -0
- package/docs/project-overview-pdr.md +142 -0
- package/docs/project-roadmap.md +400 -0
- package/docs/system-architecture.md +459 -0
- package/package.json +68 -0
- package/plans/260314-2009-ppm-implementation/phase-01-project-skeleton.md +81 -0
- package/plans/260314-2009-ppm-implementation/phase-02-backend-core.md +148 -0
- package/plans/260314-2009-ppm-implementation/phase-03-frontend-shell.md +256 -0
- package/plans/260314-2009-ppm-implementation/phase-04-file-explorer-editor.md +120 -0
- package/plans/260314-2009-ppm-implementation/phase-05-web-terminal.md +174 -0
- package/plans/260314-2009-ppm-implementation/phase-06-git-integration.md +244 -0
- package/plans/260314-2009-ppm-implementation/phase-07-ai-chat.md +242 -0
- package/plans/260314-2009-ppm-implementation/phase-08-cli-commands.md +143 -0
- package/plans/260314-2009-ppm-implementation/phase-09-pwa-build-deploy.md +209 -0
- package/plans/260314-2009-ppm-implementation/phase-10-testing.md +311 -0
- package/plans/260314-2009-ppm-implementation/plan.md +202 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-01-backend-project-router.md +145 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-02-frontend-api-migration.md +107 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-03-per-project-tabs.md +100 -0
- package/plans/260315-0356-project-scoped-api-refactor/phase-04-websocket-migration.md +66 -0
- package/plans/260315-0356-project-scoped-api-refactor/plan.md +87 -0
- package/plans/reports/brainstorm-260314-1938-final-techstack.md +342 -0
- package/plans/reports/docs-manager-260315-1314-documentation-creation.md +386 -0
- package/plans/reports/fullstack-developer-260314-2252-phase-02-backend-core.md +57 -0
- package/plans/reports/fullstack-developer-260314-2253-phase-03-frontend-shell.md +70 -0
- package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-api-terminal-ws.md +49 -0
- package/plans/reports/fullstack-developer-260314-2300-phase-04-05-file-explorer-editor-terminal.md +52 -0
- package/plans/reports/fullstack-developer-260314-2307-ai-chat-phase7.md +58 -0
- package/plans/reports/fullstack-developer-260314-2307-phase-06-git-integration.md +33 -0
- package/plans/reports/research-260314-1911-ppm-tech-stack.md +318 -0
- package/plans/reports/research-260314-1930-claude-code-integration.md +293 -0
- package/plans/reports/researcher-260314-2232-node-pty-bun-crash-analysis.md +305 -0
- package/plans/reports/researcher-260314-2232-ui-style.md +942 -0
- package/plans/reports/researcher-260315-0300-opcode-claude-interaction.md +745 -0
- package/plans/reports/researcher-260315-0303-opcode-deep-analysis.md +742 -0
- package/plans/reports/researcher-260315-0305-claude-agent-sdk-github-research.md +423 -0
- package/plans/reports/tester-260314-2053-initial-test-suite.md +81 -0
- package/ppm.example.yaml +14 -0
- package/repomix-output.xml +23745 -0
- package/scripts/build.ts +13 -0
- package/src/cli/commands/chat-cmd.ts +259 -0
- package/src/cli/commands/config-cmd.ts +121 -0
- package/src/cli/commands/git-cmd.ts +315 -0
- package/src/cli/commands/init.ts +57 -0
- package/src/cli/commands/open.ts +19 -0
- package/src/cli/commands/projects.ts +100 -0
- package/src/cli/commands/start.ts +3 -0
- package/src/cli/commands/stop.ts +33 -0
- package/src/cli/utils/project-resolver.ts +27 -0
- package/src/index.ts +59 -0
- package/src/providers/claude-agent-sdk.ts +499 -0
- package/src/providers/claude-binary-finder.ts +256 -0
- package/src/providers/claude-code-cli.ts +413 -0
- package/src/providers/claude-process-registry.ts +106 -0
- package/src/providers/mock-provider.ts +171 -0
- package/src/providers/provider.interface.ts +10 -0
- package/src/providers/registry.ts +45 -0
- package/src/server/helpers/resolve-project.ts +22 -0
- package/src/server/index.ts +181 -0
- package/src/server/middleware/auth.ts +30 -0
- package/src/server/routes/chat.ts +153 -0
- package/src/server/routes/files.ts +168 -0
- package/src/server/routes/git.ts +261 -0
- package/src/server/routes/project-scoped.ts +27 -0
- package/src/server/routes/projects.ts +57 -0
- package/src/server/routes/static.ts +26 -0
- package/src/server/ws/chat.ts +130 -0
- package/src/server/ws/terminal.ts +89 -0
- package/src/services/chat.service.ts +110 -0
- package/src/services/claude-usage.service.ts +113 -0
- package/src/services/config.service.ts +90 -0
- package/src/services/file.service.ts +261 -0
- package/src/services/git-dirs.service.ts +112 -0
- package/src/services/git.service.ts +372 -0
- package/src/services/project.service.ts +107 -0
- package/src/services/slash-items.service.ts +184 -0
- package/src/services/terminal.service.ts +212 -0
- package/src/types/api.ts +37 -0
- package/src/types/chat.ts +92 -0
- package/src/types/config.ts +41 -0
- package/src/types/git.ts +50 -0
- package/src/types/project.ts +18 -0
- package/src/types/terminal.ts +20 -0
- package/src/web/app.tsx +168 -0
- package/src/web/components/auth/login-screen.tsx +88 -0
- package/src/web/components/chat/attachment-chips.tsx +55 -0
- package/src/web/components/chat/chat-placeholder.tsx +10 -0
- package/src/web/components/chat/chat-tab.tsx +301 -0
- package/src/web/components/chat/file-picker.tsx +126 -0
- package/src/web/components/chat/message-input.tsx +420 -0
- package/src/web/components/chat/message-list.tsx +838 -0
- package/src/web/components/chat/session-picker.tsx +139 -0
- package/src/web/components/chat/slash-command-picker.tsx +135 -0
- package/src/web/components/chat/usage-badge.tsx +186 -0
- package/src/web/components/editor/code-editor.tsx +329 -0
- package/src/web/components/editor/diff-viewer.tsx +276 -0
- package/src/web/components/editor/editor-placeholder.tsx +10 -0
- package/src/web/components/explorer/file-actions.tsx +191 -0
- package/src/web/components/explorer/file-tree.tsx +298 -0
- package/src/web/components/git/git-graph.tsx +727 -0
- package/src/web/components/git/git-placeholder.tsx +55 -0
- package/src/web/components/git/git-status-panel.tsx +850 -0
- package/src/web/components/layout/mobile-drawer.tsx +137 -0
- package/src/web/components/layout/mobile-nav.tsx +103 -0
- package/src/web/components/layout/sidebar.tsx +90 -0
- package/src/web/components/layout/tab-bar.tsx +152 -0
- package/src/web/components/layout/tab-content.tsx +85 -0
- package/src/web/components/projects/dir-suggest.tsx +152 -0
- package/src/web/components/projects/project-list.tsx +187 -0
- package/src/web/components/settings/settings-tab.tsx +57 -0
- package/src/web/components/terminal/terminal-placeholder.tsx +10 -0
- package/src/web/components/terminal/terminal-tab.tsx +133 -0
- package/src/web/components/ui/button.tsx +64 -0
- package/src/web/components/ui/context-menu.tsx +250 -0
- package/src/web/components/ui/dialog.tsx +156 -0
- package/src/web/components/ui/dropdown-menu.tsx +257 -0
- package/src/web/components/ui/input.tsx +21 -0
- package/src/web/components/ui/scroll-area.tsx +56 -0
- package/src/web/components/ui/separator.tsx +26 -0
- package/src/web/components/ui/sonner.tsx +40 -0
- package/src/web/components/ui/tabs.tsx +91 -0
- package/src/web/components/ui/tooltip.tsx +57 -0
- package/src/web/hooks/use-chat.ts +420 -0
- package/src/web/hooks/use-terminal.ts +182 -0
- package/src/web/hooks/use-url-sync.ts +66 -0
- package/src/web/hooks/use-websocket.ts +48 -0
- package/src/web/index.html +16 -0
- package/src/web/lib/api-client.ts +90 -0
- package/src/web/lib/file-support.ts +68 -0
- package/src/web/lib/utils.ts +6 -0
- package/src/web/lib/ws-client.ts +100 -0
- package/src/web/main.tsx +10 -0
- package/src/web/public/icon-192.svg +5 -0
- package/src/web/public/icon-512.svg +5 -0
- package/src/web/stores/file-store.ts +81 -0
- package/src/web/stores/project-store.ts +50 -0
- package/src/web/stores/settings-store.ts +65 -0
- package/src/web/stores/tab-store.ts +187 -0
- package/src/web/styles/globals.css +227 -0
- package/src/web/vite-env.d.ts +1 -0
- package/tests/integration/api/chat-routes.test.ts +95 -0
- package/tests/integration/claude-agent-sdk-integration.test.ts +228 -0
- package/tests/integration/ws/chat-websocket.test.ts +312 -0
- package/tests/test-setup.ts +5 -0
- package/tests/unit/providers/claude-agent-sdk.test.ts +339 -0
- package/tests/unit/providers/mock-provider.test.ts +143 -0
- package/tests/unit/services/chat-service.test.ts +100 -0
- package/tsconfig.json +32 -0
- package/vite.config.ts +62 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { GitStatus } from "../../types/git.ts";
|
|
3
|
+
|
|
4
|
+
const C = {
|
|
5
|
+
reset: "\x1b[0m",
|
|
6
|
+
bold: "\x1b[1m",
|
|
7
|
+
green: "\x1b[32m",
|
|
8
|
+
red: "\x1b[31m",
|
|
9
|
+
yellow: "\x1b[33m",
|
|
10
|
+
cyan: "\x1b[36m",
|
|
11
|
+
dim: "\x1b[2m",
|
|
12
|
+
magenta: "\x1b[35m",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function statusColor(s: string): string {
|
|
16
|
+
if (s === "M") return `${C.yellow}M${C.reset}`;
|
|
17
|
+
if (s === "A") return `${C.green}A${C.reset}`;
|
|
18
|
+
if (s === "D") return `${C.red}D${C.reset}`;
|
|
19
|
+
if (s === "R") return `${C.cyan}R${C.reset}`;
|
|
20
|
+
return s;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printStatus(status: GitStatus): void {
|
|
24
|
+
console.log(`${C.bold}On branch:${C.reset} ${C.cyan}${status.current ?? "(detached)"}${C.reset}`);
|
|
25
|
+
|
|
26
|
+
if (status.staged.length === 0 && status.unstaged.length === 0 && status.untracked.length === 0) {
|
|
27
|
+
console.log(`${C.green}Nothing to commit, working tree clean${C.reset}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (status.staged.length > 0) {
|
|
32
|
+
console.log(`\n${C.bold}Staged changes:${C.reset}`);
|
|
33
|
+
for (const f of status.staged) {
|
|
34
|
+
const label = f.oldPath ? `${f.oldPath} → ${f.path}` : f.path;
|
|
35
|
+
console.log(` ${statusColor(f.status)} ${label}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (status.unstaged.length > 0) {
|
|
40
|
+
console.log(`\n${C.bold}Unstaged changes:${C.reset}`);
|
|
41
|
+
for (const f of status.unstaged) {
|
|
42
|
+
console.log(` ${statusColor(f.status)} ${f.path}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (status.untracked.length > 0) {
|
|
47
|
+
console.log(`\n${C.bold}Untracked files:${C.reset}`);
|
|
48
|
+
for (const f of status.untracked) {
|
|
49
|
+
console.log(` ${C.dim}? ${f}${C.reset}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function registerGitCommands(program: Command): void {
|
|
55
|
+
const git = program.command("git").description("Git operations for a project");
|
|
56
|
+
|
|
57
|
+
git
|
|
58
|
+
.command("status")
|
|
59
|
+
.description("Show working tree status")
|
|
60
|
+
.option("-p, --project <name>", "Project name or path")
|
|
61
|
+
.action(async (options: { project?: string }) => {
|
|
62
|
+
try {
|
|
63
|
+
const { resolveProject } = await import("../utils/project-resolver.ts");
|
|
64
|
+
const { gitService } = await import("../../services/git.service.ts");
|
|
65
|
+
const project = resolveProject(options);
|
|
66
|
+
const status = await gitService.status(project.path);
|
|
67
|
+
printStatus(status);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
git
|
|
75
|
+
.command("log")
|
|
76
|
+
.description("Show recent commits")
|
|
77
|
+
.option("-p, --project <name>", "Project name or path")
|
|
78
|
+
.option("-n, --count <n>", "Number of commits to show", "20")
|
|
79
|
+
.action(async (options: { project?: string; count?: string }) => {
|
|
80
|
+
try {
|
|
81
|
+
const { resolveProject } = await import("../utils/project-resolver.ts");
|
|
82
|
+
const { gitService } = await import("../../services/git.service.ts");
|
|
83
|
+
const project = resolveProject(options);
|
|
84
|
+
const maxCount = Math.min(parseInt(options.count ?? "20", 10), 500);
|
|
85
|
+
const { commits } = await gitService.graphData(project.path, maxCount);
|
|
86
|
+
|
|
87
|
+
if (commits.length === 0) {
|
|
88
|
+
console.log(`${C.yellow}No commits found.${C.reset}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const c of commits) {
|
|
93
|
+
const hash = `${C.yellow}${c.abbreviatedHash}${C.reset}`;
|
|
94
|
+
const subject = c.subject;
|
|
95
|
+
const author = `${C.cyan}${c.authorName}${C.reset}`;
|
|
96
|
+
const date = `${C.dim}${new Date(c.authorDate).toLocaleDateString()}${C.reset}`;
|
|
97
|
+
const refs = c.refs.length > 0
|
|
98
|
+
? ` ${C.green}(${c.refs.join(", ")})${C.reset}`
|
|
99
|
+
: "";
|
|
100
|
+
console.log(`${hash}${refs} ${subject}`);
|
|
101
|
+
console.log(` ${author} · ${date}`);
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
git
|
|
110
|
+
.command("diff")
|
|
111
|
+
.description("Show diff between refs or working tree")
|
|
112
|
+
.option("-p, --project <name>", "Project name or path")
|
|
113
|
+
.argument("[ref1]", "First ref")
|
|
114
|
+
.argument("[ref2]", "Second ref")
|
|
115
|
+
.action(async (ref1: string | undefined, ref2: string | undefined, options: { project?: string }) => {
|
|
116
|
+
try {
|
|
117
|
+
const { resolveProject } = await import("../utils/project-resolver.ts");
|
|
118
|
+
const { gitService } = await import("../../services/git.service.ts");
|
|
119
|
+
const project = resolveProject(options);
|
|
120
|
+
const diff = await gitService.diff(project.path, ref1, ref2);
|
|
121
|
+
if (!diff.trim()) {
|
|
122
|
+
console.log(`${C.dim}No differences.${C.reset}`);
|
|
123
|
+
} else {
|
|
124
|
+
// Color diff output
|
|
125
|
+
for (const line of diff.split("\n")) {
|
|
126
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
127
|
+
process.stdout.write(`${C.green}${line}${C.reset}\n`);
|
|
128
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
129
|
+
process.stdout.write(`${C.red}${line}${C.reset}\n`);
|
|
130
|
+
} else if (line.startsWith("@@")) {
|
|
131
|
+
process.stdout.write(`${C.cyan}${line}${C.reset}\n`);
|
|
132
|
+
} else {
|
|
133
|
+
process.stdout.write(`${line}\n`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
git
|
|
144
|
+
.command("stage <files...>")
|
|
145
|
+
.description('Stage files (use "." to stage all)')
|
|
146
|
+
.option("-p, --project <name>", "Project name or path")
|
|
147
|
+
.action(async (files: string[], options: { project?: string }) => {
|
|
148
|
+
try {
|
|
149
|
+
const { resolveProject } = await import("../utils/project-resolver.ts");
|
|
150
|
+
const { gitService } = await import("../../services/git.service.ts");
|
|
151
|
+
const project = resolveProject(options);
|
|
152
|
+
await gitService.stage(project.path, files);
|
|
153
|
+
console.log(`${C.green}Staged:${C.reset} ${files.join(", ")}`);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
git
|
|
161
|
+
.command("unstage <files...>")
|
|
162
|
+
.description("Unstage files")
|
|
163
|
+
.option("-p, --project <name>", "Project name or path")
|
|
164
|
+
.action(async (files: string[], options: { project?: string }) => {
|
|
165
|
+
try {
|
|
166
|
+
const { resolveProject } = await import("../utils/project-resolver.ts");
|
|
167
|
+
const { gitService } = await import("../../services/git.service.ts");
|
|
168
|
+
const project = resolveProject(options);
|
|
169
|
+
await gitService.unstage(project.path, files);
|
|
170
|
+
console.log(`${C.yellow}Unstaged:${C.reset} ${files.join(", ")}`);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
git
|
|
178
|
+
.command("commit")
|
|
179
|
+
.description("Commit staged changes")
|
|
180
|
+
.option("-p, --project <name>", "Project name or path")
|
|
181
|
+
.requiredOption("-m, --message <msg>", "Commit message")
|
|
182
|
+
.action(async (options: { project?: string; message: string }) => {
|
|
183
|
+
try {
|
|
184
|
+
const { resolveProject } = await import("../utils/project-resolver.ts");
|
|
185
|
+
const { gitService } = await import("../../services/git.service.ts");
|
|
186
|
+
const project = resolveProject(options);
|
|
187
|
+
|
|
188
|
+
// Check if there's anything to commit
|
|
189
|
+
const status = await gitService.status(project.path);
|
|
190
|
+
if (status.staged.length === 0) {
|
|
191
|
+
console.error(`${C.red}Nothing to commit.${C.reset} Stage files first with: ppm git stage <files>`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const hash = await gitService.commit(project.path, options.message);
|
|
196
|
+
console.log(`${C.green}Committed:${C.reset} ${C.yellow}${hash}${C.reset} "${options.message}"`);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
git
|
|
204
|
+
.command("push")
|
|
205
|
+
.description("Push to remote")
|
|
206
|
+
.option("-p, --project <name>", "Project name or path")
|
|
207
|
+
.option("--remote <remote>", "Remote name", "origin")
|
|
208
|
+
.option("--branch <branch>", "Branch name")
|
|
209
|
+
.action(async (options: { project?: string; remote?: string; branch?: string }) => {
|
|
210
|
+
try {
|
|
211
|
+
const { resolveProject } = await import("../utils/project-resolver.ts");
|
|
212
|
+
const { gitService } = await import("../../services/git.service.ts");
|
|
213
|
+
const project = resolveProject(options);
|
|
214
|
+
console.log(`${C.dim}Pushing...${C.reset}`);
|
|
215
|
+
await gitService.push(project.path, options.remote, options.branch);
|
|
216
|
+
console.log(`${C.green}Pushed successfully.${C.reset}`);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
git
|
|
224
|
+
.command("pull")
|
|
225
|
+
.description("Pull from remote")
|
|
226
|
+
.option("-p, --project <name>", "Project name or path")
|
|
227
|
+
.option("--remote <remote>", "Remote name")
|
|
228
|
+
.option("--branch <branch>", "Branch name")
|
|
229
|
+
.action(async (options: { project?: string; remote?: string; branch?: string }) => {
|
|
230
|
+
try {
|
|
231
|
+
const { resolveProject } = await import("../utils/project-resolver.ts");
|
|
232
|
+
const { gitService } = await import("../../services/git.service.ts");
|
|
233
|
+
const project = resolveProject(options);
|
|
234
|
+
console.log(`${C.dim}Pulling...${C.reset}`);
|
|
235
|
+
await gitService.pull(project.path, options.remote, options.branch);
|
|
236
|
+
console.log(`${C.green}Pulled successfully.${C.reset}`);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ppm git branch <subcommand>
|
|
244
|
+
const branch = git.command("branch").description("Branch operations");
|
|
245
|
+
|
|
246
|
+
branch
|
|
247
|
+
.command("create <name>")
|
|
248
|
+
.description("Create and checkout a new branch")
|
|
249
|
+
.option("-p, --project <name>", "Project name or path")
|
|
250
|
+
.option("--from <ref>", "Base ref (commit/branch/tag)")
|
|
251
|
+
.action(async (name: string, options: { project?: string; from?: string }) => {
|
|
252
|
+
try {
|
|
253
|
+
const { resolveProject } = await import("../utils/project-resolver.ts");
|
|
254
|
+
const { gitService } = await import("../../services/git.service.ts");
|
|
255
|
+
const project = resolveProject(options);
|
|
256
|
+
await gitService.createBranch(project.path, name, options.from);
|
|
257
|
+
console.log(`${C.green}Created and checked out branch:${C.reset} ${C.cyan}${name}${C.reset}`);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
branch
|
|
265
|
+
.command("checkout <name>")
|
|
266
|
+
.description("Switch to a branch")
|
|
267
|
+
.option("-p, --project <name>", "Project name or path")
|
|
268
|
+
.action(async (name: string, options: { project?: string }) => {
|
|
269
|
+
try {
|
|
270
|
+
const { resolveProject } = await import("../utils/project-resolver.ts");
|
|
271
|
+
const { gitService } = await import("../../services/git.service.ts");
|
|
272
|
+
const project = resolveProject(options);
|
|
273
|
+
await gitService.checkout(project.path, name);
|
|
274
|
+
console.log(`${C.green}Switched to branch:${C.reset} ${C.cyan}${name}${C.reset}`);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
branch
|
|
282
|
+
.command("delete <name>")
|
|
283
|
+
.description("Delete a branch")
|
|
284
|
+
.option("-p, --project <name>", "Project name or path")
|
|
285
|
+
.option("-f, --force", "Force delete")
|
|
286
|
+
.action(async (name: string, options: { project?: string; force?: boolean }) => {
|
|
287
|
+
try {
|
|
288
|
+
const { resolveProject } = await import("../utils/project-resolver.ts");
|
|
289
|
+
const { gitService } = await import("../../services/git.service.ts");
|
|
290
|
+
const project = resolveProject(options);
|
|
291
|
+
await gitService.deleteBranch(project.path, name, options.force);
|
|
292
|
+
console.log(`${C.green}Deleted branch:${C.reset} ${C.cyan}${name}${C.reset}`);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
branch
|
|
300
|
+
.command("merge <source>")
|
|
301
|
+
.description("Merge a branch into current branch")
|
|
302
|
+
.option("-p, --project <name>", "Project name or path")
|
|
303
|
+
.action(async (source: string, options: { project?: string }) => {
|
|
304
|
+
try {
|
|
305
|
+
const { resolveProject } = await import("../utils/project-resolver.ts");
|
|
306
|
+
const { gitService } = await import("../../services/git.service.ts");
|
|
307
|
+
const project = resolveProject(options);
|
|
308
|
+
await gitService.merge(project.path, source);
|
|
309
|
+
console.log(`${C.green}Merged:${C.reset} ${C.cyan}${source}${C.reset} into current branch`);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { configService } from "../../services/config.service.ts";
|
|
5
|
+
import { projectService } from "../../services/project.service.ts";
|
|
6
|
+
|
|
7
|
+
export async function initProject() {
|
|
8
|
+
const ppmDir = resolve(homedir(), ".ppm");
|
|
9
|
+
const globalConfig = resolve(ppmDir, "config.yaml");
|
|
10
|
+
|
|
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
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(`\nAdded ${added} project(s).`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const auth = configService.get("auth");
|
|
51
|
+
console.log(`\nAuth: ${auth.enabled ? "enabled" : "disabled"}`);
|
|
52
|
+
if (auth.enabled) {
|
|
53
|
+
console.log(`Token: ${auth.token}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log(`\nRun "ppm start" to start the server.`);
|
|
57
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { configService } from "../../services/config.service.ts";
|
|
2
|
+
|
|
3
|
+
export async function openBrowser() {
|
|
4
|
+
configService.load();
|
|
5
|
+
const port = configService.get("port");
|
|
6
|
+
const url = `http://localhost:${port}`;
|
|
7
|
+
|
|
8
|
+
console.log(`Opening ${url} ...`);
|
|
9
|
+
|
|
10
|
+
const platform = process.platform;
|
|
11
|
+
const cmd =
|
|
12
|
+
platform === "darwin"
|
|
13
|
+
? ["open", url]
|
|
14
|
+
: platform === "win32"
|
|
15
|
+
? ["cmd", "/c", "start", url]
|
|
16
|
+
: ["xdg-open", url];
|
|
17
|
+
|
|
18
|
+
Bun.spawn({ cmd, stdio: ["ignore", "ignore", "ignore"] });
|
|
19
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
|
|
3
|
+
const C = {
|
|
4
|
+
reset: "\x1b[0m",
|
|
5
|
+
bold: "\x1b[1m",
|
|
6
|
+
green: "\x1b[32m",
|
|
7
|
+
red: "\x1b[31m",
|
|
8
|
+
yellow: "\x1b[33m",
|
|
9
|
+
cyan: "\x1b[36m",
|
|
10
|
+
dim: "\x1b[2m",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function printTable(headers: string[], rows: string[][]): void {
|
|
14
|
+
const colWidths = headers.map((h, i) =>
|
|
15
|
+
Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const sep = colWidths.map((w) => "-".repeat(w + 2)).join("+");
|
|
19
|
+
const headerLine = headers
|
|
20
|
+
.map((h, i) => ` ${h.padEnd(colWidths[i]!)} `)
|
|
21
|
+
.join("|");
|
|
22
|
+
|
|
23
|
+
console.log(`+${sep}+`);
|
|
24
|
+
console.log(`|${C.bold}${headerLine}${C.reset}|`);
|
|
25
|
+
console.log(`+${sep}+`);
|
|
26
|
+
for (const row of rows) {
|
|
27
|
+
const line = row.map((cell, i) => ` ${(cell ?? "").padEnd(colWidths[i]!)} `).join("|");
|
|
28
|
+
console.log(`|${line}|`);
|
|
29
|
+
}
|
|
30
|
+
console.log(`+${sep}+`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function registerProjectsCommands(program: Command): void {
|
|
34
|
+
const projects = program.command("projects").description("Manage registered projects");
|
|
35
|
+
|
|
36
|
+
projects
|
|
37
|
+
.command("list")
|
|
38
|
+
.description("List all registered projects")
|
|
39
|
+
.action(async () => {
|
|
40
|
+
try {
|
|
41
|
+
const { projectService } = await import("../../services/project.service.ts");
|
|
42
|
+
const { gitService } = await import("../../services/git.service.ts");
|
|
43
|
+
const list = projectService.list();
|
|
44
|
+
|
|
45
|
+
if (list.length === 0) {
|
|
46
|
+
console.log(`${C.yellow}No projects registered.${C.reset} Run: ppm init`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rows: string[][] = [];
|
|
51
|
+
for (const p of list) {
|
|
52
|
+
let branch = "-";
|
|
53
|
+
let status = "-";
|
|
54
|
+
try {
|
|
55
|
+
const s = await gitService.status(p.path);
|
|
56
|
+
branch = s.current ?? "-";
|
|
57
|
+
const dirty = s.staged.length + s.unstaged.length + s.untracked.length;
|
|
58
|
+
status = dirty > 0 ? `${C.yellow}dirty${C.reset}` : `${C.green}clean${C.reset}`;
|
|
59
|
+
} catch {
|
|
60
|
+
status = `${C.dim}no git${C.reset}`;
|
|
61
|
+
}
|
|
62
|
+
rows.push([p.name, p.path, branch, status]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
printTable(["Name", "Path", "Branch", "Status"], rows);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
projects
|
|
73
|
+
.command("add <path>")
|
|
74
|
+
.description("Add a project to the registry")
|
|
75
|
+
.option("-n, --name <name>", "Project name (defaults to folder name)")
|
|
76
|
+
.action(async (projectPath: string, options: { name?: string }) => {
|
|
77
|
+
try {
|
|
78
|
+
const { projectService } = await import("../../services/project.service.ts");
|
|
79
|
+
const entry = projectService.add(projectPath, options.name);
|
|
80
|
+
console.log(`${C.green}Added project:${C.reset} ${entry.name} → ${entry.path}`);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
projects
|
|
88
|
+
.command("remove <name>")
|
|
89
|
+
.description("Remove a project from the registry")
|
|
90
|
+
.action(async (nameOrPath: string) => {
|
|
91
|
+
try {
|
|
92
|
+
const { projectService } = await import("../../services/project.service.ts");
|
|
93
|
+
projectService.remove(nameOrPath);
|
|
94
|
+
console.log(`${C.green}Removed project:${C.reset} ${nameOrPath}`);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error(`${C.red}Error:${C.reset}`, (err as Error).message);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { readFileSync, unlinkSync, existsSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
const PID_FILE = resolve(homedir(), ".ppm", "ppm.pid");
|
|
6
|
+
|
|
7
|
+
export async function stopServer() {
|
|
8
|
+
if (!existsSync(PID_FILE)) {
|
|
9
|
+
console.log("No PPM daemon running (PID file not found).");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
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);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
process.kill(pid);
|
|
22
|
+
unlinkSync(PID_FILE);
|
|
23
|
+
console.log(`PPM daemon stopped (PID: ${pid}).`);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
const error = e as NodeJS.ErrnoException;
|
|
26
|
+
if (error.code === "ESRCH") {
|
|
27
|
+
console.log(`Process ${pid} not found. Cleaning up PID file.`);
|
|
28
|
+
unlinkSync(PID_FILE);
|
|
29
|
+
} else {
|
|
30
|
+
console.error(`Failed to stop process ${pid}: ${error.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { projectService } from "../../services/project.service.ts";
|
|
3
|
+
import type { ProjectConfig } from "../../types/config.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CLI project resolver: CWD auto-detect + `-p` flag override.
|
|
7
|
+
* Used by CLI commands that operate on a specific project.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveProject(options: { project?: string }): ProjectConfig {
|
|
10
|
+
// Explicit -p flag
|
|
11
|
+
if (options.project) {
|
|
12
|
+
return projectService.resolve(options.project);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Auto-detect from CWD
|
|
16
|
+
const cwd = process.cwd();
|
|
17
|
+
const projects = projectService.list();
|
|
18
|
+
const match = projects.find(
|
|
19
|
+
(p) => cwd === resolve(p.path) || cwd.startsWith(resolve(p.path) + "/"),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (match) return { name: match.name, path: match.path };
|
|
23
|
+
|
|
24
|
+
throw new Error(
|
|
25
|
+
"Not in a registered project directory. Use -p <name> or register with: ppm init",
|
|
26
|
+
);
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
const program = new Command();
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.name("ppm")
|
|
8
|
+
.description("Personal Project Manager — mobile-first web IDE")
|
|
9
|
+
.version("0.1.0");
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.command("start")
|
|
13
|
+
.description("Start the PPM server")
|
|
14
|
+
.option("-p, --port <port>", "Port to listen on")
|
|
15
|
+
.option("-d, --daemon", "Run as background daemon")
|
|
16
|
+
.option("-c, --config <path>", "Path to config file")
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
const { startServer } = await import("./server/index.ts");
|
|
19
|
+
await startServer(options);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command("stop")
|
|
24
|
+
.description("Stop the PPM daemon")
|
|
25
|
+
.action(async () => {
|
|
26
|
+
const { stopServer } = await import("./cli/commands/stop.ts");
|
|
27
|
+
await stopServer();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command("open")
|
|
32
|
+
.description("Open PPM in browser")
|
|
33
|
+
.option("-c, --config <path>", "Path to config file")
|
|
34
|
+
.action(async () => {
|
|
35
|
+
const { openBrowser } = await import("./cli/commands/open.ts");
|
|
36
|
+
await openBrowser();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.command("init")
|
|
41
|
+
.description("Initialize PPM configuration — scan for git repos, create config")
|
|
42
|
+
.action(async () => {
|
|
43
|
+
const { initProject } = await import("./cli/commands/init.ts");
|
|
44
|
+
await initProject();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const { registerProjectsCommands } = await import("./cli/commands/projects.ts");
|
|
48
|
+
registerProjectsCommands(program);
|
|
49
|
+
|
|
50
|
+
const { registerConfigCommands } = await import("./cli/commands/config-cmd.ts");
|
|
51
|
+
registerConfigCommands(program);
|
|
52
|
+
|
|
53
|
+
const { registerGitCommands } = await import("./cli/commands/git-cmd.ts");
|
|
54
|
+
registerGitCommands(program);
|
|
55
|
+
|
|
56
|
+
const { registerChatCommands } = await import("./cli/commands/chat-cmd.ts");
|
|
57
|
+
registerChatCommands(program);
|
|
58
|
+
|
|
59
|
+
program.parse();
|