@intent-systems/nexus 2026.1.5-3 → 2026.1.5-4

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.
Files changed (39) hide show
  1. package/dist/capabilities/detector.js +214 -0
  2. package/dist/capabilities/registry.js +98 -0
  3. package/dist/channels/location.js +44 -0
  4. package/dist/channels/web/index.js +2 -0
  5. package/dist/control-plane/broker/broker.js +969 -0
  6. package/dist/control-plane/compaction.js +284 -0
  7. package/dist/control-plane/factory.js +31 -0
  8. package/dist/control-plane/index.js +10 -0
  9. package/dist/control-plane/odu/agents.js +187 -0
  10. package/dist/control-plane/odu/interaction-tools.js +196 -0
  11. package/dist/control-plane/odu/prompt-loader.js +95 -0
  12. package/dist/control-plane/odu/runtime.js +467 -0
  13. package/dist/control-plane/odu/types.js +6 -0
  14. package/dist/control-plane/odu-control-plane.js +314 -0
  15. package/dist/control-plane/single-agent.js +249 -0
  16. package/dist/control-plane/types.js +11 -0
  17. package/dist/credentials/store.js +323 -0
  18. package/dist/logging/redact.js +109 -0
  19. package/dist/markdown/fences.js +58 -0
  20. package/dist/memory/embeddings.js +146 -0
  21. package/dist/memory/index.js +382 -0
  22. package/dist/memory/internal.js +163 -0
  23. package/dist/pairing/pairing-store.js +194 -0
  24. package/dist/plugins/cli.js +42 -0
  25. package/dist/plugins/discovery.js +253 -0
  26. package/dist/plugins/install.js +181 -0
  27. package/dist/plugins/loader.js +290 -0
  28. package/dist/plugins/registry.js +105 -0
  29. package/dist/plugins/status.js +29 -0
  30. package/dist/plugins/tools.js +39 -0
  31. package/dist/plugins/types.js +1 -0
  32. package/dist/routing/resolve-route.js +144 -0
  33. package/dist/routing/session-key.js +63 -0
  34. package/dist/utils/provider-utils.js +28 -0
  35. package/package.json +4 -29
  36. package/patches/@mariozechner__pi-ai.patch +215 -0
  37. package/patches/playwright-core@1.57.0.patch +13 -0
  38. package/patches/qrcode-terminal.patch +12 -0
  39. package/scripts/postinstall.js +202 -0
