aboutsummaryrefslogtreecommitdiffstats
path: root/pi-ext/scripts/resolve-package-extensions.mjs
blob: 4ba070d7d8f67d850ab15260aac768654867d626 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#!/usr/bin/env node
/**
 * Print absolute extension entry files for the globally-installed pi packages
 * listed in ~/.pi/agent/settings.json `packages`, one per line.
 *
 * dev.sh runs pi with -ne (--no-extensions), which disables ALL extension
 * discovery — including npm packages like pi-subagents — so their tools/commands
 * don't load. (Package *skills* and *prompts* still load, since those use the
 * separate -ns/-np gates.) This resolver lets dev.sh re-add just the package
 * extension modules via -e, without also pulling in the global-dir copies of our
 * own in-development extensions (which we deliberately isolate).
 *
 * Skip specific packages with PI_DEV_SKIP_PACKAGES="pi-goal,pi-autoresearch".
 *
 * Resolution mirrors pi's loader: a package.json "pi.extensions" entry may point
 * at a file, an index.ts/js directory, or a discovery directory of *.ts/*.js
 * files and index-bearing subdirs.
 */
import { readFileSync, existsSync, statSync, readdirSync } from "node:fs";
import { join, resolve } from "node:path";
import { homedir } from "node:os";

const AGENT = join(homedir(), ".pi", "agent");
const NODE_MODULES = join(AGENT, "npm", "node_modules");
const SETTINGS = join(AGENT, "settings.json");

const readJSON = (p) => {
  try {
    return JSON.parse(readFileSync(p, "utf8"));
  } catch {
    return null;
  }
};

const isExtFile = (n) => n.endsWith(".ts") || n.endsWith(".js");
const out = new Set();

function piField(pkg) {
  if (!pkg) return null;
  // Extensions live under "pi.extensions" (a couple of packages nest under a
  // key matching their own name); we only need the extensions array.
  return pkg.pi ?? null;
}

function addDir(dir) {
  const field = piField(readJSON(join(dir, "package.json")));
  if (field && Array.isArray(field.extensions)) {
    for (const e of field.extensions) addEntry(dir, e);
    return;
  }
  for (const idx of ["index.ts", "index.js"]) {
    if (existsSync(join(dir, idx))) {
      out.add(join(dir, idx));
      return;
    }
  }
  // Discovery directory: direct *.ts/*.js files + index-bearing subdirs.
  let entries = [];
  try {
    entries = readdirSync(dir, { withFileTypes: true });
  } catch {
    return;
  }
  for (const ent of entries) {
    const p = join(dir, ent.name);
    if ((ent.isFile() || ent.isSymbolicLink()) && isExtFile(ent.name)) {
      out.add(p);
    } else if (ent.isDirectory()) {
      for (const idx of ["index.ts", "index.js"]) {
        if (existsSync(join(p, idx))) {
          out.add(join(p, idx));
          break;
        }
      }
    }
  }
}

function addEntry(base, entry) {
  const abs = resolve(base, entry);
  if (!existsSync(abs)) return;
  if (statSync(abs).isDirectory()) addDir(abs);
  else if (isExtFile(abs)) out.add(abs);
}

const settings = readJSON(SETTINGS) ?? {};
const packages = Array.isArray(settings.packages) ? settings.packages : [];
const skip = new Set(
  (process.env.PI_DEV_SKIP_PACKAGES ?? "")
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean),
);

for (const spec of packages) {
  if (!spec.startsWith("npm:")) continue; // git: packages resolve elsewhere
  const name = spec.slice(4).replace(/@[^/@]+$/, ""); // strip trailing @version
  if (skip.has(name)) continue;
  const dir = join(NODE_MODULES, name);
  if (!existsSync(dir)) continue;
  const field = piField(readJSON(join(dir, "package.json")));
  const entries = field && Array.isArray(field.extensions) ? field.extensions : ["."];
  for (const e of entries) addEntry(dir, e);
}

for (const p of out) console.log(p);