zammy 1.3.0 → 1.3.2

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.
@@ -0,0 +1,1229 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/ui/colors.ts
4
+ import chalk from "chalk";
5
+ var palette = {
6
+ rose: "#FF6B6B",
7
+ coral: "#FF8E72",
8
+ peach: "#FFEAA7",
9
+ mint: "#96CEB4",
10
+ teal: "#4ECDC4",
11
+ sky: "#45B7D1",
12
+ lavender: "#DDA0DD",
13
+ purple: "#9B59B6",
14
+ gold: "#FFD700",
15
+ silver: "#C0C0C0"
16
+ };
17
+ var theme = {
18
+ primary: chalk.hex(palette.teal),
19
+ secondary: chalk.hex(palette.lavender),
20
+ success: chalk.hex("#2ECC71"),
21
+ warning: chalk.hex("#F39C12"),
22
+ error: chalk.hex("#E74C3C"),
23
+ dim: chalk.hex("#6C7A89"),
24
+ highlight: chalk.bold.white,
25
+ command: chalk.bold.hex(palette.teal),
26
+ prompt: chalk.bold.hex(palette.lavender),
27
+ accent: chalk.hex(palette.rose),
28
+ info: chalk.hex(palette.sky),
29
+ muted: chalk.dim.hex("#95A5A6"),
30
+ gold: chalk.hex(palette.gold),
31
+ rose: chalk.hex(palette.rose),
32
+ mint: chalk.hex(palette.mint),
33
+ peach: chalk.hex(palette.peach),
34
+ // Gradient text effects
35
+ gradient: (text) => {
36
+ const colors = [palette.rose, palette.coral, palette.peach, palette.mint, palette.teal];
37
+ return text.split("").map(
38
+ (char, i) => chalk.hex(colors[i % colors.length])(char)
39
+ ).join("");
40
+ },
41
+ rainbow: (text) => {
42
+ const colors = ["#FF6B6B", "#FF8E72", "#FFEAA7", "#96CEB4", "#4ECDC4", "#45B7D1", "#DDA0DD"];
43
+ return text.split("").map(
44
+ (char, i) => chalk.hex(colors[i % colors.length])(char)
45
+ ).join("");
46
+ },
47
+ ocean: (text) => {
48
+ const colors = ["#0077B6", "#00B4D8", "#48CAE4", "#90E0EF", "#CAF0F8"];
49
+ return text.split("").map(
50
+ (char, i) => chalk.hex(colors[i % colors.length])(char)
51
+ ).join("");
52
+ },
53
+ sunset: (text) => {
54
+ const colors = ["#FF6B6B", "#FF8E53", "#FFA07A", "#FFB347", "#FFD700"];
55
+ return text.split("").map(
56
+ (char, i) => chalk.hex(colors[i % colors.length])(char)
57
+ ).join("");
58
+ },
59
+ // Bold variants
60
+ b: {
61
+ primary: chalk.bold.hex(palette.teal),
62
+ secondary: chalk.bold.hex(palette.lavender),
63
+ success: chalk.bold.hex("#2ECC71"),
64
+ warning: chalk.bold.hex("#F39C12"),
65
+ error: chalk.bold.hex("#E74C3C")
66
+ }
67
+ };
68
+ var symbols = {
69
+ // Basic UI
70
+ arrow: "\u276F",
71
+ // ❯
72
+ check: "\u2714",
73
+ // ✔
74
+ cross: "\u2718",
75
+ // ✘
76
+ info: "\u2139",
77
+ // ℹ
78
+ warning: "\u26A0",
79
+ // ⚠
80
+ bullet: "\u2022",
81
+ // •
82
+ // Decorative
83
+ star: "\u2605",
84
+ // ★
85
+ heart: "\u2665",
86
+ // ♥
87
+ diamond: "\u2666",
88
+ // ♦
89
+ sparkle: "\u2728",
90
+ // ✨
91
+ lightning: "\u26A1",
92
+ // ⚡
93
+ // Emoji icons
94
+ fire: "\u{1F525}",
95
+ // 🔥
96
+ rocket: "\u{1F680}",
97
+ // 🚀
98
+ dice: "\u{1F3B2}",
99
+ // 🎲
100
+ coin: "\u{1FA99}",
101
+ // 🪙
102
+ lock: "\u{1F512}",
103
+ // 🔒
104
+ clock: "\u{1F552}",
105
+ // 🕒
106
+ chart: "\u{1F4CA}",
107
+ // 📊
108
+ note: "\u{1F4DD}",
109
+ // 📝
110
+ scroll: "\u{1F4DC}",
111
+ // 📜
112
+ clipboard: "\u{1F4CB}",
113
+ // 📋
114
+ palette: "\u{1F3A8}",
115
+ // 🎨
116
+ tomato: "\u{1F345}",
117
+ // 🍅
118
+ coffee: "\u2615",
119
+ // ☕
120
+ bell: "\u{1F514}",
121
+ // 🔔
122
+ gear: "\u2699",
123
+ // ⚙
124
+ folder: "\u{1F4C1}",
125
+ // 📁
126
+ terminal: "\u{1F4BB}",
127
+ // 💻
128
+ key: "\u{1F511}",
129
+ // 🔑
130
+ link: "\u{1F517}",
131
+ // 🔗
132
+ hourglass: "\u23F3"
133
+ // ⏳
134
+ };
135
+ var boxChars = {
136
+ rounded: { tl: "\u256D", tr: "\u256E", bl: "\u2570", br: "\u256F", h: "\u2500", v: "\u2502" },
137
+ sharp: { tl: "\u250C", tr: "\u2510", bl: "\u2514", br: "\u2518", h: "\u2500", v: "\u2502" },
138
+ double: { tl: "\u2554", tr: "\u2557", bl: "\u255A", br: "\u255D", h: "\u2550", v: "\u2551" },
139
+ heavy: { tl: "\u250F", tr: "\u2513", bl: "\u2517", br: "\u251B", h: "\u2501", v: "\u2503" }
140
+ };
141
+ var box = {
142
+ topLeft: "\u256D",
143
+ // ╭
144
+ topRight: "\u256E",
145
+ // ╮
146
+ bottomLeft: "\u2570",
147
+ // ╰
148
+ bottomRight: "\u256F",
149
+ // ╯
150
+ horizontal: "\u2500",
151
+ // ─
152
+ vertical: "\u2502",
153
+ // │
154
+ draw: (content, width = 50, style = "rounded") => {
155
+ const chars = boxChars[style];
156
+ const lines = [];
157
+ const innerWidth = width - 2;
158
+ lines.push(theme.dim(`${chars.tl}${chars.h.repeat(innerWidth)}${chars.tr}`));
159
+ content.forEach((line) => {
160
+ const stripped = line.replace(/\x1B\[[0-9;]*m/g, "");
161
+ const padding = innerWidth - stripped.length;
162
+ lines.push(theme.dim(chars.v) + line + " ".repeat(Math.max(0, padding)) + theme.dim(chars.v));
163
+ });
164
+ lines.push(theme.dim(`${chars.bl}${chars.h.repeat(innerWidth)}${chars.br}`));
165
+ return lines.join("\n");
166
+ },
167
+ // Simple title box
168
+ title: (title, width = 50) => {
169
+ const chars = boxChars.rounded;
170
+ const innerWidth = width - 2;
171
+ const titleLen = title.replace(/\x1B\[[0-9;]*m/g, "").length;
172
+ const leftPad = Math.floor((innerWidth - titleLen - 2) / 2);
173
+ const rightPad = innerWidth - titleLen - 2 - leftPad;
174
+ return theme.dim(`${chars.tl}${chars.h.repeat(leftPad)} `) + title + theme.dim(` ${chars.h.repeat(rightPad)}${chars.tr}`);
175
+ }
176
+ };
177
+ var progressBar = (percent, width = 30, showPercent = true) => {
178
+ const filled = Math.round(percent / 100 * width);
179
+ const empty = width - filled;
180
+ let color = theme.success;
181
+ if (percent > 70) color = theme.warning;
182
+ if (percent > 90) color = theme.error;
183
+ const bar = color("\u2588".repeat(filled)) + theme.dim("\u2591".repeat(empty));
184
+ return showPercent ? `${bar} ${percent.toFixed(0)}%` : bar;
185
+ };
186
+ var bubble = {
187
+ say: (text, width = 50) => {
188
+ const lines = [];
189
+ const innerWidth = width - 4;
190
+ const words = text.split(" ");
191
+ let currentLine = "";
192
+ const wrappedLines = [];
193
+ for (const word of words) {
194
+ if ((currentLine + " " + word).trim().length <= innerWidth) {
195
+ currentLine = (currentLine + " " + word).trim();
196
+ } else {
197
+ if (currentLine) wrappedLines.push(currentLine);
198
+ currentLine = word;
199
+ }
200
+ }
201
+ if (currentLine) wrappedLines.push(currentLine);
202
+ lines.push(theme.dim(" \u256D" + "\u2500".repeat(innerWidth + 2) + "\u256E"));
203
+ for (const line of wrappedLines) {
204
+ const padding = innerWidth - line.length;
205
+ lines.push(theme.dim(" \u2502 ") + line + " ".repeat(padding) + theme.dim(" \u2502"));
206
+ }
207
+ lines.push(theme.dim(" \u2570" + "\u2500".repeat(innerWidth + 2) + "\u256F"));
208
+ lines.push(theme.dim(" \u2572"));
209
+ lines.push(theme.dim(" \u2572"));
210
+ return lines.join("\n");
211
+ },
212
+ think: (text, width = 50) => {
213
+ const lines = [];
214
+ const innerWidth = width - 4;
215
+ lines.push(theme.dim(" \u256D" + "\u2500".repeat(innerWidth + 2) + "\u256E"));
216
+ const padding = innerWidth - text.length;
217
+ lines.push(theme.dim(" \u2502 ") + text + " ".repeat(Math.max(0, padding)) + theme.dim(" \u2502"));
218
+ lines.push(theme.dim(" \u2570" + "\u2500".repeat(innerWidth + 2) + "\u256F"));
219
+ lines.push(theme.dim(" \u25CB"));
220
+ lines.push(theme.dim(" \u25CB"));
221
+ return lines.join("\n");
222
+ }
223
+ };
224
+ var categoryIcons = {
225
+ "Utilities": "\u{1F527}",
226
+ "Fun": "\u{1F3AE}",
227
+ "Creative": "\u{1F3A8}",
228
+ "Dev": "\u{1F4BB}",
229
+ "Info": "\u{1F4E1}"
230
+ };
231
+ var spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
232
+
233
+ // src/commands/registry.ts
234
+ var commands = /* @__PURE__ */ new Map();
235
+ function registerCommand(command) {
236
+ commands.set(command.name, { ...command, source: "core" });
237
+ }
238
+ function registerPluginCommand(command, pluginName) {
239
+ commands.set(command.name, { ...command, source: "plugin", pluginName });
240
+ }
241
+ function unregisterPluginCommands(pluginName) {
242
+ for (const [name, cmd] of commands.entries()) {
243
+ if (cmd.source === "plugin" && cmd.pluginName === pluginName) {
244
+ commands.delete(name);
245
+ }
246
+ }
247
+ }
248
+ function getCommand(name) {
249
+ return commands.get(name);
250
+ }
251
+ function getAllCommands() {
252
+ return Array.from(commands.values());
253
+ }
254
+ function getPluginCommands() {
255
+ return Array.from(commands.values()).filter((cmd) => cmd.source === "plugin");
256
+ }
257
+ function checkCommandConflict(name) {
258
+ const existing = commands.get(name);
259
+ if (!existing) {
260
+ return { exists: false };
261
+ }
262
+ return {
263
+ exists: true,
264
+ source: existing.source,
265
+ pluginName: existing.pluginName
266
+ };
267
+ }
268
+
269
+ // src/plugins/storage.ts
270
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
271
+ import { join } from "path";
272
+ function createPluginStorage(pluginName, dataDir) {
273
+ const storagePath = join(dataDir, "data.json");
274
+ if (!existsSync(dataDir)) {
275
+ mkdirSync(dataDir, { recursive: true });
276
+ }
277
+ function loadData() {
278
+ try {
279
+ if (existsSync(storagePath)) {
280
+ const content = readFileSync(storagePath, "utf-8");
281
+ return JSON.parse(content);
282
+ }
283
+ } catch {
284
+ }
285
+ return {};
286
+ }
287
+ function saveData(data) {
288
+ try {
289
+ writeFileSync(storagePath, JSON.stringify(data, null, 2), "utf-8");
290
+ } catch (error) {
291
+ console.error(`Failed to save plugin storage for ${pluginName}:`, error);
292
+ }
293
+ }
294
+ return {
295
+ get(key) {
296
+ const data = loadData();
297
+ return data[key];
298
+ },
299
+ set(key, value) {
300
+ const data = loadData();
301
+ data[key] = value;
302
+ saveData(data);
303
+ },
304
+ delete(key) {
305
+ const data = loadData();
306
+ delete data[key];
307
+ saveData(data);
308
+ },
309
+ clear() {
310
+ saveData({});
311
+ },
312
+ getAll() {
313
+ return loadData();
314
+ }
315
+ };
316
+ }
317
+
318
+ // src/plugins/api.ts
319
+ import { execSync, spawn as nodeSpawn } from "child_process";
320
+ import { join as join2 } from "path";
321
+ import { readFileSync as readFileSync2 } from "fs";
322
+ import { fileURLToPath } from "url";
323
+ import { dirname as dirname2 } from "path";
324
+ function getZammyVersion() {
325
+ try {
326
+ const __filename = fileURLToPath(import.meta.url);
327
+ const __dirname = dirname2(__filename);
328
+ const pkgPath = join2(__dirname, "..", "..", "package.json");
329
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
330
+ return pkg.version || "0.0.0";
331
+ } catch {
332
+ return "0.0.0";
333
+ }
334
+ }
335
+ function createPluginUI() {
336
+ return {
337
+ theme: {
338
+ primary: theme.primary,
339
+ secondary: theme.secondary,
340
+ accent: theme.accent,
341
+ success: theme.success,
342
+ warning: theme.warning,
343
+ error: theme.error,
344
+ info: theme.info,
345
+ dim: theme.dim,
346
+ gradient: theme.gradient
347
+ },
348
+ symbols: {
349
+ check: symbols.check,
350
+ cross: symbols.cross,
351
+ star: symbols.star,
352
+ arrow: symbols.arrow,
353
+ bullet: symbols.bullet,
354
+ folder: symbols.folder,
355
+ file: "\u{1F4C4}",
356
+ // 📄
357
+ warning: symbols.warning,
358
+ info: symbols.info,
359
+ rocket: symbols.rocket,
360
+ sparkles: symbols.sparkle
361
+ },
362
+ box: (content, options) => {
363
+ const lines = content.split("\n");
364
+ const width = Math.max(...lines.map((l) => l.replace(/\x1B\[[0-9;]*m/g, "").length)) + 4;
365
+ return box.draw(lines, width);
366
+ },
367
+ progressBar: (current, total, width) => {
368
+ const percent = Math.round(current / total * 100);
369
+ return progressBar(percent, width || 30);
370
+ }
371
+ };
372
+ }
373
+ function createPluginLogger(pluginName) {
374
+ const prefix = theme.dim(`[${pluginName}]`);
375
+ return {
376
+ info: (message) => console.log(`${prefix} ${theme.info(message)}`),
377
+ warn: (message) => console.log(`${prefix} ${theme.warning(message)}`),
378
+ error: (message) => console.log(`${prefix} ${theme.error(message)}`),
379
+ debug: (message) => {
380
+ if (process.env.ZAMMY_DEBUG) {
381
+ console.log(`${prefix} ${theme.dim(message)}`);
382
+ }
383
+ }
384
+ };
385
+ }
386
+ function createPluginShell(manifest) {
387
+ if (!manifest.permissions?.shell) {
388
+ return void 0;
389
+ }
390
+ return {
391
+ exec: (command, options) => {
392
+ try {
393
+ return execSync(command, {
394
+ encoding: "utf-8",
395
+ timeout: options?.timeout || 3e4,
396
+ stdio: ["pipe", "pipe", "pipe"]
397
+ });
398
+ } catch (error) {
399
+ if (error && typeof error === "object" && "stdout" in error) {
400
+ return error.stdout || "";
401
+ }
402
+ throw error;
403
+ }
404
+ },
405
+ spawn: (command, args) => {
406
+ return new Promise((resolve2) => {
407
+ const proc = nodeSpawn(command, args || [], {
408
+ shell: true,
409
+ stdio: ["pipe", "pipe", "pipe"]
410
+ });
411
+ let stdout = "";
412
+ let stderr = "";
413
+ proc.stdout?.on("data", (data) => {
414
+ stdout += data.toString();
415
+ });
416
+ proc.stderr?.on("data", (data) => {
417
+ stderr += data.toString();
418
+ });
419
+ proc.on("close", (code) => {
420
+ resolve2({ stdout, stderr, code: code || 0 });
421
+ });
422
+ proc.on("error", () => {
423
+ resolve2({ stdout, stderr, code: 1 });
424
+ });
425
+ });
426
+ }
427
+ };
428
+ }
429
+ function createPluginAPI(manifest, pluginPath) {
430
+ const dataDir = pluginPath;
431
+ const context = {
432
+ pluginName: manifest.name,
433
+ pluginVersion: manifest.version,
434
+ zammyVersion: getZammyVersion(),
435
+ dataDir,
436
+ cwd: process.cwd()
437
+ };
438
+ return {
439
+ registerCommand: (command) => {
440
+ registerPluginCommand(command, manifest.name);
441
+ },
442
+ registerCommands: (commands2) => {
443
+ for (const command of commands2) {
444
+ registerPluginCommand(command, manifest.name);
445
+ }
446
+ },
447
+ ui: createPluginUI(),
448
+ storage: createPluginStorage(manifest.name, dataDir),
449
+ log: createPluginLogger(manifest.name),
450
+ context,
451
+ shell: createPluginShell(manifest)
452
+ };
453
+ }
454
+
455
+ // src/plugins/loader.ts
456
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, mkdirSync as mkdirSync2 } from "fs";
457
+ import { join as join4, normalize, isAbsolute } from "path";
458
+ import { homedir } from "os";
459
+ import { pathToFileURL } from "url";
460
+
461
+ // src/utils/version.ts
462
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
463
+ import { join as join3, dirname as dirname3 } from "path";
464
+ import { fileURLToPath as fileURLToPath2 } from "url";
465
+ var cachedVersion = null;
466
+ function getZammyVersion2() {
467
+ if (cachedVersion) {
468
+ return cachedVersion;
469
+ }
470
+ try {
471
+ const __filename = fileURLToPath2(import.meta.url);
472
+ const __dirname = dirname3(__filename);
473
+ const possiblePaths = [
474
+ join3(__dirname, "package.json"),
475
+ // Same dir
476
+ join3(__dirname, "..", "package.json"),
477
+ // One up (dist/)
478
+ join3(__dirname, "..", "..", "package.json")
479
+ // Two up (src/utils/)
480
+ ];
481
+ for (const pkgPath of possiblePaths) {
482
+ if (existsSync2(pkgPath)) {
483
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
484
+ if (pkg.name === "zammy" && pkg.version) {
485
+ cachedVersion = pkg.version;
486
+ return pkg.version;
487
+ }
488
+ }
489
+ }
490
+ return "0.0.0";
491
+ } catch {
492
+ return "0.0.0";
493
+ }
494
+ }
495
+ function compareVersions(a, b) {
496
+ const partsA = a.split(".").map((n) => parseInt(n, 10) || 0);
497
+ const partsB = b.split(".").map((n) => parseInt(n, 10) || 0);
498
+ for (let i = 0; i < 3; i++) {
499
+ const numA = partsA[i] || 0;
500
+ const numB = partsB[i] || 0;
501
+ if (numA < numB) return -1;
502
+ if (numA > numB) return 1;
503
+ }
504
+ return 0;
505
+ }
506
+
507
+ // src/plugins/loader.ts
508
+ var PLUGINS_DIR = join4(homedir(), ".zammy", "plugins");
509
+ function isValidEntryPoint(basePath, entryPoint) {
510
+ const normalized = normalize(entryPoint);
511
+ if (isAbsolute(normalized)) {
512
+ return false;
513
+ }
514
+ if (normalized.includes("..")) {
515
+ return false;
516
+ }
517
+ return true;
518
+ }
519
+ function checkVersionCompatibility(manifest, zammyVersion) {
520
+ const minVersion = manifest.zammy?.minVersion;
521
+ const maxVersion = manifest.zammy?.maxVersion;
522
+ if (minVersion && compareVersions(zammyVersion, minVersion) < 0) {
523
+ return {
524
+ compatible: false,
525
+ reason: `Requires Zammy v${minVersion}+, but you have v${zammyVersion}`
526
+ };
527
+ }
528
+ if (maxVersion && compareVersions(zammyVersion, maxVersion) > 0) {
529
+ return {
530
+ compatible: false,
531
+ reason: `Incompatible with Zammy v${zammyVersion} (max: v${maxVersion})`
532
+ };
533
+ }
534
+ return { compatible: true };
535
+ }
536
+ var discoveredPlugins = /* @__PURE__ */ new Map();
537
+ var loadedPlugins = /* @__PURE__ */ new Map();
538
+ function ensurePluginsDir() {
539
+ if (!existsSync3(PLUGINS_DIR)) {
540
+ mkdirSync2(PLUGINS_DIR, { recursive: true });
541
+ }
542
+ }
543
+ function getPluginsDir() {
544
+ return PLUGINS_DIR;
545
+ }
546
+ async function discoverPlugins() {
547
+ ensurePluginsDir();
548
+ discoveredPlugins.clear();
549
+ if (!existsSync3(PLUGINS_DIR)) {
550
+ return [];
551
+ }
552
+ const zammyVersion = getZammyVersion2();
553
+ const entries = readdirSync(PLUGINS_DIR, { withFileTypes: true });
554
+ const manifests = [];
555
+ for (const entry of entries) {
556
+ if (!entry.isDirectory()) continue;
557
+ const pluginPath = join4(PLUGINS_DIR, entry.name);
558
+ const manifestPath = join4(pluginPath, "zammy-plugin.json");
559
+ if (!existsSync3(manifestPath)) continue;
560
+ try {
561
+ const manifestContent = readFileSync4(manifestPath, "utf-8");
562
+ const manifest = JSON.parse(manifestContent);
563
+ if (!manifest.name || !manifest.version || !manifest.main || !manifest.commands) {
564
+ console.log(theme.warning(` ${symbols.warning} Invalid manifest for plugin in ${entry.name}`));
565
+ continue;
566
+ }
567
+ const compatibility = checkVersionCompatibility(manifest, zammyVersion);
568
+ if (!compatibility.compatible) {
569
+ console.log(theme.warning(` ${symbols.warning} Plugin '${manifest.name}' skipped: ${compatibility.reason}`));
570
+ continue;
571
+ }
572
+ discoveredPlugins.set(manifest.name, { manifest, path: pluginPath });
573
+ manifests.push(manifest);
574
+ } catch (error) {
575
+ console.log(theme.warning(` ${symbols.warning} Failed to read manifest for ${entry.name}`));
576
+ }
577
+ }
578
+ return manifests;
579
+ }
580
+ async function loadPlugin(name) {
581
+ if (loadedPlugins.has(name)) {
582
+ const existing = loadedPlugins.get(name);
583
+ if (existing.state === "error") {
584
+ return null;
585
+ }
586
+ return existing;
587
+ }
588
+ const discovered = discoveredPlugins.get(name);
589
+ if (!discovered) {
590
+ return null;
591
+ }
592
+ const { manifest, path: pluginPath } = discovered;
593
+ try {
594
+ if (!isValidEntryPoint(pluginPath, manifest.main)) {
595
+ throw new Error("Invalid plugin entry point: path traversal not allowed");
596
+ }
597
+ const mainPath = join4(pluginPath, manifest.main);
598
+ if (!existsSync3(mainPath)) {
599
+ throw new Error(`Plugin entry point not found: ${mainPath}`);
600
+ }
601
+ const moduleUrl = pathToFileURL(mainPath).href;
602
+ const module = await import(moduleUrl);
603
+ const plugin = module.default;
604
+ if (!plugin || typeof plugin.activate !== "function") {
605
+ throw new Error("Plugin must export a default object with an activate function");
606
+ }
607
+ const api = createPluginAPI(manifest, pluginPath);
608
+ try {
609
+ await plugin.activate(api);
610
+ } catch (activationError) {
611
+ const msg = activationError instanceof Error ? activationError.message : String(activationError);
612
+ throw new Error(`Plugin activation failed: ${msg}`);
613
+ }
614
+ const loaded = {
615
+ manifest,
616
+ instance: plugin,
617
+ path: pluginPath,
618
+ state: "active"
619
+ };
620
+ loadedPlugins.set(name, loaded);
621
+ return loaded;
622
+ } catch (error) {
623
+ const errorMessage = error instanceof Error ? error.message : String(error);
624
+ console.log(theme.error(` ${symbols.cross} Failed to load plugin '${name}': ${errorMessage}`));
625
+ const failedPlugin = {
626
+ manifest,
627
+ instance: { activate: async () => {
628
+ } },
629
+ path: pluginPath,
630
+ state: "error"
631
+ };
632
+ loadedPlugins.set(name, failedPlugin);
633
+ return null;
634
+ }
635
+ }
636
+ async function unloadPlugin(name) {
637
+ const loaded = loadedPlugins.get(name);
638
+ if (!loaded) {
639
+ return false;
640
+ }
641
+ try {
642
+ if (loaded.instance.deactivate) {
643
+ await loaded.instance.deactivate();
644
+ }
645
+ unregisterPluginCommands(name);
646
+ loadedPlugins.delete(name);
647
+ return true;
648
+ } catch (error) {
649
+ const errorMessage = error instanceof Error ? error.message : String(error);
650
+ console.log(theme.error(` ${symbols.cross} Failed to unload plugin '${name}': ${errorMessage}`));
651
+ return false;
652
+ }
653
+ }
654
+ function getDiscoveredPlugins() {
655
+ return Array.from(discoveredPlugins.values()).map((p) => p.manifest);
656
+ }
657
+ function getLoadedPlugins() {
658
+ return Array.from(loadedPlugins.values());
659
+ }
660
+ function isPluginLoaded(name) {
661
+ return loadedPlugins.has(name);
662
+ }
663
+ function getPluginPath(name) {
664
+ return discoveredPlugins.get(name)?.path;
665
+ }
666
+ async function initPluginLoader() {
667
+ const manifests = await discoverPlugins();
668
+ for (const manifest of manifests) {
669
+ for (const cmdName of manifest.commands) {
670
+ const lazyExecute = async (args) => {
671
+ const loaded = await loadPlugin(manifest.name);
672
+ if (!loaded) {
673
+ console.log(theme.error(` ${symbols.cross} Failed to load plugin '${manifest.name}'`));
674
+ console.log(theme.dim(` Try reinstalling: /plugin remove ${manifest.name} && /plugin install ${manifest.name}`));
675
+ return;
676
+ }
677
+ const realCommand = getCommand(cmdName);
678
+ if (realCommand && realCommand.execute !== lazyExecute) {
679
+ await realCommand.execute(args);
680
+ } else {
681
+ console.log(theme.error(` ${symbols.cross} Plugin '${manifest.name}' did not register command '${cmdName}'`));
682
+ console.log(theme.dim(` The plugin may be misconfigured or corrupted.`));
683
+ }
684
+ };
685
+ registerPluginCommand(
686
+ {
687
+ name: cmdName,
688
+ description: `[${manifest.displayName || manifest.name}] Plugin command`,
689
+ usage: `/${cmdName}`,
690
+ execute: lazyExecute
691
+ },
692
+ manifest.name
693
+ );
694
+ }
695
+ }
696
+ }
697
+
698
+ // src/plugins/installer.ts
699
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, cpSync, rmSync, readFileSync as readFileSync5, readdirSync as readdirSync2, createReadStream, createWriteStream, lstatSync } from "fs";
700
+ import { join as join5, resolve, isAbsolute as isAbsolute2, normalize as normalize2 } from "path";
701
+ import { execFileSync, spawnSync } from "child_process";
702
+ import { tmpdir, platform } from "os";
703
+ import { createGunzip } from "zlib";
704
+ import { pipeline } from "stream/promises";
705
+
706
+ // src/utils/security.ts
707
+ function isValidPluginName(name) {
708
+ if (!name || typeof name !== "string") {
709
+ return false;
710
+ }
711
+ if (name.includes("..") || name.includes("/") || name.includes("\\")) {
712
+ if (name.startsWith("@") && name.split("/").length === 2) {
713
+ const [scope, pkg] = name.split("/");
714
+ return isValidScopedName(scope, pkg);
715
+ }
716
+ return false;
717
+ }
718
+ return /^[a-zA-Z0-9_-]+$/.test(name);
719
+ }
720
+ function isValidScopedName(scope, pkg) {
721
+ return /^@[a-z0-9-~][a-z0-9-._~]*$/.test(scope) && /^[a-z0-9-~][a-z0-9-._~]*$/.test(pkg);
722
+ }
723
+ function isValidNpmPackageName(name) {
724
+ if (!name || typeof name !== "string") {
725
+ return false;
726
+ }
727
+ if (name.startsWith("@")) {
728
+ return /^@[a-z0-9-~][a-z0-9-._~]*\/[a-z0-9-~][a-z0-9-._~]*$/.test(name);
729
+ }
730
+ return /^[a-z0-9-~][a-z0-9-._~]*$/.test(name);
731
+ }
732
+ function isValidGitHubRepo(repo) {
733
+ if (!repo || typeof repo !== "string") {
734
+ return false;
735
+ }
736
+ const path = repo.replace(/^github:/, "");
737
+ const [repoPath] = path.split("#");
738
+ return /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+$/.test(repoPath);
739
+ }
740
+ function isValidBranchName(branch) {
741
+ if (!branch || typeof branch !== "string") {
742
+ return false;
743
+ }
744
+ if (/[\s~^:?*\[\]\\]/.test(branch) || branch.includes("..")) {
745
+ return false;
746
+ }
747
+ return /^[a-zA-Z0-9/_.-]+$/.test(branch);
748
+ }
749
+ function isValidGitUrl(url) {
750
+ if (!url || typeof url !== "string") {
751
+ return false;
752
+ }
753
+ if (url.startsWith("https://") && url.endsWith(".git")) {
754
+ return true;
755
+ }
756
+ if (url.startsWith("git@") && url.includes(":") && url.endsWith(".git")) {
757
+ return true;
758
+ }
759
+ if (url.startsWith("git://") && url.endsWith(".git")) {
760
+ return true;
761
+ }
762
+ return false;
763
+ }
764
+
765
+ // src/plugins/installer.ts
766
+ var isWindows = platform() === "win32";
767
+ function validatePluginPath(basePath, targetPath) {
768
+ const normalizedTarget = normalize2(targetPath);
769
+ if (isAbsolute2(normalizedTarget)) {
770
+ return false;
771
+ }
772
+ if (normalizedTarget.includes("..")) {
773
+ return false;
774
+ }
775
+ if (targetPath.includes("\\") && !isWindows) {
776
+ return false;
777
+ }
778
+ return true;
779
+ }
780
+ function containsSymlinks(dirPath) {
781
+ try {
782
+ const entries = readdirSync2(dirPath, { withFileTypes: true });
783
+ for (const entry of entries) {
784
+ const fullPath = join5(dirPath, entry.name);
785
+ const stat = lstatSync(fullPath);
786
+ if (stat.isSymbolicLink()) {
787
+ return true;
788
+ }
789
+ if (stat.isDirectory()) {
790
+ if (containsSymlinks(fullPath)) {
791
+ return true;
792
+ }
793
+ }
794
+ }
795
+ return false;
796
+ } catch {
797
+ return false;
798
+ }
799
+ }
800
+ function checkVersionCompatibility2(manifest) {
801
+ const zammyVersion = getZammyVersion2();
802
+ const minVersion = manifest.zammy?.minVersion;
803
+ const maxVersion = manifest.zammy?.maxVersion;
804
+ if (minVersion && compareVersions(zammyVersion, minVersion) < 0) {
805
+ return {
806
+ compatible: false,
807
+ reason: `Requires Zammy v${minVersion}+, but you have v${zammyVersion}`
808
+ };
809
+ }
810
+ if (maxVersion && compareVersions(zammyVersion, maxVersion) > 0) {
811
+ return {
812
+ compatible: false,
813
+ reason: `Incompatible with Zammy v${zammyVersion} (max supported: v${maxVersion})`
814
+ };
815
+ }
816
+ return { compatible: true };
817
+ }
818
+ async function extractTarGz(tarGzPath, destDir) {
819
+ const tarGzForTar = isWindows ? tarGzPath.replace(/\\/g, "/") : tarGzPath;
820
+ const destForTar = isWindows ? destDir.replace(/\\/g, "/") : destDir;
821
+ try {
822
+ let result;
823
+ if (isWindows) {
824
+ result = spawnSync(`tar --force-local -xzf "${tarGzForTar}" -C "${destForTar}"`, {
825
+ encoding: "utf-8",
826
+ stdio: ["pipe", "pipe", "pipe"],
827
+ shell: true
828
+ });
829
+ } else {
830
+ result = spawnSync("tar", ["-xzf", tarGzPath, "-C", destDir], {
831
+ encoding: "utf-8",
832
+ stdio: ["pipe", "pipe", "pipe"]
833
+ });
834
+ }
835
+ if (result.status === 0) return;
836
+ } catch {
837
+ }
838
+ const tarPath = tarGzPath.replace(/\.tgz$|\.tar\.gz$/, ".tar");
839
+ const tarForTar = isWindows ? tarPath.replace(/\\/g, "/") : tarPath;
840
+ try {
841
+ await pipeline(
842
+ createReadStream(tarGzPath),
843
+ createGunzip(),
844
+ createWriteStream(tarPath)
845
+ );
846
+ let result;
847
+ if (isWindows) {
848
+ result = spawnSync(`tar --force-local -xf "${tarForTar}" -C "${destForTar}"`, {
849
+ encoding: "utf-8",
850
+ stdio: ["pipe", "pipe", "pipe"],
851
+ shell: true
852
+ });
853
+ } else {
854
+ result = spawnSync("tar", ["-xf", tarPath, "-C", destDir], {
855
+ encoding: "utf-8",
856
+ stdio: ["pipe", "pipe", "pipe"]
857
+ });
858
+ }
859
+ if (result.status === 0) {
860
+ rmSync(tarPath, { force: true });
861
+ return;
862
+ }
863
+ rmSync(tarPath, { force: true });
864
+ } catch (err) {
865
+ try {
866
+ rmSync(tarPath, { force: true });
867
+ } catch {
868
+ }
869
+ }
870
+ throw new Error(
871
+ "Unable to extract plugin archive. Please ensure tar is available.\n" + (isWindows ? "On Windows 10+, tar should be built-in. Try running: tar --version" : "Install tar and try again.")
872
+ );
873
+ }
874
+ function validateManifest(manifest) {
875
+ const errors = [];
876
+ if (!manifest || typeof manifest !== "object") {
877
+ return { valid: false, errors: ["Manifest must be an object"] };
878
+ }
879
+ const m = manifest;
880
+ if (!m.name || typeof m.name !== "string") {
881
+ errors.push('Missing or invalid "name" field');
882
+ } else if (!isValidPluginName(m.name)) {
883
+ errors.push("Invalid plugin name: must contain only alphanumeric, dash, underscore");
884
+ }
885
+ if (!m.version || typeof m.version !== "string") {
886
+ errors.push('Missing or invalid "version" field');
887
+ }
888
+ if (!m.main || typeof m.main !== "string") {
889
+ errors.push('Missing or invalid "main" field');
890
+ } else {
891
+ const mainPath = m.main;
892
+ if (!validatePluginPath(".", mainPath)) {
893
+ errors.push('Invalid "main" field: path traversal not allowed');
894
+ }
895
+ }
896
+ if (!m.commands || !Array.isArray(m.commands) || m.commands.length === 0) {
897
+ errors.push('Missing or invalid "commands" field (must be non-empty array)');
898
+ }
899
+ if (!m.zammy || typeof m.zammy !== "object") {
900
+ errors.push('Missing or invalid "zammy" field');
901
+ } else {
902
+ const zammy = m.zammy;
903
+ if (!zammy.minVersion || typeof zammy.minVersion !== "string") {
904
+ errors.push('Missing or invalid "zammy.minVersion" field');
905
+ }
906
+ }
907
+ return { valid: errors.length === 0, errors };
908
+ }
909
+ function checkConflicts(manifest) {
910
+ const conflicts = [];
911
+ for (const cmd of manifest.commands) {
912
+ const conflict = checkCommandConflict(cmd);
913
+ if (conflict.exists) {
914
+ if (conflict.source === "core") {
915
+ conflicts.push(`Command '/${cmd}' conflicts with core zammy command`);
916
+ } else {
917
+ conflicts.push(`Command '/${cmd}' conflicts with plugin '${conflict.pluginName}'`);
918
+ }
919
+ }
920
+ }
921
+ return { hasConflicts: conflicts.length > 0, conflicts };
922
+ }
923
+ function formatPermissions(manifest) {
924
+ const perms = [];
925
+ const p = manifest.permissions;
926
+ if (!p) return perms;
927
+ if (p.shell) {
928
+ perms.push(`${symbols.warning} shell: Can run system commands`);
929
+ }
930
+ if (p.filesystem) {
931
+ if (p.filesystem === true) {
932
+ perms.push(`${symbols.warning} filesystem: Full file system access`);
933
+ } else if (Array.isArray(p.filesystem)) {
934
+ perms.push(`${symbols.info} filesystem: Access to ${p.filesystem.join(", ")}`);
935
+ }
936
+ }
937
+ if (p.network) {
938
+ if (p.network === true) {
939
+ perms.push(`${symbols.warning} network: Full network access`);
940
+ } else if (Array.isArray(p.network)) {
941
+ perms.push(`${symbols.info} network: Access to ${p.network.join(", ")}`);
942
+ }
943
+ }
944
+ return perms;
945
+ }
946
+ async function installFromLocal(sourcePath) {
947
+ try {
948
+ const absPath = resolve(sourcePath);
949
+ if (!existsSync4(absPath)) {
950
+ return { success: false, error: `Path not found: ${absPath}` };
951
+ }
952
+ const manifestPath = join5(absPath, "zammy-plugin.json");
953
+ if (!existsSync4(manifestPath)) {
954
+ return { success: false, error: "No zammy-plugin.json found in source directory" };
955
+ }
956
+ const manifestContent = readFileSync5(manifestPath, "utf-8");
957
+ let manifest;
958
+ try {
959
+ manifest = JSON.parse(manifestContent);
960
+ } catch {
961
+ return { success: false, error: "Invalid JSON in zammy-plugin.json" };
962
+ }
963
+ const validation = validateManifest(manifest);
964
+ if (!validation.valid) {
965
+ return { success: false, error: `Invalid manifest: ${validation.errors.join(", ")}` };
966
+ }
967
+ const versionCheck = checkVersionCompatibility2(manifest);
968
+ if (!versionCheck.compatible) {
969
+ return { success: false, error: versionCheck.reason };
970
+ }
971
+ if (!validatePluginPath(absPath, manifest.main)) {
972
+ return { success: false, error: "Invalid entry point path: path traversal not allowed" };
973
+ }
974
+ const mainPath = join5(absPath, manifest.main);
975
+ if (!existsSync4(mainPath)) {
976
+ return { success: false, error: `Entry point not found: ${manifest.main}` };
977
+ }
978
+ if (containsSymlinks(absPath)) {
979
+ return { success: false, error: "Plugin contains symbolic links, which are not allowed for security reasons" };
980
+ }
981
+ ensurePluginsDir();
982
+ const targetDir = join5(getPluginsDir(), manifest.name);
983
+ if (existsSync4(targetDir)) {
984
+ rmSync(targetDir, { recursive: true, force: true });
985
+ }
986
+ cpSync(absPath, targetDir, { recursive: true });
987
+ return { success: true, manifest };
988
+ } catch (error) {
989
+ const message = error instanceof Error ? error.message : String(error);
990
+ return { success: false, error: message };
991
+ }
992
+ }
993
+ async function installFromNpm(packageName) {
994
+ if (!isValidNpmPackageName(packageName)) {
995
+ return { success: false, error: "Invalid npm package name" };
996
+ }
997
+ const tempDir = join5(tmpdir(), `zammy-plugin-${Date.now()}`);
998
+ try {
999
+ mkdirSync3(tempDir, { recursive: true });
1000
+ console.log(theme.dim(` Downloading ${packageName}...`));
1001
+ const packResult = spawnSync(`npm pack ${packageName} --pack-destination="${tempDir}"`, {
1002
+ encoding: "utf-8",
1003
+ stdio: ["pipe", "pipe", "pipe"],
1004
+ timeout: 6e4,
1005
+ shell: true
1006
+ });
1007
+ if (packResult.status !== 0) {
1008
+ throw new Error(packResult.stderr || "npm pack failed");
1009
+ }
1010
+ const files = readdirSync2(tempDir);
1011
+ const tarball = files.find((f) => f.endsWith(".tgz"));
1012
+ if (!tarball) {
1013
+ return { success: false, error: "Failed to download package" };
1014
+ }
1015
+ const extractDir = join5(tempDir, "extract");
1016
+ mkdirSync3(extractDir);
1017
+ await extractTarGz(join5(tempDir, tarball), extractDir);
1018
+ const packageDir = join5(extractDir, "package");
1019
+ if (!existsSync4(packageDir)) {
1020
+ return { success: false, error: "Invalid package structure" };
1021
+ }
1022
+ return await installFromLocal(packageDir);
1023
+ } catch (error) {
1024
+ const message = error instanceof Error ? error.message : String(error);
1025
+ return { success: false, error: `npm install failed: ${message}` };
1026
+ } finally {
1027
+ try {
1028
+ rmSync(tempDir, { recursive: true, force: true });
1029
+ } catch {
1030
+ }
1031
+ }
1032
+ }
1033
+ async function installFromGithub(repo) {
1034
+ let repoPath = repo.replace(/^github:/, "");
1035
+ let branch = "main";
1036
+ if (repoPath.includes("#")) {
1037
+ const parts = repoPath.split("#");
1038
+ repoPath = parts[0];
1039
+ branch = parts[1] || "main";
1040
+ }
1041
+ if (!isValidGitHubRepo(repoPath)) {
1042
+ return { success: false, error: "Invalid GitHub repository format. Use: user/repo or user/repo#branch" };
1043
+ }
1044
+ if (!isValidBranchName(branch)) {
1045
+ return { success: false, error: "Invalid branch name" };
1046
+ }
1047
+ const tempDir = join5(tmpdir(), `zammy-plugin-${Date.now()}`);
1048
+ try {
1049
+ mkdirSync3(tempDir, { recursive: true });
1050
+ console.log(theme.dim(` Cloning ${repoPath}...`));
1051
+ execFileSync("git", ["clone", "--depth", "1", "--branch", branch, `https://github.com/${repoPath}.git`, tempDir], {
1052
+ encoding: "utf-8",
1053
+ stdio: ["pipe", "pipe", "pipe"],
1054
+ timeout: 12e4
1055
+ });
1056
+ const pkgJsonPath = join5(tempDir, "package.json");
1057
+ if (existsSync4(pkgJsonPath)) {
1058
+ try {
1059
+ const pkgJson = JSON.parse(readFileSync5(pkgJsonPath, "utf-8"));
1060
+ if (pkgJson.scripts?.build) {
1061
+ console.log(theme.warning(` ${symbols.warning} This plugin requires building.`));
1062
+ console.log(theme.warning(` ${symbols.warning} npm install and npm run build will execute scripts from the repository.`));
1063
+ console.log(theme.dim(` Installing dependencies...`));
1064
+ const installResult = spawnSync("npm install --ignore-scripts", {
1065
+ cwd: tempDir,
1066
+ encoding: "utf-8",
1067
+ stdio: ["pipe", "pipe", "pipe"],
1068
+ timeout: 12e4,
1069
+ shell: true
1070
+ });
1071
+ if (installResult.status !== 0) {
1072
+ throw new Error(installResult.stderr || "npm install failed");
1073
+ }
1074
+ console.log(theme.dim(` Building...`));
1075
+ const buildResult = spawnSync("npm run build", {
1076
+ cwd: tempDir,
1077
+ encoding: "utf-8",
1078
+ stdio: ["pipe", "pipe", "pipe"],
1079
+ timeout: 12e4,
1080
+ shell: true
1081
+ });
1082
+ if (buildResult.status !== 0) {
1083
+ throw new Error(buildResult.stderr || "npm run build failed");
1084
+ }
1085
+ }
1086
+ } catch (buildError) {
1087
+ const msg = buildError instanceof Error ? buildError.message : String(buildError);
1088
+ return { success: false, error: `Build failed: ${msg}` };
1089
+ }
1090
+ }
1091
+ return await installFromLocal(tempDir);
1092
+ } catch (error) {
1093
+ const message = error instanceof Error ? error.message : String(error);
1094
+ return { success: false, error: `GitHub install failed: ${message}` };
1095
+ } finally {
1096
+ try {
1097
+ rmSync(tempDir, { recursive: true, force: true });
1098
+ } catch {
1099
+ }
1100
+ }
1101
+ }
1102
+ async function installFromGit(url) {
1103
+ if (!isValidGitUrl(url)) {
1104
+ return { success: false, error: "Invalid git URL. Must be https://*.git, git@*:*.git, or git://*.git" };
1105
+ }
1106
+ const tempDir = join5(tmpdir(), `zammy-plugin-${Date.now()}`);
1107
+ try {
1108
+ mkdirSync3(tempDir, { recursive: true });
1109
+ console.log(theme.dim(` Cloning from ${url}...`));
1110
+ execFileSync("git", ["clone", "--depth", "1", url, tempDir], {
1111
+ encoding: "utf-8",
1112
+ stdio: ["pipe", "pipe", "pipe"],
1113
+ timeout: 12e4
1114
+ });
1115
+ const pkgJsonPath = join5(tempDir, "package.json");
1116
+ if (existsSync4(pkgJsonPath)) {
1117
+ try {
1118
+ const pkgJson = JSON.parse(readFileSync5(pkgJsonPath, "utf-8"));
1119
+ if (pkgJson.scripts?.build) {
1120
+ console.log(theme.warning(` ${symbols.warning} This plugin requires building.`));
1121
+ console.log(theme.dim(` Installing dependencies...`));
1122
+ const installResult = spawnSync("npm install --ignore-scripts", {
1123
+ cwd: tempDir,
1124
+ encoding: "utf-8",
1125
+ stdio: ["pipe", "pipe", "pipe"],
1126
+ timeout: 12e4,
1127
+ shell: true
1128
+ });
1129
+ if (installResult.status !== 0) {
1130
+ throw new Error(installResult.stderr || "npm install failed");
1131
+ }
1132
+ console.log(theme.dim(` Building...`));
1133
+ const buildResult = spawnSync("npm run build", {
1134
+ cwd: tempDir,
1135
+ encoding: "utf-8",
1136
+ stdio: ["pipe", "pipe", "pipe"],
1137
+ timeout: 12e4,
1138
+ shell: true
1139
+ });
1140
+ if (buildResult.status !== 0) {
1141
+ throw new Error(buildResult.stderr || "npm run build failed");
1142
+ }
1143
+ }
1144
+ } catch (buildError) {
1145
+ const msg = buildError instanceof Error ? buildError.message : String(buildError);
1146
+ return { success: false, error: `Build failed: ${msg}` };
1147
+ }
1148
+ }
1149
+ return await installFromLocal(tempDir);
1150
+ } catch (error) {
1151
+ const message = error instanceof Error ? error.message : String(error);
1152
+ return { success: false, error: `Git install failed: ${message}` };
1153
+ } finally {
1154
+ try {
1155
+ rmSync(tempDir, { recursive: true, force: true });
1156
+ } catch {
1157
+ }
1158
+ }
1159
+ }
1160
+ function removePlugin(name) {
1161
+ if (!isValidPluginName(name)) {
1162
+ return { success: false, error: "Invalid plugin name" };
1163
+ }
1164
+ try {
1165
+ const pluginDir = join5(getPluginsDir(), name);
1166
+ if (!existsSync4(pluginDir)) {
1167
+ return { success: false, error: `Plugin '${name}' not found` };
1168
+ }
1169
+ rmSync(pluginDir, { recursive: true, force: true });
1170
+ return { success: true };
1171
+ } catch (error) {
1172
+ const message = error instanceof Error ? error.message : String(error);
1173
+ return { success: false, error: message };
1174
+ }
1175
+ }
1176
+ function detectSourceType(source) {
1177
+ if (source.startsWith("./") || source.startsWith("/") || source.startsWith("..") || /^[A-Za-z]:/.test(source)) {
1178
+ return "local";
1179
+ }
1180
+ if (source.startsWith("github:")) {
1181
+ return "github";
1182
+ }
1183
+ if (source.includes("github.com")) {
1184
+ return "github";
1185
+ }
1186
+ if (source.endsWith(".git") || source.startsWith("git@") || source.startsWith("git://")) {
1187
+ return "git";
1188
+ }
1189
+ if (isValidNpmPackageName(source)) {
1190
+ return "npm";
1191
+ }
1192
+ return "unknown";
1193
+ }
1194
+
1195
+ export {
1196
+ theme,
1197
+ symbols,
1198
+ box,
1199
+ progressBar,
1200
+ bubble,
1201
+ categoryIcons,
1202
+ spinnerFrames,
1203
+ registerCommand,
1204
+ registerPluginCommand,
1205
+ getCommand,
1206
+ getAllCommands,
1207
+ getPluginCommands,
1208
+ createPluginStorage,
1209
+ createPluginAPI,
1210
+ ensurePluginsDir,
1211
+ getPluginsDir,
1212
+ discoverPlugins,
1213
+ loadPlugin,
1214
+ unloadPlugin,
1215
+ getDiscoveredPlugins,
1216
+ getLoadedPlugins,
1217
+ isPluginLoaded,
1218
+ getPluginPath,
1219
+ initPluginLoader,
1220
+ validateManifest,
1221
+ checkConflicts,
1222
+ formatPermissions,
1223
+ installFromLocal,
1224
+ installFromNpm,
1225
+ installFromGithub,
1226
+ installFromGit,
1227
+ removePlugin,
1228
+ detectSourceType
1229
+ };