@@ -0,0 +1,194 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
5
+ function resolveCredentialsDir(env = process.env) {
6
+ const stateDir = resolveStateDir(env, os.homedir);
7
+ return resolveOAuthDir(env, stateDir);
8
+ }
9
+ function resolvePairingPath(provider, env = process.env) {
10
+ return path.join(resolveCredentialsDir(env), `${provider}-pairing.json`);
11
+ }
12
+ function resolveAllowFromPath(provider, env = process.env) {
13
+ return path.join(resolveCredentialsDir(env), `${provider}-allowFrom.json`);
14
+ }
15
+ function safeParseJson(raw) {
16
+ try {
17
+ return JSON.parse(raw);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ async function readJsonFile(filePath, fallback) {
24
+ try {
25
+ const raw = await fs.promises.readFile(filePath, "utf-8");
26
+ const parsed = safeParseJson(raw);
27
+ if (parsed == null)
28
+ return { value: fallback, exists: true };
29
+ return { value: parsed, exists: true };
30
+ }
31
+ catch (err) {
32
+ const code = err.code;
33
+ if (code === "ENOENT")
34
+ return { value: fallback, exists: false };
35
+ return { value: fallback, exists: false };
36
+ }
37
+ }
38
+ async function writeJsonFile(filePath, value) {
39
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
40
+ await fs.promises.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
41
+ }
42
+ function randomCode() {
43
+ // Human-friendly: 8 chars, upper, no ambiguous chars (0O1I).
44
+ const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
45
+ let out = "";
46
+ for (let i = 0; i < 8; i++) {
47
+ out += alphabet[Math.floor(Math.random() * alphabet.length)];
48
+ }
49
+ return out;
50
+ }
51
+ function normalizeId(value) {
52
+ return String(value).trim();
53
+ }
54
+ function normalizeAllowEntry(provider, entry) {
55
+ const trimmed = entry.trim();
56
+ if (!trimmed)
57
+ return "";
58
+ if (trimmed === "*")
59
+ return "";
60
+ if (provider === "telegram")
61
+ return trimmed.replace(/^(telegram|tg):/i, "");
62
+ if (provider === "signal")
63
+ return trimmed.replace(/^signal:/i, "");
64
+ if (provider === "discord")
65
+ return trimmed.replace(/^(discord|user):/i, "");
66
+ if (provider === "slack")
67
+ return trimmed.replace(/^(slack|user):/i, "");
68
+ return trimmed;
69
+ }
70
+ export async function readProviderAllowFromStore(provider, env = process.env) {
71
+ const filePath = resolveAllowFromPath(provider, env);
72
+ const { value } = await readJsonFile(filePath, {
73
+ version: 1,
74
+ allowFrom: [],
75
+ });
76
+ const list = Array.isArray(value.allowFrom) ? value.allowFrom : [];
77
+ return list
78
+ .map((v) => normalizeAllowEntry(provider, String(v)))
79
+ .filter(Boolean);
80
+ }
81
+ export async function addProviderAllowFromStoreEntry(params) {
82
+ const env = params.env ?? process.env;
83
+ const filePath = resolveAllowFromPath(params.provider, env);
84
+ const { value } = await readJsonFile(filePath, {
85
+ version: 1,
86
+ allowFrom: [],
87
+ });
88
+ const current = (Array.isArray(value.allowFrom) ? value.allowFrom : [])
89
+ .map((v) => normalizeAllowEntry(params.provider, String(v)))
90
+ .filter(Boolean);
91
+ const normalized = normalizeAllowEntry(params.provider, normalizeId(params.entry));
92
+ if (!normalized)
93
+ return { changed: false, allowFrom: current };
94
+ if (current.includes(normalized))
95
+ return { changed: false, allowFrom: current };
96
+ const next = [...current, normalized];
97
+ await writeJsonFile(filePath, {
98
+ version: 1,
99
+ allowFrom: next,
100
+ });
101
+ return { changed: true, allowFrom: next };
102
+ }
103
+ export async function listProviderPairingRequests(provider, env = process.env) {
104
+ const filePath = resolvePairingPath(provider, env);
105
+ const { value } = await readJsonFile(filePath, {
106
+ version: 1,
107
+ requests: [],
108
+ });
109
+ const reqs = Array.isArray(value.requests) ? value.requests : [];
110
+ return reqs
111
+ .filter((r) => r &&
112
+ typeof r.id === "string" &&
113
+ typeof r.code === "string" &&
114
+ typeof r.createdAt === "string")
115
+ .slice()
116
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
117
+ }
118
+ export async function upsertProviderPairingRequest(params) {
119
+ const env = params.env ?? process.env;
120
+ const filePath = resolvePairingPath(params.provider, env);
121
+ const { value } = await readJsonFile(filePath, {
122
+ version: 1,
123
+ requests: [],
124
+ });
125
+ const now = new Date().toISOString();
126
+ const id = normalizeId(params.id);
127
+ const meta = params.meta && typeof params.meta === "object"
128
+ ? Object.fromEntries(Object.entries(params.meta)
129
+ .map(([k, v]) => [k, String(v ?? "").trim()])
130
+ .filter(([_, v]) => Boolean(v)))
131
+ : undefined;
132
+ const reqs = Array.isArray(value.requests) ? value.requests : [];
133
+ const existingIdx = reqs.findIndex((r) => r.id === id);
134
+ if (existingIdx >= 0) {
135
+ const existing = reqs[existingIdx];
136
+ const existingCode = existing && typeof existing.code === "string" ? existing.code.trim() : "";
137
+ const code = existingCode || randomCode();
138
+ const next = {
139
+ id,
140
+ code,
141
+ createdAt: existing?.createdAt ?? now,
142
+ lastSeenAt: now,
143
+ meta: meta ?? existing?.meta,
144
+ };
145
+ reqs[existingIdx] = next;
146
+ await writeJsonFile(filePath, {
147
+ version: 1,
148
+ requests: reqs,
149
+ });
150
+ return { code, created: false };
151
+ }
152
+ const code = randomCode();
153
+ const next = {
154
+ id,
155
+ code,
156
+ createdAt: now,
157
+ lastSeenAt: now,
158
+ ...(meta ? { meta } : {}),
159
+ };
160
+ await writeJsonFile(filePath, {
161
+ version: 1,
162
+ requests: [...reqs, next],
163
+ });
164
+ return { code, created: true };
165
+ }
166
+ export async function approveProviderPairingCode(params) {
167
+ const env = params.env ?? process.env;
168
+ const code = params.code.trim().toUpperCase();
169
+ if (!code)
170
+ return null;
171
+ const filePath = resolvePairingPath(params.provider, env);
172
+ const { value } = await readJsonFile(filePath, {
173
+ version: 1,
174
+ requests: [],
175
+ });
176
+ const reqs = Array.isArray(value.requests) ? value.requests : [];
177
+ const idx = reqs.findIndex((r) => String(r.code ?? "").toUpperCase() === code);
178
+ if (idx < 0)
179
+ return null;
180
+ const entry = reqs[idx];
181
+ if (!entry)
182
+ return null;
183
+ reqs.splice(idx, 1);
184
+ await writeJsonFile(filePath, {
185
+ version: 1,
186
+ requests: reqs,
187
+ });
188
+ await addProviderAllowFromStoreEntry({
189
+ provider: params.provider,
190
+ entry: entry.id,
191
+ env,
192
+ });
193
+ return { id: entry.id, entry };
194
+ }
@@ -0,0 +1,42 @@
1
+ import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
2
+ import { loadConfig } from "../config/config.js";
3
+ import { createSubsystemLogger } from "../logging.js";
4
+ import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js";
5
+ import { loadNexusPlugins } from "./loader.js";
6
+ const log = createSubsystemLogger("plugins");
7
+ function resolveDefaultAgentId(cfg) {
8
+ return normalizeAgentId(cfg.routing?.defaultAgentId ?? DEFAULT_AGENT_ID);
9
+ }
10
+ export function registerPluginCliCommands(program, cfg) {
11
+ const config = cfg ?? loadConfig();
12
+ const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
13
+ const logger = {
14
+ info: (msg) => log.info(msg),
15
+ warn: (msg) => log.warn(msg),
16
+ error: (msg) => log.error(msg),
17
+ debug: (msg) => log.debug(msg),
18
+ };
19
+ const registry = loadNexusPlugins({
20
+ config,
21
+ workspaceDir,
22
+ logger,
23
+ });
24
+ for (const entry of registry.cliRegistrars) {
25
+ try {
26
+ const result = entry.register({
27
+ program,
28
+ config,
29
+ workspaceDir,
30
+ logger,
31
+ });
32
+ if (result && typeof result.then === "function") {
33
+ void result.catch((err) => {
34
+ log.warn(`plugin CLI register failed (${entry.pluginId}): ${String(err)}`);
35
+ });
36
+ }
37
+ }
38
+ catch (err) {
39
+ log.warn(`plugin CLI register failed (${entry.pluginId}): ${String(err)}`);
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,253 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { CONFIG_DIR, resolveUserPath } from "../utils.js";
4
+ const EXTENSION_EXTS = new Set([".ts", ".js", ".mts", ".cts", ".mjs", ".cjs"]);
5
+ function isExtensionFile(filePath) {
6
+ const ext = path.extname(filePath);
7
+ if (!EXTENSION_EXTS.has(ext))
8
+ return false;
9
+ return !filePath.endsWith(".d.ts");
10
+ }
11
+ function readPackageManifest(dir) {
12
+ const manifestPath = path.join(dir, "package.json");
13
+ if (!fs.existsSync(manifestPath))
14
+ return null;
15
+ try {
16
+ const raw = fs.readFileSync(manifestPath, "utf-8");
17
+ return JSON.parse(raw);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function resolvePackageExtensions(manifest) {
24
+ // Try nexus first, fall back to clawdbot for backward compat
25
+ const raw = manifest.nexus?.extensions ?? manifest.clawdbot?.extensions;
26
+ if (!Array.isArray(raw))
27
+ return [];
28
+ return raw
29
+ .map((entry) => (typeof entry === "string" ? entry.trim() : ""))
30
+ .filter(Boolean);
31
+ }
32
+ function deriveIdHint(params) {
33
+ const base = path.basename(params.filePath, path.extname(params.filePath));
34
+ const rawPackageName = params.packageName?.trim();
35
+ if (!rawPackageName)
36
+ return base;
37
+ // Prefer the unscoped name so config keys stay stable even when the npm
38
+ // package is scoped (example: @nexus/voice-call -> voice-call).
39
+ const unscoped = rawPackageName.includes("/")
40
+ ? (rawPackageName.split("/").pop() ?? rawPackageName)
41
+ : rawPackageName;
42
+ if (!params.hasMultipleExtensions)
43
+ return unscoped;
44
+ return `${unscoped}/${base}`;
45
+ }
46
+ function addCandidate(params) {
47
+ const resolved = path.resolve(params.source);
48
+ if (params.seen.has(resolved))
49
+ return;
50
+ params.seen.add(resolved);
51
+ const manifest = params.manifest ?? null;
52
+ params.candidates.push({
53
+ idHint: params.idHint,
54
+ source: resolved,
55
+ origin: params.origin,
56
+ workspaceDir: params.workspaceDir,
57
+ packageName: manifest?.name?.trim() || undefined,
58
+ packageVersion: manifest?.version?.trim() || undefined,
59
+ packageDescription: manifest?.description?.trim() || undefined,
60
+ });
61
+ }
62
+ function discoverInDirectory(params) {
63
+ if (!fs.existsSync(params.dir))
64
+ return;
65
+ let entries = [];
66
+ try {
67
+ entries = fs.readdirSync(params.dir, { withFileTypes: true });
68
+ }
69
+ catch (err) {
70
+ params.diagnostics.push({
71
+ level: "warn",
72
+ message: `failed to read extensions dir: ${params.dir} (${String(err)})`,
73
+ source: params.dir,
74
+ });
75
+ return;
76
+ }
77
+ for (const entry of entries) {
78
+ const fullPath = path.join(params.dir, entry.name);
79
+ if (entry.isFile()) {
80
+ if (!isExtensionFile(fullPath))
81
+ continue;
82
+ addCandidate({
83
+ candidates: params.candidates,
84
+ seen: params.seen,
85
+ idHint: path.basename(entry.name, path.extname(entry.name)),
86
+ source: fullPath,
87
+ origin: params.origin,
88
+ workspaceDir: params.workspaceDir,
89
+ });
90
+ }
91
+ if (!entry.isDirectory())
92
+ continue;
93
+ const manifest = readPackageManifest(fullPath);
94
+ const extensions = manifest ? resolvePackageExtensions(manifest) : [];
95
+ if (extensions.length > 0) {
96
+ for (const extPath of extensions) {
97
+ const resolved = path.resolve(fullPath, extPath);
98
+ addCandidate({
99
+ candidates: params.candidates,
100
+ seen: params.seen,
101
+ idHint: deriveIdHint({
102
+ filePath: resolved,
103
+ packageName: manifest?.name,
104
+ hasMultipleExtensions: extensions.length > 1,
105
+ }),
106
+ source: resolved,
107
+ origin: params.origin,
108
+ workspaceDir: params.workspaceDir,
109
+ manifest,
110
+ });
111
+ }
112
+ continue;
113
+ }
114
+ const indexCandidates = ["index.ts", "index.js", "index.mjs", "index.cjs"];
115
+ const indexFile = indexCandidates
116
+ .map((candidate) => path.join(fullPath, candidate))
117
+ .find((candidate) => fs.existsSync(candidate));
118
+ if (indexFile && isExtensionFile(indexFile)) {
119
+ addCandidate({
120
+ candidates: params.candidates,
121
+ seen: params.seen,
122
+ idHint: entry.name,
123
+ source: indexFile,
124
+ origin: params.origin,
125
+ workspaceDir: params.workspaceDir,
126
+ });
127
+ }
128
+ }
129
+ }
130
+ function discoverFromPath(params) {
131
+ const resolved = resolveUserPath(params.rawPath);
132
+ if (!fs.existsSync(resolved)) {
133
+ params.diagnostics.push({
134
+ level: "warn",
135
+ message: `plugin path not found: ${resolved}`,
136
+ source: resolved,
137
+ });
138
+ return;
139
+ }
140
+ const stat = fs.statSync(resolved);
141
+ if (stat.isFile()) {
142
+ if (!isExtensionFile(resolved)) {
143
+ params.diagnostics.push({
144
+ level: "warn",
145
+ message: `plugin path is not a supported file: ${resolved}`,
146
+ source: resolved,
147
+ });
148
+ return;
149
+ }
150
+ addCandidate({
151
+ candidates: params.candidates,
152
+ seen: params.seen,
153
+ idHint: path.basename(resolved, path.extname(resolved)),
154
+ source: resolved,
155
+ origin: params.origin,
156
+ workspaceDir: params.workspaceDir,
157
+ });
158
+ return;
159
+ }
160
+ if (stat.isDirectory()) {
161
+ const manifest = readPackageManifest(resolved);
162
+ const extensions = manifest ? resolvePackageExtensions(manifest) : [];
163
+ if (extensions.length > 0) {
164
+ for (const extPath of extensions) {
165
+ const source = path.resolve(resolved, extPath);
166
+ addCandidate({
167
+ candidates: params.candidates,
168
+ seen: params.seen,
169
+ idHint: deriveIdHint({
170
+ filePath: source,
171
+ packageName: manifest?.name,
172
+ hasMultipleExtensions: extensions.length > 1,
173
+ }),
174
+ source,
175
+ origin: params.origin,
176
+ workspaceDir: params.workspaceDir,
177
+ manifest,
178
+ });
179
+ }
180
+ return;
181
+ }
182
+ const indexCandidates = ["index.ts", "index.js", "index.mjs", "index.cjs"];
183
+ const indexFile = indexCandidates
184
+ .map((candidate) => path.join(resolved, candidate))
185
+ .find((candidate) => fs.existsSync(candidate));
186
+ if (indexFile && isExtensionFile(indexFile)) {
187
+ addCandidate({
188
+ candidates: params.candidates,
189
+ seen: params.seen,
190
+ idHint: path.basename(resolved),
191
+ source: indexFile,
192
+ origin: params.origin,
193
+ workspaceDir: params.workspaceDir,
194
+ });
195
+ return;
196
+ }
197
+ discoverInDirectory({
198
+ dir: resolved,
199
+ origin: params.origin,
200
+ workspaceDir: params.workspaceDir,
201
+ candidates: params.candidates,
202
+ diagnostics: params.diagnostics,
203
+ seen: params.seen,
204
+ });
205
+ return;
206
+ }
207
+ }
208
+ export function discoverNexusPlugins(params) {
209
+ const candidates = [];
210
+ const diagnostics = [];
211
+ const seen = new Set();
212
+ const globalDir = path.join(CONFIG_DIR, "extensions");
213
+ discoverInDirectory({
214
+ dir: globalDir,
215
+ origin: "global",
216
+ candidates,
217
+ diagnostics,
218
+ seen,
219
+ });
220
+ const workspaceDir = params.workspaceDir?.trim();
221
+ if (workspaceDir) {
222
+ const workspaceRoot = resolveUserPath(workspaceDir);
223
+ // Try .nexus/extensions first, fall back to .clawdbot/extensions
224
+ const nexusExt = path.join(workspaceRoot, ".nexus", "extensions");
225
+ const clawdbotExt = path.join(workspaceRoot, ".clawdbot", "extensions");
226
+ const workspaceExt = fs.existsSync(nexusExt) ? nexusExt : clawdbotExt;
227
+ discoverInDirectory({
228
+ dir: workspaceExt,
229
+ origin: "workspace",
230
+ workspaceDir: workspaceRoot,
231
+ candidates,
232
+ diagnostics,
233
+ seen,
234
+ });
235
+ }
236
+ const extra = params.extraPaths ?? [];
237
+ for (const extraPath of extra) {
238
+ if (typeof extraPath !== "string")
239
+ continue;
240
+ const trimmed = extraPath.trim();
241
+ if (!trimmed)
242
+ continue;
243
+ discoverFromPath({
244
+ rawPath: trimmed,
245
+ origin: "config",
246
+ workspaceDir: workspaceDir?.trim() || undefined,
247
+ candidates,
248
+ diagnostics,
249
+ seen,
250
+ });
251
+ }
252
+ return { candidates, diagnostics };
253
+ }
@@ -0,0 +1,181 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { runCommandWithTimeout } from "../process/exec.js";
5
+ import { CONFIG_DIR, resolveUserPath } from "../utils.js";
6
+ const defaultLogger = {};
7
+ function unscopedPackageName(name) {
8
+ const trimmed = name.trim();
9
+ if (!trimmed)
10
+ return trimmed;
11
+ return trimmed.includes("/")
12
+ ? (trimmed.split("/").pop() ?? trimmed)
13
+ : trimmed;
14
+ }
15
+ function safeDirName(input) {
16
+ const trimmed = input.trim();
17
+ if (!trimmed)
18
+ return trimmed;
19
+ return trimmed.replaceAll("/", "__");
20
+ }
21
+ async function readJsonFile(filePath) {
22
+ const raw = await fs.readFile(filePath, "utf-8");
23
+ return JSON.parse(raw);
24
+ }
25
+ async function fileExists(filePath) {
26
+ try {
27
+ await fs.stat(filePath);
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ async function resolvePackedPackageDir(extractDir) {
35
+ const direct = path.join(extractDir, "package");
36
+ if (await fileExists(direct))
37
+ return direct;
38
+ const entries = await fs.readdir(extractDir, { withFileTypes: true });
39
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
40
+ if (dirs.length !== 1) {
41
+ throw new Error(`unexpected archive layout (dirs: ${dirs.join(", ")})`);
42
+ }
43
+ const onlyDir = dirs[0];
44
+ if (!onlyDir) {
45
+ throw new Error("unexpected archive layout (no package dir found)");
46
+ }
47
+ return path.join(extractDir, onlyDir);
48
+ }
49
+ async function ensureNexusExtensions(manifest) {
50
+ // Try nexus first, fall back to clawdbot for backward compat
51
+ const extensions = manifest.nexus?.extensions ?? manifest.clawdbot?.extensions;
52
+ if (!Array.isArray(extensions)) {
53
+ throw new Error("package.json missing nexus.extensions (or clawdbot.extensions)");
54
+ }
55
+ const list = extensions
56
+ .map((e) => (typeof e === "string" ? e.trim() : ""))
57
+ .filter(Boolean);
58
+ if (list.length === 0) {
59
+ throw new Error("package.json nexus.extensions is empty");
60
+ }
61
+ return list;
62
+ }
63
+ export async function installPluginFromArchive(params) {
64
+ const logger = params.logger ?? defaultLogger;
65
+ const timeoutMs = params.timeoutMs ?? 120_000;
66
+ const archivePath = resolveUserPath(params.archivePath);
67
+ if (!(await fileExists(archivePath))) {
68
+ return { ok: false, error: `archive not found: ${archivePath}` };
69
+ }
70
+ const extensionsDir = params.extensionsDir
71
+ ? resolveUserPath(params.extensionsDir)
72
+ : path.join(CONFIG_DIR, "extensions");
73
+ await fs.mkdir(extensionsDir, { recursive: true });
74
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "nexus-plugin-"));
75
+ const extractDir = path.join(tmpDir, "extract");
76
+ await fs.mkdir(extractDir, { recursive: true });
77
+ logger.info?.(`Extracting ${archivePath}…`);
78
+ const tarRes = await runCommandWithTimeout(["tar", "-xzf", archivePath, "-C", extractDir], { timeoutMs });
79
+ if (tarRes.code !== 0) {
80
+ return {
81
+ ok: false,
82
+ error: `failed to extract archive: ${tarRes.stderr.trim() || tarRes.stdout.trim()}`,
83
+ };
84
+ }
85
+ let packageDir = "";
86
+ try {
87
+ packageDir = await resolvePackedPackageDir(extractDir);
88
+ }
89
+ catch (err) {
90
+ return { ok: false, error: String(err) };
91
+ }
92
+ const manifestPath = path.join(packageDir, "package.json");
93
+ if (!(await fileExists(manifestPath))) {
94
+ return { ok: false, error: "extracted package missing package.json" };
95
+ }
96
+ let manifest;
97
+ try {
98
+ manifest = await readJsonFile(manifestPath);
99
+ }
100
+ catch (err) {
101
+ return { ok: false, error: `invalid package.json: ${String(err)}` };
102
+ }
103
+ let extensions;
104
+ try {
105
+ extensions = await ensureNexusExtensions(manifest);
106
+ }
107
+ catch (err) {
108
+ return { ok: false, error: String(err) };
109
+ }
110
+ const pkgName = typeof manifest.name === "string" ? manifest.name : "";
111
+ const pluginId = pkgName ? unscopedPackageName(pkgName) : "plugin";
112
+ const targetDir = path.join(extensionsDir, safeDirName(pluginId));
113
+ if (await fileExists(targetDir)) {
114
+ return {
115
+ ok: false,
116
+ error: `plugin already exists: ${targetDir} (delete it first)`,
117
+ };
118
+ }
119
+ logger.info?.(`Installing to ${targetDir}…`);
120
+ await fs.cp(packageDir, targetDir, { recursive: true });
121
+ for (const entry of extensions) {
122
+ const resolvedEntry = path.resolve(targetDir, entry);
123
+ if (!(await fileExists(resolvedEntry))) {
124
+ logger.warn?.(`extension entry not found: ${entry}`);
125
+ }
126
+ }
127
+ const deps = manifest.dependencies ?? {};
128
+ const hasDeps = Object.keys(deps).length > 0;
129
+ if (hasDeps) {
130
+ logger.info?.("Installing plugin dependencies…");
131
+ const npmRes = await runCommandWithTimeout(["npm", "install", "--omit=dev", "--silent"], { timeoutMs: Math.max(timeoutMs, 300_000), cwd: targetDir });
132
+ if (npmRes.code !== 0) {
133
+ return {
134
+ ok: false,
135
+ error: `npm install failed: ${npmRes.stderr.trim() || npmRes.stdout.trim()}`,
136
+ };
137
+ }
138
+ }
139
+ return {
140
+ ok: true,
141
+ pluginId,
142
+ targetDir,
143
+ manifestName: pkgName || undefined,
144
+ extensions,
145
+ };
146
+ }
147
+ export async function installPluginFromNpmSpec(params) {
148
+ const logger = params.logger ?? defaultLogger;
149
+ const timeoutMs = params.timeoutMs ?? 120_000;
150
+ const spec = params.spec.trim();
151
+ if (!spec)
152
+ return { ok: false, error: "missing npm spec" };
153
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "nexus-npm-pack-"));
154
+ logger.info?.(`Downloading ${spec}…`);
155
+ const res = await runCommandWithTimeout(["npm", "pack", spec], {
156
+ timeoutMs: Math.max(timeoutMs, 300_000),
157
+ cwd: tmpDir,
158
+ env: { COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" },
159
+ });
160
+ if (res.code !== 0) {
161
+ return {
162
+ ok: false,
163
+ error: `npm pack failed: ${res.stderr.trim() || res.stdout.trim()}`,
164
+ };
165
+ }
166
+ const packed = (res.stdout || "")
167
+ .split("\n")
168
+ .map((l) => l.trim())
169
+ .filter(Boolean)
170
+ .pop();
171
+ if (!packed) {
172
+ return { ok: false, error: "npm pack produced no archive" };
173
+ }
174
+ const archivePath = path.join(tmpDir, packed);
175
+ return await installPluginFromArchive({
176
+ archivePath,
177
+ extensionsDir: params.extensionsDir,
178
+ timeoutMs,
179
+ logger,
180
+ });
181
+ }