@caupulican/pi-adaptative 0.81.35 → 0.81.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +10 -8
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts +5 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +17 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autonomy/approval-gate.d.ts.map +1 -1
- package/dist/core/autonomy/approval-gate.js +2 -0
- package/dist/core/autonomy/approval-gate.js.map +1 -1
- package/dist/core/autonomy/foreground-envelope.d.ts.map +1 -1
- package/dist/core/autonomy/foreground-envelope.js +2 -0
- package/dist/core/autonomy/foreground-envelope.js.map +1 -1
- package/dist/core/autonomy/gates.d.ts.map +1 -1
- package/dist/core/autonomy/gates.js +9 -2
- package/dist/core/autonomy/gates.js.map +1 -1
- package/dist/core/autonomy/risk-assessment.d.ts.map +1 -1
- package/dist/core/autonomy/risk-assessment.js +2 -0
- package/dist/core/autonomy/risk-assessment.js.map +1 -1
- package/dist/core/background-lane-controller.d.ts.map +1 -1
- package/dist/core/background-lane-controller.js +1 -1
- package/dist/core/background-lane-controller.js.map +1 -1
- package/dist/core/bash-execution-controller.d.ts +9 -4
- package/dist/core/bash-execution-controller.d.ts.map +1 -1
- package/dist/core/bash-execution-controller.js +7 -6
- package/dist/core/bash-execution-controller.js.map +1 -1
- package/dist/core/bash-executor.d.ts +2 -0
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +3 -2
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/context/sqlite-runtime-index.d.ts.map +1 -1
- package/dist/core/context/sqlite-runtime-index.js +13 -7
- package/dist/core/context/sqlite-runtime-index.js.map +1 -1
- package/dist/core/default-tool-surface.d.ts +7 -0
- package/dist/core/default-tool-surface.d.ts.map +1 -1
- package/dist/core/default-tool-surface.js +29 -10
- package/dist/core/default-tool-surface.js.map +1 -1
- package/dist/core/delegation/worker-actions.d.ts.map +1 -1
- package/dist/core/delegation/worker-actions.js +1 -1
- package/dist/core/delegation/worker-actions.js.map +1 -1
- package/dist/core/doctor.d.ts +10 -10
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +30 -20
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/exec.d.ts +2 -0
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +1 -0
- package/dist/core/exec.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +3 -7
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +13 -3
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +3 -0
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/memory/memory-manager.d.ts.map +1 -1
- package/dist/core/memory/memory-manager.js +1 -0
- package/dist/core/memory/memory-manager.js.map +1 -1
- package/dist/core/model-capability.d.ts.map +1 -1
- package/dist/core/model-capability.js +2 -0
- package/dist/core/model-capability.js.map +1 -1
- package/dist/core/model-router/tool-escalation.d.ts.map +1 -1
- package/dist/core/model-router/tool-escalation.js +17 -3
- package/dist/core/model-router/tool-escalation.js.map +1 -1
- package/dist/core/models/local-runtime.d.ts.map +1 -1
- package/dist/core/models/local-runtime.js +4 -3
- package/dist/core/models/local-runtime.js.map +1 -1
- package/dist/core/python-runtime.d.ts +46 -0
- package/dist/core/python-runtime.d.ts.map +1 -0
- package/dist/core/python-runtime.js +157 -0
- package/dist/core/python-runtime.js.map +1 -0
- package/dist/core/runtime-builder.d.ts +4 -0
- package/dist/core/runtime-builder.d.ts.map +1 -1
- package/dist/core/runtime-builder.js +31 -9
- package/dist/core/runtime-builder.js.map +1 -1
- package/dist/core/security/untrusted-boundary.d.ts +2 -2
- package/dist/core/security/untrusted-boundary.d.ts.map +1 -1
- package/dist/core/security/untrusted-boundary.js +4 -3
- package/dist/core/security/untrusted-boundary.js.map +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +2 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +8 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +13 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tasks/session-task-state.d.ts +10 -0
- package/dist/core/tasks/session-task-state.d.ts.map +1 -0
- package/dist/core/tasks/session-task-state.js +29 -0
- package/dist/core/tasks/session-task-state.js.map +1 -0
- package/dist/core/tasks/task-command.d.ts +30 -0
- package/dist/core/tasks/task-command.d.ts.map +1 -0
- package/dist/core/tasks/task-command.js +79 -0
- package/dist/core/tasks/task-command.js.map +1 -0
- package/dist/core/tasks/task-state.d.ts +71 -0
- package/dist/core/tasks/task-state.d.ts.map +1 -0
- package/dist/core/tasks/task-state.js +372 -0
- package/dist/core/tasks/task-state.js.map +1 -0
- package/dist/core/tool-selection/tool-selection-controller.d.ts.map +1 -1
- package/dist/core/tool-selection/tool-selection-controller.js +1 -1
- package/dist/core/tool-selection/tool-selection-controller.js.map +1 -1
- package/dist/core/tools/bash.d.ts +20 -2
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +124 -199
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +55 -44
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +8 -4
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +9 -6
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +24 -9
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/python.d.ts +64 -0
- package/dist/core/tools/python.d.ts.map +1 -0
- package/dist/core/tools/python.js +241 -0
- package/dist/core/tools/python.js.map +1 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js +1 -1
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/shell-contract-router.d.ts +13 -0
- package/dist/core/tools/shell-contract-router.d.ts.map +1 -0
- package/dist/core/tools/shell-contract-router.js +330 -0
- package/dist/core/tools/shell-contract-router.js.map +1 -0
- package/dist/core/tools/task-steps.d.ts +46 -0
- package/dist/core/tools/task-steps.d.ts.map +1 -0
- package/dist/core/tools/task-steps.js +160 -0
- package/dist/core/tools/task-steps.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +12 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/session-flow-commands.d.ts +10 -0
- package/dist/modes/interactive/session-flow-commands.d.ts.map +1 -1
- package/dist/modes/interactive/session-flow-commands.js +43 -0
- package/dist/modes/interactive/session-flow-commands.js.map +1 -1
- package/dist/utils/shell.d.ts +6 -8
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +75 -69
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +23 -3
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +94 -11
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/development.md +2 -0
- package/docs/index.md +2 -0
- package/docs/packages.md +1 -1
- package/docs/python.md +43 -0
- package/docs/settings.md +9 -5
- package/docs/task-steps.md +90 -0
- package/docs/termux.md +7 -0
- package/docs/usage.md +5 -6
- package/docs/windows.md +15 -12
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +32 -37
- package/package.json +9 -7
- package/scripts/postinstall-python-runtime.mjs +20 -0
package/dist/core/doctor.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type FffInstallOutcome
|
|
1
|
+
import { type FffInstallOutcome } from "../utils/tools-manager.ts";
|
|
2
2
|
import { OllamaRuntime } from "./models/local-runtime.ts";
|
|
3
|
+
import { type PythonRuntimeOutcome } from "./python-runtime.ts";
|
|
3
4
|
/**
|
|
4
5
|
* Environment doctor: verifies required tooling and installs what it safely
|
|
5
6
|
* can, instead of leaving provisioning entirely to lazy first-use (the exact
|
|
@@ -8,13 +9,10 @@ import { OllamaRuntime } from "./models/local-runtime.ts";
|
|
|
8
9
|
* for the underlying lazy-install fix; this module is the proactive half).
|
|
9
10
|
*
|
|
10
11
|
* Two tool kinds, two different postures:
|
|
11
|
-
* - "managed": pi
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* MODE only -- exact manual steps are reported, never executed, never
|
|
16
|
-
* `curl | sh`, never `sudo`. This matches the harness's deliberate
|
|
17
|
-
* no-silent-installer stance for tooling outside its own managed bin dir.
|
|
12
|
+
* - "managed": pi owns provisioning and attempts it when absent (fff-node,
|
|
13
|
+
* plus pinned uv and the Python interpreter resolved through it).
|
|
14
|
+
* - "system": pi does not own the install (ripgrep and ollama). GUIDE MODE
|
|
15
|
+
* only -- exact manual steps are reported, never executed.
|
|
18
16
|
*/
|
|
19
17
|
export type DoctorToolKind = "managed" | "system";
|
|
20
18
|
export interface DoctorCheck {
|
|
@@ -41,7 +39,9 @@ export interface DoctorDeps {
|
|
|
41
39
|
ensureFffNodePackage: (silent?: boolean) => Promise<unknown | undefined>;
|
|
42
40
|
getLastFffInstallOutcome: () => FffInstallOutcome | undefined;
|
|
43
41
|
getRgPath: () => string | null;
|
|
44
|
-
|
|
42
|
+
ensurePythonRuntime: (options: {
|
|
43
|
+
silent: boolean;
|
|
44
|
+
}) => Promise<PythonRuntimeOutcome>;
|
|
45
45
|
/** Best-effort `<command> --version` probe; undefined if it can't be run. Used only to enrich a status line, never for presence detection. */
|
|
46
46
|
probeVersion: (command: string, versionArgs?: readonly string[]) => string | undefined;
|
|
47
47
|
ollamaRuntime: DoctorOllamaRuntime;
|
|
@@ -62,5 +62,5 @@ export declare function formatDoctorReport(report: DoctorReport): string;
|
|
|
62
62
|
* succeeds. Must never fail the update itself: any error here is swallowed
|
|
63
63
|
* and reported as a skipped check, not surfaced as an update failure.
|
|
64
64
|
*/
|
|
65
|
-
export declare function runUpdatePreflight(): Promise<void>;
|
|
65
|
+
export declare function runUpdatePreflight(deps?: DoctorDeps): Promise<void>;
|
|
66
66
|
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/core/doctor.ts"],"names":[],"mappings":"AAEA,OAAO,EAGN,KAAK,iBAAiB,EAKtB,KAAK,gBAAgB,EACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE1D;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,QAAQ,CAAC;AAElD,MAAM,WAAW,WAAW;IAC3B,yEAAyE;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,qHAAqH;AACrH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,cAAc,CAAC,CAAC;AAEjF,MAAM,WAAW,UAAU;IAC1B,2BAA2B,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC;IACvD,oBAAoB,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzE,wBAAwB,EAAE,MAAM,iBAAiB,GAAG,SAAS,CAAC;IAC9D,SAAS,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,MAAM,gBAAgB,CAAC;IACrC,8IAA8I;IAC9I,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,GAAG,SAAS,CAAC;IACvF,aAAa,EAAE,mBAAmB,CAAC;CACnC;AAED,MAAM,WAAW,gBAAgB;IAChC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAyHD,wBAAsB,SAAS,CAC9B,IAAI,GAAE,UAA2B,EACjC,OAAO,GAAE,gBAAqB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAIvB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAW/D;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAOxD","sourcesContent":["import chalk from \"chalk\";\nimport { getAgentDir } from \"../config.ts\";\nimport {\n\tdetectPython,\n\tensureFffNodePackage,\n\ttype FffInstallOutcome,\n\tgetLastFffInstallOutcome,\n\tgetToolPath,\n\tloadAvailableFffNodePackage,\n\tprobeVersion,\n\ttype SystemToolStatus,\n} from \"../utils/tools-manager.ts\";\nimport { OllamaRuntime } from \"./models/local-runtime.ts\";\n\n/**\n * Environment doctor: verifies required tooling and installs what it safely\n * can, instead of leaving provisioning entirely to lazy first-use (the exact\n * gap behind \"ran `pi-adaptative update` on another machine and fff-node\n * never got installed\" -- see fff-lazy-install.test.ts/fff-search-tools.test.ts\n * for the underlying lazy-install fix; this module is the proactive half).\n *\n * Two tool kinds, two different postures:\n * - \"managed\": pi already owns the install path (fff-node into\n * ~/.pi/agent/bin via tools-manager.ts's ensureFffNodePackage). The doctor\n * actually attempts the install when missing.\n * - \"system\": pi does not own the install (ripgrep, ollama, python). GUIDE\n * MODE only -- exact manual steps are reported, never executed, never\n * `curl | sh`, never `sudo`. This matches the harness's deliberate\n * no-silent-installer stance for tooling outside its own managed bin dir.\n */\n\nexport type DoctorToolKind = \"managed\" | \"system\";\n\nexport interface DoctorCheck {\n\t/** Stable identifier, e.g. \"fff-node\", \"ripgrep\", \"ollama\", \"python\". */\n\tid: string;\n\t/** Human-readable label for the report. */\n\tlabel: string;\n\tkind: DoctorToolKind;\n\tpresent: boolean;\n\t/** Version/path/status summary, present or not. */\n\tdetail?: string;\n\t/** Only set for a \"managed\" tool: whether an install was just attempted. */\n\tinstallAttempted?: boolean;\n\t/** Only set for a \"system\" tool that's missing: exact manual steps, never executed. */\n\tguide?: string[];\n}\n\nexport interface DoctorReport {\n\tchecks: DoctorCheck[];\n}\n\n/** Minimal slice of OllamaRuntime the doctor needs -- lets tests inject a fake without touching the real runtime. */\nexport type DoctorOllamaRuntime = Pick<OllamaRuntime, \"detect\" | \"installGuide\">;\n\nexport interface DoctorDeps {\n\tloadAvailableFffNodePackage: () => unknown | undefined;\n\tensureFffNodePackage: (silent?: boolean) => Promise<unknown | undefined>;\n\tgetLastFffInstallOutcome: () => FffInstallOutcome | undefined;\n\tgetRgPath: () => string | null;\n\tdetectPython: () => SystemToolStatus;\n\t/** Best-effort `<command> --version` probe; undefined if it can't be run. Used only to enrich a status line, never for presence detection. */\n\tprobeVersion: (command: string, versionArgs?: readonly string[]) => string | undefined;\n\tollamaRuntime: DoctorOllamaRuntime;\n}\n\nexport interface RunDoctorOptions {\n\t/**\n\t * Whether the fff-node managed install (if one is attempted) stays quiet.\n\t * Default true, matching runUpdatePreflight's existing background\n\t * behavior. The interactive `doctor` command passes `false` so a\n\t * multi-second install doesn't read as a silent hang.\n\t */\n\tsilent?: boolean;\n}\n\nconst realDoctorDeps: DoctorDeps = {\n\tloadAvailableFffNodePackage,\n\tensureFffNodePackage,\n\tgetLastFffInstallOutcome,\n\tgetRgPath: () => getToolPath(\"rg\"),\n\tdetectPython: () => detectPython(),\n\tprobeVersion,\n\tollamaRuntime: new OllamaRuntime({ agentDir: getAgentDir() }),\n};\n\nconst RIPGREP_GUIDE = [\n\t\"ripgrep (rg) was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install ripgrep\",\n\t\" - Debian/Ubuntu: apt install ripgrep\",\n\t\" - Arch: pacman -S ripgrep\",\n\t\" - Or download a release: https://github.com/BurntSushi/ripgrep/releases\",\n];\n\nconst PYTHON_GUIDE = [\n\t\"python3 was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install python3\",\n\t\" - Debian/Ubuntu: apt install python3\",\n\t\" - Arch: pacman -S python\",\n\t\" - Or download from https://www.python.org/downloads/\",\n];\n\nfunction describeFffOutcome(outcome: FffInstallOutcome | undefined): string {\n\tswitch (outcome?.status) {\n\t\tcase \"already-available\":\n\t\t\treturn \"already available\";\n\t\tcase \"installed\":\n\t\t\treturn \"installed just now\";\n\t\tcase \"offline\":\n\t\t\treturn \"offline mode enabled, skipped\";\n\t\tcase \"unsupported-platform\":\n\t\t\treturn \"no native build for this platform\";\n\t\tcase \"install-failed\":\n\t\t\treturn `install failed: ${outcome.reason}`;\n\t\tdefault:\n\t\t\treturn \"unavailable\";\n\t}\n}\n\n/** First line only -- a compiled-features/build-flags tail (e.g. rg --version) is noise in a one-line status report. */\nfunction firstLine(text: string | undefined): string | undefined {\n\treturn text?.split(\"\\n\")[0];\n}\n\n/** MANAGED tool: pi owns this install path, so the doctor actually attempts it when missing. */\nasync function checkFffNode(deps: DoctorDeps, silent: boolean): Promise<DoctorCheck> {\n\t// Delegate entirely to ensureFffNodePackage rather than re-deriving \"is it\n\t// already there\" here: that function (and its offline/unsupported-platform/\n\t// cooldown handling, see tools-manager.ts) is the single source of truth for\n\t// whether an install should even be attempted -- the doctor must not\n\t// duplicate or second-guess that logic, only report its outcome honestly.\n\tconst installed = await deps.ensureFffNodePackage(silent);\n\tconst outcome = deps.getLastFffInstallOutcome();\n\treturn {\n\t\tid: \"fff-node\",\n\t\tlabel: \"FFF native search (@ff-labs/fff-node)\",\n\t\tkind: \"managed\",\n\t\tpresent: Boolean(installed),\n\t\tinstallAttempted: outcome !== undefined && outcome.status !== \"already-available\",\n\t\tdetail: describeFffOutcome(outcome),\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkRipgrep(deps: DoctorDeps): DoctorCheck {\n\tconst path = deps.getRgPath();\n\tif (!path) {\n\t\treturn { id: \"ripgrep\", label: \"ripgrep (rg)\", kind: \"system\", present: false, guide: RIPGREP_GUIDE };\n\t}\n\tconst version = firstLine(deps.probeVersion(path));\n\treturn {\n\t\tid: \"ripgrep\",\n\t\tlabel: \"ripgrep (rg)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: version ? `${path} (${version})` : path,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor (OllamaRuntime.installGuide() is itself already guide-mode-only). */\nasync function checkOllama(deps: DoctorDeps): Promise<DoctorCheck> {\n\tconst status = await deps.ollamaRuntime.detect();\n\tif (!status.binaryPath) {\n\t\treturn {\n\t\t\tid: \"ollama\",\n\t\t\tlabel: \"Ollama (local model runtime)\",\n\t\t\tkind: \"system\",\n\t\t\tpresent: false,\n\t\t\tguide: deps.ollamaRuntime.installGuide(),\n\t\t};\n\t}\n\tconst version = firstLine(deps.probeVersion(status.binaryPath));\n\tconst versionSuffix = version ? `, ${version}` : \"\";\n\tconst activeStore = status.activeStore\n\t\t? `${status.activeStore.path} [${status.activeStore.kind}, ${status.activeStore.modelCount} model(s)]`\n\t\t: `none; pi-owned store ${status.ownedModelsDir} has ${status.ownedStore.modelCount} model(s)`;\n\treturn {\n\t\tid: \"ollama\",\n\t\tlabel: \"Ollama (local model runtime)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: `binary: ${status.binaryPath} [${status.binarySource}]${versionSuffix}; server: ${status.serverUp ? \"up\" : \"down\"} at ${status.serverUrl}; active store: ${activeStore}`,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkPython(deps: DoctorDeps): DoctorCheck {\n\tconst status = deps.detectPython();\n\tif (!status.present) {\n\t\treturn { id: \"python\", label: \"Python (python3)\", kind: \"system\", present: false, guide: PYTHON_GUIDE };\n\t}\n\tconst detail = status.version ? `${status.command} (${status.version})` : status.command;\n\treturn { id: \"python\", label: \"Python (python3)\", kind: \"system\", present: true, detail };\n}\n\nexport async function runDoctor(\n\tdeps: DoctorDeps = realDoctorDeps,\n\toptions: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n\tconst silent = options.silent ?? true;\n\tconst [fffNode, ollama] = await Promise.all([checkFffNode(deps, silent), checkOllama(deps)]);\n\treturn { checks: [fffNode, checkRipgrep(deps), ollama, checkPython(deps)] };\n}\n\nexport function formatDoctorReport(report: DoctorReport): string {\n\tconst lines: string[] = [];\n\tfor (const check of report.checks) {\n\t\tconst status = check.present ? chalk.green(\"[OK]\") : chalk.yellow(\"[MISSING]\");\n\t\tconst detail = check.detail ? ` -- ${check.detail}` : \"\";\n\t\tlines.push(`${status} ${check.label}${detail}`);\n\t\tif (!check.present && check.guide) {\n\t\t\tfor (const guideLine of check.guide) lines.push(` ${guideLine}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Best-effort preflight meant to be called right after `pi-adaptative update`\n * succeeds. Must never fail the update itself: any error here is swallowed\n * and reported as a skipped check, not surfaced as an update failure.\n */\nexport async function runUpdatePreflight(): Promise<void> {\n\ttry {\n\t\tconst report = await runDoctor();\n\t\tconsole.log(`\\n${formatDoctorReport(report)}\\n`);\n\t} catch (error) {\n\t\tconsole.log(chalk.dim(`(environment check skipped: ${error instanceof Error ? error.message : String(error)})`));\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/core/doctor.ts"],"names":[],"mappings":"AAEA,OAAO,EAEN,KAAK,iBAAiB,EAKtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAErF;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,QAAQ,CAAC;AAElD,MAAM,WAAW,WAAW;IAC3B,yEAAyE;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,qHAAqH;AACrH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,cAAc,CAAC,CAAC;AAEjF,MAAM,WAAW,UAAU;IAC1B,2BAA2B,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC;IACvD,oBAAoB,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzE,wBAAwB,EAAE,MAAM,iBAAiB,GAAG,SAAS,CAAC;IAC9D,SAAS,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACrF,8IAA8I;IAC9I,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,GAAG,SAAS,CAAC;IACvF,aAAa,EAAE,mBAAmB,CAAC;CACnC;AAED,MAAM,WAAW,gBAAgB;IAChC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AA6HD,wBAAsB,SAAS,CAC9B,IAAI,GAAE,UAA2B,EACjC,OAAO,GAAE,gBAAqB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAQvB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAW/D;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,GAAE,UAA2B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOzF","sourcesContent":["import chalk from \"chalk\";\nimport { getAgentDir } from \"../config.ts\";\nimport {\n\tensureFffNodePackage,\n\ttype FffInstallOutcome,\n\tgetLastFffInstallOutcome,\n\tgetToolPath,\n\tloadAvailableFffNodePackage,\n\tprobeVersion,\n} from \"../utils/tools-manager.ts\";\nimport { OllamaRuntime } from \"./models/local-runtime.ts\";\nimport { ensurePythonRuntime, type PythonRuntimeOutcome } from \"./python-runtime.ts\";\n\n/**\n * Environment doctor: verifies required tooling and installs what it safely\n * can, instead of leaving provisioning entirely to lazy first-use (the exact\n * gap behind \"ran `pi-adaptative update` on another machine and fff-node\n * never got installed\" -- see fff-lazy-install.test.ts/fff-search-tools.test.ts\n * for the underlying lazy-install fix; this module is the proactive half).\n *\n * Two tool kinds, two different postures:\n * - \"managed\": pi owns provisioning and attempts it when absent (fff-node,\n * plus pinned uv and the Python interpreter resolved through it).\n * - \"system\": pi does not own the install (ripgrep and ollama). GUIDE MODE\n * only -- exact manual steps are reported, never executed.\n */\n\nexport type DoctorToolKind = \"managed\" | \"system\";\n\nexport interface DoctorCheck {\n\t/** Stable identifier, e.g. \"fff-node\", \"ripgrep\", \"ollama\", \"python\". */\n\tid: string;\n\t/** Human-readable label for the report. */\n\tlabel: string;\n\tkind: DoctorToolKind;\n\tpresent: boolean;\n\t/** Version/path/status summary, present or not. */\n\tdetail?: string;\n\t/** Only set for a \"managed\" tool: whether an install was just attempted. */\n\tinstallAttempted?: boolean;\n\t/** Only set for a \"system\" tool that's missing: exact manual steps, never executed. */\n\tguide?: string[];\n}\n\nexport interface DoctorReport {\n\tchecks: DoctorCheck[];\n}\n\n/** Minimal slice of OllamaRuntime the doctor needs -- lets tests inject a fake without touching the real runtime. */\nexport type DoctorOllamaRuntime = Pick<OllamaRuntime, \"detect\" | \"installGuide\">;\n\nexport interface DoctorDeps {\n\tloadAvailableFffNodePackage: () => unknown | undefined;\n\tensureFffNodePackage: (silent?: boolean) => Promise<unknown | undefined>;\n\tgetLastFffInstallOutcome: () => FffInstallOutcome | undefined;\n\tgetRgPath: () => string | null;\n\tensurePythonRuntime: (options: { silent: boolean }) => Promise<PythonRuntimeOutcome>;\n\t/** Best-effort `<command> --version` probe; undefined if it can't be run. Used only to enrich a status line, never for presence detection. */\n\tprobeVersion: (command: string, versionArgs?: readonly string[]) => string | undefined;\n\tollamaRuntime: DoctorOllamaRuntime;\n}\n\nexport interface RunDoctorOptions {\n\t/**\n\t * Whether the fff-node managed install (if one is attempted) stays quiet.\n\t * Default true, matching runUpdatePreflight's existing background\n\t * behavior. The interactive `doctor` command passes `false` so a\n\t * multi-second install doesn't read as a silent hang.\n\t */\n\tsilent?: boolean;\n}\n\nconst realDoctorDeps: DoctorDeps = {\n\tloadAvailableFffNodePackage,\n\tensureFffNodePackage,\n\tgetLastFffInstallOutcome,\n\tgetRgPath: () => getToolPath(\"rg\"),\n\tensurePythonRuntime,\n\tprobeVersion,\n\tollamaRuntime: new OllamaRuntime({ agentDir: getAgentDir() }),\n};\n\nconst RIPGREP_GUIDE = [\n\t\"ripgrep (rg) was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install ripgrep\",\n\t\" - Debian/Ubuntu: apt install ripgrep\",\n\t\" - Arch: pacman -S ripgrep\",\n\t\" - Or download a release: https://github.com/BurntSushi/ripgrep/releases\",\n];\n\nfunction describeFffOutcome(outcome: FffInstallOutcome | undefined): string {\n\tswitch (outcome?.status) {\n\t\tcase \"already-available\":\n\t\t\treturn \"already available\";\n\t\tcase \"installed\":\n\t\t\treturn \"installed just now\";\n\t\tcase \"offline\":\n\t\t\treturn \"offline mode enabled, skipped\";\n\t\tcase \"unsupported-platform\":\n\t\t\treturn \"no native build for this platform\";\n\t\tcase \"install-failed\":\n\t\t\treturn `install failed: ${outcome.reason}`;\n\t\tdefault:\n\t\t\treturn \"unavailable\";\n\t}\n}\n\n/** First line only -- a compiled-features/build-flags tail (e.g. rg --version) is noise in a one-line status report. */\nfunction firstLine(text: string | undefined): string | undefined {\n\treturn text?.split(\"\\n\")[0];\n}\n\n/** MANAGED tool: pi owns this install path, so the doctor actually attempts it when missing. */\nasync function checkFffNode(deps: DoctorDeps, silent: boolean): Promise<DoctorCheck> {\n\t// Delegate entirely to ensureFffNodePackage rather than re-deriving \"is it\n\t// already there\" here: that function (and its offline/unsupported-platform/\n\t// cooldown handling, see tools-manager.ts) is the single source of truth for\n\t// whether an install should even be attempted -- the doctor must not\n\t// duplicate or second-guess that logic, only report its outcome honestly.\n\tconst installed = await deps.ensureFffNodePackage(silent);\n\tconst outcome = deps.getLastFffInstallOutcome();\n\treturn {\n\t\tid: \"fff-node\",\n\t\tlabel: \"FFF native search (@ff-labs/fff-node)\",\n\t\tkind: \"managed\",\n\t\tpresent: Boolean(installed),\n\t\tinstallAttempted: outcome !== undefined && outcome.status !== \"already-available\",\n\t\tdetail: describeFffOutcome(outcome),\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkRipgrep(deps: DoctorDeps): DoctorCheck {\n\tconst path = deps.getRgPath();\n\tif (!path) {\n\t\treturn { id: \"ripgrep\", label: \"ripgrep (rg)\", kind: \"system\", present: false, guide: RIPGREP_GUIDE };\n\t}\n\tconst version = firstLine(deps.probeVersion(path));\n\treturn {\n\t\tid: \"ripgrep\",\n\t\tlabel: \"ripgrep (rg)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: version ? `${path} (${version})` : path,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor (OllamaRuntime.installGuide() is itself already guide-mode-only). */\nasync function checkOllama(deps: DoctorDeps): Promise<DoctorCheck> {\n\tconst status = await deps.ollamaRuntime.detect();\n\tif (!status.binaryPath) {\n\t\treturn {\n\t\t\tid: \"ollama\",\n\t\t\tlabel: \"Ollama (local model runtime)\",\n\t\t\tkind: \"system\",\n\t\t\tpresent: false,\n\t\t\tguide: deps.ollamaRuntime.installGuide(),\n\t\t};\n\t}\n\tconst version = firstLine(deps.probeVersion(status.binaryPath));\n\tconst versionSuffix = version ? `, ${version}` : \"\";\n\tconst activeStore = status.activeStore\n\t\t? `${status.activeStore.path} [${status.activeStore.kind}, ${status.activeStore.modelCount} model(s)]`\n\t\t: `none; pi-owned store ${status.ownedModelsDir} has ${status.ownedStore.modelCount} model(s)`;\n\treturn {\n\t\tid: \"ollama\",\n\t\tlabel: \"Ollama (local model runtime)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: `binary: ${status.binaryPath} [${status.binarySource}]${versionSuffix}; server: ${status.serverUp ? \"up\" : \"down\"} at ${status.serverUrl}; active store: ${activeStore}`,\n\t};\n}\n\n/** MANAGED tool: uv and Python share one deduplicated runtime manager with bounded provisioning. */\nasync function checkPython(deps: DoctorDeps, silent: boolean): Promise<DoctorCheck> {\n\tconst outcome = await deps.ensurePythonRuntime({ silent });\n\tif (outcome.status !== \"ready\") {\n\t\treturn {\n\t\t\tid: \"python\",\n\t\t\tlabel: \"Python (uv-managed)\",\n\t\t\tkind: \"managed\",\n\t\t\tpresent: false,\n\t\t\tdetail: outcome.reason,\n\t\t};\n\t}\n\treturn {\n\t\tid: \"python\",\n\t\tlabel: \"Python (uv-managed)\",\n\t\tkind: \"managed\",\n\t\tpresent: true,\n\t\tinstallAttempted: outcome.pythonInstalled,\n\t\tdetail: `uv: ${outcome.uvPath}; python: ${outcome.pythonPath}${outcome.pythonInstalled ? \" (installed just now)\" : \"\"}`,\n\t};\n}\n\nexport async function runDoctor(\n\tdeps: DoctorDeps = realDoctorDeps,\n\toptions: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n\tconst silent = options.silent ?? true;\n\tconst [fffNode, ollama, python] = await Promise.all([\n\t\tcheckFffNode(deps, silent),\n\t\tcheckOllama(deps),\n\t\tcheckPython(deps, silent),\n\t]);\n\treturn { checks: [fffNode, checkRipgrep(deps), ollama, python] };\n}\n\nexport function formatDoctorReport(report: DoctorReport): string {\n\tconst lines: string[] = [];\n\tfor (const check of report.checks) {\n\t\tconst status = check.present ? chalk.green(\"[OK]\") : chalk.yellow(\"[MISSING]\");\n\t\tconst detail = check.detail ? ` -- ${check.detail}` : \"\";\n\t\tlines.push(`${status} ${check.label}${detail}`);\n\t\tif (!check.present && check.guide) {\n\t\t\tfor (const guideLine of check.guide) lines.push(` ${guideLine}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Best-effort preflight meant to be called right after `pi-adaptative update`\n * succeeds. Must never fail the update itself: any error here is swallowed\n * and reported as a skipped check, not surfaced as an update failure.\n */\nexport async function runUpdatePreflight(deps: DoctorDeps = realDoctorDeps): Promise<void> {\n\ttry {\n\t\tconst report = await runDoctor(deps);\n\t\tconsole.log(`\\n${formatDoctorReport(report)}\\n`);\n\t} catch (error) {\n\t\tconsole.log(chalk.dim(`(environment check skipped: ${error instanceof Error ? error.message : String(error)})`));\n\t}\n}\n"]}
|
package/dist/core/doctor.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { getAgentDir } from "../config.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ensureFffNodePackage, getLastFffInstallOutcome, getToolPath, loadAvailableFffNodePackage, probeVersion, } from "../utils/tools-manager.js";
|
|
4
4
|
import { OllamaRuntime } from "./models/local-runtime.js";
|
|
5
|
+
import { ensurePythonRuntime } from "./python-runtime.js";
|
|
5
6
|
const realDoctorDeps = {
|
|
6
7
|
loadAvailableFffNodePackage,
|
|
7
8
|
ensureFffNodePackage,
|
|
8
9
|
getLastFffInstallOutcome,
|
|
9
10
|
getRgPath: () => getToolPath("rg"),
|
|
10
|
-
|
|
11
|
+
ensurePythonRuntime,
|
|
11
12
|
probeVersion,
|
|
12
13
|
ollamaRuntime: new OllamaRuntime({ agentDir: getAgentDir() }),
|
|
13
14
|
};
|
|
@@ -18,13 +19,6 @@ const RIPGREP_GUIDE = [
|
|
|
18
19
|
" - Arch: pacman -S ripgrep",
|
|
19
20
|
" - Or download a release: https://github.com/BurntSushi/ripgrep/releases",
|
|
20
21
|
];
|
|
21
|
-
const PYTHON_GUIDE = [
|
|
22
|
-
"python3 was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):",
|
|
23
|
-
" - macOS (Homebrew): brew install python3",
|
|
24
|
-
" - Debian/Ubuntu: apt install python3",
|
|
25
|
-
" - Arch: pacman -S python",
|
|
26
|
-
" - Or download from https://www.python.org/downloads/",
|
|
27
|
-
];
|
|
28
22
|
function describeFffOutcome(outcome) {
|
|
29
23
|
switch (outcome?.status) {
|
|
30
24
|
case "already-available":
|
|
@@ -103,19 +97,35 @@ async function checkOllama(deps) {
|
|
|
103
97
|
detail: `binary: ${status.binaryPath} [${status.binarySource}]${versionSuffix}; server: ${status.serverUp ? "up" : "down"} at ${status.serverUrl}; active store: ${activeStore}`,
|
|
104
98
|
};
|
|
105
99
|
}
|
|
106
|
-
/**
|
|
107
|
-
function checkPython(deps) {
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
return {
|
|
100
|
+
/** MANAGED tool: uv and Python share one deduplicated runtime manager with bounded provisioning. */
|
|
101
|
+
async function checkPython(deps, silent) {
|
|
102
|
+
const outcome = await deps.ensurePythonRuntime({ silent });
|
|
103
|
+
if (outcome.status !== "ready") {
|
|
104
|
+
return {
|
|
105
|
+
id: "python",
|
|
106
|
+
label: "Python (uv-managed)",
|
|
107
|
+
kind: "managed",
|
|
108
|
+
present: false,
|
|
109
|
+
detail: outcome.reason,
|
|
110
|
+
};
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
return {
|
|
113
|
+
id: "python",
|
|
114
|
+
label: "Python (uv-managed)",
|
|
115
|
+
kind: "managed",
|
|
116
|
+
present: true,
|
|
117
|
+
installAttempted: outcome.pythonInstalled,
|
|
118
|
+
detail: `uv: ${outcome.uvPath}; python: ${outcome.pythonPath}${outcome.pythonInstalled ? " (installed just now)" : ""}`,
|
|
119
|
+
};
|
|
114
120
|
}
|
|
115
121
|
export async function runDoctor(deps = realDoctorDeps, options = {}) {
|
|
116
122
|
const silent = options.silent ?? true;
|
|
117
|
-
const [fffNode, ollama] = await Promise.all([
|
|
118
|
-
|
|
123
|
+
const [fffNode, ollama, python] = await Promise.all([
|
|
124
|
+
checkFffNode(deps, silent),
|
|
125
|
+
checkOllama(deps),
|
|
126
|
+
checkPython(deps, silent),
|
|
127
|
+
]);
|
|
128
|
+
return { checks: [fffNode, checkRipgrep(deps), ollama, python] };
|
|
119
129
|
}
|
|
120
130
|
export function formatDoctorReport(report) {
|
|
121
131
|
const lines = [];
|
|
@@ -135,9 +145,9 @@ export function formatDoctorReport(report) {
|
|
|
135
145
|
* succeeds. Must never fail the update itself: any error here is swallowed
|
|
136
146
|
* and reported as a skipped check, not surfaced as an update failure.
|
|
137
147
|
*/
|
|
138
|
-
export async function runUpdatePreflight() {
|
|
148
|
+
export async function runUpdatePreflight(deps = realDoctorDeps) {
|
|
139
149
|
try {
|
|
140
|
-
const report = await runDoctor();
|
|
150
|
+
const report = await runDoctor(deps);
|
|
141
151
|
console.log(`\n${formatDoctorReport(report)}\n`);
|
|
142
152
|
}
|
|
143
153
|
catch (error) {
|
package/dist/core/doctor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/core/doctor.ts"],"names":[],"mappings":"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","sourcesContent":["import chalk from \"chalk\";\nimport { getAgentDir } from \"../config.ts\";\nimport {\n\tdetectPython,\n\tensureFffNodePackage,\n\ttype FffInstallOutcome,\n\tgetLastFffInstallOutcome,\n\tgetToolPath,\n\tloadAvailableFffNodePackage,\n\tprobeVersion,\n\ttype SystemToolStatus,\n} from \"../utils/tools-manager.ts\";\nimport { OllamaRuntime } from \"./models/local-runtime.ts\";\n\n/**\n * Environment doctor: verifies required tooling and installs what it safely\n * can, instead of leaving provisioning entirely to lazy first-use (the exact\n * gap behind \"ran `pi-adaptative update` on another machine and fff-node\n * never got installed\" -- see fff-lazy-install.test.ts/fff-search-tools.test.ts\n * for the underlying lazy-install fix; this module is the proactive half).\n *\n * Two tool kinds, two different postures:\n * - \"managed\": pi already owns the install path (fff-node into\n * ~/.pi/agent/bin via tools-manager.ts's ensureFffNodePackage). The doctor\n * actually attempts the install when missing.\n * - \"system\": pi does not own the install (ripgrep, ollama, python). GUIDE\n * MODE only -- exact manual steps are reported, never executed, never\n * `curl | sh`, never `sudo`. This matches the harness's deliberate\n * no-silent-installer stance for tooling outside its own managed bin dir.\n */\n\nexport type DoctorToolKind = \"managed\" | \"system\";\n\nexport interface DoctorCheck {\n\t/** Stable identifier, e.g. \"fff-node\", \"ripgrep\", \"ollama\", \"python\". */\n\tid: string;\n\t/** Human-readable label for the report. */\n\tlabel: string;\n\tkind: DoctorToolKind;\n\tpresent: boolean;\n\t/** Version/path/status summary, present or not. */\n\tdetail?: string;\n\t/** Only set for a \"managed\" tool: whether an install was just attempted. */\n\tinstallAttempted?: boolean;\n\t/** Only set for a \"system\" tool that's missing: exact manual steps, never executed. */\n\tguide?: string[];\n}\n\nexport interface DoctorReport {\n\tchecks: DoctorCheck[];\n}\n\n/** Minimal slice of OllamaRuntime the doctor needs -- lets tests inject a fake without touching the real runtime. */\nexport type DoctorOllamaRuntime = Pick<OllamaRuntime, \"detect\" | \"installGuide\">;\n\nexport interface DoctorDeps {\n\tloadAvailableFffNodePackage: () => unknown | undefined;\n\tensureFffNodePackage: (silent?: boolean) => Promise<unknown | undefined>;\n\tgetLastFffInstallOutcome: () => FffInstallOutcome | undefined;\n\tgetRgPath: () => string | null;\n\tdetectPython: () => SystemToolStatus;\n\t/** Best-effort `<command> --version` probe; undefined if it can't be run. Used only to enrich a status line, never for presence detection. */\n\tprobeVersion: (command: string, versionArgs?: readonly string[]) => string | undefined;\n\tollamaRuntime: DoctorOllamaRuntime;\n}\n\nexport interface RunDoctorOptions {\n\t/**\n\t * Whether the fff-node managed install (if one is attempted) stays quiet.\n\t * Default true, matching runUpdatePreflight's existing background\n\t * behavior. The interactive `doctor` command passes `false` so a\n\t * multi-second install doesn't read as a silent hang.\n\t */\n\tsilent?: boolean;\n}\n\nconst realDoctorDeps: DoctorDeps = {\n\tloadAvailableFffNodePackage,\n\tensureFffNodePackage,\n\tgetLastFffInstallOutcome,\n\tgetRgPath: () => getToolPath(\"rg\"),\n\tdetectPython: () => detectPython(),\n\tprobeVersion,\n\tollamaRuntime: new OllamaRuntime({ agentDir: getAgentDir() }),\n};\n\nconst RIPGREP_GUIDE = [\n\t\"ripgrep (rg) was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install ripgrep\",\n\t\" - Debian/Ubuntu: apt install ripgrep\",\n\t\" - Arch: pacman -S ripgrep\",\n\t\" - Or download a release: https://github.com/BurntSushi/ripgrep/releases\",\n];\n\nconst PYTHON_GUIDE = [\n\t\"python3 was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install python3\",\n\t\" - Debian/Ubuntu: apt install python3\",\n\t\" - Arch: pacman -S python\",\n\t\" - Or download from https://www.python.org/downloads/\",\n];\n\nfunction describeFffOutcome(outcome: FffInstallOutcome | undefined): string {\n\tswitch (outcome?.status) {\n\t\tcase \"already-available\":\n\t\t\treturn \"already available\";\n\t\tcase \"installed\":\n\t\t\treturn \"installed just now\";\n\t\tcase \"offline\":\n\t\t\treturn \"offline mode enabled, skipped\";\n\t\tcase \"unsupported-platform\":\n\t\t\treturn \"no native build for this platform\";\n\t\tcase \"install-failed\":\n\t\t\treturn `install failed: ${outcome.reason}`;\n\t\tdefault:\n\t\t\treturn \"unavailable\";\n\t}\n}\n\n/** First line only -- a compiled-features/build-flags tail (e.g. rg --version) is noise in a one-line status report. */\nfunction firstLine(text: string | undefined): string | undefined {\n\treturn text?.split(\"\\n\")[0];\n}\n\n/** MANAGED tool: pi owns this install path, so the doctor actually attempts it when missing. */\nasync function checkFffNode(deps: DoctorDeps, silent: boolean): Promise<DoctorCheck> {\n\t// Delegate entirely to ensureFffNodePackage rather than re-deriving \"is it\n\t// already there\" here: that function (and its offline/unsupported-platform/\n\t// cooldown handling, see tools-manager.ts) is the single source of truth for\n\t// whether an install should even be attempted -- the doctor must not\n\t// duplicate or second-guess that logic, only report its outcome honestly.\n\tconst installed = await deps.ensureFffNodePackage(silent);\n\tconst outcome = deps.getLastFffInstallOutcome();\n\treturn {\n\t\tid: \"fff-node\",\n\t\tlabel: \"FFF native search (@ff-labs/fff-node)\",\n\t\tkind: \"managed\",\n\t\tpresent: Boolean(installed),\n\t\tinstallAttempted: outcome !== undefined && outcome.status !== \"already-available\",\n\t\tdetail: describeFffOutcome(outcome),\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkRipgrep(deps: DoctorDeps): DoctorCheck {\n\tconst path = deps.getRgPath();\n\tif (!path) {\n\t\treturn { id: \"ripgrep\", label: \"ripgrep (rg)\", kind: \"system\", present: false, guide: RIPGREP_GUIDE };\n\t}\n\tconst version = firstLine(deps.probeVersion(path));\n\treturn {\n\t\tid: \"ripgrep\",\n\t\tlabel: \"ripgrep (rg)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: version ? `${path} (${version})` : path,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor (OllamaRuntime.installGuide() is itself already guide-mode-only). */\nasync function checkOllama(deps: DoctorDeps): Promise<DoctorCheck> {\n\tconst status = await deps.ollamaRuntime.detect();\n\tif (!status.binaryPath) {\n\t\treturn {\n\t\t\tid: \"ollama\",\n\t\t\tlabel: \"Ollama (local model runtime)\",\n\t\t\tkind: \"system\",\n\t\t\tpresent: false,\n\t\t\tguide: deps.ollamaRuntime.installGuide(),\n\t\t};\n\t}\n\tconst version = firstLine(deps.probeVersion(status.binaryPath));\n\tconst versionSuffix = version ? `, ${version}` : \"\";\n\tconst activeStore = status.activeStore\n\t\t? `${status.activeStore.path} [${status.activeStore.kind}, ${status.activeStore.modelCount} model(s)]`\n\t\t: `none; pi-owned store ${status.ownedModelsDir} has ${status.ownedStore.modelCount} model(s)`;\n\treturn {\n\t\tid: \"ollama\",\n\t\tlabel: \"Ollama (local model runtime)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: `binary: ${status.binaryPath} [${status.binarySource}]${versionSuffix}; server: ${status.serverUp ? \"up\" : \"down\"} at ${status.serverUrl}; active store: ${activeStore}`,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkPython(deps: DoctorDeps): DoctorCheck {\n\tconst status = deps.detectPython();\n\tif (!status.present) {\n\t\treturn { id: \"python\", label: \"Python (python3)\", kind: \"system\", present: false, guide: PYTHON_GUIDE };\n\t}\n\tconst detail = status.version ? `${status.command} (${status.version})` : status.command;\n\treturn { id: \"python\", label: \"Python (python3)\", kind: \"system\", present: true, detail };\n}\n\nexport async function runDoctor(\n\tdeps: DoctorDeps = realDoctorDeps,\n\toptions: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n\tconst silent = options.silent ?? true;\n\tconst [fffNode, ollama] = await Promise.all([checkFffNode(deps, silent), checkOllama(deps)]);\n\treturn { checks: [fffNode, checkRipgrep(deps), ollama, checkPython(deps)] };\n}\n\nexport function formatDoctorReport(report: DoctorReport): string {\n\tconst lines: string[] = [];\n\tfor (const check of report.checks) {\n\t\tconst status = check.present ? chalk.green(\"[OK]\") : chalk.yellow(\"[MISSING]\");\n\t\tconst detail = check.detail ? ` -- ${check.detail}` : \"\";\n\t\tlines.push(`${status} ${check.label}${detail}`);\n\t\tif (!check.present && check.guide) {\n\t\t\tfor (const guideLine of check.guide) lines.push(` ${guideLine}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Best-effort preflight meant to be called right after `pi-adaptative update`\n * succeeds. Must never fail the update itself: any error here is swallowed\n * and reported as a skipped check, not surfaced as an update failure.\n */\nexport async function runUpdatePreflight(): Promise<void> {\n\ttry {\n\t\tconst report = await runDoctor();\n\t\tconsole.log(`\\n${formatDoctorReport(report)}\\n`);\n\t} catch (error) {\n\t\tconsole.log(chalk.dim(`(environment check skipped: ${error instanceof Error ? error.message : String(error)})`));\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/core/doctor.ts"],"names":[],"mappings":"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","sourcesContent":["import chalk from \"chalk\";\nimport { getAgentDir } from \"../config.ts\";\nimport {\n\tensureFffNodePackage,\n\ttype FffInstallOutcome,\n\tgetLastFffInstallOutcome,\n\tgetToolPath,\n\tloadAvailableFffNodePackage,\n\tprobeVersion,\n} from \"../utils/tools-manager.ts\";\nimport { OllamaRuntime } from \"./models/local-runtime.ts\";\nimport { ensurePythonRuntime, type PythonRuntimeOutcome } from \"./python-runtime.ts\";\n\n/**\n * Environment doctor: verifies required tooling and installs what it safely\n * can, instead of leaving provisioning entirely to lazy first-use (the exact\n * gap behind \"ran `pi-adaptative update` on another machine and fff-node\n * never got installed\" -- see fff-lazy-install.test.ts/fff-search-tools.test.ts\n * for the underlying lazy-install fix; this module is the proactive half).\n *\n * Two tool kinds, two different postures:\n * - \"managed\": pi owns provisioning and attempts it when absent (fff-node,\n * plus pinned uv and the Python interpreter resolved through it).\n * - \"system\": pi does not own the install (ripgrep and ollama). GUIDE MODE\n * only -- exact manual steps are reported, never executed.\n */\n\nexport type DoctorToolKind = \"managed\" | \"system\";\n\nexport interface DoctorCheck {\n\t/** Stable identifier, e.g. \"fff-node\", \"ripgrep\", \"ollama\", \"python\". */\n\tid: string;\n\t/** Human-readable label for the report. */\n\tlabel: string;\n\tkind: DoctorToolKind;\n\tpresent: boolean;\n\t/** Version/path/status summary, present or not. */\n\tdetail?: string;\n\t/** Only set for a \"managed\" tool: whether an install was just attempted. */\n\tinstallAttempted?: boolean;\n\t/** Only set for a \"system\" tool that's missing: exact manual steps, never executed. */\n\tguide?: string[];\n}\n\nexport interface DoctorReport {\n\tchecks: DoctorCheck[];\n}\n\n/** Minimal slice of OllamaRuntime the doctor needs -- lets tests inject a fake without touching the real runtime. */\nexport type DoctorOllamaRuntime = Pick<OllamaRuntime, \"detect\" | \"installGuide\">;\n\nexport interface DoctorDeps {\n\tloadAvailableFffNodePackage: () => unknown | undefined;\n\tensureFffNodePackage: (silent?: boolean) => Promise<unknown | undefined>;\n\tgetLastFffInstallOutcome: () => FffInstallOutcome | undefined;\n\tgetRgPath: () => string | null;\n\tensurePythonRuntime: (options: { silent: boolean }) => Promise<PythonRuntimeOutcome>;\n\t/** Best-effort `<command> --version` probe; undefined if it can't be run. Used only to enrich a status line, never for presence detection. */\n\tprobeVersion: (command: string, versionArgs?: readonly string[]) => string | undefined;\n\tollamaRuntime: DoctorOllamaRuntime;\n}\n\nexport interface RunDoctorOptions {\n\t/**\n\t * Whether the fff-node managed install (if one is attempted) stays quiet.\n\t * Default true, matching runUpdatePreflight's existing background\n\t * behavior. The interactive `doctor` command passes `false` so a\n\t * multi-second install doesn't read as a silent hang.\n\t */\n\tsilent?: boolean;\n}\n\nconst realDoctorDeps: DoctorDeps = {\n\tloadAvailableFffNodePackage,\n\tensureFffNodePackage,\n\tgetLastFffInstallOutcome,\n\tgetRgPath: () => getToolPath(\"rg\"),\n\tensurePythonRuntime,\n\tprobeVersion,\n\tollamaRuntime: new OllamaRuntime({ agentDir: getAgentDir() }),\n};\n\nconst RIPGREP_GUIDE = [\n\t\"ripgrep (rg) was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install ripgrep\",\n\t\" - Debian/Ubuntu: apt install ripgrep\",\n\t\" - Arch: pacman -S ripgrep\",\n\t\" - Or download a release: https://github.com/BurntSushi/ripgrep/releases\",\n];\n\nfunction describeFffOutcome(outcome: FffInstallOutcome | undefined): string {\n\tswitch (outcome?.status) {\n\t\tcase \"already-available\":\n\t\t\treturn \"already available\";\n\t\tcase \"installed\":\n\t\t\treturn \"installed just now\";\n\t\tcase \"offline\":\n\t\t\treturn \"offline mode enabled, skipped\";\n\t\tcase \"unsupported-platform\":\n\t\t\treturn \"no native build for this platform\";\n\t\tcase \"install-failed\":\n\t\t\treturn `install failed: ${outcome.reason}`;\n\t\tdefault:\n\t\t\treturn \"unavailable\";\n\t}\n}\n\n/** First line only -- a compiled-features/build-flags tail (e.g. rg --version) is noise in a one-line status report. */\nfunction firstLine(text: string | undefined): string | undefined {\n\treturn text?.split(\"\\n\")[0];\n}\n\n/** MANAGED tool: pi owns this install path, so the doctor actually attempts it when missing. */\nasync function checkFffNode(deps: DoctorDeps, silent: boolean): Promise<DoctorCheck> {\n\t// Delegate entirely to ensureFffNodePackage rather than re-deriving \"is it\n\t// already there\" here: that function (and its offline/unsupported-platform/\n\t// cooldown handling, see tools-manager.ts) is the single source of truth for\n\t// whether an install should even be attempted -- the doctor must not\n\t// duplicate or second-guess that logic, only report its outcome honestly.\n\tconst installed = await deps.ensureFffNodePackage(silent);\n\tconst outcome = deps.getLastFffInstallOutcome();\n\treturn {\n\t\tid: \"fff-node\",\n\t\tlabel: \"FFF native search (@ff-labs/fff-node)\",\n\t\tkind: \"managed\",\n\t\tpresent: Boolean(installed),\n\t\tinstallAttempted: outcome !== undefined && outcome.status !== \"already-available\",\n\t\tdetail: describeFffOutcome(outcome),\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkRipgrep(deps: DoctorDeps): DoctorCheck {\n\tconst path = deps.getRgPath();\n\tif (!path) {\n\t\treturn { id: \"ripgrep\", label: \"ripgrep (rg)\", kind: \"system\", present: false, guide: RIPGREP_GUIDE };\n\t}\n\tconst version = firstLine(deps.probeVersion(path));\n\treturn {\n\t\tid: \"ripgrep\",\n\t\tlabel: \"ripgrep (rg)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: version ? `${path} (${version})` : path,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor (OllamaRuntime.installGuide() is itself already guide-mode-only). */\nasync function checkOllama(deps: DoctorDeps): Promise<DoctorCheck> {\n\tconst status = await deps.ollamaRuntime.detect();\n\tif (!status.binaryPath) {\n\t\treturn {\n\t\t\tid: \"ollama\",\n\t\t\tlabel: \"Ollama (local model runtime)\",\n\t\t\tkind: \"system\",\n\t\t\tpresent: false,\n\t\t\tguide: deps.ollamaRuntime.installGuide(),\n\t\t};\n\t}\n\tconst version = firstLine(deps.probeVersion(status.binaryPath));\n\tconst versionSuffix = version ? `, ${version}` : \"\";\n\tconst activeStore = status.activeStore\n\t\t? `${status.activeStore.path} [${status.activeStore.kind}, ${status.activeStore.modelCount} model(s)]`\n\t\t: `none; pi-owned store ${status.ownedModelsDir} has ${status.ownedStore.modelCount} model(s)`;\n\treturn {\n\t\tid: \"ollama\",\n\t\tlabel: \"Ollama (local model runtime)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: `binary: ${status.binaryPath} [${status.binarySource}]${versionSuffix}; server: ${status.serverUp ? \"up\" : \"down\"} at ${status.serverUrl}; active store: ${activeStore}`,\n\t};\n}\n\n/** MANAGED tool: uv and Python share one deduplicated runtime manager with bounded provisioning. */\nasync function checkPython(deps: DoctorDeps, silent: boolean): Promise<DoctorCheck> {\n\tconst outcome = await deps.ensurePythonRuntime({ silent });\n\tif (outcome.status !== \"ready\") {\n\t\treturn {\n\t\t\tid: \"python\",\n\t\t\tlabel: \"Python (uv-managed)\",\n\t\t\tkind: \"managed\",\n\t\t\tpresent: false,\n\t\t\tdetail: outcome.reason,\n\t\t};\n\t}\n\treturn {\n\t\tid: \"python\",\n\t\tlabel: \"Python (uv-managed)\",\n\t\tkind: \"managed\",\n\t\tpresent: true,\n\t\tinstallAttempted: outcome.pythonInstalled,\n\t\tdetail: `uv: ${outcome.uvPath}; python: ${outcome.pythonPath}${outcome.pythonInstalled ? \" (installed just now)\" : \"\"}`,\n\t};\n}\n\nexport async function runDoctor(\n\tdeps: DoctorDeps = realDoctorDeps,\n\toptions: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n\tconst silent = options.silent ?? true;\n\tconst [fffNode, ollama, python] = await Promise.all([\n\t\tcheckFffNode(deps, silent),\n\t\tcheckOllama(deps),\n\t\tcheckPython(deps, silent),\n\t]);\n\treturn { checks: [fffNode, checkRipgrep(deps), ollama, python] };\n}\n\nexport function formatDoctorReport(report: DoctorReport): string {\n\tconst lines: string[] = [];\n\tfor (const check of report.checks) {\n\t\tconst status = check.present ? chalk.green(\"[OK]\") : chalk.yellow(\"[MISSING]\");\n\t\tconst detail = check.detail ? ` -- ${check.detail}` : \"\";\n\t\tlines.push(`${status} ${check.label}${detail}`);\n\t\tif (!check.present && check.guide) {\n\t\t\tfor (const guideLine of check.guide) lines.push(` ${guideLine}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Best-effort preflight meant to be called right after `pi-adaptative update`\n * succeeds. Must never fail the update itself: any error here is swallowed\n * and reported as a skipped check, not surfaced as an update failure.\n */\nexport async function runUpdatePreflight(deps: DoctorDeps = realDoctorDeps): Promise<void> {\n\ttry {\n\t\tconst report = await runDoctor(deps);\n\t\tconsole.log(`\\n${formatDoctorReport(report)}\\n`);\n\t} catch (error) {\n\t\tconsole.log(chalk.dim(`(environment check skipped: ${error instanceof Error ? error.message : String(error)})`));\n\t}\n}\n"]}
|
package/dist/core/exec.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface ExecOptions {
|
|
|
11
11
|
timeout?: number;
|
|
12
12
|
/** Working directory */
|
|
13
13
|
cwd?: string;
|
|
14
|
+
/** Environment for the child process. Defaults to process.env. */
|
|
15
|
+
env?: NodeJS.ProcessEnv;
|
|
14
16
|
/**
|
|
15
17
|
* Maximum output retained per stream, in UTF-16 code units (~bytes for ASCII).
|
|
16
18
|
* Output is kept as a rolling tail; when exceeded, the oldest output is dropped
|
package/dist/core/exec.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/core/exec.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,wCAAwC;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,0EAA0E;IAC1E,eAAe,EAAE,OAAO,CAAC;IACzB,0EAA0E;IAC1E,eAAe,EAAE,OAAO,CAAC;IACzB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,IAAI,MAAM,CAAC;IACf,SAAS,IAAI,OAAO,CAAC;CACrB;AAED,uGAAuG;AACvG,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAyB/E;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/core/exec.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,wCAAwC;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,0EAA0E;IAC1E,eAAe,EAAE,OAAO,CAAC;IACzB,0EAA0E;IAC1E,eAAe,EAAE,OAAO,CAAC;IACzB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,IAAI,MAAM,CAAC;IACf,SAAS,IAAI,OAAO,CAAC;CACrB;AAED,uGAAuG;AACvG,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAyB/E;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC,CAmErB","sourcesContent":["/**\n * Shared command execution utilities for extensions and custom tools.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport { waitForChildProcessWithTermination } from \"../utils/child-process.ts\";\n\n/** Default per-stream retention for command output, in UTF-16 code units (~bytes for ASCII). */\nconst DEFAULT_EXEC_MAX_BUFFER = 16 * 1024 * 1024;\nconst DEFAULT_EXEC_TIMEOUT_MS = 10 * 60_000;\nconst EXEC_KILL_GRACE_MS = 5_000;\n\n/**\n * Options for executing shell commands.\n */\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the command */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Working directory */\n\tcwd?: string;\n\t/** Environment for the child process. Defaults to process.env. */\n\tenv?: NodeJS.ProcessEnv;\n\t/**\n\t * Maximum output retained per stream, in UTF-16 code units (~bytes for ASCII).\n\t * Output is kept as a rolling tail; when exceeded, the oldest output is dropped\n\t * and the matching truncation flag is set on the result. Defaults to 16 MiB.\n\t */\n\tmaxBuffer?: number;\n}\n\n/**\n * Result of executing a shell command.\n */\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\tkilled: boolean;\n\t/** True when stdout exceeded maxBuffer and only its tail was retained. */\n\tstdoutTruncated: boolean;\n\t/** True when stderr exceeded maxBuffer and only its tail was retained. */\n\tstderrTruncated: boolean;\n\t/** Present when the process failed to spawn (e.g. ENOENT); code is 1 in that case. */\n\terrorMessage?: string;\n}\n\nexport interface RollingOutputBuffer {\n\tpush(chunk: string): void;\n\ttext(): string;\n\ttruncated(): boolean;\n}\n\n/** Bounded child-process output accumulator: keeps a rolling tail of at most maxUnits UTF-16 units. */\nexport function createRollingOutputBuffer(maxUnits: number): RollingOutputBuffer {\n\tconst chunks: string[] = [];\n\tlet units = 0;\n\tlet truncated = false;\n\treturn {\n\t\tpush(chunk: string): void {\n\t\t\tchunks.push(chunk);\n\t\t\tunits += chunk.length;\n\t\t\twhile (units > maxUnits && chunks.length > 1) {\n\t\t\t\tunits -= chunks.shift()?.length ?? 0;\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t\tif (units > maxUnits) {\n\t\t\t\tchunks[0] = chunks[0].slice(-maxUnits);\n\t\t\t\tunits = chunks[0].length;\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t},\n\t\ttext(): string {\n\t\t\treturn chunks.join(\"\");\n\t\t},\n\t\ttruncated(): boolean {\n\t\t\treturn truncated;\n\t\t},\n\t};\n}\n\n/**\n * Execute a shell command and return stdout/stderr/code.\n * Supports timeout and abort signal. Output retention per stream is bounded\n * (rolling tail, see ExecOptions.maxBuffer) so a chatty child process cannot\n * grow the host heap without bound.\n */\nexport async function execCommand(\n\tcommand: string,\n\targs: string[],\n\tcwd: string,\n\toptions?: ExecOptions,\n): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tenv: options?.env,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\tdetached: process.platform !== \"win32\",\n\t\t});\n\n\t\tlet spawnError: string | undefined;\n\t\tproc.once(\"error\", (err) => {\n\t\t\tspawnError = err.message;\n\t\t});\n\n\t\tconst maxBuffer =\n\t\t\toptions?.maxBuffer !== undefined && options.maxBuffer > 0 ? options.maxBuffer : DEFAULT_EXEC_MAX_BUFFER;\n\t\tconst stdout = createRollingOutputBuffer(maxBuffer);\n\t\tconst stderr = createRollingOutputBuffer(maxBuffer);\n\t\tconst stdoutDecoder = new StringDecoder(\"utf8\");\n\t\tconst stderrDecoder = new StringDecoder(\"utf8\");\n\n\t\tproc.stdout?.on(\"data\", (data: Buffer) => {\n\t\t\tstdout.push(stdoutDecoder.write(data));\n\t\t});\n\n\t\tproc.stderr?.on(\"data\", (data: Buffer) => {\n\t\t\tstderr.push(stderrDecoder.write(data));\n\t\t});\n\n\t\t// Settle from the child's terminal event or a bounded abort/timeout escalation.\n\t\t// This never waits for a second close event after termination has been acknowledged.\n\t\twaitForChildProcessWithTermination(proc, {\n\t\t\tsignal: options?.signal,\n\t\t\ttimeoutMs: options?.timeout && options.timeout > 0 ? options.timeout : DEFAULT_EXEC_TIMEOUT_MS,\n\t\t\tkillGraceMs: EXEC_KILL_GRACE_MS,\n\t\t})\n\t\t\t.then((terminal) => {\n\t\t\t\tstdout.push(stdoutDecoder.end());\n\t\t\t\tstderr.push(stderrDecoder.end());\n\t\t\t\tresolve({\n\t\t\t\t\tstdout: stdout.text(),\n\t\t\t\t\tstderr: stderr.text(),\n\t\t\t\t\tcode: terminal.code ?? 1,\n\t\t\t\t\tkilled: terminal.reason !== \"exited\",\n\t\t\t\t\tstdoutTruncated: stdout.truncated(),\n\t\t\t\t\tstderrTruncated: stderr.truncated(),\n\t\t\t\t\t...(spawnError !== undefined ? { errorMessage: spawnError } : {}),\n\t\t\t\t});\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tstdout.push(stdoutDecoder.end());\n\t\t\t\tstderr.push(stderrDecoder.end());\n\t\t\t\tif (spawnError === undefined) {\n\t\t\t\t\tspawnError = err instanceof Error ? err.message : String(err);\n\t\t\t\t}\n\t\t\t\tresolve({\n\t\t\t\t\tstdout: stdout.text(),\n\t\t\t\t\tstderr: stderr.text(),\n\t\t\t\t\tcode: 1,\n\t\t\t\t\tkilled: false,\n\t\t\t\t\tstdoutTruncated: stdout.truncated(),\n\t\t\t\t\tstderrTruncated: stderr.truncated(),\n\t\t\t\t\terrorMessage: spawnError,\n\t\t\t\t});\n\t\t\t});\n\t});\n}\n"]}
|
package/dist/core/exec.js
CHANGED
|
@@ -45,6 +45,7 @@ export async function execCommand(command, args, cwd, options) {
|
|
|
45
45
|
return new Promise((resolve) => {
|
|
46
46
|
const proc = spawn(command, args, {
|
|
47
47
|
cwd,
|
|
48
|
+
env: options?.env,
|
|
48
49
|
shell: false,
|
|
49
50
|
stdio: ["ignore", "pipe", "pipe"],
|
|
50
51
|
detached: process.platform !== "win32",
|
package/dist/core/exec.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/core/exec.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,kCAAkC,EAAE,MAAM,2BAA2B,CAAC;AAE/E,gGAAgG;AAChG,MAAM,uBAAuB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AACjD,MAAM,uBAAuB,GAAG,EAAE,GAAG,MAAM,CAAC;AAC5C,MAAM,kBAAkB,GAAG,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/core/exec.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared command execution utilities for extensions and custom tools.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport { waitForChildProcessWithTermination } from \"../utils/child-process.ts\";\n\n/** Default per-stream retention for command output, in UTF-16 code units (~bytes for ASCII). */\nconst DEFAULT_EXEC_MAX_BUFFER = 16 * 1024 * 1024;\nconst DEFAULT_EXEC_TIMEOUT_MS = 10 * 60_000;\nconst EXEC_KILL_GRACE_MS = 5_000;\n\n/**\n * Options for executing shell commands.\n */\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the command */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Working directory */\n\tcwd?: string;\n\t/** Environment for the child process. Defaults to process.env. */\n\tenv?: NodeJS.ProcessEnv;\n\t/**\n\t * Maximum output retained per stream, in UTF-16 code units (~bytes for ASCII).\n\t * Output is kept as a rolling tail; when exceeded, the oldest output is dropped\n\t * and the matching truncation flag is set on the result. Defaults to 16 MiB.\n\t */\n\tmaxBuffer?: number;\n}\n\n/**\n * Result of executing a shell command.\n */\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\tkilled: boolean;\n\t/** True when stdout exceeded maxBuffer and only its tail was retained. */\n\tstdoutTruncated: boolean;\n\t/** True when stderr exceeded maxBuffer and only its tail was retained. */\n\tstderrTruncated: boolean;\n\t/** Present when the process failed to spawn (e.g. ENOENT); code is 1 in that case. */\n\terrorMessage?: string;\n}\n\nexport interface RollingOutputBuffer {\n\tpush(chunk: string): void;\n\ttext(): string;\n\ttruncated(): boolean;\n}\n\n/** Bounded child-process output accumulator: keeps a rolling tail of at most maxUnits UTF-16 units. */\nexport function createRollingOutputBuffer(maxUnits: number): RollingOutputBuffer {\n\tconst chunks: string[] = [];\n\tlet units = 0;\n\tlet truncated = false;\n\treturn {\n\t\tpush(chunk: string): void {\n\t\t\tchunks.push(chunk);\n\t\t\tunits += chunk.length;\n\t\t\twhile (units > maxUnits && chunks.length > 1) {\n\t\t\t\tunits -= chunks.shift()?.length ?? 0;\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t\tif (units > maxUnits) {\n\t\t\t\tchunks[0] = chunks[0].slice(-maxUnits);\n\t\t\t\tunits = chunks[0].length;\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t},\n\t\ttext(): string {\n\t\t\treturn chunks.join(\"\");\n\t\t},\n\t\ttruncated(): boolean {\n\t\t\treturn truncated;\n\t\t},\n\t};\n}\n\n/**\n * Execute a shell command and return stdout/stderr/code.\n * Supports timeout and abort signal. Output retention per stream is bounded\n * (rolling tail, see ExecOptions.maxBuffer) so a chatty child process cannot\n * grow the host heap without bound.\n */\nexport async function execCommand(\n\tcommand: string,\n\targs: string[],\n\tcwd: string,\n\toptions?: ExecOptions,\n): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tenv: options?.env,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\tdetached: process.platform !== \"win32\",\n\t\t});\n\n\t\tlet spawnError: string | undefined;\n\t\tproc.once(\"error\", (err) => {\n\t\t\tspawnError = err.message;\n\t\t});\n\n\t\tconst maxBuffer =\n\t\t\toptions?.maxBuffer !== undefined && options.maxBuffer > 0 ? options.maxBuffer : DEFAULT_EXEC_MAX_BUFFER;\n\t\tconst stdout = createRollingOutputBuffer(maxBuffer);\n\t\tconst stderr = createRollingOutputBuffer(maxBuffer);\n\t\tconst stdoutDecoder = new StringDecoder(\"utf8\");\n\t\tconst stderrDecoder = new StringDecoder(\"utf8\");\n\n\t\tproc.stdout?.on(\"data\", (data: Buffer) => {\n\t\t\tstdout.push(stdoutDecoder.write(data));\n\t\t});\n\n\t\tproc.stderr?.on(\"data\", (data: Buffer) => {\n\t\t\tstderr.push(stderrDecoder.write(data));\n\t\t});\n\n\t\t// Settle from the child's terminal event or a bounded abort/timeout escalation.\n\t\t// This never waits for a second close event after termination has been acknowledged.\n\t\twaitForChildProcessWithTermination(proc, {\n\t\t\tsignal: options?.signal,\n\t\t\ttimeoutMs: options?.timeout && options.timeout > 0 ? options.timeout : DEFAULT_EXEC_TIMEOUT_MS,\n\t\t\tkillGraceMs: EXEC_KILL_GRACE_MS,\n\t\t})\n\t\t\t.then((terminal) => {\n\t\t\t\tstdout.push(stdoutDecoder.end());\n\t\t\t\tstderr.push(stderrDecoder.end());\n\t\t\t\tresolve({\n\t\t\t\t\tstdout: stdout.text(),\n\t\t\t\t\tstderr: stderr.text(),\n\t\t\t\t\tcode: terminal.code ?? 1,\n\t\t\t\t\tkilled: terminal.reason !== \"exited\",\n\t\t\t\t\tstdoutTruncated: stdout.truncated(),\n\t\t\t\t\tstderrTruncated: stderr.truncated(),\n\t\t\t\t\t...(spawnError !== undefined ? { errorMessage: spawnError } : {}),\n\t\t\t\t});\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tstdout.push(stdoutDecoder.end());\n\t\t\t\tstderr.push(stderrDecoder.end());\n\t\t\t\tif (spawnError === undefined) {\n\t\t\t\t\tspawnError = err instanceof Error ? err.message : String(err);\n\t\t\t\t}\n\t\t\t\tresolve({\n\t\t\t\t\tstdout: stdout.text(),\n\t\t\t\t\tstderr: stderr.text(),\n\t\t\t\t\tcode: 1,\n\t\t\t\t\tkilled: false,\n\t\t\t\t\tstdoutTruncated: stdout.truncated(),\n\t\t\t\t\tstderrTruncated: stderr.truncated(),\n\t\t\t\t\terrorMessage: spawnError,\n\t\t\t\t});\n\t\t\t});\n\t});\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAuBH,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAIhE,OAAO,KAAK,EACX,SAAS,EAET,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EAKpB,MAAM,YAAY,CAAC;AAkJpB;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,gBAAgB,CAiEzD;AAgTD;;;;;GAKG;AACH,wBAAsB,kCAAkC,CACvD,UAAU,EAAE,SAAS,EAAE,EACvB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACxD,OAAO,CAAC,IAAI,CAAC,CAuCf;AA4CD,wBAAsB,aAAa,CAClC,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,gBAAgB,EACzB,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,OAAO,CAAC;IAAE,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAyBhE;AA2GD;;GAEG;AACH,wBAAsB,wBAAwB,CAC7C,OAAO,EAAE,gBAAgB,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,gBAAgB,EACzB,aAAa,SAAa,EAC1B,OAAO,GAAE;IAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAO,GACzC,OAAO,CAAC,SAAS,CAAC,CAapB;AAED;;GAEG;AACH,wBAAsB,cAAc,CACnC,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,iBAAiB,CAAC,EACxC,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,oBAAoB,CAAC,CA0C/B;AAED,UAAU,gBAAgB;IACzB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,8FAA8F;IAC9F,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAaD,UAAU,iBAAiB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC/B;AA2ID;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAsB,EAChC,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,oBAAoB,CAAC,CA4C/B","sourcesContent":["/**\n * Extension loader - loads TypeScript extension modules using jiti.\n *\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport * as _bundledPiAgentCore from \"@caupulican/pi-agent-core\";\nimport * as _bundledPiAi from \"@caupulican/pi-ai\";\nimport * as _bundledPiAiOauth from \"@caupulican/pi-ai/oauth\";\nimport type { KeyId } from \"@caupulican/pi-tui\";\nimport * as _bundledPiTui from \"@caupulican/pi-tui\";\nimport { createJiti } from \"jiti/static\";\n// Static imports of packages that extensions may use.\n// These MUST be static so Bun bundles them into the compiled binary.\n// The virtualModules option then makes them available to extensions.\nimport * as _bundledTypebox from \"typebox\";\nimport * as _bundledTypeboxCompile from \"typebox/compile\";\nimport * as _bundledTypeboxValue from \"typebox/value\";\nimport { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from \"../../config.ts\";\n// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,\n// avoiding a circular dependency. Extensions can import from @caupulican/pi-adaptative.\nimport * as _bundledPiCodingAgent from \"../../index.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\nimport { createEventBus, type EventBus } from \"../event-bus.ts\";\nimport type { ExecOptions } from \"../exec.ts\";\nimport { execCommand } from \"../exec.ts\";\nimport { createSyntheticSourceInfo } from \"../source-info.ts\";\nimport type {\n\tExtension,\n\tExtensionAPI,\n\tExtensionFactory,\n\tExtensionRuntime,\n\tLoadExtensionsResult,\n\tMessageRenderer,\n\tProviderConfig,\n\tRegisteredCommand,\n\tToolDefinition,\n} from \"./types.ts\";\n\nconst EXTENSION_FACTORY_TIMEOUT_MS = 30_000;\nconst EXTENSION_DISPOSAL_TIMEOUT_MS = 5_000;\nconst inactiveExtensions = new WeakSet<Extension>();\n\nclass ExtensionFactoryTimeoutError extends Error {}\n\n/** Modules available to extensions via virtualModules (for compiled Bun binary) */\nconst VIRTUAL_MODULES: Record<string, unknown> = {\n\ttypebox: _bundledTypebox,\n\t\"typebox/compile\": _bundledTypeboxCompile,\n\t\"typebox/value\": _bundledTypeboxValue,\n\t\"@sinclair/typebox\": _bundledTypebox,\n\t\"@sinclair/typebox/compile\": _bundledTypeboxCompile,\n\t\"@sinclair/typebox/value\": _bundledTypeboxValue,\n\t\"@caupulican/pi-agent-core\": _bundledPiAgentCore,\n\t\"@caupulican/pi-tui\": _bundledPiTui,\n\t\"@caupulican/pi-ai\": _bundledPiAi,\n\t\"@caupulican/pi-ai/oauth\": _bundledPiAiOauth,\n\t\"@caupulican/pi-adaptative\": _bundledPiCodingAgent,\n\t\"@earendil-works/pi-agent-core\": _bundledPiAgentCore,\n\t\"@earendil-works/pi-tui\": _bundledPiTui,\n\t\"@earendil-works/pi-ai\": _bundledPiAi,\n\t\"@earendil-works/pi-ai/oauth\": _bundledPiAiOauth,\n\t\"@earendil-works/pi-coding-agent\": _bundledPiCodingAgent,\n\t\"@mariozechner/pi-agent-core\": _bundledPiAgentCore,\n\t\"@mariozechner/pi-tui\": _bundledPiTui,\n\t\"@mariozechner/pi-ai\": _bundledPiAi,\n\t\"@mariozechner/pi-ai/oauth\": _bundledPiAiOauth,\n\t\"@mariozechner/pi-coding-agent\": _bundledPiCodingAgent,\n};\n\nfunction uniquePaths(paths: string[]): string[] {\n\treturn [...new Set(paths)];\n}\n\nfunction safeRealpath(filePath: string): string {\n\ttry {\n\t\treturn fs.realpathSync(filePath);\n\t} catch {\n\t\treturn filePath;\n\t}\n}\n\n/**\n * Get aliases for jiti (used in Node.js/development mode).\n * In Bun binary mode, virtualModules is used instead.\n */\nlet _aliases: Record<string, string> | null = null;\n\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst loaderFile = fileURLToPath(import.meta.url);\n\tconst realLoaderFile = safeRealpath(loaderFile);\n\tconst __dirname = path.dirname(loaderFile);\n\tconst realDirname = path.dirname(realLoaderFile);\n\tconst packageIndex = [\n\t\tpath.resolve(__dirname, \"../..\", \"index.ts\"),\n\t\tpath.resolve(__dirname, \"../..\", \"index.js\"),\n\t].find((candidate) => fs.existsSync(candidate));\n\tif (!packageIndex) throw new Error(\"Unable to resolve the coding-agent package entry\");\n\n\tconst moduleRequires = uniquePaths([loaderFile, realLoaderFile]).map((file) => createRequire(file));\n\tconst resolveModule = (specifier: string): string => {\n\t\tfor (const moduleRequire of moduleRequires) {\n\t\t\ttry {\n\t\t\t\treturn moduleRequire.resolve(specifier);\n\t\t\t} catch {\n\t\t\t\t// Try the next resolution base. Linked global installs may resolve the\n\t\t\t\t// loader through the global symlink path while workspace dependencies are\n\t\t\t\t// hoisted beside the real source path.\n\t\t\t}\n\t\t}\n\t\treturn fileURLToPath(import.meta.resolve(specifier));\n\t};\n\n\tconst typeboxEntry = resolveModule(\"typebox\");\n\tconst typeboxCompileEntry = resolveModule(\"typebox/compile\");\n\tconst typeboxValueEntry = resolveModule(\"typebox/value\");\n\n\tconst packagesRoots = uniquePaths([\n\t\tpath.resolve(__dirname, \"../../../../\"),\n\t\tpath.resolve(realDirname, \"../../../../\"),\n\t]);\n\tconst resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string => {\n\t\tfor (const packagesRoot of packagesRoots) {\n\t\t\tconst workspacePath = path.join(packagesRoot, workspaceRelativePath);\n\t\t\tif (fs.existsSync(workspacePath)) {\n\t\t\t\treturn workspacePath;\n\t\t\t}\n\t\t}\n\t\treturn resolveModule(specifier);\n\t};\n\n\tconst piCodingAgentEntry = packageIndex;\n\tconst piAgentCoreEntry = resolveWorkspaceOrImport(\"agent/src/index.ts\", \"@caupulican/pi-agent-core\");\n\tconst piAgentCoreNodeEntry = resolveWorkspaceOrImport(\"agent/src/node.ts\", \"@caupulican/pi-agent-core/node\");\n\tconst piAgentCorePathsEntry = resolveWorkspaceOrImport(\n\t\t\"agent/src/utils/paths.ts\",\n\t\t\"@caupulican/pi-agent-core/paths\",\n\t);\n\tconst piTuiEntry = resolveWorkspaceOrImport(\"tui/src/index.ts\", \"@caupulican/pi-tui\");\n\tconst piAiEntry = resolveWorkspaceOrImport(\"ai/src/index.ts\", \"@caupulican/pi-ai\");\n\tconst piAiOauthEntry = resolveWorkspaceOrImport(\"ai/src/oauth.ts\", \"@caupulican/pi-ai/oauth\");\n\n\t_aliases = {\n\t\t\"@caupulican/pi-agent-core/node\": piAgentCoreNodeEntry,\n\t\t\"@caupulican/pi-agent-core/paths\": piAgentCorePathsEntry,\n\t\t\"@caupulican/pi-ai/oauth\": piAiOauthEntry,\n\t\t\"@caupulican/pi-adaptative\": piCodingAgentEntry,\n\t\t\"@caupulican/pi-agent-core\": piAgentCoreEntry,\n\t\t\"@caupulican/pi-tui\": piTuiEntry,\n\t\t\"@caupulican/pi-ai\": piAiEntry,\n\t\t\"@earendil-works/pi-agent-core/node\": piAgentCoreNodeEntry,\n\t\t\"@earendil-works/pi-agent-core/paths\": piAgentCorePathsEntry,\n\t\t\"@earendil-works/pi-ai/oauth\": piAiOauthEntry,\n\t\t\"@earendil-works/pi-coding-agent\": piCodingAgentEntry,\n\t\t\"@earendil-works/pi-agent-core\": piAgentCoreEntry,\n\t\t\"@earendil-works/pi-tui\": piTuiEntry,\n\t\t\"@earendil-works/pi-ai\": piAiEntry,\n\t\t\"@mariozechner/pi-agent-core/node\": piAgentCoreNodeEntry,\n\t\t\"@mariozechner/pi-agent-core/paths\": piAgentCorePathsEntry,\n\t\t\"@mariozechner/pi-ai/oauth\": piAiOauthEntry,\n\t\t\"@mariozechner/pi-coding-agent\": piCodingAgentEntry,\n\t\t\"@mariozechner/pi-agent-core\": piAgentCoreEntry,\n\t\t\"@mariozechner/pi-tui\": piTuiEntry,\n\t\t\"@mariozechner/pi-ai\": piAiEntry,\n\t\ttypebox: typeboxEntry,\n\t\t\"typebox/compile\": typeboxCompileEntry,\n\t\t\"typebox/value\": typeboxValueEntry,\n\t\t\"@sinclair/typebox\": typeboxEntry,\n\t\t\"@sinclair/typebox/compile\": typeboxCompileEntry,\n\t\t\"@sinclair/typebox/value\": typeboxValueEntry,\n\t};\n\n\treturn _aliases;\n}\n\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\nfunction yieldToEventLoop(): Promise<void> {\n\treturn new Promise((resolve) => setImmediate(resolve));\n}\n\n/**\n * Create a runtime with throwing stubs for action methods.\n * Runner.bindCore() replaces these with real implementations.\n */\nexport function createExtensionRuntime(): ExtensionRuntime {\n\tconst notInitialized = () => {\n\t\tthrow new Error(\"Extension runtime not initialized. Action methods cannot be called during extension loading.\");\n\t};\n\tconst state: { staleMessage?: string } = {};\n\tconst assertActive = () => {\n\t\tif (state.staleMessage) {\n\t\t\tthrow new Error(state.staleMessage);\n\t\t}\n\t};\n\n\tconst providersByExtension = new Map<string, Set<string>>();\n\tconst providerRegistrations = new Map<string, { config: ProviderConfig; extensionPath?: string }>();\n\tconst memoryProvidersByExtension = new Map<string, Set<Parameters<ExtensionRuntime[\"registerMemoryProvider\"]>[0]>>();\n\tconst contextMemoryProvidersByExtension = new Map<\n\t\tstring,\n\t\tSet<Parameters<ExtensionRuntime[\"registerContextMemoryProvider\"]>[0]>\n\t>();\n\n\tconst runtime: ExtensionRuntime = {\n\t\tsendMessage: notInitialized,\n\t\tsendUserMessage: notInitialized,\n\t\tappendEntry: notInitialized,\n\t\tsetSessionName: notInitialized,\n\t\tgetSessionName: notInitialized,\n\t\tsetLabel: notInitialized,\n\t\tgetActiveTools: notInitialized,\n\t\tgetAllTools: notInitialized,\n\t\tsetActiveTools: notInitialized,\n\t\t// registerTool() is valid during extension load; refresh is only needed post-bind.\n\t\trefreshTools: () => {},\n\t\tgetCommands: notInitialized,\n\t\tsetModel: notInitialized,\n\t\tgetThinkingLevel: notInitialized,\n\t\tsetThinkingLevel: notInitialized,\n\t\tgetExternalResourceRoots: notInitialized,\n\t\tregisterMemoryProvider: notInitialized,\n\t\tregisterContextMemoryProvider: notInitialized,\n\t\treportSpawnedUsage: notInitialized,\n\t\tflagValues: new Map(),\n\t\tpendingProviderRegistrations: [],\n\t\tassertActive,\n\t\tinvalidate: (message) => {\n\t\t\tstate.staleMessage ??=\n\t\t\t\tmessage ??\n\t\t\t\t\"This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().\";\n\t\t},\n\t\t// Pre-bind: queue registrations so bindCore() can flush them once the\n\t\t// model registry is available. bindCore() replaces both with direct calls.\n\t\tregisterProvider: (name, config, extensionPath = \"<unknown>\") => {\n\t\t\truntime.pendingProviderRegistrations.push({ name, config, extensionPath });\n\t\t},\n\t\tunregisterProvider: (name) => {\n\t\t\truntime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);\n\t\t},\n\t\tprovidersByExtension,\n\t\tproviderRegistrations,\n\t\tgetProvidersForExtension: (extensionPath: string) => {\n\t\t\treturn [...(providersByExtension.get(extensionPath) ?? [])];\n\t\t},\n\t\tmemoryProvidersByExtension,\n\t\tcontextMemoryProvidersByExtension,\n\t};\n\n\treturn runtime;\n}\n\n/**\n * Create the ExtensionAPI for an extension.\n * Registration methods write to the extension object.\n * Action methods delegate to the shared runtime.\n */\nfunction createExtensionAPI(\n\textension: Extension,\n\truntime: ExtensionRuntime,\n\tcwd: string,\n\teventBus: EventBus,\n): ExtensionAPI {\n\t// A factory can time out while its promise continues running. Route every later API call through\n\t// an extension-generation guard so stale async work cannot register tools, handlers, or providers.\n\tconst sharedRuntime = runtime;\n\truntime = new Proxy(sharedRuntime, {\n\t\tget(target, property, receiver) {\n\t\t\tif (property === \"assertActive\") {\n\t\t\t\treturn () => {\n\t\t\t\t\ttarget.assertActive();\n\t\t\t\t\tif (inactiveExtensions.has(extension)) {\n\t\t\t\t\t\tthrow new Error(`Extension generation is no longer active: ${extension.path}`);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn Reflect.get(target, property, receiver);\n\t\t},\n\t});\n\n\tconst api = {\n\t\t// Registration methods - write to extension\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\truntime.assertActive();\n\t\t\tconst list = extension.handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\textension.handlers.set(event, list);\n\t\t},\n\n\t\tregisterTool(tool: ToolDefinition): void {\n\t\t\truntime.assertActive();\n\t\t\textension.tools.set(tool.name, {\n\t\t\t\tdefinition: tool,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t});\n\t\t\truntime.refreshTools();\n\t\t},\n\n\t\tregisterCommand(name: string, options: Omit<RegisteredCommand, \"name\" | \"sourceInfo\">): void {\n\t\t\truntime.assertActive();\n\t\t\textension.commands.set(name, {\n\t\t\t\tname,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t\t...options,\n\t\t\t});\n\t\t},\n\n\t\tregisterShortcut(\n\t\t\tshortcut: KeyId,\n\t\t\toptions: {\n\t\t\t\tdescription?: string;\n\t\t\t\thandler: (ctx: import(\"./types.ts\").ExtensionContext) => Promise<void> | void;\n\t\t\t},\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });\n\t\t},\n\n\t\tregisterFlag(\n\t\t\tname: string,\n\t\t\toptions: { description?: string; type: \"boolean\" | \"string\"; default?: boolean | string },\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.flags.set(name, { name, extensionPath: extension.path, ...options });\n\t\t\tif (options.default !== undefined && !runtime.flagValues.has(name)) {\n\t\t\t\truntime.flagValues.set(name, options.default);\n\t\t\t}\n\t\t},\n\n\t\tregisterMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {\n\t\t\truntime.assertActive();\n\t\t\textension.messageRenderers.set(customType, renderer as MessageRenderer);\n\t\t},\n\n\t\t// Flag access - checks extension registered it, reads from runtime\n\t\tgetFlag(name: string): boolean | string | undefined {\n\t\t\truntime.assertActive();\n\t\t\tif (!extension.flags.has(name)) return undefined;\n\t\t\treturn runtime.flagValues.get(name);\n\t\t},\n\n\t\t// Action methods - delegate to shared runtime\n\t\tsendMessage(message, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendMessage(message, options);\n\t\t},\n\n\t\tsendUserMessage(content, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendUserMessage(content, options);\n\t\t},\n\n\t\tappendEntry(customType: string, data?: unknown): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.appendEntry(customType, data);\n\t\t},\n\n\t\tsetSessionName(name: string): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setSessionName(name);\n\t\t},\n\n\t\tgetSessionName(): string | undefined {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getSessionName();\n\t\t},\n\n\t\tsetLabel(entryId: string, label: string | undefined): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setLabel(entryId, label);\n\t\t},\n\n\t\texec(command: string, args: string[], options?: ExecOptions) {\n\t\t\truntime.assertActive();\n\t\t\treturn execCommand(command, args, options?.cwd ?? cwd, options);\n\t\t},\n\n\t\tgetActiveTools(): string[] {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getActiveTools();\n\t\t},\n\n\t\tgetAllTools() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getAllTools();\n\t\t},\n\n\t\tsetActiveTools(toolNames: string[]): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setActiveTools(toolNames);\n\t\t},\n\n\t\tgetCommands() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getCommands();\n\t\t},\n\n\t\tgetExternalResourceRoots() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getExternalResourceRoots();\n\t\t},\n\n\t\tregisterMemoryProvider(provider) {\n\t\t\truntime.assertActive();\n\t\t\truntime.registerMemoryProvider(provider);\n\t\t\tconst providers = runtime.memoryProvidersByExtension.get(extension.path) ?? new Set();\n\t\t\tproviders.add(provider);\n\t\t\truntime.memoryProvidersByExtension.set(extension.path, providers);\n\t\t},\n\n\t\tregisterContextMemoryProvider(provider) {\n\t\t\truntime.assertActive();\n\t\t\truntime.registerContextMemoryProvider(provider);\n\t\t\tconst providers = runtime.contextMemoryProvidersByExtension.get(extension.path) ?? new Set();\n\t\t\tproviders.add(provider);\n\t\t\truntime.contextMemoryProvidersByExtension.set(extension.path, providers);\n\t\t},\n\n\t\treportSpawnedUsage(usage, opts) {\n\t\t\truntime.assertActive();\n\t\t\truntime.reportSpawnedUsage(usage, opts);\n\t\t},\n\n\t\tsetModel(model) {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.setModel(model);\n\t\t},\n\n\t\tgetThinkingLevel() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getThinkingLevel();\n\t\t},\n\n\t\tsetThinkingLevel(level) {\n\t\t\truntime.assertActive();\n\t\t\truntime.setThinkingLevel(level);\n\t\t},\n\n\t\tregisterProvider(name: string, config: ProviderConfig) {\n\t\t\truntime.assertActive();\n\t\t\truntime.registerProvider(name, config, extension.path);\n\t\t},\n\n\t\tunregisterProvider(name: string) {\n\t\t\truntime.assertActive();\n\t\t\truntime.unregisterProvider(name, extension.path);\n\t\t},\n\n\t\tonDispose(fn: () => void | Promise<void>): void {\n\t\t\truntime.assertActive();\n\t\t\textension.disposers.push(fn);\n\t\t},\n\n\t\t// Track bus subscriptions per extension generation so hot reloads can\n\t\t// unsubscribe replaced generations (see disposeExtensionEventSubscriptions).\n\t\tevents: {\n\t\t\temit: (channel: string, data: unknown) => {\n\t\t\t\truntime.assertActive();\n\t\t\t\teventBus.emit(channel, data);\n\t\t\t},\n\t\t\ton: (channel: string, handler: (data: unknown) => void) => {\n\t\t\t\truntime.assertActive();\n\t\t\t\tconst unsubscribe = eventBus.on(channel, handler);\n\t\t\t\textension.eventUnsubscribes.push(unsubscribe);\n\t\t\t\treturn unsubscribe;\n\t\t\t},\n\t\t},\n\t} as ExtensionAPI;\n\n\treturn api;\n}\n\nasync function runExtensionFactory(\n\tfactory: ExtensionFactory,\n\tapi: ExtensionAPI,\n\ttimeoutMs: number = EXTENSION_FACTORY_TIMEOUT_MS,\n): Promise<void> {\n\tlet timeout: ReturnType<typeof setTimeout> | undefined;\n\tconst completion = Promise.resolve().then(() => factory(api));\n\ttry {\n\t\tawait Promise.race([\n\t\t\tcompletion,\n\t\t\tnew Promise<never>((_resolve, reject) => {\n\t\t\t\ttimeout = setTimeout(\n\t\t\t\t\t() => reject(new ExtensionFactoryTimeoutError(`Extension factory timed out after ${timeoutMs}ms`)),\n\t\t\t\t\tMath.max(0, timeoutMs),\n\t\t\t\t);\n\t\t\t}),\n\t\t]);\n\t} finally {\n\t\tif (timeout) clearTimeout(timeout);\n\t}\n}\n\nasync function loadExtensionModule(extensionPath: string, opts?: { fresh?: boolean; moduleTimeoutMs?: number }) {\n\tconst jiti = createJiti(import.meta.url, {\n\t\tmoduleCache: false,\n\t\t// In Bun binary: use virtualModules for bundled packages (no filesystem resolution)\n\t\t// Also disable tryNative so jiti handles ALL imports (not just the entry point)\n\t\t// In Node.js/dev: use aliases to resolve to node_modules paths\n\t\t...(isBunBinary ? { virtualModules: VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() }),\n\t});\n\n\t// When fresh mode is enabled, append a cache-busting query string to force re-import\n\tlet resolvedPath = extensionPath;\n\tif (opts?.fresh) {\n\t\tconst separator = extensionPath.includes(\"?\") ? \"&\" : \"?\";\n\t\tresolvedPath = `${extensionPath}${separator}fresh=${Date.now()}`;\n\t}\n\n\tconst timeoutMs = opts?.moduleTimeoutMs ?? EXTENSION_FACTORY_TIMEOUT_MS;\n\tlet timeout: ReturnType<typeof setTimeout> | undefined;\n\ttry {\n\t\tconst module = await Promise.race([\n\t\t\tjiti.import(resolvedPath, { default: true }),\n\t\t\tnew Promise<never>((_resolve, reject) => {\n\t\t\t\ttimeout = setTimeout(\n\t\t\t\t\t() => reject(new Error(`Extension module import timed out after ${timeoutMs}ms: ${extensionPath}`)),\n\t\t\t\t\tMath.max(0, timeoutMs),\n\t\t\t\t);\n\t\t\t}),\n\t\t]);\n\t\tconst factory = module as ExtensionFactory;\n\t\treturn typeof factory !== \"function\" ? undefined : factory;\n\t} finally {\n\t\tif (timeout) clearTimeout(timeout);\n\t}\n}\n\n/**\n * Create an Extension object with empty collections.\n */\nfunction createExtension(extensionPath: string, resolvedPath: string): Extension {\n\tconst source =\n\t\textensionPath.startsWith(\"<\") && extensionPath.endsWith(\">\")\n\t\t\t? extensionPath.slice(1, -1).split(\":\")[0] || \"temporary\"\n\t\t\t: \"local\";\n\tconst baseDir = extensionPath.startsWith(\"<\") ? undefined : path.dirname(resolvedPath);\n\n\treturn {\n\t\tpath: extensionPath,\n\t\tresolvedPath,\n\t\tsourceInfo: createSyntheticSourceInfo(extensionPath, { source, baseDir }),\n\t\thandlers: new Map(),\n\t\ttools: new Map(),\n\t\tmessageRenderers: new Map(),\n\t\tcommands: new Map(),\n\t\tflags: new Map(),\n\t\tshortcuts: new Map(),\n\t\teventUnsubscribes: [],\n\t\tdisposers: [],\n\t};\n}\n\n/**\n * Unsubscribe a replaced extension generation's pi.events handlers from the shared\n * event bus and invoke cleanup callbacks. Without this, every hot reload leaves the\n * previous generation's handlers subscribed, pinning the old module graph in memory\n * and double-processing events.\n */\nexport async function disposeExtensionEventSubscriptions(\n\textensions: Extension[],\n\toptions: { deactivate?: boolean; timeoutMs?: number } = {},\n): Promise<void> {\n\tconst deadline = Date.now() + (options.timeoutMs ?? EXTENSION_DISPOSAL_TIMEOUT_MS);\n\tfor (const extension of extensions) {\n\t\tif (options.deactivate ?? true) inactiveExtensions.add(extension);\n\t\t// Unsubscribe event listeners\n\t\tfor (const unsubscribe of extension.eventUnsubscribes) {\n\t\t\ttry {\n\t\t\t\tunsubscribe();\n\t\t\t} catch {\n\t\t\t\t// Disposal must never break a reload.\n\t\t\t}\n\t\t}\n\t\textension.eventUnsubscribes.length = 0;\n\n\t\t// Invoke every cleanup callback, but never let one unresolved async disposer block reload or\n\t\t// shutdown indefinitely. Late continuations retain rejection handlers and the inactive API guard.\n\t\tfor (const disposer of extension.disposers) {\n\t\t\ttry {\n\t\t\t\tconst result = disposer();\n\t\t\t\tif (result !== undefined) {\n\t\t\t\t\tconst completion = Promise.resolve(result).catch(() => undefined);\n\t\t\t\t\tconst remainingMs = Math.max(0, deadline - Date.now());\n\t\t\t\t\tif (remainingMs > 0) {\n\t\t\t\t\t\tlet timeout: ReturnType<typeof setTimeout> | undefined;\n\t\t\t\t\t\tawait Promise.race([\n\t\t\t\t\t\t\tcompletion,\n\t\t\t\t\t\t\tnew Promise<void>((resolve) => {\n\t\t\t\t\t\t\t\ttimeout = setTimeout(resolve, remainingMs);\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t]);\n\t\t\t\t\t\tif (timeout) clearTimeout(timeout);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Disposal must never break a reload.\n\t\t\t}\n\t\t}\n\t\textension.disposers.length = 0;\n\t}\n}\n\ninterface ExtensionLoadRuntimeSnapshot {\n\tpendingProviderRegistrations: ExtensionRuntime[\"pendingProviderRegistrations\"];\n\tflagValues: Map<string, boolean | string>;\n\tproviderRegistrations: ExtensionRuntime[\"providerRegistrations\"];\n}\n\nfunction snapshotExtensionLoadRuntime(runtime: ExtensionRuntime): ExtensionLoadRuntimeSnapshot {\n\treturn {\n\t\tpendingProviderRegistrations: [...runtime.pendingProviderRegistrations],\n\t\tflagValues: new Map(runtime.flagValues),\n\t\tproviderRegistrations: new Map(runtime.providerRegistrations),\n\t};\n}\n\nfunction restoreExtensionLoadRuntime(runtime: ExtensionRuntime, snapshot: ExtensionLoadRuntimeSnapshot): void {\n\tfor (const [name, current] of runtime.providerRegistrations) {\n\t\tconst previous = snapshot.providerRegistrations.get(name);\n\t\tif (!previous || previous.config !== current.config || previous.extensionPath !== current.extensionPath) {\n\t\t\ttry {\n\t\t\t\truntime.unregisterProvider(name, current.extensionPath);\n\t\t\t} catch {\n\t\t\t\t// Preserve the original factory failure; reload rollback remains the outer safety net.\n\t\t\t}\n\t\t}\n\t}\n\tfor (const [name, previous] of snapshot.providerRegistrations) {\n\t\tconst current = runtime.providerRegistrations.get(name);\n\t\tif (current?.config === previous.config && current.extensionPath === previous.extensionPath) continue;\n\t\ttry {\n\t\t\truntime.registerProvider(name, previous.config, previous.extensionPath);\n\t\t} catch {\n\t\t\t// Preserve the original factory failure; reload rollback remains the outer safety net.\n\t\t}\n\t}\n\n\truntime.pendingProviderRegistrations = snapshot.pendingProviderRegistrations;\n\truntime.flagValues.clear();\n\tfor (const [name, value] of snapshot.flagValues) {\n\t\truntime.flagValues.set(name, value);\n\t}\n}\n\nexport async function loadExtension(\n\textensionPath: string,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\topts?: { fresh?: boolean; factoryTimeoutMs?: number; moduleTimeoutMs?: number },\n): Promise<{ extension: Extension | null; error: string | null }> {\n\tconst resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true });\n\n\ttry {\n\t\tconst factory = await loadExtensionModule(resolvedPath, opts);\n\t\tif (!factory) {\n\t\t\treturn { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` };\n\t\t}\n\n\t\tconst extension = createExtension(extensionPath, resolvedPath);\n\t\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\t\tconst runtimeSnapshot = snapshotExtensionLoadRuntime(runtime);\n\t\ttry {\n\t\t\tawait runExtensionFactory(factory, api, opts?.factoryTimeoutMs);\n\t\t} catch (err) {\n\t\t\tawait disposeExtensionEventSubscriptions([extension]);\n\t\t\trestoreExtensionLoadRuntime(runtime, runtimeSnapshot);\n\t\t\tthrow err;\n\t\t}\n\n\t\treturn { extension, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { extension: null, error: `Failed to load extension: ${message}` };\n\t}\n}\n\nfunction createLazyToolDefinition(\n\textension: Extension,\n\tmanifest: LazyToolManifest,\n\tload: () => Promise<void>,\n): ToolDefinition {\n\tconst name = String(manifest.name).trim();\n\tconst definition: ToolDefinition = {\n\t\tname,\n\t\tlabel: typeof manifest.label === \"string\" && manifest.label.trim() ? manifest.label : name,\n\t\tdescription:\n\t\t\ttypeof manifest.description === \"string\" && manifest.description.trim()\n\t\t\t\t? manifest.description\n\t\t\t\t: `Lazy extension tool ${name}`,\n\t\tparameters: (manifest.parameters ?? defaultLazyToolParameters()) as ToolDefinition[\"parameters\"],\n\t\tpromptSnippet: typeof manifest.promptSnippet === \"string\" ? manifest.promptSnippet : undefined,\n\t\tpromptGuidelines: Array.isArray(manifest.promptGuidelines)\n\t\t\t? manifest.promptGuidelines.filter((item): item is string => typeof item === \"string\")\n\t\t\t: undefined,\n\t\ttoolGroup: typeof manifest.toolGroup === \"string\" ? manifest.toolGroup : undefined,\n\t\texecutionMode:\n\t\t\tmanifest.executionMode === \"parallel\" || manifest.executionMode === \"sequential\"\n\t\t\t\t? manifest.executionMode\n\t\t\t\t: undefined,\n\t\texecute: async (toolCallId, params, signal, onUpdate, ctx) => {\n\t\t\tawait load();\n\t\t\tconst loadedDefinition = extension.tools.get(name)?.definition;\n\t\t\tif (!loadedDefinition || loadedDefinition === definition) {\n\t\t\t\tthrow new Error(`Lazy extension ${extension.path} did not register tool ${name}`);\n\t\t\t}\n\t\t\treturn loadedDefinition.execute(toolCallId, params, signal, onUpdate, ctx);\n\t\t},\n\t};\n\treturn definition;\n}\n\nfunction createLazyExtension(\n\textensionPath: string,\n\tresolvedPath: string,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\tlazyTools: LazyToolManifest[],\n): Extension {\n\tconst extension = createExtension(extensionPath, resolvedPath);\n\tlet restoreLazyToolPlaceholders = (): void => {};\n\tconst load = async (): Promise<void> => {\n\t\tif (inactiveExtensions.has(extension)) {\n\t\t\tthrow new Error(`Lazy extension generation is no longer active: ${extension.path}`);\n\t\t}\n\t\tif (extension.lazy?.loaded) return;\n\t\tif (extension.lazy?.loading) return extension.lazy.loading;\n\n\t\tconst loading = (async () => {\n\t\t\tconst factory = await loadExtensionModule(resolvedPath);\n\t\t\tif (!factory) {\n\t\t\t\tthrow new Error(`Extension does not export a valid factory function: ${extensionPath}`);\n\t\t\t}\n\t\t\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\t\t\tconst runtimeSnapshot = snapshotExtensionLoadRuntime(runtime);\n\t\t\ttry {\n\t\t\t\tawait runExtensionFactory(factory, api);\n\t\t\t} catch (error) {\n\t\t\t\tawait disposeExtensionEventSubscriptions([extension], { deactivate: false });\n\t\t\t\trestoreExtensionLoadRuntime(runtime, runtimeSnapshot);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (extension.lazy) {\n\t\t\t\textension.lazy.loaded = true;\n\t\t\t\textension.lazy.loading = undefined;\n\t\t\t}\n\t\t})();\n\n\t\tif (extension.lazy) extension.lazy.loading = loading;\n\t\ttry {\n\t\t\tawait loading;\n\t\t} catch (err) {\n\t\t\tawait disposeExtensionEventSubscriptions([extension], {\n\t\t\t\tdeactivate: err instanceof ExtensionFactoryTimeoutError,\n\t\t\t});\n\t\t\textension.handlers.clear();\n\t\t\textension.messageRenderers.clear();\n\t\t\textension.commands.clear();\n\t\t\textension.flags.clear();\n\t\t\textension.shortcuts.clear();\n\t\t\textension.tools.clear();\n\t\t\trestoreLazyToolPlaceholders();\n\t\t\tif (extension.lazy) extension.lazy.loading = undefined;\n\t\t\tthrow err;\n\t\t}\n\t};\n\n\textension.lazy = { loaded: false, load };\n\trestoreLazyToolPlaceholders = () => {\n\t\tfor (const tool of lazyTools) {\n\t\t\tconst name = String(tool.name).trim();\n\t\t\textension.tools.set(name, {\n\t\t\t\tdefinition: createLazyToolDefinition(extension, tool, load),\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t});\n\t\t}\n\t};\n\trestoreLazyToolPlaceholders();\n\treturn extension;\n}\n\n/**\n * Create an Extension from an inline factory function.\n */\nexport async function loadExtensionFromFactory(\n\tfactory: ExtensionFactory,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\textensionPath = \"<inline>\",\n\toptions: { factoryTimeoutMs?: number } = {},\n): Promise<Extension> {\n\tconst extension = createExtension(extensionPath, extensionPath);\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst api = createExtensionAPI(extension, runtime, resolvedCwd, eventBus);\n\tconst runtimeSnapshot = snapshotExtensionLoadRuntime(runtime);\n\ttry {\n\t\tawait runExtensionFactory(factory, api, options.factoryTimeoutMs);\n\t} catch (err) {\n\t\tawait disposeExtensionEventSubscriptions([extension]);\n\t\trestoreExtensionLoadRuntime(runtime, runtimeSnapshot);\n\t\tthrow err;\n\t}\n\treturn extension;\n}\n\n/**\n * Load extensions from paths.\n */\nexport async function loadExtensions(\n\tpaths: Array<string | ExtensionLoadSpec>,\n\tcwd: string,\n\teventBus?: EventBus,\n): Promise<LoadExtensionsResult> {\n\tconst extensions: Extension[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst resolvedEventBus = eventBus ?? createEventBus();\n\tconst runtime = createExtensionRuntime();\n\n\tfor (const spec of paths) {\n\t\tconst normalized = typeof spec === \"string\" ? { path: spec } : spec;\n\t\tconst extPath = normalized.path;\n\t\tconst resolvedPath = resolvePath(extPath, resolvedCwd, { normalizeUnicodeSpaces: true });\n\t\tconst lazyTools = (normalized.lazyTools ?? inferLazyToolsForExtensionPath(resolvedPath))?.filter(\n\t\t\t(tool) => typeof tool.name === \"string\" && tool.name.trim(),\n\t\t);\n\t\t// Extension imports can be CPU-heavy under jiti. Yield around each load so\n\t\t// interactive reloads can repaint/status-update instead of freezing the TUI\n\t\t// for the whole extension set. Lazy tool manifests skip import entirely here.\n\t\tawait yieldToEventLoop();\n\t\tif (lazyTools?.length) {\n\t\t\textensions.push(createLazyExtension(extPath, resolvedPath, resolvedCwd, resolvedEventBus, runtime, lazyTools));\n\t\t\tawait yieldToEventLoop();\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { extension, error } = await loadExtension(extPath, resolvedCwd, resolvedEventBus, runtime);\n\t\tawait yieldToEventLoop();\n\n\t\tif (error) {\n\t\t\terrors.push({ path: extPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (extension) {\n\t\t\textensions.push(extension);\n\t\t}\n\t}\n\n\treturn {\n\t\textensions,\n\t\terrors,\n\t\truntime,\n\t};\n}\n\ninterface LazyToolManifest {\n\tname?: unknown;\n\tlabel?: unknown;\n\tdescription?: unknown;\n\tparameters?: unknown;\n\tpromptSnippet?: unknown;\n\tpromptGuidelines?: unknown;\n\ttoolGroup?: unknown;\n\texecutionMode?: unknown;\n\t/** Optional extension entry this tool belongs to when a package declares multiple entries. */\n\textension?: unknown;\n}\n\ninterface PiManifest {\n\textensions?: string[];\n\tthemes?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n\t/** Tool metadata for opt-in lazy extension loading. Factories stay unloaded until one of these tools runs. */\n\tlazyTools?: LazyToolManifest[];\n\t/** Alternate nested spelling: { tools: [...] }. */\n\tlazy?: { tools?: LazyToolManifest[] } | boolean;\n}\n\ninterface ExtensionLoadSpec {\n\tpath: string;\n\tlazyTools?: LazyToolManifest[];\n}\n\nfunction getManifestLazyTools(manifest: PiManifest | null): LazyToolManifest[] | undefined {\n\tif (!manifest) return undefined;\n\tconst tools = Array.isArray(manifest.lazyTools)\n\t\t? manifest.lazyTools\n\t\t: typeof manifest.lazy === \"object\" && Array.isArray(manifest.lazy.tools)\n\t\t\t? manifest.lazy.tools\n\t\t\t: undefined;\n\treturn tools?.filter((tool) => tool && typeof tool.name === \"string\" && tool.name.trim());\n}\n\nfunction defaultLazyToolParameters(): ToolDefinition[\"parameters\"] {\n\treturn { type: \"object\", properties: {}, additionalProperties: false } as ToolDefinition[\"parameters\"];\n}\n\nfunction lazyToolsForEntry(tools: LazyToolManifest[] | undefined, dir: string, entryPath: string, entryCount: number) {\n\tif (!tools?.length) return undefined;\n\tconst selected = tools.filter((tool) => {\n\t\tif (typeof tool.extension !== \"string\" || !tool.extension.trim()) {\n\t\t\treturn entryCount === 1;\n\t\t}\n\t\treturn path.resolve(dir, tool.extension) === entryPath;\n\t});\n\treturn selected.length > 0 ? selected : undefined;\n}\n\nfunction inferLazyToolsForExtensionPath(resolvedPath: string): LazyToolManifest[] | undefined {\n\tconst dir = path.dirname(resolvedPath);\n\tconst manifest = readPiManifest(path.join(dir, \"package.json\"));\n\tconst lazyTools = getManifestLazyTools(manifest);\n\tif (!manifest?.extensions?.length || !lazyTools?.length) return undefined;\n\tconst resolvedEntries = manifest.extensions.map((extPath) => path.resolve(dir, extPath));\n\tif (!resolvedEntries.includes(resolvedPath)) return undefined;\n\treturn lazyToolsForEntry(lazyTools, dir, resolvedPath, resolvedEntries.length);\n}\n\nfunction readPiManifest(packageJsonPath: string): PiManifest | null {\n\ttry {\n\t\tconst content = fs.readFileSync(packageJsonPath, \"utf-8\");\n\t\tconst pkg = JSON.parse(content);\n\t\tif (pkg.pi && typeof pkg.pi === \"object\") {\n\t\t\treturn pkg.pi as PiManifest;\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction isExtensionFile(name: string): boolean {\n\treturn name.endsWith(\".ts\") || name.endsWith(\".js\");\n}\n\n/**\n * Resolve extension entry points from a directory.\n *\n * Checks for:\n * 1. package.json with \"pi.extensions\" field -> returns declared paths\n * 2. index.ts or index.js -> returns the index file\n *\n * Returns resolved paths or null if no entry points found.\n */\nfunction resolveExtensionEntries(dir: string): ExtensionLoadSpec[] | null {\n\t// Check for package.json with \"pi\" field first\n\tconst packageJsonPath = path.join(dir, \"package.json\");\n\tif (fs.existsSync(packageJsonPath)) {\n\t\tconst manifest = readPiManifest(packageJsonPath);\n\t\tif (manifest?.extensions?.length) {\n\t\t\tconst resolvedEntries = manifest.extensions\n\t\t\t\t.map((extPath) => path.resolve(dir, extPath))\n\t\t\t\t.filter((resolvedExtPath) => fs.existsSync(resolvedExtPath));\n\t\t\tif (resolvedEntries.length > 0) {\n\t\t\t\tconst lazyTools = getManifestLazyTools(manifest);\n\t\t\t\treturn resolvedEntries.map((entryPath) => ({\n\t\t\t\t\tpath: entryPath,\n\t\t\t\t\tlazyTools: lazyToolsForEntry(lazyTools, dir, entryPath, resolvedEntries.length),\n\t\t\t\t}));\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for index.ts or index.js\n\tconst indexTs = path.join(dir, \"index.ts\");\n\tconst indexJs = path.join(dir, \"index.js\");\n\tif (fs.existsSync(indexTs)) {\n\t\treturn [{ path: indexTs }];\n\t}\n\tif (fs.existsSync(indexJs)) {\n\t\treturn [{ path: indexJs }];\n\t}\n\n\treturn null;\n}\n\n/**\n * Discover extensions in a directory.\n *\n * Discovery rules:\n * 1. Direct files: `extensions/*.ts` or `*.js` → load\n * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load\n * 3. Subdirectory with package.json: `extensions/* /package.json` with \"pi\" field → load what it declares\n *\n * No recursion beyond one level. Complex packages must use package.json manifest.\n */\nfunction discoverExtensionsInDir(dir: string): ExtensionLoadSpec[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst discovered: ExtensionLoadSpec[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst entryPath = path.join(dir, entry.name);\n\n\t\t\t// 1. Direct files: *.ts or *.js\n\t\t\tif ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {\n\t\t\t\tdiscovered.push({ path: entryPath });\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 2 & 3. Subdirectories\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\tconst entries = resolveExtensionEntries(entryPath);\n\t\t\t\tif (entries) {\n\t\t\t\t\tdiscovered.push(...entries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn discovered;\n}\n\n/**\n * Discover and load extensions from standard locations.\n */\nexport async function discoverAndLoadExtensions(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n\teventBus?: EventBus,\n): Promise<LoadExtensionsResult> {\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst resolvedAgentDir = resolvePath(agentDir);\n\tconst allPaths: ExtensionLoadSpec[] = [];\n\tconst seen = new Set<string>();\n\n\tconst addPaths = (paths: Array<string | ExtensionLoadSpec>) => {\n\t\tfor (const spec of paths) {\n\t\t\tconst normalized = typeof spec === \"string\" ? { path: spec } : spec;\n\t\t\tconst resolved = path.resolve(normalized.path);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(normalized);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/\n\tconst localExtDir = path.join(resolvedCwd, CONFIG_DIR_NAME, \"extensions\");\n\taddPaths(discoverExtensionsInDir(localExtDir));\n\n\t// 2. Global extensions: agentDir/extensions/\n\tconst globalExtDir = path.join(resolvedAgentDir, \"extensions\");\n\taddPaths(discoverExtensionsInDir(globalExtDir));\n\n\t// 3. Explicitly configured paths\n\tfor (const p of configuredPaths) {\n\t\tconst resolved = resolvePath(p, resolvedCwd, { normalizeUnicodeSpaces: true });\n\t\tif (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n\t\t\t// Check for package.json with pi manifest or index.ts\n\t\t\tconst entries = resolveExtensionEntries(resolved);\n\t\t\tif (entries) {\n\t\t\t\taddPaths(entries);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// No explicit entries - discover individual files in directory\n\t\t\taddPaths(discoverExtensionsInDir(resolved));\n\t\t\tcontinue;\n\t\t}\n\n\t\taddPaths([resolved]);\n\t}\n\n\treturn loadExtensions(allPaths, resolvedCwd, eventBus);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAuBH,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAIhE,OAAO,KAAK,EACX,SAAS,EAET,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EAKpB,MAAM,YAAY,CAAC;AAkJpB;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,gBAAgB,CAiEzD;AA2SD;;;;;GAKG;AACH,wBAAsB,kCAAkC,CACvD,UAAU,EAAE,SAAS,EAAE,EACvB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACxD,OAAO,CAAC,IAAI,CAAC,CAuCf;AA4CD,wBAAsB,aAAa,CAClC,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,gBAAgB,EACzB,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,OAAO,CAAC;IAAE,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAyBhE;AA2GD;;GAEG;AACH,wBAAsB,wBAAwB,CAC7C,OAAO,EAAE,gBAAgB,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,gBAAgB,EACzB,aAAa,SAAa,EAC1B,OAAO,GAAE;IAAE,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAO,GACzC,OAAO,CAAC,SAAS,CAAC,CAapB;AAED;;GAEG;AACH,wBAAsB,cAAc,CACnC,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,iBAAiB,CAAC,EACxC,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,oBAAoB,CAAC,CA0C/B;AAED,UAAU,gBAAgB;IACzB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,8FAA8F;IAC9F,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAaD,UAAU,iBAAiB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC/B;AA2ID;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAsB,EAChC,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,oBAAoB,CAAC,CA4C/B","sourcesContent":["/**\n * Extension loader - loads TypeScript extension modules using jiti.\n *\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport * as _bundledPiAgentCore from \"@caupulican/pi-agent-core\";\nimport * as _bundledPiAi from \"@caupulican/pi-ai\";\nimport * as _bundledPiAiOauth from \"@caupulican/pi-ai/oauth\";\nimport type { KeyId } from \"@caupulican/pi-tui\";\nimport * as _bundledPiTui from \"@caupulican/pi-tui\";\nimport { createJiti } from \"jiti/static\";\n// Static imports of packages that extensions may use.\n// These MUST be static so Bun bundles them into the compiled binary.\n// The virtualModules option then makes them available to extensions.\nimport * as _bundledTypebox from \"typebox\";\nimport * as _bundledTypeboxCompile from \"typebox/compile\";\nimport * as _bundledTypeboxValue from \"typebox/value\";\nimport { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from \"../../config.ts\";\n// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,\n// avoiding a circular dependency. Extensions can import from @caupulican/pi-adaptative.\nimport * as _bundledPiCodingAgent from \"../../index.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\nimport { createEventBus, type EventBus } from \"../event-bus.ts\";\nimport type { ExecOptions } from \"../exec.ts\";\nimport { execCommand } from \"../exec.ts\";\nimport { createSyntheticSourceInfo } from \"../source-info.ts\";\nimport type {\n\tExtension,\n\tExtensionAPI,\n\tExtensionFactory,\n\tExtensionRuntime,\n\tLoadExtensionsResult,\n\tMessageRenderer,\n\tProviderConfig,\n\tRegisteredCommand,\n\tToolDefinition,\n} from \"./types.ts\";\n\nconst EXTENSION_FACTORY_TIMEOUT_MS = 30_000;\nconst EXTENSION_DISPOSAL_TIMEOUT_MS = 5_000;\nconst inactiveExtensions = new WeakSet<Extension>();\n\nclass ExtensionFactoryTimeoutError extends Error {}\n\n/** Modules available to extensions via virtualModules (for compiled Bun binary) */\nconst VIRTUAL_MODULES: Record<string, unknown> = {\n\ttypebox: _bundledTypebox,\n\t\"typebox/compile\": _bundledTypeboxCompile,\n\t\"typebox/value\": _bundledTypeboxValue,\n\t\"@sinclair/typebox\": _bundledTypebox,\n\t\"@sinclair/typebox/compile\": _bundledTypeboxCompile,\n\t\"@sinclair/typebox/value\": _bundledTypeboxValue,\n\t\"@caupulican/pi-agent-core\": _bundledPiAgentCore,\n\t\"@caupulican/pi-tui\": _bundledPiTui,\n\t\"@caupulican/pi-ai\": _bundledPiAi,\n\t\"@caupulican/pi-ai/oauth\": _bundledPiAiOauth,\n\t\"@caupulican/pi-adaptative\": _bundledPiCodingAgent,\n\t\"@earendil-works/pi-agent-core\": _bundledPiAgentCore,\n\t\"@earendil-works/pi-tui\": _bundledPiTui,\n\t\"@earendil-works/pi-ai\": _bundledPiAi,\n\t\"@earendil-works/pi-ai/oauth\": _bundledPiAiOauth,\n\t\"@earendil-works/pi-coding-agent\": _bundledPiCodingAgent,\n\t\"@mariozechner/pi-agent-core\": _bundledPiAgentCore,\n\t\"@mariozechner/pi-tui\": _bundledPiTui,\n\t\"@mariozechner/pi-ai\": _bundledPiAi,\n\t\"@mariozechner/pi-ai/oauth\": _bundledPiAiOauth,\n\t\"@mariozechner/pi-coding-agent\": _bundledPiCodingAgent,\n};\n\nfunction uniquePaths(paths: string[]): string[] {\n\treturn [...new Set(paths)];\n}\n\nfunction safeRealpath(filePath: string): string {\n\ttry {\n\t\treturn fs.realpathSync(filePath);\n\t} catch {\n\t\treturn filePath;\n\t}\n}\n\n/**\n * Get aliases for jiti (used in Node.js/development mode).\n * In Bun binary mode, virtualModules is used instead.\n */\nlet _aliases: Record<string, string> | null = null;\n\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst loaderFile = fileURLToPath(import.meta.url);\n\tconst realLoaderFile = safeRealpath(loaderFile);\n\tconst __dirname = path.dirname(loaderFile);\n\tconst realDirname = path.dirname(realLoaderFile);\n\tconst packageIndex = [\n\t\tpath.resolve(__dirname, \"../..\", \"index.ts\"),\n\t\tpath.resolve(__dirname, \"../..\", \"index.js\"),\n\t].find((candidate) => fs.existsSync(candidate));\n\tif (!packageIndex) throw new Error(\"Unable to resolve the coding-agent package entry\");\n\n\tconst moduleRequires = uniquePaths([loaderFile, realLoaderFile]).map((file) => createRequire(file));\n\tconst resolveModule = (specifier: string): string => {\n\t\tfor (const moduleRequire of moduleRequires) {\n\t\t\ttry {\n\t\t\t\treturn moduleRequire.resolve(specifier);\n\t\t\t} catch {\n\t\t\t\t// Try the next resolution base. Linked global installs may resolve the\n\t\t\t\t// loader through the global symlink path while workspace dependencies are\n\t\t\t\t// hoisted beside the real source path.\n\t\t\t}\n\t\t}\n\t\treturn fileURLToPath(import.meta.resolve(specifier));\n\t};\n\n\tconst typeboxEntry = resolveModule(\"typebox\");\n\tconst typeboxCompileEntry = resolveModule(\"typebox/compile\");\n\tconst typeboxValueEntry = resolveModule(\"typebox/value\");\n\n\tconst packagesRoots = uniquePaths([\n\t\tpath.resolve(__dirname, \"../../../../\"),\n\t\tpath.resolve(realDirname, \"../../../../\"),\n\t]);\n\tconst resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string => {\n\t\tfor (const packagesRoot of packagesRoots) {\n\t\t\tconst workspacePath = path.join(packagesRoot, workspaceRelativePath);\n\t\t\tif (fs.existsSync(workspacePath)) {\n\t\t\t\treturn workspacePath;\n\t\t\t}\n\t\t}\n\t\treturn resolveModule(specifier);\n\t};\n\n\tconst piCodingAgentEntry = packageIndex;\n\tconst piAgentCoreEntry = resolveWorkspaceOrImport(\"agent/src/index.ts\", \"@caupulican/pi-agent-core\");\n\tconst piAgentCoreNodeEntry = resolveWorkspaceOrImport(\"agent/src/node.ts\", \"@caupulican/pi-agent-core/node\");\n\tconst piAgentCorePathsEntry = resolveWorkspaceOrImport(\n\t\t\"agent/src/utils/paths.ts\",\n\t\t\"@caupulican/pi-agent-core/paths\",\n\t);\n\tconst piTuiEntry = resolveWorkspaceOrImport(\"tui/src/index.ts\", \"@caupulican/pi-tui\");\n\tconst piAiEntry = resolveWorkspaceOrImport(\"ai/src/index.ts\", \"@caupulican/pi-ai\");\n\tconst piAiOauthEntry = resolveWorkspaceOrImport(\"ai/src/oauth.ts\", \"@caupulican/pi-ai/oauth\");\n\n\t_aliases = {\n\t\t\"@caupulican/pi-agent-core/node\": piAgentCoreNodeEntry,\n\t\t\"@caupulican/pi-agent-core/paths\": piAgentCorePathsEntry,\n\t\t\"@caupulican/pi-ai/oauth\": piAiOauthEntry,\n\t\t\"@caupulican/pi-adaptative\": piCodingAgentEntry,\n\t\t\"@caupulican/pi-agent-core\": piAgentCoreEntry,\n\t\t\"@caupulican/pi-tui\": piTuiEntry,\n\t\t\"@caupulican/pi-ai\": piAiEntry,\n\t\t\"@earendil-works/pi-agent-core/node\": piAgentCoreNodeEntry,\n\t\t\"@earendil-works/pi-agent-core/paths\": piAgentCorePathsEntry,\n\t\t\"@earendil-works/pi-ai/oauth\": piAiOauthEntry,\n\t\t\"@earendil-works/pi-coding-agent\": piCodingAgentEntry,\n\t\t\"@earendil-works/pi-agent-core\": piAgentCoreEntry,\n\t\t\"@earendil-works/pi-tui\": piTuiEntry,\n\t\t\"@earendil-works/pi-ai\": piAiEntry,\n\t\t\"@mariozechner/pi-agent-core/node\": piAgentCoreNodeEntry,\n\t\t\"@mariozechner/pi-agent-core/paths\": piAgentCorePathsEntry,\n\t\t\"@mariozechner/pi-ai/oauth\": piAiOauthEntry,\n\t\t\"@mariozechner/pi-coding-agent\": piCodingAgentEntry,\n\t\t\"@mariozechner/pi-agent-core\": piAgentCoreEntry,\n\t\t\"@mariozechner/pi-tui\": piTuiEntry,\n\t\t\"@mariozechner/pi-ai\": piAiEntry,\n\t\ttypebox: typeboxEntry,\n\t\t\"typebox/compile\": typeboxCompileEntry,\n\t\t\"typebox/value\": typeboxValueEntry,\n\t\t\"@sinclair/typebox\": typeboxEntry,\n\t\t\"@sinclair/typebox/compile\": typeboxCompileEntry,\n\t\t\"@sinclair/typebox/value\": typeboxValueEntry,\n\t};\n\n\treturn _aliases;\n}\n\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\nfunction yieldToEventLoop(): Promise<void> {\n\treturn new Promise((resolve) => setImmediate(resolve));\n}\n\n/**\n * Create a runtime with throwing stubs for action methods.\n * Runner.bindCore() replaces these with real implementations.\n */\nexport function createExtensionRuntime(): ExtensionRuntime {\n\tconst notInitialized = () => {\n\t\tthrow new Error(\"Extension runtime not initialized. Action methods cannot be called during extension loading.\");\n\t};\n\tconst state: { staleMessage?: string } = {};\n\tconst assertActive = () => {\n\t\tif (state.staleMessage) {\n\t\t\tthrow new Error(state.staleMessage);\n\t\t}\n\t};\n\n\tconst providersByExtension = new Map<string, Set<string>>();\n\tconst providerRegistrations = new Map<string, { config: ProviderConfig; extensionPath?: string }>();\n\tconst memoryProvidersByExtension = new Map<string, Set<Parameters<ExtensionRuntime[\"registerMemoryProvider\"]>[0]>>();\n\tconst contextMemoryProvidersByExtension = new Map<\n\t\tstring,\n\t\tSet<Parameters<ExtensionRuntime[\"registerContextMemoryProvider\"]>[0]>\n\t>();\n\n\tconst runtime: ExtensionRuntime = {\n\t\tsendMessage: notInitialized,\n\t\tsendUserMessage: notInitialized,\n\t\tappendEntry: notInitialized,\n\t\tsetSessionName: notInitialized,\n\t\tgetSessionName: notInitialized,\n\t\tsetLabel: notInitialized,\n\t\tgetActiveTools: notInitialized,\n\t\tgetAllTools: notInitialized,\n\t\tsetActiveTools: notInitialized,\n\t\t// registerTool() is valid during extension load; refresh is only needed post-bind.\n\t\trefreshTools: () => {},\n\t\tgetCommands: notInitialized,\n\t\tsetModel: notInitialized,\n\t\tgetThinkingLevel: notInitialized,\n\t\tsetThinkingLevel: notInitialized,\n\t\tgetExternalResourceRoots: notInitialized,\n\t\tregisterMemoryProvider: notInitialized,\n\t\tregisterContextMemoryProvider: notInitialized,\n\t\treportSpawnedUsage: notInitialized,\n\t\tflagValues: new Map(),\n\t\tpendingProviderRegistrations: [],\n\t\tassertActive,\n\t\tinvalidate: (message) => {\n\t\t\tstate.staleMessage ??=\n\t\t\t\tmessage ??\n\t\t\t\t\"This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().\";\n\t\t},\n\t\t// Pre-bind: queue registrations so bindCore() can flush them once the\n\t\t// model registry is available. bindCore() replaces both with direct calls.\n\t\tregisterProvider: (name, config, extensionPath = \"<unknown>\") => {\n\t\t\truntime.pendingProviderRegistrations.push({ name, config, extensionPath });\n\t\t},\n\t\tunregisterProvider: (name) => {\n\t\t\truntime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);\n\t\t},\n\t\tprovidersByExtension,\n\t\tproviderRegistrations,\n\t\tgetProvidersForExtension: (extensionPath: string) => {\n\t\t\treturn [...(providersByExtension.get(extensionPath) ?? [])];\n\t\t},\n\t\tmemoryProvidersByExtension,\n\t\tcontextMemoryProvidersByExtension,\n\t};\n\n\treturn runtime;\n}\n\n/**\n * Create the ExtensionAPI for an extension.\n * Registration methods write to the extension object.\n * Action methods delegate to the shared runtime.\n */\nfunction createExtensionAPI(\n\textension: Extension,\n\truntime: ExtensionRuntime,\n\tcwd: string,\n\teventBus: EventBus,\n): ExtensionAPI {\n\t// A factory can time out while its promise continues running. Route every later API call through\n\t// an extension-generation guard so stale async work cannot register tools, handlers, or providers.\n\tconst sharedRuntime = runtime;\n\truntime = new Proxy(sharedRuntime, {\n\t\tget(target, property, receiver) {\n\t\t\tif (property === \"assertActive\") {\n\t\t\t\treturn () => {\n\t\t\t\t\ttarget.assertActive();\n\t\t\t\t\tif (inactiveExtensions.has(extension)) {\n\t\t\t\t\t\tthrow new Error(`Extension generation is no longer active: ${extension.path}`);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn Reflect.get(target, property, receiver);\n\t\t},\n\t});\n\n\tconst api = {\n\t\t// Registration methods - write to extension\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\truntime.assertActive();\n\t\t\tconst list = extension.handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\textension.handlers.set(event, list);\n\t\t},\n\n\t\tregisterTool(tool: ToolDefinition): void {\n\t\t\truntime.assertActive();\n\t\t\textension.tools.set(tool.name, {\n\t\t\t\tdefinition: tool,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t});\n\t\t\truntime.refreshTools();\n\t\t},\n\n\t\tregisterCommand(name: string, options: Omit<RegisteredCommand, \"name\" | \"sourceInfo\">): void {\n\t\t\truntime.assertActive();\n\t\t\textension.commands.set(name, {\n\t\t\t\tname,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t\t...options,\n\t\t\t});\n\t\t},\n\n\t\tregisterShortcut(\n\t\t\tshortcut: KeyId,\n\t\t\toptions: {\n\t\t\t\tdescription?: string;\n\t\t\t\thandler: (ctx: import(\"./types.ts\").ExtensionContext) => Promise<void> | void;\n\t\t\t},\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });\n\t\t},\n\n\t\tregisterFlag(\n\t\t\tname: string,\n\t\t\toptions: { description?: string; type: \"boolean\" | \"string\"; default?: boolean | string },\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.flags.set(name, { name, extensionPath: extension.path, ...options });\n\t\t\tif (options.default !== undefined && !runtime.flagValues.has(name)) {\n\t\t\t\truntime.flagValues.set(name, options.default);\n\t\t\t}\n\t\t},\n\n\t\tregisterMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {\n\t\t\truntime.assertActive();\n\t\t\textension.messageRenderers.set(customType, renderer as MessageRenderer);\n\t\t},\n\n\t\t// Flag access - checks extension registered it, reads from runtime\n\t\tgetFlag(name: string): boolean | string | undefined {\n\t\t\truntime.assertActive();\n\t\t\tif (!extension.flags.has(name)) return undefined;\n\t\t\treturn runtime.flagValues.get(name);\n\t\t},\n\n\t\t// Action methods - delegate to shared runtime\n\t\tsendMessage(message, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendMessage(message, options);\n\t\t},\n\n\t\tsendUserMessage(content, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendUserMessage(content, options);\n\t\t},\n\n\t\tappendEntry(customType: string, data?: unknown): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.appendEntry(customType, data);\n\t\t},\n\n\t\tsetSessionName(name: string): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setSessionName(name);\n\t\t},\n\n\t\tgetSessionName(): string | undefined {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getSessionName();\n\t\t},\n\n\t\tsetLabel(entryId: string, label: string | undefined): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setLabel(entryId, label);\n\t\t},\n\n\t\texec(command: string, args: string[], options?: ExecOptions) {\n\t\t\truntime.assertActive();\n\t\t\treturn execCommand(command, args, options?.cwd ?? cwd, options);\n\t\t},\n\n\t\tgetActiveTools(): string[] {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getActiveTools();\n\t\t},\n\n\t\tgetAllTools() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getAllTools();\n\t\t},\n\n\t\tsetActiveTools(toolNames: string[]): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setActiveTools(toolNames);\n\t\t},\n\n\t\tgetCommands() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getCommands();\n\t\t},\n\n\t\tgetExternalResourceRoots() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getExternalResourceRoots();\n\t\t},\n\n\t\tregisterMemoryProvider(provider) {\n\t\t\truntime.assertActive();\n\t\t\truntime.registerMemoryProvider(provider);\n\t\t\tconst providers = runtime.memoryProvidersByExtension.get(extension.path) ?? new Set();\n\t\t\tproviders.add(provider);\n\t\t\truntime.memoryProvidersByExtension.set(extension.path, providers);\n\t\t},\n\n\t\tregisterContextMemoryProvider(provider) {\n\t\t\truntime.assertActive();\n\t\t\truntime.registerContextMemoryProvider(provider);\n\t\t\tconst providers = runtime.contextMemoryProvidersByExtension.get(extension.path) ?? new Set();\n\t\t\tproviders.add(provider);\n\t\t\truntime.contextMemoryProvidersByExtension.set(extension.path, providers);\n\t\t},\n\n\t\treportSpawnedUsage(usage, opts) {\n\t\t\truntime.assertActive();\n\t\t\truntime.reportSpawnedUsage(usage, opts);\n\t\t},\n\n\t\tsetModel(model) {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.setModel(model);\n\t\t},\n\n\t\tgetThinkingLevel() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getThinkingLevel();\n\t\t},\n\n\t\tsetThinkingLevel(level) {\n\t\t\truntime.assertActive();\n\t\t\truntime.setThinkingLevel(level);\n\t\t},\n\n\t\tregisterProvider(name: string, config: ProviderConfig) {\n\t\t\truntime.assertActive();\n\t\t\truntime.registerProvider(name, config, extension.path);\n\t\t},\n\n\t\tunregisterProvider(name: string) {\n\t\t\truntime.assertActive();\n\t\t\truntime.unregisterProvider(name, extension.path);\n\t\t},\n\n\t\tonDispose(fn: () => void | Promise<void>): void {\n\t\t\truntime.assertActive();\n\t\t\textension.disposers.push(fn);\n\t\t},\n\n\t\t// Track bus subscriptions per extension generation so hot reloads can\n\t\t// unsubscribe replaced generations (see disposeExtensionEventSubscriptions).\n\t\tevents: {\n\t\t\temit: (channel: string, data: unknown) => {\n\t\t\t\truntime.assertActive();\n\t\t\t\teventBus.emit(channel, data);\n\t\t\t},\n\t\t\ton: (channel: string, handler: (data: unknown) => void) => {\n\t\t\t\truntime.assertActive();\n\t\t\t\tconst unsubscribe = eventBus.on(channel, handler);\n\t\t\t\textension.eventUnsubscribes.push(unsubscribe);\n\t\t\t\treturn unsubscribe;\n\t\t\t},\n\t\t},\n\t} as ExtensionAPI;\n\n\treturn api;\n}\n\nasync function runExtensionFactory(\n\tfactory: ExtensionFactory,\n\tapi: ExtensionAPI,\n\ttimeoutMs: number = EXTENSION_FACTORY_TIMEOUT_MS,\n): Promise<void> {\n\tlet timeout: ReturnType<typeof setTimeout> | undefined;\n\tconst completion = Promise.resolve().then(() => factory(api));\n\ttry {\n\t\tawait Promise.race([\n\t\t\tcompletion,\n\t\t\tnew Promise<never>((_resolve, reject) => {\n\t\t\t\ttimeout = setTimeout(\n\t\t\t\t\t() => reject(new ExtensionFactoryTimeoutError(`Extension factory timed out after ${timeoutMs}ms`)),\n\t\t\t\t\tMath.max(0, timeoutMs),\n\t\t\t\t);\n\t\t\t}),\n\t\t]);\n\t} finally {\n\t\tif (timeout) clearTimeout(timeout);\n\t}\n}\n\nasync function loadExtensionModule(extensionPath: string, opts?: { fresh?: boolean; moduleTimeoutMs?: number }) {\n\tconst jiti = createJiti(import.meta.url, {\n\t\tmoduleCache: false,\n\t\t// In Bun binary: use virtualModules for bundled packages (no filesystem resolution)\n\t\t// Also disable tryNative so jiti handles ALL imports (not just the entry point)\n\t\t// In Node.js/dev: use aliases to resolve to node_modules paths\n\t\t...(isBunBinary ? { virtualModules: VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() }),\n\t});\n\n\t// Every call gets a fresh jiti instance with moduleCache disabled. Do not append a query string:\n\t// jiti's Windows path resolver treats it as part of the .ts filename.\n\tconst timeoutMs = opts?.moduleTimeoutMs ?? EXTENSION_FACTORY_TIMEOUT_MS;\n\tlet timeout: ReturnType<typeof setTimeout> | undefined;\n\ttry {\n\t\tconst module = await Promise.race([\n\t\t\tjiti.import(extensionPath, { default: true }),\n\t\t\tnew Promise<never>((_resolve, reject) => {\n\t\t\t\ttimeout = setTimeout(\n\t\t\t\t\t() => reject(new Error(`Extension module import timed out after ${timeoutMs}ms: ${extensionPath}`)),\n\t\t\t\t\tMath.max(0, timeoutMs),\n\t\t\t\t);\n\t\t\t}),\n\t\t]);\n\t\tconst factory = module as ExtensionFactory;\n\t\treturn typeof factory !== \"function\" ? undefined : factory;\n\t} finally {\n\t\tif (timeout) clearTimeout(timeout);\n\t}\n}\n\n/**\n * Create an Extension object with empty collections.\n */\nfunction createExtension(extensionPath: string, resolvedPath: string): Extension {\n\tconst source =\n\t\textensionPath.startsWith(\"<\") && extensionPath.endsWith(\">\")\n\t\t\t? extensionPath.slice(1, -1).split(\":\")[0] || \"temporary\"\n\t\t\t: \"local\";\n\tconst baseDir = extensionPath.startsWith(\"<\") ? undefined : path.dirname(resolvedPath);\n\n\treturn {\n\t\tpath: extensionPath,\n\t\tresolvedPath,\n\t\tsourceInfo: createSyntheticSourceInfo(extensionPath, { source, baseDir }),\n\t\thandlers: new Map(),\n\t\ttools: new Map(),\n\t\tmessageRenderers: new Map(),\n\t\tcommands: new Map(),\n\t\tflags: new Map(),\n\t\tshortcuts: new Map(),\n\t\teventUnsubscribes: [],\n\t\tdisposers: [],\n\t};\n}\n\n/**\n * Unsubscribe a replaced extension generation's pi.events handlers from the shared\n * event bus and invoke cleanup callbacks. Without this, every hot reload leaves the\n * previous generation's handlers subscribed, pinning the old module graph in memory\n * and double-processing events.\n */\nexport async function disposeExtensionEventSubscriptions(\n\textensions: Extension[],\n\toptions: { deactivate?: boolean; timeoutMs?: number } = {},\n): Promise<void> {\n\tconst deadline = Date.now() + (options.timeoutMs ?? EXTENSION_DISPOSAL_TIMEOUT_MS);\n\tfor (const extension of extensions) {\n\t\tif (options.deactivate ?? true) inactiveExtensions.add(extension);\n\t\t// Unsubscribe event listeners\n\t\tfor (const unsubscribe of extension.eventUnsubscribes) {\n\t\t\ttry {\n\t\t\t\tunsubscribe();\n\t\t\t} catch {\n\t\t\t\t// Disposal must never break a reload.\n\t\t\t}\n\t\t}\n\t\textension.eventUnsubscribes.length = 0;\n\n\t\t// Invoke every cleanup callback, but never let one unresolved async disposer block reload or\n\t\t// shutdown indefinitely. Late continuations retain rejection handlers and the inactive API guard.\n\t\tfor (const disposer of extension.disposers) {\n\t\t\ttry {\n\t\t\t\tconst result = disposer();\n\t\t\t\tif (result !== undefined) {\n\t\t\t\t\tconst completion = Promise.resolve(result).catch(() => undefined);\n\t\t\t\t\tconst remainingMs = Math.max(0, deadline - Date.now());\n\t\t\t\t\tif (remainingMs > 0) {\n\t\t\t\t\t\tlet timeout: ReturnType<typeof setTimeout> | undefined;\n\t\t\t\t\t\tawait Promise.race([\n\t\t\t\t\t\t\tcompletion,\n\t\t\t\t\t\t\tnew Promise<void>((resolve) => {\n\t\t\t\t\t\t\t\ttimeout = setTimeout(resolve, remainingMs);\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t]);\n\t\t\t\t\t\tif (timeout) clearTimeout(timeout);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Disposal must never break a reload.\n\t\t\t}\n\t\t}\n\t\textension.disposers.length = 0;\n\t}\n}\n\ninterface ExtensionLoadRuntimeSnapshot {\n\tpendingProviderRegistrations: ExtensionRuntime[\"pendingProviderRegistrations\"];\n\tflagValues: Map<string, boolean | string>;\n\tproviderRegistrations: ExtensionRuntime[\"providerRegistrations\"];\n}\n\nfunction snapshotExtensionLoadRuntime(runtime: ExtensionRuntime): ExtensionLoadRuntimeSnapshot {\n\treturn {\n\t\tpendingProviderRegistrations: [...runtime.pendingProviderRegistrations],\n\t\tflagValues: new Map(runtime.flagValues),\n\t\tproviderRegistrations: new Map(runtime.providerRegistrations),\n\t};\n}\n\nfunction restoreExtensionLoadRuntime(runtime: ExtensionRuntime, snapshot: ExtensionLoadRuntimeSnapshot): void {\n\tfor (const [name, current] of runtime.providerRegistrations) {\n\t\tconst previous = snapshot.providerRegistrations.get(name);\n\t\tif (!previous || previous.config !== current.config || previous.extensionPath !== current.extensionPath) {\n\t\t\ttry {\n\t\t\t\truntime.unregisterProvider(name, current.extensionPath);\n\t\t\t} catch {\n\t\t\t\t// Preserve the original factory failure; reload rollback remains the outer safety net.\n\t\t\t}\n\t\t}\n\t}\n\tfor (const [name, previous] of snapshot.providerRegistrations) {\n\t\tconst current = runtime.providerRegistrations.get(name);\n\t\tif (current?.config === previous.config && current.extensionPath === previous.extensionPath) continue;\n\t\ttry {\n\t\t\truntime.registerProvider(name, previous.config, previous.extensionPath);\n\t\t} catch {\n\t\t\t// Preserve the original factory failure; reload rollback remains the outer safety net.\n\t\t}\n\t}\n\n\truntime.pendingProviderRegistrations = snapshot.pendingProviderRegistrations;\n\truntime.flagValues.clear();\n\tfor (const [name, value] of snapshot.flagValues) {\n\t\truntime.flagValues.set(name, value);\n\t}\n}\n\nexport async function loadExtension(\n\textensionPath: string,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\topts?: { fresh?: boolean; factoryTimeoutMs?: number; moduleTimeoutMs?: number },\n): Promise<{ extension: Extension | null; error: string | null }> {\n\tconst resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true });\n\n\ttry {\n\t\tconst factory = await loadExtensionModule(resolvedPath, opts);\n\t\tif (!factory) {\n\t\t\treturn { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` };\n\t\t}\n\n\t\tconst extension = createExtension(extensionPath, resolvedPath);\n\t\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\t\tconst runtimeSnapshot = snapshotExtensionLoadRuntime(runtime);\n\t\ttry {\n\t\t\tawait runExtensionFactory(factory, api, opts?.factoryTimeoutMs);\n\t\t} catch (err) {\n\t\t\tawait disposeExtensionEventSubscriptions([extension]);\n\t\t\trestoreExtensionLoadRuntime(runtime, runtimeSnapshot);\n\t\t\tthrow err;\n\t\t}\n\n\t\treturn { extension, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { extension: null, error: `Failed to load extension: ${message}` };\n\t}\n}\n\nfunction createLazyToolDefinition(\n\textension: Extension,\n\tmanifest: LazyToolManifest,\n\tload: () => Promise<void>,\n): ToolDefinition {\n\tconst name = String(manifest.name).trim();\n\tconst definition: ToolDefinition = {\n\t\tname,\n\t\tlabel: typeof manifest.label === \"string\" && manifest.label.trim() ? manifest.label : name,\n\t\tdescription:\n\t\t\ttypeof manifest.description === \"string\" && manifest.description.trim()\n\t\t\t\t? manifest.description\n\t\t\t\t: `Lazy extension tool ${name}`,\n\t\tparameters: (manifest.parameters ?? defaultLazyToolParameters()) as ToolDefinition[\"parameters\"],\n\t\tpromptSnippet: typeof manifest.promptSnippet === \"string\" ? manifest.promptSnippet : undefined,\n\t\tpromptGuidelines: Array.isArray(manifest.promptGuidelines)\n\t\t\t? manifest.promptGuidelines.filter((item): item is string => typeof item === \"string\")\n\t\t\t: undefined,\n\t\ttoolGroup: typeof manifest.toolGroup === \"string\" ? manifest.toolGroup : undefined,\n\t\texecutionMode:\n\t\t\tmanifest.executionMode === \"parallel\" || manifest.executionMode === \"sequential\"\n\t\t\t\t? manifest.executionMode\n\t\t\t\t: undefined,\n\t\texecute: async (toolCallId, params, signal, onUpdate, ctx) => {\n\t\t\tawait load();\n\t\t\tconst loadedDefinition = extension.tools.get(name)?.definition;\n\t\t\tif (!loadedDefinition || loadedDefinition === definition) {\n\t\t\t\tthrow new Error(`Lazy extension ${extension.path} did not register tool ${name}`);\n\t\t\t}\n\t\t\treturn loadedDefinition.execute(toolCallId, params, signal, onUpdate, ctx);\n\t\t},\n\t};\n\treturn definition;\n}\n\nfunction createLazyExtension(\n\textensionPath: string,\n\tresolvedPath: string,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\tlazyTools: LazyToolManifest[],\n): Extension {\n\tconst extension = createExtension(extensionPath, resolvedPath);\n\tlet restoreLazyToolPlaceholders = (): void => {};\n\tconst load = async (): Promise<void> => {\n\t\tif (inactiveExtensions.has(extension)) {\n\t\t\tthrow new Error(`Lazy extension generation is no longer active: ${extension.path}`);\n\t\t}\n\t\tif (extension.lazy?.loaded) return;\n\t\tif (extension.lazy?.loading) return extension.lazy.loading;\n\n\t\tconst loading = (async () => {\n\t\t\tconst factory = await loadExtensionModule(resolvedPath);\n\t\t\tif (!factory) {\n\t\t\t\tthrow new Error(`Extension does not export a valid factory function: ${extensionPath}`);\n\t\t\t}\n\t\t\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\t\t\tconst runtimeSnapshot = snapshotExtensionLoadRuntime(runtime);\n\t\t\ttry {\n\t\t\t\tawait runExtensionFactory(factory, api);\n\t\t\t} catch (error) {\n\t\t\t\tawait disposeExtensionEventSubscriptions([extension], { deactivate: false });\n\t\t\t\trestoreExtensionLoadRuntime(runtime, runtimeSnapshot);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (extension.lazy) {\n\t\t\t\textension.lazy.loaded = true;\n\t\t\t\textension.lazy.loading = undefined;\n\t\t\t}\n\t\t})();\n\n\t\tif (extension.lazy) extension.lazy.loading = loading;\n\t\ttry {\n\t\t\tawait loading;\n\t\t} catch (err) {\n\t\t\tawait disposeExtensionEventSubscriptions([extension], {\n\t\t\t\tdeactivate: err instanceof ExtensionFactoryTimeoutError,\n\t\t\t});\n\t\t\textension.handlers.clear();\n\t\t\textension.messageRenderers.clear();\n\t\t\textension.commands.clear();\n\t\t\textension.flags.clear();\n\t\t\textension.shortcuts.clear();\n\t\t\textension.tools.clear();\n\t\t\trestoreLazyToolPlaceholders();\n\t\t\tif (extension.lazy) extension.lazy.loading = undefined;\n\t\t\tthrow err;\n\t\t}\n\t};\n\n\textension.lazy = { loaded: false, load };\n\trestoreLazyToolPlaceholders = () => {\n\t\tfor (const tool of lazyTools) {\n\t\t\tconst name = String(tool.name).trim();\n\t\t\textension.tools.set(name, {\n\t\t\t\tdefinition: createLazyToolDefinition(extension, tool, load),\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t});\n\t\t}\n\t};\n\trestoreLazyToolPlaceholders();\n\treturn extension;\n}\n\n/**\n * Create an Extension from an inline factory function.\n */\nexport async function loadExtensionFromFactory(\n\tfactory: ExtensionFactory,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\textensionPath = \"<inline>\",\n\toptions: { factoryTimeoutMs?: number } = {},\n): Promise<Extension> {\n\tconst extension = createExtension(extensionPath, extensionPath);\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst api = createExtensionAPI(extension, runtime, resolvedCwd, eventBus);\n\tconst runtimeSnapshot = snapshotExtensionLoadRuntime(runtime);\n\ttry {\n\t\tawait runExtensionFactory(factory, api, options.factoryTimeoutMs);\n\t} catch (err) {\n\t\tawait disposeExtensionEventSubscriptions([extension]);\n\t\trestoreExtensionLoadRuntime(runtime, runtimeSnapshot);\n\t\tthrow err;\n\t}\n\treturn extension;\n}\n\n/**\n * Load extensions from paths.\n */\nexport async function loadExtensions(\n\tpaths: Array<string | ExtensionLoadSpec>,\n\tcwd: string,\n\teventBus?: EventBus,\n): Promise<LoadExtensionsResult> {\n\tconst extensions: Extension[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst resolvedEventBus = eventBus ?? createEventBus();\n\tconst runtime = createExtensionRuntime();\n\n\tfor (const spec of paths) {\n\t\tconst normalized = typeof spec === \"string\" ? { path: spec } : spec;\n\t\tconst extPath = normalized.path;\n\t\tconst resolvedPath = resolvePath(extPath, resolvedCwd, { normalizeUnicodeSpaces: true });\n\t\tconst lazyTools = (normalized.lazyTools ?? inferLazyToolsForExtensionPath(resolvedPath))?.filter(\n\t\t\t(tool) => typeof tool.name === \"string\" && tool.name.trim(),\n\t\t);\n\t\t// Extension imports can be CPU-heavy under jiti. Yield around each load so\n\t\t// interactive reloads can repaint/status-update instead of freezing the TUI\n\t\t// for the whole extension set. Lazy tool manifests skip import entirely here.\n\t\tawait yieldToEventLoop();\n\t\tif (lazyTools?.length) {\n\t\t\textensions.push(createLazyExtension(extPath, resolvedPath, resolvedCwd, resolvedEventBus, runtime, lazyTools));\n\t\t\tawait yieldToEventLoop();\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { extension, error } = await loadExtension(extPath, resolvedCwd, resolvedEventBus, runtime);\n\t\tawait yieldToEventLoop();\n\n\t\tif (error) {\n\t\t\terrors.push({ path: extPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (extension) {\n\t\t\textensions.push(extension);\n\t\t}\n\t}\n\n\treturn {\n\t\textensions,\n\t\terrors,\n\t\truntime,\n\t};\n}\n\ninterface LazyToolManifest {\n\tname?: unknown;\n\tlabel?: unknown;\n\tdescription?: unknown;\n\tparameters?: unknown;\n\tpromptSnippet?: unknown;\n\tpromptGuidelines?: unknown;\n\ttoolGroup?: unknown;\n\texecutionMode?: unknown;\n\t/** Optional extension entry this tool belongs to when a package declares multiple entries. */\n\textension?: unknown;\n}\n\ninterface PiManifest {\n\textensions?: string[];\n\tthemes?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n\t/** Tool metadata for opt-in lazy extension loading. Factories stay unloaded until one of these tools runs. */\n\tlazyTools?: LazyToolManifest[];\n\t/** Alternate nested spelling: { tools: [...] }. */\n\tlazy?: { tools?: LazyToolManifest[] } | boolean;\n}\n\ninterface ExtensionLoadSpec {\n\tpath: string;\n\tlazyTools?: LazyToolManifest[];\n}\n\nfunction getManifestLazyTools(manifest: PiManifest | null): LazyToolManifest[] | undefined {\n\tif (!manifest) return undefined;\n\tconst tools = Array.isArray(manifest.lazyTools)\n\t\t? manifest.lazyTools\n\t\t: typeof manifest.lazy === \"object\" && Array.isArray(manifest.lazy.tools)\n\t\t\t? manifest.lazy.tools\n\t\t\t: undefined;\n\treturn tools?.filter((tool) => tool && typeof tool.name === \"string\" && tool.name.trim());\n}\n\nfunction defaultLazyToolParameters(): ToolDefinition[\"parameters\"] {\n\treturn { type: \"object\", properties: {}, additionalProperties: false } as ToolDefinition[\"parameters\"];\n}\n\nfunction lazyToolsForEntry(tools: LazyToolManifest[] | undefined, dir: string, entryPath: string, entryCount: number) {\n\tif (!tools?.length) return undefined;\n\tconst selected = tools.filter((tool) => {\n\t\tif (typeof tool.extension !== \"string\" || !tool.extension.trim()) {\n\t\t\treturn entryCount === 1;\n\t\t}\n\t\treturn path.resolve(dir, tool.extension) === entryPath;\n\t});\n\treturn selected.length > 0 ? selected : undefined;\n}\n\nfunction inferLazyToolsForExtensionPath(resolvedPath: string): LazyToolManifest[] | undefined {\n\tconst dir = path.dirname(resolvedPath);\n\tconst manifest = readPiManifest(path.join(dir, \"package.json\"));\n\tconst lazyTools = getManifestLazyTools(manifest);\n\tif (!manifest?.extensions?.length || !lazyTools?.length) return undefined;\n\tconst resolvedEntries = manifest.extensions.map((extPath) => path.resolve(dir, extPath));\n\tif (!resolvedEntries.includes(resolvedPath)) return undefined;\n\treturn lazyToolsForEntry(lazyTools, dir, resolvedPath, resolvedEntries.length);\n}\n\nfunction readPiManifest(packageJsonPath: string): PiManifest | null {\n\ttry {\n\t\tconst content = fs.readFileSync(packageJsonPath, \"utf-8\");\n\t\tconst pkg = JSON.parse(content);\n\t\tif (pkg.pi && typeof pkg.pi === \"object\") {\n\t\t\treturn pkg.pi as PiManifest;\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction isExtensionFile(name: string): boolean {\n\treturn name.endsWith(\".ts\") || name.endsWith(\".js\");\n}\n\n/**\n * Resolve extension entry points from a directory.\n *\n * Checks for:\n * 1. package.json with \"pi.extensions\" field -> returns declared paths\n * 2. index.ts or index.js -> returns the index file\n *\n * Returns resolved paths or null if no entry points found.\n */\nfunction resolveExtensionEntries(dir: string): ExtensionLoadSpec[] | null {\n\t// Check for package.json with \"pi\" field first\n\tconst packageJsonPath = path.join(dir, \"package.json\");\n\tif (fs.existsSync(packageJsonPath)) {\n\t\tconst manifest = readPiManifest(packageJsonPath);\n\t\tif (manifest?.extensions?.length) {\n\t\t\tconst resolvedEntries = manifest.extensions\n\t\t\t\t.map((extPath) => path.resolve(dir, extPath))\n\t\t\t\t.filter((resolvedExtPath) => fs.existsSync(resolvedExtPath));\n\t\t\tif (resolvedEntries.length > 0) {\n\t\t\t\tconst lazyTools = getManifestLazyTools(manifest);\n\t\t\t\treturn resolvedEntries.map((entryPath) => ({\n\t\t\t\t\tpath: entryPath,\n\t\t\t\t\tlazyTools: lazyToolsForEntry(lazyTools, dir, entryPath, resolvedEntries.length),\n\t\t\t\t}));\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for index.ts or index.js\n\tconst indexTs = path.join(dir, \"index.ts\");\n\tconst indexJs = path.join(dir, \"index.js\");\n\tif (fs.existsSync(indexTs)) {\n\t\treturn [{ path: indexTs }];\n\t}\n\tif (fs.existsSync(indexJs)) {\n\t\treturn [{ path: indexJs }];\n\t}\n\n\treturn null;\n}\n\n/**\n * Discover extensions in a directory.\n *\n * Discovery rules:\n * 1. Direct files: `extensions/*.ts` or `*.js` → load\n * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load\n * 3. Subdirectory with package.json: `extensions/* /package.json` with \"pi\" field → load what it declares\n *\n * No recursion beyond one level. Complex packages must use package.json manifest.\n */\nfunction discoverExtensionsInDir(dir: string): ExtensionLoadSpec[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst discovered: ExtensionLoadSpec[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst entryPath = path.join(dir, entry.name);\n\n\t\t\t// 1. Direct files: *.ts or *.js\n\t\t\tif ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {\n\t\t\t\tdiscovered.push({ path: entryPath });\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 2 & 3. Subdirectories\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\tconst entries = resolveExtensionEntries(entryPath);\n\t\t\t\tif (entries) {\n\t\t\t\t\tdiscovered.push(...entries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn discovered;\n}\n\n/**\n * Discover and load extensions from standard locations.\n */\nexport async function discoverAndLoadExtensions(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n\teventBus?: EventBus,\n): Promise<LoadExtensionsResult> {\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst resolvedAgentDir = resolvePath(agentDir);\n\tconst allPaths: ExtensionLoadSpec[] = [];\n\tconst seen = new Set<string>();\n\n\tconst addPaths = (paths: Array<string | ExtensionLoadSpec>) => {\n\t\tfor (const spec of paths) {\n\t\t\tconst normalized = typeof spec === \"string\" ? { path: spec } : spec;\n\t\t\tconst resolved = path.resolve(normalized.path);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(normalized);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/\n\tconst localExtDir = path.join(resolvedCwd, CONFIG_DIR_NAME, \"extensions\");\n\taddPaths(discoverExtensionsInDir(localExtDir));\n\n\t// 2. Global extensions: agentDir/extensions/\n\tconst globalExtDir = path.join(resolvedAgentDir, \"extensions\");\n\taddPaths(discoverExtensionsInDir(globalExtDir));\n\n\t// 3. Explicitly configured paths\n\tfor (const p of configuredPaths) {\n\t\tconst resolved = resolvePath(p, resolvedCwd, { normalizeUnicodeSpaces: true });\n\t\tif (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n\t\t\t// Check for package.json with pi manifest or index.ts\n\t\t\tconst entries = resolveExtensionEntries(resolved);\n\t\t\tif (entries) {\n\t\t\t\taddPaths(entries);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// No explicit entries - discover individual files in directory\n\t\t\taddPaths(discoverExtensionsInDir(resolved));\n\t\t\tcontinue;\n\t\t}\n\n\t\taddPaths([resolved]);\n\t}\n\n\treturn loadExtensions(allPaths, resolvedCwd, eventBus);\n}\n"]}
|
|
@@ -418,17 +418,13 @@ async function loadExtensionModule(extensionPath, opts) {
|
|
|
418
418
|
// In Node.js/dev: use aliases to resolve to node_modules paths
|
|
419
419
|
...(isBunBinary ? { virtualModules: VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() }),
|
|
420
420
|
});
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
if (opts?.fresh) {
|
|
424
|
-
const separator = extensionPath.includes("?") ? "&" : "?";
|
|
425
|
-
resolvedPath = `${extensionPath}${separator}fresh=${Date.now()}`;
|
|
426
|
-
}
|
|
421
|
+
// Every call gets a fresh jiti instance with moduleCache disabled. Do not append a query string:
|
|
422
|
+
// jiti's Windows path resolver treats it as part of the .ts filename.
|
|
427
423
|
const timeoutMs = opts?.moduleTimeoutMs ?? EXTENSION_FACTORY_TIMEOUT_MS;
|
|
428
424
|
let timeout;
|
|
429
425
|
try {
|
|
430
426
|
const module = await Promise.race([
|
|
431
|
-
jiti.import(
|
|
427
|
+
jiti.import(extensionPath, { default: true }),
|
|
432
428
|
new Promise((_resolve, reject) => {
|
|
433
429
|
timeout = setTimeout(() => reject(new Error(`Extension module import timed out after ${timeoutMs}ms: ${extensionPath}`)), Math.max(0, timeoutMs));
|
|
434
430
|
}),
|