@hienlh/ppm 0.13.67 → 0.13.69
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/CHANGELOG.md +10 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +3 -1
- package/dist/web/assets/{audio-preview-Iq-XRBGw.js → audio-preview-C8NNPTUn.js} +1 -1
- package/dist/web/assets/{chat-tab-DkVXRD9e.js → chat-tab-Cdu1qwBF.js} +3 -3
- package/dist/web/assets/{code-editor-M6wHw8AZ.js → code-editor-D7wfu-4O.js} +2 -2
- package/dist/web/assets/{conflict-editor-D_8t44Wi.js → conflict-editor-C4b6hljX.js} +1 -1
- package/dist/web/assets/{database-viewer-Cj5yCn4w.js → database-viewer-CHGY2nqV.js} +1 -1
- package/dist/web/assets/{diff-viewer-BgPv67fJ.js → diff-viewer-DHaCYCXp.js} +1 -1
- package/dist/web/assets/{docx-preview-BbmDvXdS.js → docx-preview-RrQCPnLk.js} +1 -1
- package/dist/web/assets/{extension-webview-CP_AtfYs.js → extension-webview-DlbHEkiH.js} +1 -1
- package/dist/web/assets/{git-log-panel-DPRoZgWG.js → git-log-panel-D-Ntnv3m.js} +1 -1
- package/dist/web/assets/{glide-data-grid-BrtUKC3w.js → glide-data-grid-BzsZNd19.js} +1 -1
- package/dist/web/assets/{image-preview-BFj-ipom.js → image-preview-Dc6CL-hL.js} +1 -1
- package/dist/web/assets/index-By8-648j.js +27 -0
- package/dist/web/assets/keybindings-store-ULrepar2.js +1 -0
- package/dist/web/assets/{markdown-renderer-B63eYfrn.js → markdown-renderer-DJIOhSF1.js} +1 -1
- package/dist/web/assets/notification-store-xdIEKclm.js +1 -0
- package/dist/web/assets/{pdf-preview-JOwOGTIk.js → pdf-preview-CLRIg41K.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-DJRRbLGF.js → port-forwarding-tab-BnTIZHAc.js} +1 -1
- package/dist/web/assets/{postgres-viewer-AIOBOfCg.js → postgres-viewer-DOdXyW0T.js} +1 -1
- package/dist/web/assets/{settings-tab-BMHf9pO5.js → settings-tab-CujGYYDD.js} +1 -1
- package/dist/web/assets/{sql-query-editor-Dw9UvzWt.js → sql-query-editor-BCztpoy4.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-HusTxs1Z.js → sqlite-viewer-71ij4M_o.js} +1 -1
- package/dist/web/assets/{system-monitor-tab-BNJIkOan.js → system-monitor-tab-Br7LiuTx.js} +1 -1
- package/dist/web/assets/{terminal-tab-W1VShnP7.js → terminal-tab-D9_-ww9U.js} +1 -1
- package/dist/web/assets/{video-preview-BPAYbuvs.js → video-preview-CgV_9kiy.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/server/routes/projects.ts +37 -0
- package/src/services/extension.service.ts +9 -1
- package/src/services/git.service.ts +15 -0
- package/src/web/components/layout/add-project-form.tsx +213 -70
- package/dist/web/assets/index-CJZZ6v1o.js +0 -27
- package/dist/web/assets/keybindings-store-BOV4khyp.js +0 -1
- package/dist/web/assets/notification-store-BklO85um.js +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import type { ExtensionManifest, ExtensionInfo, RpcMessage } from "../types/extension.ts";
|
|
4
|
-
import { getExtensions, getExtensionById, insertExtension, updateExtension, getExtensionStorage, setExtensionStorageValue } from "./db.service.ts";
|
|
4
|
+
import { getExtensions, getExtensionById, insertExtension, updateExtension, deleteExtension, deleteExtensionStorage, getExtensionStorage, setExtensionStorageValue } from "./db.service.ts";
|
|
5
5
|
import { contributionRegistry } from "./contribution-registry.ts";
|
|
6
6
|
import { RpcChannel } from "./extension-rpc.ts";
|
|
7
7
|
import { parseManifest, discoverManifests, discoverBundledManifests } from "./extension-manifest.ts";
|
|
@@ -243,7 +243,15 @@ class ExtensionService {
|
|
|
243
243
|
});
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
+
// Clean up stale DB records for extensions no longer on disk
|
|
247
|
+
const discoveredIds = new Set(manifests.map((m) => m.id));
|
|
246
248
|
for (const row of getExtensions()) {
|
|
249
|
+
if (!discoveredIds.has(row.id)) {
|
|
250
|
+
console.log(`[ExtService] startup: removing stale DB record for ${row.id}`);
|
|
251
|
+
deleteExtensionStorage(row.id);
|
|
252
|
+
deleteExtension(row.id);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
247
255
|
if (row.enabled !== 1) continue;
|
|
248
256
|
console.log(`[ExtService] startup: activating ${row.id}...`);
|
|
249
257
|
try { await this.activate(row.id); } catch (e) {
|
|
@@ -541,6 +541,21 @@ class GitService {
|
|
|
541
541
|
await this.git(projectPath).raw(["worktree", "prune"]);
|
|
542
542
|
}
|
|
543
543
|
|
|
544
|
+
/** Clone a git repo into targetDir/repoName. Returns the full cloned path. */
|
|
545
|
+
async cloneRepo(url: string, targetDir: string, name?: string): Promise<string> {
|
|
546
|
+
const repoName = name || this.parseRepoNameFromUrl(url);
|
|
547
|
+
if (!repoName) throw new Error("Cannot determine repo name from URL");
|
|
548
|
+
const fullPath = path.resolve(targetDir, repoName);
|
|
549
|
+
await simpleGit().clone(url, fullPath);
|
|
550
|
+
return fullPath;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private parseRepoNameFromUrl(url: string): string | null {
|
|
554
|
+
// Handles SSH (git@host:owner/repo.git) and HTTPS (https://host/owner/repo.git)
|
|
555
|
+
const match = url.match(/[/:]([^/:]+?)(?:\.git)?\s*$/);
|
|
556
|
+
return match?.[1] ?? null;
|
|
557
|
+
}
|
|
558
|
+
|
|
544
559
|
private parseRemoteUrl(
|
|
545
560
|
url: string,
|
|
546
561
|
): { host: string; owner: string; repo: string } | null {
|
|
@@ -20,17 +20,45 @@ interface AddProjectFormProps {
|
|
|
20
20
|
|
|
21
21
|
export function AddProjectForm({ onSuccess, onCancel, footerClassName }: AddProjectFormProps) {
|
|
22
22
|
const { addProject } = useProjectStore(useShallow((s) => ({ addProject: s.addProject })));
|
|
23
|
+
|
|
24
|
+
// ── Tab state ──────────────────────────────────────────────────────────────
|
|
25
|
+
const [tab, setTab] = useState<"local" | "clone">("local");
|
|
26
|
+
|
|
27
|
+
// ── Local folder state ─────────────────────────────────────────────────────
|
|
23
28
|
const [path, setPath] = useState("");
|
|
24
29
|
const [name, setName] = useState("");
|
|
25
30
|
const [suggestions, setSuggestions] = useState<SuggestedDir[]>([]);
|
|
26
31
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
27
32
|
const [loading, setLoading] = useState(false);
|
|
28
33
|
const [submitting, setSubmitting] = useState(false);
|
|
34
|
+
|
|
35
|
+
// ── Clone state ────────────────────────────────────────────────────────────
|
|
36
|
+
const [gitUrl, setGitUrl] = useState("");
|
|
37
|
+
const [cloneDir, setCloneDir] = useState("~/Projects");
|
|
38
|
+
const [cloneName, setCloneName] = useState("");
|
|
39
|
+
const [cloning, setCloning] = useState(false);
|
|
40
|
+
|
|
41
|
+
// ── Shared ─────────────────────────────────────────────────────────────────
|
|
29
42
|
const [error, setError] = useState("");
|
|
43
|
+
|
|
30
44
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
31
45
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
32
46
|
|
|
33
|
-
//
|
|
47
|
+
// Load last clone dir on mount
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
api.get<{ dir: string | null }>("/api/projects/last-clone-dir")
|
|
50
|
+
.then((res) => { if (res?.dir) setCloneDir(res.dir); })
|
|
51
|
+
.catch(() => {});
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
// Auto-parse repo name from Git URL
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!gitUrl.trim()) { setCloneName(""); return; }
|
|
57
|
+
const match = gitUrl.match(/[/:]([^/:]+?)(?:\.git)?\s*$/);
|
|
58
|
+
if (match?.[1]) setCloneName(match[1]);
|
|
59
|
+
}, [gitUrl]);
|
|
60
|
+
|
|
61
|
+
// Fetch suggestions when local path changes
|
|
34
62
|
useEffect(() => {
|
|
35
63
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
36
64
|
if (!path.trim()) { setSuggestions([]); setShowSuggestions(false); return; }
|
|
@@ -80,84 +108,199 @@ export function AddProjectForm({ onSuccess, onCancel, footerClassName }: AddProj
|
|
|
80
108
|
}
|
|
81
109
|
}
|
|
82
110
|
|
|
111
|
+
async function handleClone(e?: React.FormEvent) {
|
|
112
|
+
e?.preventDefault();
|
|
113
|
+
if (!gitUrl.trim()) { setError("Git URL is required"); return; }
|
|
114
|
+
if (!cloneDir.trim()) { setError("Clone directory is required"); return; }
|
|
115
|
+
setError("");
|
|
116
|
+
setCloning(true);
|
|
117
|
+
try {
|
|
118
|
+
const result = await api.post<{ path: string; project: unknown }>(
|
|
119
|
+
"/api/projects/git/clone",
|
|
120
|
+
{ url: gitUrl.trim(), targetDir: cloneDir.trim(), name: cloneName.trim() || undefined },
|
|
121
|
+
);
|
|
122
|
+
// Server already added project — refresh store and select it
|
|
123
|
+
const { fetchProjects, setActiveProject } = useProjectStore.getState();
|
|
124
|
+
await fetchProjects();
|
|
125
|
+
const projects = useProjectStore.getState().projects;
|
|
126
|
+
const newProj = projects.find((p) => p.path === result.path);
|
|
127
|
+
if (newProj) setActiveProject(newProj);
|
|
128
|
+
onSuccess();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
setError(err instanceof Error ? err.message : "Clone failed");
|
|
131
|
+
} finally {
|
|
132
|
+
setCloning(false);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
83
136
|
return (
|
|
84
|
-
<
|
|
85
|
-
{/*
|
|
86
|
-
<div
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
137
|
+
<div className="flex flex-col gap-3">
|
|
138
|
+
{/* Tab bar */}
|
|
139
|
+
<div className="flex gap-1 border-b border-border">
|
|
140
|
+
{(["local", "clone"] as const).map((t) => (
|
|
141
|
+
<button
|
|
142
|
+
key={t}
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={() => { setTab(t); setError(""); }}
|
|
145
|
+
className={cn(
|
|
146
|
+
"px-3 py-1.5 text-sm font-medium border-b-2 -mb-px transition-colors",
|
|
147
|
+
tab === t
|
|
148
|
+
? "border-primary text-foreground"
|
|
149
|
+
: "border-transparent text-text-secondary hover:text-foreground",
|
|
150
|
+
)}
|
|
151
|
+
>
|
|
152
|
+
{t === "local" ? "Local folder" : "Clone from Git"}
|
|
153
|
+
</button>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{tab === "local" ? (
|
|
158
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
|
159
|
+
{/* Path input with suggestions */}
|
|
160
|
+
<div ref={wrapperRef} className="relative">
|
|
161
|
+
<label className="block text-xs font-medium text-foreground mb-1">Project path</label>
|
|
162
|
+
<div className="flex gap-1.5 items-center">
|
|
163
|
+
<div className="relative flex items-center flex-1">
|
|
164
|
+
<FolderOpen className="absolute left-2.5 size-3.5 text-text-subtle pointer-events-none" />
|
|
165
|
+
<input
|
|
166
|
+
type="text"
|
|
167
|
+
value={path}
|
|
168
|
+
onChange={(e) => { setPath(e.target.value); setError(""); }}
|
|
169
|
+
onFocus={() => suggestions.length > 0 && setShowSuggestions(true)}
|
|
170
|
+
placeholder="/path/to/project"
|
|
171
|
+
className="w-full pl-8 pr-3 py-2 rounded-md border border-border bg-background text-sm focus:outline-none focus:ring-1 focus:ring-primary"
|
|
172
|
+
autoFocus
|
|
173
|
+
autoComplete="off"
|
|
174
|
+
/>
|
|
175
|
+
{loading && <Loader2 className="absolute right-2.5 size-3.5 text-text-subtle animate-spin" />}
|
|
176
|
+
</div>
|
|
177
|
+
<BrowseButton
|
|
178
|
+
mode="folder"
|
|
179
|
+
onSelect={(selectedPath) => {
|
|
180
|
+
setPath(selectedPath);
|
|
181
|
+
if (!name) setName(selectedPath.split("/").pop() ?? "");
|
|
182
|
+
setError("");
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* Suggestions dropdown */}
|
|
188
|
+
{showSuggestions && suggestions.length > 0 && (
|
|
189
|
+
<div className="absolute top-full left-0 right-0 mt-1 z-10 bg-popover border border-border rounded-md shadow-md max-h-48 overflow-y-auto">
|
|
190
|
+
{suggestions.map((dir) => (
|
|
191
|
+
<button
|
|
192
|
+
key={dir.path}
|
|
193
|
+
type="button"
|
|
194
|
+
onMouseDown={() => selectSuggestion(dir)}
|
|
195
|
+
className="w-full flex flex-col items-start px-3 py-2 text-left hover:bg-accent/50 transition-colors"
|
|
196
|
+
>
|
|
197
|
+
<span className="text-sm font-medium truncate w-full">{dir.name}</span>
|
|
198
|
+
<span className="text-xs text-text-subtle truncate w-full">{dir.path}</span>
|
|
199
|
+
</button>
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{/* Optional name */}
|
|
206
|
+
<div>
|
|
207
|
+
<label className="block text-xs font-medium text-foreground mb-1">Display name <span className="text-muted-foreground">(optional)</span></label>
|
|
208
|
+
<input
|
|
209
|
+
type="text"
|
|
210
|
+
value={name}
|
|
211
|
+
onChange={(e) => setName(e.target.value)}
|
|
212
|
+
placeholder="my-project"
|
|
213
|
+
className="w-full px-3 py-2 rounded-md border border-border bg-background text-sm focus:outline-none focus:ring-1 focus:ring-primary"
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
218
|
+
|
|
219
|
+
<div className={cn("flex justify-end gap-2 pt-1", footerClassName)}>
|
|
220
|
+
<button
|
|
221
|
+
type="button"
|
|
222
|
+
onClick={onCancel}
|
|
223
|
+
className="px-3 py-1.5 text-sm text-text-secondary hover:text-foreground transition-colors"
|
|
224
|
+
>
|
|
225
|
+
Cancel
|
|
226
|
+
</button>
|
|
227
|
+
<button
|
|
228
|
+
type="submit"
|
|
229
|
+
disabled={submitting || !path.trim()}
|
|
230
|
+
className="px-3 py-1.5 text-sm bg-primary text-white rounded-md hover:bg-primary/90 transition-colors disabled:opacity-50"
|
|
231
|
+
>
|
|
232
|
+
{submitting ? "Adding…" : "Add Project"}
|
|
233
|
+
</button>
|
|
234
|
+
</div>
|
|
235
|
+
</form>
|
|
236
|
+
) : (
|
|
237
|
+
<form onSubmit={handleClone} className="flex flex-col gap-3">
|
|
238
|
+
{/* Git URL */}
|
|
239
|
+
<div>
|
|
240
|
+
<label className="block text-xs font-medium text-foreground mb-1">Git URL</label>
|
|
91
241
|
<input
|
|
92
242
|
type="text"
|
|
93
|
-
value={
|
|
94
|
-
onChange={(e) => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
className="w-full pl-8 pr-3 py-2 rounded-md border border-border bg-background text-sm focus:outline-none focus:ring-1 focus:ring-primary"
|
|
243
|
+
value={gitUrl}
|
|
244
|
+
onChange={(e) => { setGitUrl(e.target.value); setError(""); }}
|
|
245
|
+
placeholder="https://github.com/user/repo.git"
|
|
246
|
+
className="w-full px-3 py-2 rounded-md border border-border bg-background text-sm focus:outline-none focus:ring-1 focus:ring-primary"
|
|
98
247
|
autoFocus
|
|
99
248
|
autoComplete="off"
|
|
100
249
|
/>
|
|
101
|
-
{loading && <Loader2 className="absolute right-2.5 size-3.5 text-text-subtle animate-spin" />}
|
|
102
250
|
</div>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
onMouseDown={() => selectSuggestion(dir)}
|
|
121
|
-
className="w-full flex flex-col items-start px-3 py-2 text-left hover:bg-accent/50 transition-colors"
|
|
122
|
-
>
|
|
123
|
-
<span className="text-sm font-medium truncate w-full">{dir.name}</span>
|
|
124
|
-
<span className="text-xs text-text-subtle truncate w-full">{dir.path}</span>
|
|
125
|
-
</button>
|
|
126
|
-
))}
|
|
251
|
+
|
|
252
|
+
{/* Clone to directory */}
|
|
253
|
+
<div>
|
|
254
|
+
<label className="block text-xs font-medium text-foreground mb-1">Clone to</label>
|
|
255
|
+
<div className="flex gap-1.5 items-center">
|
|
256
|
+
<input
|
|
257
|
+
type="text"
|
|
258
|
+
value={cloneDir}
|
|
259
|
+
onChange={(e) => { setCloneDir(e.target.value); setError(""); }}
|
|
260
|
+
placeholder="~/Projects"
|
|
261
|
+
className="flex-1 px-3 py-2 rounded-md border border-border bg-background text-sm focus:outline-none focus:ring-1 focus:ring-primary"
|
|
262
|
+
/>
|
|
263
|
+
<BrowseButton
|
|
264
|
+
mode="folder"
|
|
265
|
+
onSelect={(p) => { setCloneDir(p); setError(""); }}
|
|
266
|
+
/>
|
|
267
|
+
</div>
|
|
127
268
|
</div>
|
|
128
|
-
)}
|
|
129
|
-
</div>
|
|
130
269
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
270
|
+
{/* Repo name override */}
|
|
271
|
+
<div>
|
|
272
|
+
<label className="block text-xs font-medium text-foreground mb-1">
|
|
273
|
+
Name <span className="text-muted-foreground">(auto-parsed from URL)</span>
|
|
274
|
+
</label>
|
|
275
|
+
<input
|
|
276
|
+
type="text"
|
|
277
|
+
value={cloneName}
|
|
278
|
+
onChange={(e) => setCloneName(e.target.value)}
|
|
279
|
+
placeholder="repo-name"
|
|
280
|
+
className="w-full px-3 py-2 rounded-md border border-border bg-background text-sm focus:outline-none focus:ring-1 focus:ring-primary"
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
142
283
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
284
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
285
|
+
|
|
286
|
+
<div className={cn("flex justify-end gap-2 pt-1", footerClassName)}>
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
onClick={onCancel}
|
|
290
|
+
className="px-3 py-1.5 text-sm text-text-secondary hover:text-foreground transition-colors"
|
|
291
|
+
>
|
|
292
|
+
Cancel
|
|
293
|
+
</button>
|
|
294
|
+
<button
|
|
295
|
+
type="submit"
|
|
296
|
+
disabled={cloning || !gitUrl.trim() || !cloneDir.trim()}
|
|
297
|
+
className="px-3 py-1.5 text-sm bg-primary text-white rounded-md hover:bg-primary/90 transition-colors disabled:opacity-50"
|
|
298
|
+
>
|
|
299
|
+
{cloning ? <><Loader2 className="inline size-3.5 animate-spin mr-1.5" />Cloning…</> : "Clone & Add"}
|
|
300
|
+
</button>
|
|
301
|
+
</div>
|
|
302
|
+
</form>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
162
305
|
);
|
|
163
306
|
}
|