@dex-ai/coding-agent 0.1.92
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/bin/dex.ts +402 -0
- package/package.json +45 -0
- package/src/__tests__/command-validation.test.ts +205 -0
- package/src/__tests__/history.test.ts +183 -0
- package/src/cli-extension.ts +153 -0
- package/src/commands/extension-loader.ts +399 -0
- package/src/commands/extension.ts +924 -0
- package/src/commands/update.ts +419 -0
- package/src/env.d.ts +5 -0
- package/src/extensions/cli-tui-components/ActivityPanel.vue +24 -0
- package/src/extensions/cli-tui-components/ActivityPanel.vue.compiled.ts +96 -0
- package/src/extensions/cli-tui-components/App.vue +127 -0
- package/src/extensions/cli-tui-components/App.vue.compiled.ts +374 -0
- package/src/extensions/cli-tui-components/ApprovalPrompt.vue +30 -0
- package/src/extensions/cli-tui-components/ApprovalPrompt.vue.compiled.ts +72 -0
- package/src/extensions/cli-tui-components/AskPanel.vue +228 -0
- package/src/extensions/cli-tui-components/AskPanel.vue.compiled.ts +419 -0
- package/src/extensions/cli-tui-components/CommandPalette.vue +19 -0
- package/src/extensions/cli-tui-components/CommandPalette.vue.compiled.ts +65 -0
- package/src/extensions/cli-tui-components/ConfirmModal.vue +29 -0
- package/src/extensions/cli-tui-components/ConfirmModal.vue.compiled.ts +72 -0
- package/src/extensions/cli-tui-components/DiffView.vue +139 -0
- package/src/extensions/cli-tui-components/DiffView.vue.compiled.ts +274 -0
- package/src/extensions/cli-tui-components/FormModal.vue +58 -0
- package/src/extensions/cli-tui-components/FormModal.vue.compiled.ts +156 -0
- package/src/extensions/cli-tui-components/Header.vue +13 -0
- package/src/extensions/cli-tui-components/Header.vue.compiled.ts +42 -0
- package/src/extensions/cli-tui-components/InputArea.vue +202 -0
- package/src/extensions/cli-tui-components/InputArea.vue.compiled.ts +243 -0
- package/src/extensions/cli-tui-components/InteractivePanel.vue +32 -0
- package/src/extensions/cli-tui-components/InteractivePanel.vue.compiled.ts +103 -0
- package/src/extensions/cli-tui-components/ListModal.vue +58 -0
- package/src/extensions/cli-tui-components/ListModal.vue.compiled.ts +130 -0
- package/src/extensions/cli-tui-components/MarkdownContent.ts +54 -0
- package/src/extensions/cli-tui-components/Messages.vue +68 -0
- package/src/extensions/cli-tui-components/Messages.vue.compiled.ts +253 -0
- package/src/extensions/cli-tui-components/Modal.vue +56 -0
- package/src/extensions/cli-tui-components/Modal.vue.compiled.ts +61 -0
- package/src/extensions/cli-tui-components/SettingsPanel.vue +178 -0
- package/src/extensions/cli-tui-components/SettingsPanel.vue.compiled.ts +359 -0
- package/src/extensions/cli-tui-components/Spinner.vue +19 -0
- package/src/extensions/cli-tui-components/Spinner.vue.compiled.ts +42 -0
- package/src/extensions/cli-tui-components/StatusBar.vue +45 -0
- package/src/extensions/cli-tui-components/StatusBar.vue.compiled.ts +106 -0
- package/src/extensions/cli-tui-components/SteeringPreview.vue +11 -0
- package/src/extensions/cli-tui-components/SteeringPreview.vue.compiled.ts +38 -0
- package/src/extensions/cli-tui-components/ThinkingBlock.vue +40 -0
- package/src/extensions/cli-tui-components/ThinkingBlock.vue.compiled.ts +82 -0
- package/src/extensions/cli-tui-components/ToolCall.vue +114 -0
- package/src/extensions/cli-tui-components/ToolCall.vue.compiled.ts +319 -0
- package/src/extensions/cli-tui-components/UserMessage.vue +40 -0
- package/src/extensions/cli-tui-components/UserMessage.vue.compiled.ts +148 -0
- package/src/extensions/cli-tui-components/ask-panel-controller.ts +573 -0
- package/src/extensions/cli-tui-components/settings-panel-controller.ts +958 -0
- package/src/extensions/cli-tui.ts +2349 -0
- package/src/extensions/debug.ts +46 -0
- package/src/extensions/headless.ts +55 -0
- package/src/extensions/modal-system.ts +719 -0
- package/src/host.ts +505 -0
- package/src/index.ts +9 -0
- package/src/input/history.ts +233 -0
- package/src/input/index.ts +6 -0
- package/src/panels/dynamic-panel.ts +5 -0
- package/src/panels/index.ts +43 -0
- package/src/panels/state.ts +73 -0
- package/src/panels/types.ts +79 -0
- package/src/panels/widget.ts +25 -0
- package/src/provider-registry.ts +44 -0
- package/src/stderr-capture.ts +248 -0
- package/src/types.ts +20 -0
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dex extension` CLI subcommand — manage installed extensions.
|
|
3
|
+
*
|
|
4
|
+
* Each extension is installed in its own isolated directory with a dedicated
|
|
5
|
+
* package.json, lockfile, and node_modules/. No shared dependency tree.
|
|
6
|
+
*
|
|
7
|
+
* Commands:
|
|
8
|
+
* dex extension install npm:<package> Install from npm (user scope)
|
|
9
|
+
* dex extension install file:<path> Install from local path (user scope)
|
|
10
|
+
* dex extension install npm:<package> --local Install to project scope
|
|
11
|
+
* dex extension uninstall <name> Remove an extension
|
|
12
|
+
* dex extension uninstall <name> --local Remove from project scope
|
|
13
|
+
* dex extension update <name> Update an extension
|
|
14
|
+
* dex extension update --all Update all npm extensions
|
|
15
|
+
* dex extension list List installed extensions
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
existsSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
rmSync,
|
|
25
|
+
} from "node:fs";
|
|
26
|
+
import { join, resolve } from "node:path";
|
|
27
|
+
import { homedir } from "node:os";
|
|
28
|
+
import { spawnSync } from "node:child_process";
|
|
29
|
+
|
|
30
|
+
/* ------------------------------------------------------------------ */
|
|
31
|
+
/* Constants */
|
|
32
|
+
/* ------------------------------------------------------------------ */
|
|
33
|
+
|
|
34
|
+
const USER_EXTENSIONS_DIR = join(homedir(), ".dex", "extensions");
|
|
35
|
+
const USER_PACKAGES_DIR = join(USER_EXTENSIONS_DIR, "packages");
|
|
36
|
+
const USER_REGISTRY_PATH = join(USER_EXTENSIONS_DIR, "registry.json");
|
|
37
|
+
|
|
38
|
+
function getProjectExtensionsDir(cwd: string): string {
|
|
39
|
+
return join(cwd, ".dex", "extensions");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getProjectRegistryPath(cwd: string): string {
|
|
43
|
+
return join(getProjectExtensionsDir(cwd), "registry.json");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ------------------------------------------------------------------ */
|
|
47
|
+
/* Types */
|
|
48
|
+
/* ------------------------------------------------------------------ */
|
|
49
|
+
|
|
50
|
+
interface RegistryEntry {
|
|
51
|
+
source: "npm" | "file";
|
|
52
|
+
package: string;
|
|
53
|
+
version?: string;
|
|
54
|
+
path?: string;
|
|
55
|
+
type?: "native" | "pi-compat";
|
|
56
|
+
installedAt: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface Registry {
|
|
60
|
+
version: 1;
|
|
61
|
+
extensions: Record<string, RegistryEntry>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ------------------------------------------------------------------ */
|
|
65
|
+
/* Registry helpers */
|
|
66
|
+
/* ------------------------------------------------------------------ */
|
|
67
|
+
|
|
68
|
+
function loadRegistry(registryPath: string): Registry {
|
|
69
|
+
if (!existsSync(registryPath)) {
|
|
70
|
+
return { version: 1, extensions: {} };
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const data = JSON.parse(readFileSync(registryPath, "utf-8"));
|
|
74
|
+
if (data.version === 1 && data.extensions) return data;
|
|
75
|
+
return { version: 1, extensions: {} };
|
|
76
|
+
} catch {
|
|
77
|
+
return { version: 1, extensions: {} };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function saveRegistry(registryPath: string, registry: Registry): void {
|
|
82
|
+
const dir = join(registryPath, "..");
|
|
83
|
+
mkdirSync(dir, { recursive: true });
|
|
84
|
+
writeFileSync(registryPath, JSON.stringify(registry, null, 2) + "\n");
|
|
85
|
+
|
|
86
|
+
// Ensure extensions/config.json exists alongside registry
|
|
87
|
+
const configPath = join(dir, "config.json");
|
|
88
|
+
if (!existsSync(configPath)) {
|
|
89
|
+
writeFileSync(configPath, "{}\n");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* ------------------------------------------------------------------ */
|
|
94
|
+
/* Package manager */
|
|
95
|
+
/* ------------------------------------------------------------------ */
|
|
96
|
+
|
|
97
|
+
type PackageManagerName = "npm" | "bun" | "pnpm" | "yarn";
|
|
98
|
+
|
|
99
|
+
function getConfiguredPackageManager(): PackageManagerName {
|
|
100
|
+
const settingsPath = join(homedir(), ".dex", "settings.json");
|
|
101
|
+
if (existsSync(settingsPath)) {
|
|
102
|
+
try {
|
|
103
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
104
|
+
const pm = settings.packageManager;
|
|
105
|
+
if (pm === "npm" || pm === "bun" || pm === "pnpm" || pm === "yarn") {
|
|
106
|
+
return pm;
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Fall through to default
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return "npm";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface PackageManagerCommands {
|
|
116
|
+
install: (cwd: string) => { command: string; args: string[] };
|
|
117
|
+
add: (cwd: string, pkg: string) => { command: string; args: string[] };
|
|
118
|
+
update: (cwd: string, pkg: string) => { command: string; args: string[] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Copy .npmrc from ~/.dex/app to an extension directory so that npm resolves
|
|
123
|
+
* packages against the configured registry (e.g. local Verdaccio).
|
|
124
|
+
*/
|
|
125
|
+
function ensureNpmrc(extensionDir: string): void {
|
|
126
|
+
const appNpmrc = join(homedir(), ".dex", "app", ".npmrc");
|
|
127
|
+
if (!existsSync(appNpmrc)) return;
|
|
128
|
+
const destNpmrc = join(extensionDir, ".npmrc");
|
|
129
|
+
// Always overwrite to keep in sync with the app config
|
|
130
|
+
try {
|
|
131
|
+
const contents = readFileSync(appNpmrc, "utf-8");
|
|
132
|
+
mkdirSync(extensionDir, { recursive: true });
|
|
133
|
+
writeFileSync(destNpmrc, contents);
|
|
134
|
+
} catch {
|
|
135
|
+
// Non-fatal — npm will fall back to default registry
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getPackageManagerCommands(
|
|
140
|
+
pm: PackageManagerName,
|
|
141
|
+
): PackageManagerCommands {
|
|
142
|
+
switch (pm) {
|
|
143
|
+
case "bun":
|
|
144
|
+
return {
|
|
145
|
+
install: (cwd) => ({ command: "bun", args: ["install"] }),
|
|
146
|
+
add: (cwd, pkg) => ({ command: "bun", args: ["add", pkg] }),
|
|
147
|
+
update: (cwd, pkg) => ({ command: "bun", args: ["update", pkg] }),
|
|
148
|
+
};
|
|
149
|
+
case "pnpm":
|
|
150
|
+
return {
|
|
151
|
+
install: (cwd) => ({ command: "pnpm", args: ["install"] }),
|
|
152
|
+
add: (cwd, pkg) => ({ command: "pnpm", args: ["add", pkg] }),
|
|
153
|
+
update: (cwd, pkg) => ({ command: "pnpm", args: ["update", pkg] }),
|
|
154
|
+
};
|
|
155
|
+
case "yarn":
|
|
156
|
+
return {
|
|
157
|
+
install: (cwd) => ({ command: "yarn", args: ["install"] }),
|
|
158
|
+
add: (cwd, pkg) => ({ command: "yarn", args: ["add", pkg] }),
|
|
159
|
+
update: (cwd, pkg) => ({ command: "yarn", args: ["upgrade", pkg] }),
|
|
160
|
+
};
|
|
161
|
+
case "npm":
|
|
162
|
+
default:
|
|
163
|
+
return {
|
|
164
|
+
install: (cwd) => ({ command: "npm", args: ["install"] }),
|
|
165
|
+
add: (cwd, pkg) => ({ command: "npm", args: ["install", pkg] }),
|
|
166
|
+
// Use `install @latest --prefer-online` instead of `update` —
|
|
167
|
+
// npm update doesn't reliably extract new tarballs from local registries.
|
|
168
|
+
update: (cwd, pkg) => ({
|
|
169
|
+
command: "npm",
|
|
170
|
+
args: ["install", `${pkg}@latest`, "--prefer-online"],
|
|
171
|
+
}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function runPackageManager(
|
|
177
|
+
cmd: { command: string; args: string[] },
|
|
178
|
+
cwd: string,
|
|
179
|
+
): { success: boolean } {
|
|
180
|
+
const result = spawnSync(cmd.command, cmd.args, {
|
|
181
|
+
cwd,
|
|
182
|
+
stdio: "inherit",
|
|
183
|
+
});
|
|
184
|
+
return { success: result.status === 0 };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* ------------------------------------------------------------------ */
|
|
188
|
+
/* Source parsing */
|
|
189
|
+
/* ------------------------------------------------------------------ */
|
|
190
|
+
|
|
191
|
+
interface ParsedSource {
|
|
192
|
+
type: "npm" | "file";
|
|
193
|
+
ref: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function parseSource(source: string): ParsedSource | null {
|
|
197
|
+
if (source.startsWith("npm:")) {
|
|
198
|
+
return { type: "npm", ref: source.slice(4) };
|
|
199
|
+
}
|
|
200
|
+
if (source.startsWith("file:")) {
|
|
201
|
+
return { type: "file", ref: source.slice(5) };
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Derive extension name from npm package name (strip scope). */
|
|
207
|
+
function deriveNameFromPackage(packageName: string): string {
|
|
208
|
+
return packageName.replace(/^@[^/]+\//, "");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Derive extension name from tarball filename. */
|
|
212
|
+
function deriveNameFromTarball(tarballPath: string): string {
|
|
213
|
+
const base = tarballPath.split("/").pop() ?? tarballPath;
|
|
214
|
+
// e.g. dex-ai-devtools-extension-0.1.0.tgz -> devtools-extension
|
|
215
|
+
// Strip version suffix and .tgz
|
|
216
|
+
const withoutExt = base.replace(/\.tgz$|\.tar\.gz$/, "");
|
|
217
|
+
// Strip dex-ai- prefix and version suffix (-0.1.0, -1.2.3, etc.)
|
|
218
|
+
const withoutPrefix = withoutExt.replace(/^dex-ai-/, "");
|
|
219
|
+
const withoutVersion = withoutPrefix.replace(/-\d+\.\d+\.\d+(-[\w.]+)?$/, "");
|
|
220
|
+
return withoutVersion || withoutPrefix;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Find the actual primary package name installed in an extension's node_modules. */
|
|
224
|
+
function findInstalledPackageName(extensionDir: string): string | null {
|
|
225
|
+
// Read the extension's package.json to find what was installed as a dependency
|
|
226
|
+
const extPkgJsonPath = join(extensionDir, "package.json");
|
|
227
|
+
if (existsSync(extPkgJsonPath)) {
|
|
228
|
+
try {
|
|
229
|
+
const pkg = JSON.parse(readFileSync(extPkgJsonPath, "utf-8"));
|
|
230
|
+
const deps = pkg.dependencies ?? {};
|
|
231
|
+
const depNames = Object.keys(deps);
|
|
232
|
+
if (depNames.length > 0) {
|
|
233
|
+
// Return the first (and should be only) dependency
|
|
234
|
+
return depNames[0]!;
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// Fall through to node_modules scan
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Fallback: scan node_modules for scoped @dex-ai packages first
|
|
242
|
+
const nmDir = join(extensionDir, "node_modules");
|
|
243
|
+
if (!existsSync(nmDir)) return null;
|
|
244
|
+
|
|
245
|
+
const entries = readdirSync(nmDir);
|
|
246
|
+
|
|
247
|
+
// Prefer scoped @dex-ai packages
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
if (entry === "@dex-ai") {
|
|
250
|
+
const scopeDir = join(nmDir, entry);
|
|
251
|
+
const scopeEntries = readdirSync(scopeDir);
|
|
252
|
+
// Find one that matches the extension dir name
|
|
253
|
+
for (const sub of scopeEntries) {
|
|
254
|
+
if (sub.includes("extension")) {
|
|
255
|
+
return `${entry}/${sub}`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Otherwise return the first
|
|
259
|
+
if (scopeEntries.length > 0) {
|
|
260
|
+
return `${entry}/${scopeEntries[0]}`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Fallback: first non-hidden package
|
|
266
|
+
for (const entry of entries) {
|
|
267
|
+
if (entry.startsWith(".")) continue;
|
|
268
|
+
if (entry.startsWith("@")) continue;
|
|
269
|
+
const pkgJsonPath = join(nmDir, entry, "package.json");
|
|
270
|
+
if (existsSync(pkgJsonPath)) {
|
|
271
|
+
return entry;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Resolve a file path (supports ~). */
|
|
279
|
+
function resolveFilePath(filePath: string): string {
|
|
280
|
+
if (filePath.startsWith("~/")) {
|
|
281
|
+
return join(homedir(), filePath.slice(2));
|
|
282
|
+
}
|
|
283
|
+
if (filePath.startsWith("~")) {
|
|
284
|
+
return join(homedir(), filePath.slice(1));
|
|
285
|
+
}
|
|
286
|
+
return resolve(filePath);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* ------------------------------------------------------------------ */
|
|
290
|
+
/* Package inspection */
|
|
291
|
+
/* ------------------------------------------------------------------ */
|
|
292
|
+
|
|
293
|
+
interface PackageInfo {
|
|
294
|
+
name: string;
|
|
295
|
+
version?: string;
|
|
296
|
+
hasDexField: boolean;
|
|
297
|
+
hasPiField: boolean;
|
|
298
|
+
dexExtensions?: string[];
|
|
299
|
+
piExtensions?: string[];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Read package.json from an installed extension's node_modules. */
|
|
303
|
+
function readInstalledPackageInfo(
|
|
304
|
+
extensionDir: string,
|
|
305
|
+
packageName: string,
|
|
306
|
+
): PackageInfo | null {
|
|
307
|
+
const pkgJsonPath = join(
|
|
308
|
+
extensionDir,
|
|
309
|
+
"node_modules",
|
|
310
|
+
packageName,
|
|
311
|
+
"package.json",
|
|
312
|
+
);
|
|
313
|
+
if (!existsSync(pkgJsonPath)) return null;
|
|
314
|
+
try {
|
|
315
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
316
|
+
return {
|
|
317
|
+
name: pkg.name ?? packageName,
|
|
318
|
+
version: pkg.version,
|
|
319
|
+
hasDexField: !!(pkg.dex && typeof pkg.dex === "object"),
|
|
320
|
+
hasPiField: !!(pkg.pi && typeof pkg.pi === "object"),
|
|
321
|
+
dexExtensions: pkg.dex?.extensions,
|
|
322
|
+
piExtensions: pkg.pi?.extensions,
|
|
323
|
+
};
|
|
324
|
+
} catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Read package.json from a source directory (for file: installs). */
|
|
330
|
+
function readSourcePackageInfo(dirPath: string): PackageInfo | null {
|
|
331
|
+
const pkgJsonPath = join(dirPath, "package.json");
|
|
332
|
+
if (!existsSync(pkgJsonPath)) return null;
|
|
333
|
+
try {
|
|
334
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
335
|
+
return {
|
|
336
|
+
name: pkg.name ?? "unknown",
|
|
337
|
+
version: pkg.version,
|
|
338
|
+
hasDexField: !!(pkg.dex && typeof pkg.dex === "object"),
|
|
339
|
+
hasPiField: !!(pkg.pi && typeof pkg.pi === "object"),
|
|
340
|
+
dexExtensions: pkg.dex?.extensions,
|
|
341
|
+
piExtensions: pkg.pi?.extensions,
|
|
342
|
+
};
|
|
343
|
+
} catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* ------------------------------------------------------------------ */
|
|
349
|
+
/* Extension directory management */
|
|
350
|
+
/* ------------------------------------------------------------------ */
|
|
351
|
+
|
|
352
|
+
function getExtensionDir(name: string, local: boolean, cwd: string): string {
|
|
353
|
+
if (local) {
|
|
354
|
+
return join(getProjectExtensionsDir(cwd), name);
|
|
355
|
+
}
|
|
356
|
+
return join(USER_PACKAGES_DIR, name);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function createExtensionPackageJson(
|
|
360
|
+
extensionDir: string,
|
|
361
|
+
name: string,
|
|
362
|
+
packageName: string,
|
|
363
|
+
depValue: string,
|
|
364
|
+
): void {
|
|
365
|
+
mkdirSync(extensionDir, { recursive: true });
|
|
366
|
+
const pkgJson = {
|
|
367
|
+
name: `dex-ext-${name}`,
|
|
368
|
+
private: true,
|
|
369
|
+
type: "module",
|
|
370
|
+
dependencies: {
|
|
371
|
+
[packageName]: depValue,
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
writeFileSync(
|
|
375
|
+
join(extensionDir, "package.json"),
|
|
376
|
+
JSON.stringify(pkgJson, null, 2) + "\n",
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* ------------------------------------------------------------------ */
|
|
381
|
+
/* Install */
|
|
382
|
+
/* ------------------------------------------------------------------ */
|
|
383
|
+
|
|
384
|
+
function cmdInstall(source: string, local: boolean): void {
|
|
385
|
+
if (!source) {
|
|
386
|
+
console.error("Usage: dex extension install <source> [-l|--local]");
|
|
387
|
+
console.error("");
|
|
388
|
+
console.error("Sources:");
|
|
389
|
+
console.error(" npm:<package> Install from npm registry");
|
|
390
|
+
console.error(" file:<path> Install from local filesystem");
|
|
391
|
+
console.error("");
|
|
392
|
+
console.error("Options:");
|
|
393
|
+
console.error(" -l, --local Install to project scope");
|
|
394
|
+
console.error("");
|
|
395
|
+
console.error("Examples:");
|
|
396
|
+
console.error(" dex extension install npm:@dex-ai/memory");
|
|
397
|
+
console.error(" dex extension install npm:pi-lens");
|
|
398
|
+
console.error(" dex extension install file:./my-extension");
|
|
399
|
+
console.error(
|
|
400
|
+
" dex extension install npm:@dex-ai/devtools-extension --local",
|
|
401
|
+
);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const parsed = parseSource(source);
|
|
406
|
+
if (!parsed) {
|
|
407
|
+
console.error(`Invalid source format: ${source}`);
|
|
408
|
+
console.error("Use npm:<package> or file:<path>");
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const cwd = process.cwd();
|
|
413
|
+
|
|
414
|
+
if (parsed.type === "npm") {
|
|
415
|
+
installFromNpm(parsed.ref, local, cwd);
|
|
416
|
+
} else {
|
|
417
|
+
installFromFile(parsed.ref, local, cwd);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function installFromNpm(
|
|
422
|
+
packageName: string,
|
|
423
|
+
local: boolean,
|
|
424
|
+
cwd: string,
|
|
425
|
+
): void {
|
|
426
|
+
// Detect if this is a tarball path (e.g. /path/to/foo-0.1.0.tgz)
|
|
427
|
+
const isTarball =
|
|
428
|
+
packageName.endsWith(".tgz") || packageName.endsWith(".tar.gz");
|
|
429
|
+
|
|
430
|
+
const name = isTarball
|
|
431
|
+
? deriveNameFromTarball(packageName)
|
|
432
|
+
: deriveNameFromPackage(packageName);
|
|
433
|
+
const extensionDir = getExtensionDir(name, local, cwd);
|
|
434
|
+
const registryPath = local ? getProjectRegistryPath(cwd) : USER_REGISTRY_PATH;
|
|
435
|
+
const registry = loadRegistry(registryPath);
|
|
436
|
+
const scope = local ? "project" : "user";
|
|
437
|
+
|
|
438
|
+
if (registry.extensions[name]) {
|
|
439
|
+
console.log(`Updating existing extension "${name}"...`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
console.log(`Installing ${packageName} (${scope} scope)...`);
|
|
443
|
+
|
|
444
|
+
// Create isolated extension directory with its own package.json
|
|
445
|
+
// For tarballs, use the file path directly as the dep value
|
|
446
|
+
const depValue = isTarball ? packageName : "latest";
|
|
447
|
+
const depKey = isTarball ? name : packageName;
|
|
448
|
+
|
|
449
|
+
// For tarballs, we use the tarball name as both dep key and use `npm install`
|
|
450
|
+
// which resolves the tarball. For registry packages, use packageName@latest.
|
|
451
|
+
if (isTarball) {
|
|
452
|
+
// npm install <tarball> installs directly — use add command
|
|
453
|
+
mkdirSync(extensionDir, { recursive: true });
|
|
454
|
+
ensureNpmrc(extensionDir);
|
|
455
|
+
writeFileSync(
|
|
456
|
+
join(extensionDir, "package.json"),
|
|
457
|
+
JSON.stringify(
|
|
458
|
+
{ name: `dex-ext-${name}`, private: true, type: "module" },
|
|
459
|
+
null,
|
|
460
|
+
2,
|
|
461
|
+
) + "\n",
|
|
462
|
+
);
|
|
463
|
+
const pm = getConfiguredPackageManager();
|
|
464
|
+
const cmds = getPackageManagerCommands(pm);
|
|
465
|
+
const addCmd = cmds.add(extensionDir, packageName);
|
|
466
|
+
const { success } = runPackageManager(addCmd, extensionDir);
|
|
467
|
+
|
|
468
|
+
if (!success) {
|
|
469
|
+
rmSync(extensionDir, { recursive: true, force: true });
|
|
470
|
+
console.error(`Failed to install ${packageName}`);
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
createExtensionPackageJson(extensionDir, name, packageName, "latest");
|
|
475
|
+
ensureNpmrc(extensionDir);
|
|
476
|
+
|
|
477
|
+
// Run package manager install
|
|
478
|
+
const pm = getConfiguredPackageManager();
|
|
479
|
+
const cmds = getPackageManagerCommands(pm);
|
|
480
|
+
const installCmd = cmds.install(extensionDir);
|
|
481
|
+
const { success } = runPackageManager(installCmd, extensionDir);
|
|
482
|
+
|
|
483
|
+
if (!success) {
|
|
484
|
+
rmSync(extensionDir, { recursive: true, force: true });
|
|
485
|
+
console.error(`Failed to install ${packageName}`);
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Read installed package info — for tarballs, find the actual package name from node_modules
|
|
491
|
+
const actualPackageName = isTarball
|
|
492
|
+
? findInstalledPackageName(extensionDir)
|
|
493
|
+
: packageName;
|
|
494
|
+
const info = actualPackageName
|
|
495
|
+
? readInstalledPackageInfo(extensionDir, actualPackageName)
|
|
496
|
+
: null;
|
|
497
|
+
const type = info?.hasPiField ? "pi-compat" : "native";
|
|
498
|
+
|
|
499
|
+
// Check pi-compat requirements
|
|
500
|
+
if (type === "pi-compat") {
|
|
501
|
+
// Verify @dex-ai/pi-compat is installed somewhere
|
|
502
|
+
const piCompatInstalled =
|
|
503
|
+
existsSync(join(USER_PACKAGES_DIR, "pi-compat")) ||
|
|
504
|
+
(local && existsSync(join(getProjectExtensionsDir(cwd), "pi-compat")));
|
|
505
|
+
if (!piCompatInstalled) {
|
|
506
|
+
console.error("");
|
|
507
|
+
console.error(
|
|
508
|
+
`Warning: "${name}" is a pi-compat extension and requires @dex-ai/pi-compat.`,
|
|
509
|
+
);
|
|
510
|
+
console.error("Install it with:");
|
|
511
|
+
console.error(" dex extension install npm:@dex-ai/pi-compat");
|
|
512
|
+
console.error("");
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Update registry
|
|
517
|
+
registry.extensions[name] = {
|
|
518
|
+
source: "npm",
|
|
519
|
+
package: actualPackageName ?? packageName,
|
|
520
|
+
...(info?.version ? { version: info.version } : {}),
|
|
521
|
+
type,
|
|
522
|
+
installedAt: new Date().toISOString(),
|
|
523
|
+
};
|
|
524
|
+
saveRegistry(registryPath, registry);
|
|
525
|
+
|
|
526
|
+
console.log("");
|
|
527
|
+
console.log(
|
|
528
|
+
`Installed ${name}${info?.version ? ` v${info.version}` : ""} (${scope} scope)`,
|
|
529
|
+
);
|
|
530
|
+
if (type === "pi-compat") {
|
|
531
|
+
console.log(" Type: pi-compat");
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function installFromFile(filePath: string, local: boolean, cwd: string): void {
|
|
536
|
+
const resolved = resolveFilePath(filePath);
|
|
537
|
+
|
|
538
|
+
if (!existsSync(resolved)) {
|
|
539
|
+
console.error(`Path not found: ${resolved}`);
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Read package.json from the source to get the package name
|
|
544
|
+
const info = readSourcePackageInfo(resolved);
|
|
545
|
+
if (!info) {
|
|
546
|
+
console.error(`No package.json found at: ${resolved}`);
|
|
547
|
+
console.error(
|
|
548
|
+
"For file: installs, the path must be a directory with a package.json.",
|
|
549
|
+
);
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const packageName = info.name;
|
|
554
|
+
const name = deriveNameFromPackage(packageName);
|
|
555
|
+
const extensionDir = getExtensionDir(name, local, cwd);
|
|
556
|
+
const registryPath = local ? getProjectRegistryPath(cwd) : USER_REGISTRY_PATH;
|
|
557
|
+
const registry = loadRegistry(registryPath);
|
|
558
|
+
const scope = local ? "project" : "user";
|
|
559
|
+
|
|
560
|
+
console.log(`Installing ${packageName} from ${resolved} (${scope} scope)...`);
|
|
561
|
+
|
|
562
|
+
// Create isolated extension directory with file: dependency
|
|
563
|
+
createExtensionPackageJson(
|
|
564
|
+
extensionDir,
|
|
565
|
+
name,
|
|
566
|
+
packageName,
|
|
567
|
+
`file:${resolved}`,
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
// Run package manager install (installs via symlink + transitive deps)
|
|
571
|
+
const pm = getConfiguredPackageManager();
|
|
572
|
+
const cmds = getPackageManagerCommands(pm);
|
|
573
|
+
const installCmd = cmds.install(extensionDir);
|
|
574
|
+
const { success } = runPackageManager(installCmd, extensionDir);
|
|
575
|
+
|
|
576
|
+
if (!success) {
|
|
577
|
+
rmSync(extensionDir, { recursive: true, force: true });
|
|
578
|
+
console.error(`Failed to install from ${resolved}`);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Read installed package info for type detection
|
|
583
|
+
const installedInfo = readInstalledPackageInfo(extensionDir, packageName);
|
|
584
|
+
const type = installedInfo?.hasPiField ? "pi-compat" : "native";
|
|
585
|
+
|
|
586
|
+
// Check pi-compat requirements
|
|
587
|
+
if (type === "pi-compat") {
|
|
588
|
+
const piCompatInstalled =
|
|
589
|
+
existsSync(join(USER_PACKAGES_DIR, "pi-compat")) ||
|
|
590
|
+
(local && existsSync(join(getProjectExtensionsDir(cwd), "pi-compat")));
|
|
591
|
+
if (!piCompatInstalled) {
|
|
592
|
+
console.error("");
|
|
593
|
+
console.error(
|
|
594
|
+
`Warning: "${name}" is a pi-compat extension and requires @dex-ai/pi-compat.`,
|
|
595
|
+
);
|
|
596
|
+
console.error("Install it with:");
|
|
597
|
+
console.error(" dex extension install npm:@dex-ai/pi-compat");
|
|
598
|
+
console.error("");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Update registry
|
|
603
|
+
registry.extensions[name] = {
|
|
604
|
+
source: "file",
|
|
605
|
+
package: packageName,
|
|
606
|
+
path: resolved,
|
|
607
|
+
...(installedInfo?.version ? { version: installedInfo.version } : {}),
|
|
608
|
+
type,
|
|
609
|
+
installedAt: new Date().toISOString(),
|
|
610
|
+
};
|
|
611
|
+
saveRegistry(registryPath, registry);
|
|
612
|
+
|
|
613
|
+
console.log("");
|
|
614
|
+
console.log(`Installed ${name} (file) (${scope} scope)`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* ------------------------------------------------------------------ */
|
|
618
|
+
/* Uninstall */
|
|
619
|
+
/* ------------------------------------------------------------------ */
|
|
620
|
+
|
|
621
|
+
function cmdUninstall(name: string, local: boolean): void {
|
|
622
|
+
if (!name) {
|
|
623
|
+
console.error("Usage: dex extension uninstall <name> [-l|--local]");
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const cwd = process.cwd();
|
|
628
|
+
const registryPath = local ? getProjectRegistryPath(cwd) : USER_REGISTRY_PATH;
|
|
629
|
+
const registry = loadRegistry(registryPath);
|
|
630
|
+
const entry = registry.extensions[name];
|
|
631
|
+
|
|
632
|
+
if (!entry) {
|
|
633
|
+
console.error(`Extension "${name}" is not installed.`);
|
|
634
|
+
const installed = Object.keys(registry.extensions);
|
|
635
|
+
if (installed.length > 0) {
|
|
636
|
+
console.error("");
|
|
637
|
+
console.error("Installed extensions:");
|
|
638
|
+
for (const key of installed) {
|
|
639
|
+
console.error(` ${key}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Remove the extension directory
|
|
646
|
+
const extensionDir = getExtensionDir(name, local, cwd);
|
|
647
|
+
if (existsSync(extensionDir)) {
|
|
648
|
+
console.log(`Removing ${name}...`);
|
|
649
|
+
rmSync(extensionDir, { recursive: true, force: true });
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Remove from registry
|
|
653
|
+
delete registry.extensions[name];
|
|
654
|
+
saveRegistry(registryPath, registry);
|
|
655
|
+
|
|
656
|
+
console.log(`Uninstalled "${name}"`);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/* ------------------------------------------------------------------ */
|
|
660
|
+
/* Update */
|
|
661
|
+
/* ------------------------------------------------------------------ */
|
|
662
|
+
|
|
663
|
+
function cmdUpdate(nameOrFlag: string, local: boolean): void {
|
|
664
|
+
const cwd = process.cwd();
|
|
665
|
+
const registryPath = local ? getProjectRegistryPath(cwd) : USER_REGISTRY_PATH;
|
|
666
|
+
const registry = loadRegistry(registryPath);
|
|
667
|
+
|
|
668
|
+
if (nameOrFlag === "--all") {
|
|
669
|
+
const npmEntries = Object.entries(registry.extensions).filter(
|
|
670
|
+
([_, e]) => e.source === "npm",
|
|
671
|
+
);
|
|
672
|
+
if (npmEntries.length === 0) {
|
|
673
|
+
console.log("No npm extensions installed.");
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
console.log(`Updating ${npmEntries.length} extension(s)...`);
|
|
677
|
+
console.log("");
|
|
678
|
+
for (const [name, entry] of npmEntries) {
|
|
679
|
+
updateSingle(name, entry, registry, local, cwd);
|
|
680
|
+
}
|
|
681
|
+
saveRegistry(registryPath, registry);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (!nameOrFlag) {
|
|
686
|
+
console.error(
|
|
687
|
+
"Usage: dex extension update <name> | dex extension update --all",
|
|
688
|
+
);
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const entry = registry.extensions[nameOrFlag];
|
|
693
|
+
if (!entry) {
|
|
694
|
+
console.error(`Extension "${nameOrFlag}" is not installed.`);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
if (entry.source !== "npm") {
|
|
698
|
+
console.error(
|
|
699
|
+
`Extension "${nameOrFlag}" is a file install. Run "npm install" in the source directory to update.`,
|
|
700
|
+
);
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
updateSingle(nameOrFlag, entry, registry, local, cwd);
|
|
705
|
+
saveRegistry(registryPath, registry);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function updateSingle(
|
|
709
|
+
name: string,
|
|
710
|
+
entry: RegistryEntry,
|
|
711
|
+
registry: Registry,
|
|
712
|
+
local: boolean,
|
|
713
|
+
cwd: string,
|
|
714
|
+
): void {
|
|
715
|
+
const oldVersion = entry.version ?? "unknown";
|
|
716
|
+
const extensionDir = getExtensionDir(name, local, cwd);
|
|
717
|
+
|
|
718
|
+
if (!existsSync(extensionDir)) {
|
|
719
|
+
console.error(
|
|
720
|
+
` Extension directory not found for "${name}". Re-install it.`,
|
|
721
|
+
);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
console.log(`Updating ${entry.package}...`);
|
|
726
|
+
|
|
727
|
+
// Ensure .npmrc is present so we resolve against the configured registry
|
|
728
|
+
ensureNpmrc(extensionDir);
|
|
729
|
+
|
|
730
|
+
const pm = getConfiguredPackageManager();
|
|
731
|
+
const cmds = getPackageManagerCommands(pm);
|
|
732
|
+
const updateCmd = cmds.update(extensionDir, entry.package);
|
|
733
|
+
const { success } = runPackageManager(updateCmd, extensionDir);
|
|
734
|
+
|
|
735
|
+
if (!success) {
|
|
736
|
+
console.error(` Failed to update ${name}`);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Read new version
|
|
741
|
+
const info = readInstalledPackageInfo(extensionDir, entry.package);
|
|
742
|
+
const newVersion = info?.version ?? "unknown";
|
|
743
|
+
|
|
744
|
+
registry.extensions[name] = {
|
|
745
|
+
...entry,
|
|
746
|
+
...(info?.version ? { version: info.version } : {}),
|
|
747
|
+
installedAt: new Date().toISOString(),
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
if (oldVersion !== newVersion) {
|
|
751
|
+
console.log(` ${name}: ${oldVersion} -> ${newVersion}`);
|
|
752
|
+
} else {
|
|
753
|
+
console.log(` ${name}: already at latest (${newVersion})`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* ------------------------------------------------------------------ */
|
|
758
|
+
/* List */
|
|
759
|
+
/* ------------------------------------------------------------------ */
|
|
760
|
+
|
|
761
|
+
function cmdList(): void {
|
|
762
|
+
const cwd = process.cwd();
|
|
763
|
+
const userRegistry = loadRegistry(USER_REGISTRY_PATH);
|
|
764
|
+
const projectRegistryPath = getProjectRegistryPath(cwd);
|
|
765
|
+
|
|
766
|
+
// Don't show project scope if it resolves to the same file as user scope
|
|
767
|
+
const isSamePath =
|
|
768
|
+
resolve(projectRegistryPath) === resolve(USER_REGISTRY_PATH);
|
|
769
|
+
const projectRegistry = isSamePath
|
|
770
|
+
? { version: 1 as const, extensions: {} }
|
|
771
|
+
: loadRegistry(projectRegistryPath);
|
|
772
|
+
|
|
773
|
+
const userEntries = Object.entries(userRegistry.extensions);
|
|
774
|
+
const projectEntries = Object.entries(projectRegistry.extensions);
|
|
775
|
+
|
|
776
|
+
if (userEntries.length === 0 && projectEntries.length === 0) {
|
|
777
|
+
console.log("No extensions installed.");
|
|
778
|
+
console.log("");
|
|
779
|
+
console.log("Install one with:");
|
|
780
|
+
console.log(" dex extension install npm:<package>");
|
|
781
|
+
console.log(" dex extension install file:<path>");
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
console.log("Installed extensions:");
|
|
786
|
+
console.log("");
|
|
787
|
+
|
|
788
|
+
if (userEntries.length > 0) {
|
|
789
|
+
console.log(" User:");
|
|
790
|
+
for (const [name, entry] of userEntries) {
|
|
791
|
+
// For file: sources, read live version from disk
|
|
792
|
+
let version = entry.version ?? "";
|
|
793
|
+
if (entry.source === "file" && entry.path) {
|
|
794
|
+
const livePkgPath = join(entry.path, "package.json");
|
|
795
|
+
if (existsSync(livePkgPath)) {
|
|
796
|
+
try {
|
|
797
|
+
const livePkg = JSON.parse(readFileSync(livePkgPath, "utf-8"));
|
|
798
|
+
if (livePkg.version) version = livePkg.version;
|
|
799
|
+
} catch {
|
|
800
|
+
/* use registry version */
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const versionStr = version ? `v${version}` : "";
|
|
805
|
+
const source =
|
|
806
|
+
entry.source === "npm"
|
|
807
|
+
? `npm:${entry.package}`
|
|
808
|
+
: `file:${entry.path ?? entry.package}`;
|
|
809
|
+
const type = entry.type === "pi-compat" ? " [pi-compat]" : "";
|
|
810
|
+
console.log(
|
|
811
|
+
` ${name.padEnd(24)} ${versionStr.padEnd(10)} ${source}${type}`,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (projectEntries.length > 0) {
|
|
817
|
+
if (userEntries.length > 0) console.log("");
|
|
818
|
+
console.log(` Project (${cwd}):`);
|
|
819
|
+
for (const [name, entry] of projectEntries) {
|
|
820
|
+
let version = entry.version ?? "";
|
|
821
|
+
if (entry.source === "file" && entry.path) {
|
|
822
|
+
const livePkgPath = join(entry.path, "package.json");
|
|
823
|
+
if (existsSync(livePkgPath)) {
|
|
824
|
+
try {
|
|
825
|
+
const livePkg = JSON.parse(readFileSync(livePkgPath, "utf-8"));
|
|
826
|
+
if (livePkg.version) version = livePkg.version;
|
|
827
|
+
} catch {
|
|
828
|
+
/* use registry version */
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const versionStr = version ? `v${version}` : "";
|
|
833
|
+
const source =
|
|
834
|
+
entry.source === "npm"
|
|
835
|
+
? `npm:${entry.package}`
|
|
836
|
+
: `file:${entry.path ?? entry.package}`;
|
|
837
|
+
const type = entry.type === "pi-compat" ? " [pi-compat]" : "";
|
|
838
|
+
console.log(
|
|
839
|
+
` ${name.padEnd(24)} ${versionStr.padEnd(10)} ${source}${type}`,
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/* ------------------------------------------------------------------ */
|
|
846
|
+
/* Help */
|
|
847
|
+
/* ------------------------------------------------------------------ */
|
|
848
|
+
|
|
849
|
+
function printHelp(): void {
|
|
850
|
+
console.log(`
|
|
851
|
+
dex extension — manage Dex extensions
|
|
852
|
+
|
|
853
|
+
Usage:
|
|
854
|
+
dex extension install <source> [-l] Install an extension
|
|
855
|
+
dex extension uninstall <name> [-l] Remove an extension
|
|
856
|
+
dex extension update <name> Update an npm extension
|
|
857
|
+
dex extension update --all Update all npm extensions
|
|
858
|
+
dex extension list List installed extensions
|
|
859
|
+
|
|
860
|
+
Sources:
|
|
861
|
+
npm:<package> Install from npm registry
|
|
862
|
+
file:<path> Install from local filesystem (directory with package.json)
|
|
863
|
+
|
|
864
|
+
Options:
|
|
865
|
+
-l, --local Operate on project scope (${process.cwd()}/.dex/extensions/)
|
|
866
|
+
Default is user scope (~/.dex/extensions/packages/)
|
|
867
|
+
|
|
868
|
+
Examples:
|
|
869
|
+
dex extension install npm:@dex-ai/memory
|
|
870
|
+
dex extension install npm:@dex-ai/devtools-extension
|
|
871
|
+
dex extension install npm:pi-lens
|
|
872
|
+
dex extension install file:./my-extension
|
|
873
|
+
dex extension install npm:@dex-ai/mcp-extension --local
|
|
874
|
+
dex extension uninstall memory-extension
|
|
875
|
+
dex extension update pi-lens
|
|
876
|
+
dex extension update --all
|
|
877
|
+
dex extension list
|
|
878
|
+
`);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/* ------------------------------------------------------------------ */
|
|
882
|
+
/* Main dispatch */
|
|
883
|
+
/* ------------------------------------------------------------------ */
|
|
884
|
+
|
|
885
|
+
export function runExtensionCommand(args: string[]): void {
|
|
886
|
+
const subcommand = args[0] ?? "list";
|
|
887
|
+
const rest = args.slice(1);
|
|
888
|
+
|
|
889
|
+
// Parse --local / -l flag from remaining args
|
|
890
|
+
const localIdx = rest.findIndex((a) => a === "--local" || a === "-l");
|
|
891
|
+
const local = localIdx !== -1;
|
|
892
|
+
const cleanArgs = local
|
|
893
|
+
? [...rest.slice(0, localIdx), ...rest.slice(localIdx + 1)]
|
|
894
|
+
: rest;
|
|
895
|
+
|
|
896
|
+
switch (subcommand) {
|
|
897
|
+
case "install":
|
|
898
|
+
case "add":
|
|
899
|
+
cmdInstall(cleanArgs[0] ?? "", local);
|
|
900
|
+
break;
|
|
901
|
+
case "uninstall":
|
|
902
|
+
case "remove":
|
|
903
|
+
case "rm":
|
|
904
|
+
cmdUninstall(cleanArgs[0] ?? "", local);
|
|
905
|
+
break;
|
|
906
|
+
case "update":
|
|
907
|
+
case "upgrade":
|
|
908
|
+
cmdUpdate(cleanArgs[0] ?? "", local);
|
|
909
|
+
break;
|
|
910
|
+
case "list":
|
|
911
|
+
case "ls":
|
|
912
|
+
cmdList();
|
|
913
|
+
break;
|
|
914
|
+
case "help":
|
|
915
|
+
case "--help":
|
|
916
|
+
case "-h":
|
|
917
|
+
printHelp();
|
|
918
|
+
break;
|
|
919
|
+
default:
|
|
920
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
921
|
+
printHelp();
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
}
|