@joe-sh/pj 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1124 @@
1
+ import { execa } from 'execa';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ import { createWriteStream } from 'fs';
5
+ import { pipeline } from 'stream/promises';
6
+ import { Readable } from 'stream';
7
+ import * as tar from 'tar';
8
+ import * as os from 'os';
9
+ import * as yaml from 'yaml';
10
+
11
+ // src/cli/executor.ts
12
+
13
+ // src/api/types.ts
14
+ var PjBinaryError = class extends Error {
15
+ constructor(message, cause) {
16
+ super(message, { cause });
17
+ this.cause = cause;
18
+ this.name = "PjBinaryError";
19
+ }
20
+ };
21
+ var PjExecutionError = class extends Error {
22
+ exitCode;
23
+ stderr;
24
+ constructor(message, exitCode, stderr) {
25
+ super(message);
26
+ this.name = "PjExecutionError";
27
+ this.exitCode = exitCode;
28
+ this.stderr = stderr;
29
+ }
30
+ };
31
+ var PjConfigError = class extends Error {
32
+ constructor(message, cause) {
33
+ super(message, { cause });
34
+ this.cause = cause;
35
+ this.name = "PjConfigError";
36
+ }
37
+ };
38
+ var PJ_TARGET_VERSION = "1.4";
39
+ var GITHUB_OWNER = "josephschmitt";
40
+ var GITHUB_REPO = "pj";
41
+ var GITHUB_API_URL = "https://api.github.com";
42
+ var RELEASES_URL = `${GITHUB_API_URL}/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases`;
43
+ var LATEST_RELEASE_URL = `${RELEASES_URL}/latest`;
44
+ var BINARY_NAME = "pj";
45
+ var BINARY_NAME_WIN = "pj.exe";
46
+ function getBinaryName() {
47
+ return process.platform === "win32" ? BINARY_NAME_WIN : BINARY_NAME;
48
+ }
49
+ function getCacheDir() {
50
+ const xdgCache = process.env["XDG_CACHE_HOME"];
51
+ if (xdgCache) {
52
+ return path.join(xdgCache, "pj-node");
53
+ }
54
+ if (process.platform === "win32") {
55
+ const localAppData = process.env["LOCALAPPDATA"];
56
+ if (localAppData) {
57
+ return path.join(localAppData, "pj-node", "cache");
58
+ }
59
+ return path.join(os.homedir(), "AppData", "Local", "pj-node", "cache");
60
+ }
61
+ return path.join(os.homedir(), ".cache", "pj-node");
62
+ }
63
+ function getBinaryCacheDir() {
64
+ return path.join(getCacheDir(), "bin");
65
+ }
66
+ function getMetadataPath() {
67
+ return path.join(getCacheDir(), "metadata.json");
68
+ }
69
+ function getPjConfigDir() {
70
+ const xdgConfig = process.env["XDG_CONFIG_HOME"];
71
+ if (xdgConfig) {
72
+ return path.join(xdgConfig, "pj");
73
+ }
74
+ if (process.platform === "win32") {
75
+ const appData = process.env["APPDATA"];
76
+ if (appData) {
77
+ return path.join(appData, "pj");
78
+ }
79
+ return path.join(os.homedir(), "AppData", "Roaming", "pj");
80
+ }
81
+ return path.join(os.homedir(), ".config", "pj");
82
+ }
83
+ function getPjConfigPath() {
84
+ return path.join(getPjConfigDir(), "config.yaml");
85
+ }
86
+ function getPjCacheDir() {
87
+ const xdgCache = process.env["XDG_CACHE_HOME"];
88
+ if (xdgCache) {
89
+ return path.join(xdgCache, "pj");
90
+ }
91
+ if (process.platform === "win32") {
92
+ const localAppData = process.env["LOCALAPPDATA"];
93
+ if (localAppData) {
94
+ return path.join(localAppData, "pj", "cache");
95
+ }
96
+ return path.join(os.homedir(), "AppData", "Local", "pj", "cache");
97
+ }
98
+ return path.join(os.homedir(), ".cache", "pj");
99
+ }
100
+ var USER_AGENT = "@joe-sh/pj";
101
+ var HTTP_TIMEOUT = 3e4;
102
+ var UPDATE_CHECK_INTERVAL_DAYS = 7;
103
+
104
+ // src/binary/platform.ts
105
+ function detectPlatform() {
106
+ const nodeOs = process.platform;
107
+ const nodeArch = process.arch;
108
+ let os3;
109
+ let pjOs;
110
+ switch (nodeOs) {
111
+ case "darwin":
112
+ os3 = "darwin";
113
+ pjOs = "darwin";
114
+ break;
115
+ case "linux":
116
+ os3 = "linux";
117
+ pjOs = "linux";
118
+ break;
119
+ case "win32":
120
+ os3 = "win32";
121
+ pjOs = "windows";
122
+ break;
123
+ default:
124
+ throw new PjBinaryError(`Unsupported operating system: ${nodeOs}`);
125
+ }
126
+ let arch;
127
+ let pjArch;
128
+ switch (nodeArch) {
129
+ case "x64":
130
+ arch = "x64";
131
+ pjArch = "amd64";
132
+ break;
133
+ case "arm64":
134
+ arch = "arm64";
135
+ pjArch = "arm64";
136
+ break;
137
+ default:
138
+ throw new PjBinaryError(`Unsupported architecture: ${nodeArch}`);
139
+ }
140
+ return { os: os3, arch, pjOs, pjArch };
141
+ }
142
+ function getAssetFilename(version, platform) {
143
+ const p = platform ?? detectPlatform();
144
+ const versionWithoutV = version.startsWith("v") ? version.slice(1) : version;
145
+ return `pj_${versionWithoutV}_${p.pjOs}_${p.pjArch}.tar.gz`;
146
+ }
147
+ function isPlatformSupported() {
148
+ try {
149
+ detectPlatform();
150
+ return true;
151
+ } catch {
152
+ return false;
153
+ }
154
+ }
155
+
156
+ // src/binary/version.ts
157
+ function parseVersion(version) {
158
+ const normalized = version.replace(/^v/, "");
159
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(normalized);
160
+ if (!match || !match[1] || !match[2] || !match[3]) {
161
+ return null;
162
+ }
163
+ return {
164
+ major: parseInt(match[1], 10),
165
+ minor: parseInt(match[2], 10),
166
+ patch: parseInt(match[3], 10),
167
+ raw: normalized
168
+ };
169
+ }
170
+ function parseTargetVersion(target) {
171
+ const match = /^(\d+)\.(\d+)$/.exec(target);
172
+ if (!match || !match[1] || !match[2]) {
173
+ return null;
174
+ }
175
+ return {
176
+ major: parseInt(match[1], 10),
177
+ minor: parseInt(match[2], 10)
178
+ };
179
+ }
180
+ function isVersionCompatible(version, target = PJ_TARGET_VERSION) {
181
+ const parsed = parseVersion(version);
182
+ const targetParsed = parseTargetVersion(target);
183
+ if (!parsed || !targetParsed) {
184
+ return false;
185
+ }
186
+ return parsed.major === targetParsed.major && parsed.minor === targetParsed.minor;
187
+ }
188
+ function compareVersions(a, b) {
189
+ const parsedA = parseVersion(a);
190
+ const parsedB = parseVersion(b);
191
+ if (!parsedA || !parsedB) {
192
+ return 0;
193
+ }
194
+ if (parsedA.major !== parsedB.major) {
195
+ return parsedA.major - parsedB.major;
196
+ }
197
+ if (parsedA.minor !== parsedB.minor) {
198
+ return parsedA.minor - parsedB.minor;
199
+ }
200
+ return parsedA.patch - parsedB.patch;
201
+ }
202
+ function findHighestCompatibleVersion(versions, target = PJ_TARGET_VERSION) {
203
+ const compatible = versions.filter((v) => isVersionCompatible(v, target));
204
+ if (compatible.length === 0) {
205
+ return null;
206
+ }
207
+ compatible.sort((a, b) => compareVersions(b, a));
208
+ return compatible[0] ?? null;
209
+ }
210
+
211
+ // src/binary/manager.ts
212
+ var BinaryManager = class {
213
+ cachedBinaryPath = null;
214
+ /**
215
+ * Get the path to the pj binary, downloading if necessary
216
+ */
217
+ async getBinaryPath(options) {
218
+ const envPath = process.env["PJ_BINARY_PATH"];
219
+ if (envPath) {
220
+ if (await this.isValidBinary(envPath)) {
221
+ return envPath;
222
+ }
223
+ throw new PjBinaryError(
224
+ `PJ_BINARY_PATH is set but binary is not valid: ${envPath}`
225
+ );
226
+ }
227
+ const globalPath = await this.findGlobalBinary();
228
+ if (globalPath) {
229
+ return globalPath;
230
+ }
231
+ const cachedPath = await this.getCachedBinaryPath();
232
+ if (cachedPath && !options?.force) {
233
+ const needsUpdate = await this.shouldCheckForUpdate();
234
+ if (needsUpdate && options?.version === void 0) {
235
+ try {
236
+ await this.updateBinary(options);
237
+ } catch {
238
+ }
239
+ }
240
+ return cachedPath;
241
+ }
242
+ return await this.downloadBinary(options);
243
+ }
244
+ /**
245
+ * Get the current binary status
246
+ */
247
+ async getStatus() {
248
+ const envPath = process.env["PJ_BINARY_PATH"];
249
+ if (envPath && await this.isValidBinary(envPath)) {
250
+ const version = await this.getVersion(envPath);
251
+ return {
252
+ available: true,
253
+ path: envPath,
254
+ version,
255
+ source: "env"
256
+ };
257
+ }
258
+ const globalPath = await this.findGlobalBinary();
259
+ if (globalPath) {
260
+ const version = await this.getVersion(globalPath);
261
+ return {
262
+ available: true,
263
+ path: globalPath,
264
+ version,
265
+ source: "global"
266
+ };
267
+ }
268
+ const cachedPath = await this.getCachedBinaryPath();
269
+ if (cachedPath) {
270
+ const version = await this.getVersion(cachedPath);
271
+ return {
272
+ available: true,
273
+ path: cachedPath,
274
+ version,
275
+ source: "cache"
276
+ };
277
+ }
278
+ return {
279
+ available: false,
280
+ path: null,
281
+ version: null,
282
+ source: null
283
+ };
284
+ }
285
+ /**
286
+ * Download and install the pj binary.
287
+ * If no specific version is requested, downloads the highest compatible version
288
+ * within the target major.minor range.
289
+ */
290
+ async downloadBinary(options) {
291
+ const release = options?.version ? await this.getRelease(options.version) : await this.getCompatibleRelease();
292
+ const platform = detectPlatform();
293
+ const assetName = getAssetFilename(release.version, platform);
294
+ const asset = release.assets.find((a) => a.name === assetName);
295
+ if (!asset) {
296
+ throw new PjBinaryError(
297
+ `No binary available for ${platform.pjOs}/${platform.pjArch}. Expected asset: ${assetName}`
298
+ );
299
+ }
300
+ const cacheDir = getBinaryCacheDir();
301
+ await fs.mkdir(cacheDir, { recursive: true });
302
+ const tarballPath = path.join(cacheDir, assetName);
303
+ await this.downloadAsset(asset, tarballPath, options?.onProgress);
304
+ const binaryName = getBinaryName();
305
+ await tar.extract({
306
+ file: tarballPath,
307
+ cwd: cacheDir,
308
+ filter: (entryPath) => path.basename(entryPath) === binaryName
309
+ });
310
+ await fs.unlink(tarballPath);
311
+ const binaryPath = path.join(cacheDir, binaryName);
312
+ if (process.platform !== "win32") {
313
+ await fs.chmod(binaryPath, 493);
314
+ }
315
+ if (!await this.isValidBinary(binaryPath)) {
316
+ throw new PjBinaryError("Downloaded binary failed verification");
317
+ }
318
+ await this.saveMetadata({
319
+ version: release.version,
320
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
321
+ lastUpdateCheck: (/* @__PURE__ */ new Date()).toISOString(),
322
+ source: "download"
323
+ });
324
+ this.cachedBinaryPath = binaryPath;
325
+ return binaryPath;
326
+ }
327
+ /**
328
+ * Update the binary to the highest compatible version within the target range.
329
+ */
330
+ async updateBinary(options) {
331
+ const compatible = await this.getCompatibleRelease();
332
+ const metadata = await this.getMetadata();
333
+ if (metadata?.version === compatible.version && !options?.force) {
334
+ await this.saveMetadata({
335
+ ...metadata,
336
+ lastUpdateCheck: (/* @__PURE__ */ new Date()).toISOString()
337
+ });
338
+ const cachedPath = await this.getCachedBinaryPath();
339
+ if (cachedPath) {
340
+ return cachedPath;
341
+ }
342
+ }
343
+ return await this.downloadBinary({
344
+ ...options,
345
+ version: compatible.version
346
+ });
347
+ }
348
+ /**
349
+ * Get the version of a pj binary
350
+ */
351
+ async getVersion(binaryPath) {
352
+ try {
353
+ const result = await execa(binaryPath, ["--version"], { timeout: 5e3 });
354
+ const match = /(?:pj\s+)?(?:version\s+)?v?(\d+\.\d+\.\d+)/i.exec(
355
+ result.stdout
356
+ );
357
+ return match?.[1] ?? null;
358
+ } catch {
359
+ return null;
360
+ }
361
+ }
362
+ /**
363
+ * Check if a binary path is a valid pj binary
364
+ */
365
+ async isValidBinary(binaryPath) {
366
+ try {
367
+ await fs.access(binaryPath, fs.constants.X_OK);
368
+ const version = await this.getVersion(binaryPath);
369
+ return version !== null;
370
+ } catch {
371
+ return false;
372
+ }
373
+ }
374
+ /**
375
+ * Find the globally installed pj binary.
376
+ * Only returns the binary if it's version-compatible with our target.
377
+ */
378
+ async findGlobalBinary() {
379
+ let binaryPath = null;
380
+ try {
381
+ const result = await execa("which", ["pj"], { timeout: 5e3 });
382
+ binaryPath = result.stdout.trim();
383
+ } catch {
384
+ }
385
+ if (!binaryPath && process.platform === "win32") {
386
+ try {
387
+ const result = await execa("where", ["pj"], { timeout: 5e3 });
388
+ binaryPath = result.stdout.trim().split("\n")[0] ?? null;
389
+ } catch {
390
+ }
391
+ }
392
+ if (!binaryPath) {
393
+ return null;
394
+ }
395
+ if (!await this.isValidBinary(binaryPath)) {
396
+ return null;
397
+ }
398
+ const version = await this.getVersion(binaryPath);
399
+ if (!version || !isVersionCompatible(version, PJ_TARGET_VERSION)) {
400
+ return null;
401
+ }
402
+ return binaryPath;
403
+ }
404
+ /**
405
+ * Get the path to the cached binary if it exists and is version-compatible.
406
+ */
407
+ async getCachedBinaryPath() {
408
+ if (this.cachedBinaryPath) {
409
+ if (await this.isValidBinary(this.cachedBinaryPath)) {
410
+ const version = await this.getVersion(this.cachedBinaryPath);
411
+ if (version && isVersionCompatible(version, PJ_TARGET_VERSION)) {
412
+ return this.cachedBinaryPath;
413
+ }
414
+ }
415
+ this.cachedBinaryPath = null;
416
+ }
417
+ const binaryPath = path.join(getBinaryCacheDir(), getBinaryName());
418
+ if (await this.isValidBinary(binaryPath)) {
419
+ const version = await this.getVersion(binaryPath);
420
+ if (version && isVersionCompatible(version, PJ_TARGET_VERSION)) {
421
+ this.cachedBinaryPath = binaryPath;
422
+ return binaryPath;
423
+ }
424
+ }
425
+ return null;
426
+ }
427
+ /**
428
+ * Check if we should check for updates
429
+ */
430
+ async shouldCheckForUpdate() {
431
+ const metadata = await this.getMetadata();
432
+ if (!metadata) {
433
+ return true;
434
+ }
435
+ const lastCheck = new Date(metadata.lastUpdateCheck);
436
+ const now = /* @__PURE__ */ new Date();
437
+ const daysSinceCheck = (now.getTime() - lastCheck.getTime()) / (1e3 * 60 * 60 * 24);
438
+ return daysSinceCheck >= UPDATE_CHECK_INTERVAL_DAYS;
439
+ }
440
+ /**
441
+ * Get cached metadata
442
+ */
443
+ async getMetadata() {
444
+ try {
445
+ const content = await fs.readFile(getMetadataPath(), "utf-8");
446
+ return JSON.parse(content);
447
+ } catch {
448
+ return null;
449
+ }
450
+ }
451
+ /**
452
+ * Save metadata to cache
453
+ */
454
+ async saveMetadata(metadata) {
455
+ const metadataPath = getMetadataPath();
456
+ await fs.mkdir(path.dirname(metadataPath), { recursive: true });
457
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
458
+ }
459
+ /**
460
+ * Get all releases from GitHub (up to 100 most recent)
461
+ */
462
+ async getAllReleases() {
463
+ const response = await fetch(`${RELEASES_URL}?per_page=100`, {
464
+ headers: {
465
+ Accept: "application/vnd.github.v3+json",
466
+ "User-Agent": USER_AGENT
467
+ },
468
+ signal: AbortSignal.timeout(HTTP_TIMEOUT)
469
+ });
470
+ if (!response.ok) {
471
+ throw new PjBinaryError(
472
+ `Failed to fetch releases: ${String(response.status)} ${response.statusText}`
473
+ );
474
+ }
475
+ const data = await response.json();
476
+ return data.filter((release) => !release.prerelease).map((release) => this.parseRelease(release));
477
+ }
478
+ /**
479
+ * Get the highest compatible release within the target major.minor range.
480
+ */
481
+ async getCompatibleRelease() {
482
+ const releases = await this.getAllReleases();
483
+ const versions = releases.map((r) => r.version);
484
+ const compatibleVersion = findHighestCompatibleVersion(
485
+ versions,
486
+ PJ_TARGET_VERSION
487
+ );
488
+ if (!compatibleVersion) {
489
+ throw new PjBinaryError(
490
+ `No compatible pj release found for version range ${PJ_TARGET_VERSION}.x. Available versions: ${versions.join(", ")}`
491
+ );
492
+ }
493
+ const release = releases.find((r) => r.version === compatibleVersion);
494
+ if (!release) {
495
+ throw new PjBinaryError(
496
+ `Failed to find release for version ${compatibleVersion}`
497
+ );
498
+ }
499
+ return release;
500
+ }
501
+ /**
502
+ * Get the latest release from GitHub
503
+ */
504
+ async getLatestRelease() {
505
+ const response = await fetch(LATEST_RELEASE_URL, {
506
+ headers: {
507
+ Accept: "application/vnd.github.v3+json",
508
+ "User-Agent": USER_AGENT
509
+ },
510
+ signal: AbortSignal.timeout(HTTP_TIMEOUT)
511
+ });
512
+ if (!response.ok) {
513
+ throw new PjBinaryError(
514
+ `Failed to fetch latest release: ${String(response.status)} ${response.statusText}`
515
+ );
516
+ }
517
+ const data = await response.json();
518
+ return this.parseRelease(data);
519
+ }
520
+ /**
521
+ * Get a specific release from GitHub
522
+ */
523
+ async getRelease(version) {
524
+ const tag = version.startsWith("v") ? version : `v${version}`;
525
+ const url = `${LATEST_RELEASE_URL.replace("/latest", "")}/${tag}`;
526
+ const response = await fetch(url, {
527
+ headers: {
528
+ Accept: "application/vnd.github.v3+json",
529
+ "User-Agent": USER_AGENT
530
+ },
531
+ signal: AbortSignal.timeout(HTTP_TIMEOUT)
532
+ });
533
+ if (!response.ok) {
534
+ throw new PjBinaryError(
535
+ `Failed to fetch release ${version}: ${String(response.status)} ${response.statusText}`
536
+ );
537
+ }
538
+ const data = await response.json();
539
+ return this.parseRelease(data);
540
+ }
541
+ /**
542
+ * Parse GitHub release response
543
+ */
544
+ parseRelease(data) {
545
+ return {
546
+ tagName: data.tag_name,
547
+ version: data.tag_name.replace(/^v/, ""),
548
+ name: data.name,
549
+ prerelease: data.prerelease,
550
+ assets: data.assets.map((asset) => ({
551
+ name: asset.name,
552
+ downloadUrl: asset.browser_download_url,
553
+ size: asset.size,
554
+ contentType: asset.content_type
555
+ }))
556
+ };
557
+ }
558
+ /**
559
+ * Download an asset to a file
560
+ */
561
+ async downloadAsset(asset, destPath, onProgress) {
562
+ const response = await fetch(asset.downloadUrl, {
563
+ headers: {
564
+ "User-Agent": USER_AGENT
565
+ },
566
+ signal: AbortSignal.timeout(HTTP_TIMEOUT * 10)
567
+ // Longer timeout for downloads
568
+ });
569
+ if (!response.ok) {
570
+ throw new PjBinaryError(
571
+ `Failed to download asset: ${String(response.status)} ${response.statusText}`
572
+ );
573
+ }
574
+ if (!response.body) {
575
+ throw new PjBinaryError("Response body is empty");
576
+ }
577
+ const total = asset.size;
578
+ let downloaded = 0;
579
+ const progressStream = new TransformStream({
580
+ transform(chunk, controller) {
581
+ downloaded += chunk.length;
582
+ onProgress?.({
583
+ downloaded,
584
+ total,
585
+ percent: Math.round(downloaded / total * 100)
586
+ });
587
+ controller.enqueue(chunk);
588
+ }
589
+ });
590
+ const fileStream = createWriteStream(destPath);
591
+ await pipeline(
592
+ Readable.fromWeb(response.body.pipeThrough(progressStream)),
593
+ fileStream
594
+ );
595
+ }
596
+ /**
597
+ * Clear the binary cache
598
+ */
599
+ async clearCache() {
600
+ const cacheDir = getBinaryCacheDir();
601
+ try {
602
+ await fs.rm(cacheDir, { recursive: true, force: true });
603
+ } catch {
604
+ }
605
+ const metadataPath = getMetadataPath();
606
+ try {
607
+ await fs.unlink(metadataPath);
608
+ } catch {
609
+ }
610
+ this.cachedBinaryPath = null;
611
+ }
612
+ };
613
+ var binaryManager = null;
614
+ function getBinaryManager() {
615
+ binaryManager ??= new BinaryManager();
616
+ return binaryManager;
617
+ }
618
+
619
+ // src/cli/executor.ts
620
+ function buildArgs(options) {
621
+ const args = [];
622
+ if (options?.paths) {
623
+ for (const p of options.paths) {
624
+ args.push("--path", p);
625
+ }
626
+ }
627
+ if (options?.markers) {
628
+ for (const m of options.markers) {
629
+ args.push("--marker", m);
630
+ }
631
+ }
632
+ if (options?.excludes) {
633
+ for (const e of options.excludes) {
634
+ args.push("--exclude", e);
635
+ }
636
+ }
637
+ if (options?.maxDepth !== void 0) {
638
+ args.push("--max-depth", String(options.maxDepth));
639
+ }
640
+ if (options?.noIgnore) {
641
+ args.push("--no-ignore");
642
+ }
643
+ if (options?.nested === false) {
644
+ args.push("--no-nested");
645
+ }
646
+ if (options?.noCache) {
647
+ args.push("--no-cache");
648
+ }
649
+ if (options?.icons) {
650
+ args.push("--icons");
651
+ }
652
+ if (options?.configPath) {
653
+ args.push("--config", options.configPath);
654
+ }
655
+ if (options?.verbose) {
656
+ args.push("--verbose");
657
+ }
658
+ args.push("--json");
659
+ return args;
660
+ }
661
+ function parseJsonOutput(output) {
662
+ if (!output.trim()) {
663
+ return [];
664
+ }
665
+ try {
666
+ const parsed = JSON.parse(output);
667
+ return parsed.map((p) => ({
668
+ path: p.path,
669
+ name: p.name,
670
+ marker: p.marker,
671
+ icon: p.icon,
672
+ priority: p.priority
673
+ }));
674
+ } catch (error) {
675
+ throw new PjExecutionError(
676
+ `Failed to parse pj output: ${error instanceof Error ? error.message : String(error)}`
677
+ );
678
+ }
679
+ }
680
+ async function executePj(args, execaOptions) {
681
+ const binaryManager2 = getBinaryManager();
682
+ const binaryPath = await binaryManager2.getBinaryPath();
683
+ try {
684
+ const result = await execa(binaryPath, args, {
685
+ timeout: 6e4,
686
+ // 1 minute timeout
687
+ ...execaOptions
688
+ });
689
+ return {
690
+ stdout: String(result.stdout ?? ""),
691
+ stderr: String(result.stderr ?? ""),
692
+ exitCode: result.exitCode ?? 0
693
+ };
694
+ } catch (error) {
695
+ if (error instanceof Error && "exitCode" in error) {
696
+ const execaError = error;
697
+ throw new PjExecutionError(
698
+ `pj command failed: ${execaError.message}`,
699
+ execaError.exitCode,
700
+ execaError.stderr
701
+ );
702
+ }
703
+ throw error;
704
+ }
705
+ }
706
+ async function executePjWithStdin(args, stdin, execaOptions) {
707
+ const binaryManager2 = getBinaryManager();
708
+ const binaryPath = await binaryManager2.getBinaryPath();
709
+ try {
710
+ const result = await execa(binaryPath, args, {
711
+ input: stdin,
712
+ timeout: 6e4,
713
+ ...execaOptions
714
+ });
715
+ return {
716
+ stdout: String(result.stdout ?? ""),
717
+ stderr: String(result.stderr ?? ""),
718
+ exitCode: result.exitCode ?? 0
719
+ };
720
+ } catch (error) {
721
+ if (error instanceof Error && "exitCode" in error) {
722
+ const execaError = error;
723
+ throw new PjExecutionError(
724
+ `pj command failed: ${execaError.message}`,
725
+ execaError.exitCode,
726
+ execaError.stderr
727
+ );
728
+ }
729
+ throw error;
730
+ }
731
+ }
732
+
733
+ // src/api/discover.ts
734
+ async function discover(options) {
735
+ const args = buildArgs(options);
736
+ const result = await executePj(args);
737
+ return parseJsonOutput(result.stdout);
738
+ }
739
+ async function discoverFromPaths(paths, options) {
740
+ const optsWithoutPaths = {};
741
+ if (options?.markers !== void 0) optsWithoutPaths.markers = options.markers;
742
+ if (options?.excludes !== void 0) optsWithoutPaths.excludes = options.excludes;
743
+ if (options?.maxDepth !== void 0) optsWithoutPaths.maxDepth = options.maxDepth;
744
+ if (options?.noIgnore !== void 0) optsWithoutPaths.noIgnore = options.noIgnore;
745
+ if (options?.nested !== void 0) optsWithoutPaths.nested = options.nested;
746
+ if (options?.noCache !== void 0) optsWithoutPaths.noCache = options.noCache;
747
+ if (options?.icons !== void 0) optsWithoutPaths.icons = options.icons;
748
+ if (options?.configPath !== void 0) optsWithoutPaths.configPath = options.configPath;
749
+ if (options?.verbose !== void 0) optsWithoutPaths.verbose = options.verbose;
750
+ const args = buildArgs(optsWithoutPaths);
751
+ const stdin = paths.join("\n");
752
+ const result = await executePjWithStdin(args, stdin);
753
+ return parseJsonOutput(result.stdout);
754
+ }
755
+ async function findProject(name, options) {
756
+ const projects = await discover(options);
757
+ return projects.find(
758
+ (p) => p.name === name || p.name.toLowerCase() === name.toLowerCase()
759
+ );
760
+ }
761
+ async function findProjects(pattern, options) {
762
+ const projects = await discover(options);
763
+ const regex = typeof pattern === "string" ? new RegExp(pattern, "i") : pattern;
764
+ return projects.filter((p) => regex.test(p.name) || regex.test(p.path));
765
+ }
766
+ async function discoverByMarker(options) {
767
+ const projects = await discover(options);
768
+ const grouped = /* @__PURE__ */ new Map();
769
+ for (const project of projects) {
770
+ const existing = grouped.get(project.marker);
771
+ if (existing) {
772
+ existing.push(project);
773
+ } else {
774
+ grouped.set(project.marker, [project]);
775
+ }
776
+ }
777
+ return grouped;
778
+ }
779
+ async function countByMarker(options) {
780
+ const grouped = await discoverByMarker(options);
781
+ const counts = /* @__PURE__ */ new Map();
782
+ for (const [marker, projects] of grouped) {
783
+ counts.set(marker, projects.length);
784
+ }
785
+ return counts;
786
+ }
787
+ var DEFAULT_CONFIG = {
788
+ paths: [
789
+ path.join(os.homedir(), "projects"),
790
+ path.join(os.homedir(), "code"),
791
+ path.join(os.homedir(), "development")
792
+ ],
793
+ markers: [
794
+ ".git",
795
+ "go.mod",
796
+ "package.json",
797
+ "Cargo.toml",
798
+ "pyproject.toml",
799
+ ".vscode",
800
+ ".idea",
801
+ "Makefile"
802
+ ],
803
+ exclude: ["node_modules", "vendor", ".cache", "target", "dist", "build"],
804
+ maxDepth: 3,
805
+ cacheTTL: 300,
806
+ noIgnore: false,
807
+ noNested: false,
808
+ icons: {
809
+ ".git": "\uF1D3 ",
810
+ //
811
+ "package.json": "\uE718 ",
812
+ //
813
+ "go.mod": "\uE626 ",
814
+ //
815
+ "Cargo.toml": "\uE7A8 ",
816
+ //
817
+ "pyproject.toml": "\uE73C ",
818
+ //
819
+ ".vscode": "\uE70C ",
820
+ //
821
+ ".idea": "\uE7B5 ",
822
+ //
823
+ Makefile: "\uE779 "
824
+ //
825
+ }
826
+ };
827
+ async function loadConfig(configPath) {
828
+ const filePath = configPath ?? getPjConfigPath();
829
+ try {
830
+ const content = await fs.readFile(filePath, "utf-8");
831
+ const parsed = yaml.parse(content);
832
+ return mergeConfig(parsed);
833
+ } catch (error) {
834
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
835
+ return { ...DEFAULT_CONFIG };
836
+ }
837
+ throw new PjConfigError(
838
+ `Failed to load config from ${filePath}`,
839
+ error instanceof Error ? error : void 0
840
+ );
841
+ }
842
+ }
843
+ async function saveConfig(config, configPath) {
844
+ const filePath = configPath ?? getPjConfigPath();
845
+ const rawConfig = {};
846
+ if (config.paths !== void 0) rawConfig.paths = config.paths;
847
+ if (config.markers !== void 0) rawConfig.markers = config.markers;
848
+ if (config.exclude !== void 0) rawConfig.exclude = config.exclude;
849
+ if (config.maxDepth !== void 0) rawConfig.max_depth = config.maxDepth;
850
+ if (config.cacheTTL !== void 0) rawConfig.cache_ttl = config.cacheTTL;
851
+ if (config.noIgnore !== void 0) rawConfig.no_ignore = config.noIgnore;
852
+ if (config.noNested !== void 0) rawConfig.no_nested = config.noNested;
853
+ if (config.icons !== void 0) rawConfig.icons = config.icons;
854
+ const content = yaml.stringify(rawConfig, { indent: 2 });
855
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
856
+ await fs.writeFile(filePath, content, "utf-8");
857
+ }
858
+ async function configExists(configPath) {
859
+ const filePath = configPath ?? getPjConfigPath();
860
+ try {
861
+ await fs.access(filePath);
862
+ return true;
863
+ } catch {
864
+ return false;
865
+ }
866
+ }
867
+ function getConfigPath() {
868
+ return getPjConfigPath();
869
+ }
870
+ function mergeConfig(raw) {
871
+ return {
872
+ paths: raw.paths ?? DEFAULT_CONFIG.paths,
873
+ markers: raw.markers ?? DEFAULT_CONFIG.markers,
874
+ exclude: raw.exclude ?? DEFAULT_CONFIG.exclude,
875
+ maxDepth: raw.max_depth ?? DEFAULT_CONFIG.maxDepth,
876
+ cacheTTL: raw.cache_ttl ?? DEFAULT_CONFIG.cacheTTL,
877
+ noIgnore: raw.no_ignore ?? DEFAULT_CONFIG.noIgnore,
878
+ noNested: raw.no_nested ?? DEFAULT_CONFIG.noNested,
879
+ icons: raw.icons ?? DEFAULT_CONFIG.icons
880
+ };
881
+ }
882
+ function expandPath(p) {
883
+ if (p.startsWith("~/")) {
884
+ return path.join(os.homedir(), p.slice(2));
885
+ }
886
+ if (p === "~") {
887
+ return os.homedir();
888
+ }
889
+ return p;
890
+ }
891
+ function expandConfigPaths(config) {
892
+ return {
893
+ ...config,
894
+ paths: config.paths.map(expandPath)
895
+ };
896
+ }
897
+ async function clearCache() {
898
+ await executePj(["--clear-cache"]);
899
+ }
900
+ async function getCacheInfo() {
901
+ const cacheDir = getPjCacheDir();
902
+ try {
903
+ const stats = await fs.stat(cacheDir);
904
+ if (!stats.isDirectory()) {
905
+ return {
906
+ exists: false,
907
+ path: cacheDir,
908
+ fileCount: 0,
909
+ totalSize: 0
910
+ };
911
+ }
912
+ const files = await fs.readdir(cacheDir);
913
+ let totalSize = 0;
914
+ let fileCount = 0;
915
+ for (const file of files) {
916
+ if (file.endsWith(".json")) {
917
+ const filePath = path.join(cacheDir, file);
918
+ const fileStats = await fs.stat(filePath);
919
+ totalSize += fileStats.size;
920
+ fileCount++;
921
+ }
922
+ }
923
+ return {
924
+ exists: true,
925
+ path: cacheDir,
926
+ fileCount,
927
+ totalSize
928
+ };
929
+ } catch (error) {
930
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
931
+ return {
932
+ exists: false,
933
+ path: cacheDir,
934
+ fileCount: 0,
935
+ totalSize: 0
936
+ };
937
+ }
938
+ throw error;
939
+ }
940
+ }
941
+ function getCachePath() {
942
+ return getPjCacheDir();
943
+ }
944
+
945
+ // src/api/pj.ts
946
+ var Pj = class {
947
+ config;
948
+ /**
949
+ * Create a new Pj instance
950
+ *
951
+ * @param config - Optional configuration overrides
952
+ */
953
+ constructor(config) {
954
+ this.config = { ...DEFAULT_CONFIG, ...config };
955
+ }
956
+ /**
957
+ * Discover all projects
958
+ *
959
+ * @param options - Discovery options
960
+ * @returns Array of discovered projects
961
+ */
962
+ async discover(options) {
963
+ return discover(this.mergeOptions(options));
964
+ }
965
+ /**
966
+ * Discover projects from specific paths
967
+ *
968
+ * Bypasses configured paths and searches only the provided paths.
969
+ * Useful for integration with other tools.
970
+ *
971
+ * @param paths - Paths to search
972
+ * @param options - Additional discovery options
973
+ */
974
+ async discoverFromPaths(paths, options) {
975
+ return discoverFromPaths(paths, this.mergeOptions(options));
976
+ }
977
+ /**
978
+ * Find a project by name
979
+ *
980
+ * @param name - Project name to find
981
+ * @param options - Discovery options
982
+ */
983
+ async findProject(name, options) {
984
+ return findProject(name, this.mergeOptions(options));
985
+ }
986
+ /**
987
+ * Find projects matching a pattern
988
+ *
989
+ * @param pattern - String or regex pattern to match
990
+ * @param options - Discovery options
991
+ */
992
+ async findProjects(pattern, options) {
993
+ return findProjects(pattern, this.mergeOptions(options));
994
+ }
995
+ /**
996
+ * Get projects grouped by marker type
997
+ *
998
+ * @param options - Discovery options
999
+ */
1000
+ async discoverByMarker(options) {
1001
+ return discoverByMarker(this.mergeOptions(options));
1002
+ }
1003
+ /**
1004
+ * Count projects by marker type
1005
+ *
1006
+ * @param options - Discovery options
1007
+ */
1008
+ async countByMarker(options) {
1009
+ return countByMarker(this.mergeOptions(options));
1010
+ }
1011
+ /**
1012
+ * Clear the pj project cache
1013
+ */
1014
+ async clearCache() {
1015
+ return clearCache();
1016
+ }
1017
+ /**
1018
+ * Get information about the pj cache
1019
+ */
1020
+ async getCacheInfo() {
1021
+ return getCacheInfo();
1022
+ }
1023
+ /**
1024
+ * Load configuration from file
1025
+ *
1026
+ * @param configPath - Optional path to config file
1027
+ */
1028
+ async loadConfig(configPath) {
1029
+ const loaded = await loadConfig(configPath);
1030
+ this.config = loaded;
1031
+ return loaded;
1032
+ }
1033
+ /**
1034
+ * Save configuration to file
1035
+ *
1036
+ * @param config - Configuration to save (uses current config if not provided)
1037
+ * @param configPath - Optional path to config file
1038
+ */
1039
+ async saveConfig(config, configPath) {
1040
+ const toSave = config ? { ...this.config, ...config } : this.config;
1041
+ await saveConfig(toSave, configPath);
1042
+ }
1043
+ /**
1044
+ * Get current configuration
1045
+ */
1046
+ getConfig() {
1047
+ return { ...this.config };
1048
+ }
1049
+ /**
1050
+ * Update configuration
1051
+ *
1052
+ * @param config - Partial configuration to merge
1053
+ */
1054
+ setConfig(config) {
1055
+ this.config = { ...this.config, ...config };
1056
+ }
1057
+ /**
1058
+ * Ensure the pj binary is available
1059
+ *
1060
+ * Downloads the binary if not already installed.
1061
+ *
1062
+ * @param options - Binary options
1063
+ * @returns Path to the binary
1064
+ */
1065
+ async ensureBinary(options) {
1066
+ const manager = getBinaryManager();
1067
+ return manager.getBinaryPath(options);
1068
+ }
1069
+ /**
1070
+ * Get the status of the pj binary
1071
+ */
1072
+ async getBinaryStatus() {
1073
+ const manager = getBinaryManager();
1074
+ return manager.getStatus();
1075
+ }
1076
+ /**
1077
+ * Update the pj binary to the latest version
1078
+ *
1079
+ * @param options - Binary options
1080
+ * @returns Path to the updated binary
1081
+ */
1082
+ async updateBinary(options) {
1083
+ const manager = getBinaryManager();
1084
+ return manager.updateBinary(options);
1085
+ }
1086
+ /**
1087
+ * Get the version of the pj binary
1088
+ */
1089
+ async getBinaryVersion() {
1090
+ const manager = getBinaryManager();
1091
+ const status = await manager.getStatus();
1092
+ return status.version;
1093
+ }
1094
+ /**
1095
+ * Merge instance config with provided options
1096
+ */
1097
+ mergeOptions(options) {
1098
+ const merged = {
1099
+ paths: options?.paths ?? this.config.paths,
1100
+ markers: options?.markers ?? this.config.markers,
1101
+ excludes: options?.excludes ?? this.config.exclude,
1102
+ maxDepth: options?.maxDepth ?? this.config.maxDepth,
1103
+ noIgnore: options?.noIgnore ?? this.config.noIgnore,
1104
+ nested: options?.nested ?? !this.config.noNested
1105
+ };
1106
+ if (options?.icons !== void 0) {
1107
+ merged.icons = options.icons;
1108
+ }
1109
+ if (options?.noCache !== void 0) {
1110
+ merged.noCache = options.noCache;
1111
+ }
1112
+ if (options?.configPath !== void 0) {
1113
+ merged.configPath = options.configPath;
1114
+ }
1115
+ if (options?.verbose !== void 0) {
1116
+ merged.verbose = options.verbose;
1117
+ }
1118
+ return merged;
1119
+ }
1120
+ };
1121
+
1122
+ export { BinaryManager, DEFAULT_CONFIG, GITHUB_OWNER, GITHUB_REPO, PJ_TARGET_VERSION, Pj, PjBinaryError, PjConfigError, PjExecutionError, buildArgs, clearCache, compareVersions, configExists, countByMarker, detectPlatform, discover, discoverByMarker, discoverFromPaths, executePj, expandConfigPaths, expandPath, findHighestCompatibleVersion, findProject, findProjects, getAssetFilename, getBinaryCacheDir, getBinaryManager, getBinaryName, getCacheDir, getCacheInfo, getCachePath, getConfigPath, isPlatformSupported, isVersionCompatible, loadConfig, parseJsonOutput, parseTargetVersion, parseVersion, saveConfig };
1123
+ //# sourceMappingURL=index.js.map
1124
+ //# sourceMappingURL=index.js.map