@nexrall/code-core 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/plugins/installer.d.ts +65 -0
- package/dist/plugins/installer.d.ts.map +1 -0
- package/dist/plugins/installer.js +300 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -27,4 +27,5 @@ __exportStar(require("./commands/loader"), exports);
|
|
|
27
27
|
__exportStar(require("./agent/agentTypes"), exports);
|
|
28
28
|
__exportStar(require("./permissions/rules"), exports);
|
|
29
29
|
__exportStar(require("./plugins/index"), exports);
|
|
30
|
+
__exportStar(require("./plugins/installer"), exports);
|
|
30
31
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface PluginSource {
|
|
2
|
+
kind: 'github' | 'local';
|
|
3
|
+
/** e.g. "owner/repo" (github) or absolute dir (local) */
|
|
4
|
+
repo: string;
|
|
5
|
+
/** branch / tag / sha — defaults to HEAD */
|
|
6
|
+
ref?: string;
|
|
7
|
+
/** subdirectory inside the repo that is the plugin root */
|
|
8
|
+
subdir?: string;
|
|
9
|
+
/** the original user-supplied spec (for receipts / display) */
|
|
10
|
+
raw: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PluginInspection {
|
|
13
|
+
commands: string[];
|
|
14
|
+
agents: string[];
|
|
15
|
+
hasHooks: boolean;
|
|
16
|
+
hasMcp: boolean;
|
|
17
|
+
meta: {
|
|
18
|
+
name?: string;
|
|
19
|
+
version?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface InstallReceipt {
|
|
24
|
+
source: string;
|
|
25
|
+
kind: 'github' | 'local';
|
|
26
|
+
ref?: string;
|
|
27
|
+
subdir?: string;
|
|
28
|
+
installedAt: string;
|
|
29
|
+
}
|
|
30
|
+
export interface InstallResult {
|
|
31
|
+
name: string;
|
|
32
|
+
dir: string;
|
|
33
|
+
scope: 'project' | 'global';
|
|
34
|
+
inspection: PluginInspection;
|
|
35
|
+
}
|
|
36
|
+
/** Parse a user-supplied plugin spec into a structured source. Throws on junk. */
|
|
37
|
+
export declare function parsePluginSource(spec: string): PluginSource;
|
|
38
|
+
/** What does this plugin contain? Callers must warn on hasHooks/hasMcp. */
|
|
39
|
+
export declare function inspectPluginDir(dir: string): PluginInspection;
|
|
40
|
+
export interface InstallOptions {
|
|
41
|
+
/** 'global' (default) → ~/.nexrall/plugins; 'project' → <workDir>/.nexrall/plugins */
|
|
42
|
+
scope?: 'project' | 'global';
|
|
43
|
+
workDir?: string;
|
|
44
|
+
/** Override the installed directory name (defaults to plugin.json name, subdir, or repo name). */
|
|
45
|
+
name?: string;
|
|
46
|
+
/** Overwrite an existing plugin of the same name. */
|
|
47
|
+
force?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Called after inspection, before finalising. Return false to abort.
|
|
50
|
+
* Callers should surface hooks/MCP warnings here.
|
|
51
|
+
*/
|
|
52
|
+
confirm?: (inspection: PluginInspection, name: string) => Promise<boolean> | boolean;
|
|
53
|
+
}
|
|
54
|
+
export declare function installPlugin(spec: string, opts?: InstallOptions): Promise<InstallResult>;
|
|
55
|
+
export declare function readReceipt(pluginDir: string): InstallReceipt | null;
|
|
56
|
+
export declare function removePlugin(name: string, opts?: {
|
|
57
|
+
scope?: 'project' | 'global';
|
|
58
|
+
workDir?: string;
|
|
59
|
+
}): string;
|
|
60
|
+
export declare function updatePlugin(name: string, opts?: {
|
|
61
|
+
scope?: 'project' | 'global';
|
|
62
|
+
workDir?: string;
|
|
63
|
+
confirm?: InstallOptions['confirm'];
|
|
64
|
+
}): Promise<InstallResult>;
|
|
65
|
+
//# sourceMappingURL=installer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../src/plugins/installer.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B,UAAU,EAAE,gBAAgB,CAAC;CAC9B;AAMD,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CA+C5D;AAYD,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAiB9D;AAyDD,MAAM,WAAW,cAAc;IAC7B,sFAAsF;IACtF,KAAK,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kGAAkG;IAClG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CACtF;AAQD,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,aAAa,CAAC,CA+EnG;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAOpE;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,MAAM,CAYhH;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,CAAA;CAAO,GACjG,OAAO,CAAC,aAAa,CAAC,CAexB"}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parsePluginSource = parsePluginSource;
|
|
37
|
+
exports.inspectPluginDir = inspectPluginDir;
|
|
38
|
+
exports.installPlugin = installPlugin;
|
|
39
|
+
exports.readReceipt = readReceipt;
|
|
40
|
+
exports.removePlugin = removePlugin;
|
|
41
|
+
exports.updatePlugin = updatePlugin;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const os = __importStar(require("os"));
|
|
45
|
+
const child_process_1 = require("child_process");
|
|
46
|
+
const RECEIPT_FILE = '_install.json';
|
|
47
|
+
// ─── Source parsing ───────────────────────────────────────────────────────────
|
|
48
|
+
/** Parse a user-supplied plugin spec into a structured source. Throws on junk. */
|
|
49
|
+
function parsePluginSource(spec) {
|
|
50
|
+
const raw = spec.trim();
|
|
51
|
+
if (!raw)
|
|
52
|
+
throw new Error('Empty plugin source.');
|
|
53
|
+
// Local directory: ./x, ../x, /abs, ~/x
|
|
54
|
+
if (raw.startsWith('./') || raw.startsWith('../') || raw.startsWith('/') || raw.startsWith('~/')) {
|
|
55
|
+
const abs = path.resolve(raw.replace(/^~(?=\/)/, os.homedir()));
|
|
56
|
+
return { kind: 'local', repo: abs, raw };
|
|
57
|
+
}
|
|
58
|
+
// Full GitHub URL → strip to path
|
|
59
|
+
let rest = raw;
|
|
60
|
+
const urlMatch = /^https?:\/\/(?:www\.)?github\.com\/(.+)$/i.exec(rest);
|
|
61
|
+
if (urlMatch)
|
|
62
|
+
rest = urlMatch[1];
|
|
63
|
+
rest = rest.replace(/\.git$/, '').replace(/\/+$/, '');
|
|
64
|
+
// ref via #
|
|
65
|
+
let ref;
|
|
66
|
+
const hashIdx = rest.indexOf('#');
|
|
67
|
+
if (hashIdx !== -1) {
|
|
68
|
+
ref = rest.slice(hashIdx + 1) || undefined;
|
|
69
|
+
rest = rest.slice(0, hashIdx);
|
|
70
|
+
}
|
|
71
|
+
const parts = rest.split('/').filter(Boolean);
|
|
72
|
+
if (parts.length < 2) {
|
|
73
|
+
throw new Error(`Invalid plugin source "${spec}". Use owner/repo, owner/repo#ref, owner/repo/subdir, a GitHub URL, or a local path.`);
|
|
74
|
+
}
|
|
75
|
+
// GitHub URLs may include /tree/<ref>/<subdir>
|
|
76
|
+
if (parts[2] === 'tree' && parts.length >= 4) {
|
|
77
|
+
ref = ref ?? parts[3];
|
|
78
|
+
return { kind: 'github', repo: `${parts[0]}/${parts[1]}`, ref, subdir: parts.slice(4).join('/') || undefined, raw };
|
|
79
|
+
}
|
|
80
|
+
const owner = parts[0];
|
|
81
|
+
const repoName = parts[1];
|
|
82
|
+
if (!/^[\w.-]+$/.test(owner) || !/^[\w.-]+$/.test(repoName)) {
|
|
83
|
+
throw new Error(`Invalid GitHub owner/repo in "${spec}".`);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
kind: 'github',
|
|
87
|
+
repo: `${owner}/${repoName}`,
|
|
88
|
+
ref,
|
|
89
|
+
subdir: parts.slice(2).join('/') || undefined,
|
|
90
|
+
raw,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// ─── Inspection (safety surface) ──────────────────────────────────────────────
|
|
94
|
+
function listMd(dir) {
|
|
95
|
+
try {
|
|
96
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, ''));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** What does this plugin contain? Callers must warn on hasHooks/hasMcp. */
|
|
103
|
+
function inspectPluginDir(dir) {
|
|
104
|
+
let meta = {};
|
|
105
|
+
try {
|
|
106
|
+
const j = JSON.parse(fs.readFileSync(path.join(dir, 'plugin.json'), 'utf-8'));
|
|
107
|
+
meta = {
|
|
108
|
+
name: typeof j.name === 'string' ? j.name : undefined,
|
|
109
|
+
version: typeof j.version === 'string' ? j.version : undefined,
|
|
110
|
+
description: typeof j.description === 'string' ? j.description : undefined,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch { /* optional */ }
|
|
114
|
+
return {
|
|
115
|
+
commands: listMd(path.join(dir, 'commands')),
|
|
116
|
+
agents: listMd(path.join(dir, 'agents')),
|
|
117
|
+
hasHooks: fs.existsSync(path.join(dir, 'hooks.json')),
|
|
118
|
+
hasMcp: fs.existsSync(path.join(dir, 'mcp.json')),
|
|
119
|
+
meta,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Is this directory a plausible plugin root? */
|
|
123
|
+
function looksLikePlugin(dir) {
|
|
124
|
+
const i = inspectPluginDir(dir);
|
|
125
|
+
return i.commands.length > 0 || i.agents.length > 0 || i.hasHooks || i.hasMcp || i.meta.name !== undefined;
|
|
126
|
+
}
|
|
127
|
+
// ─── Download & extract ───────────────────────────────────────────────────────
|
|
128
|
+
async function downloadTarball(repo, ref, destFile) {
|
|
129
|
+
const r = ref || 'HEAD';
|
|
130
|
+
const url = `https://codeload.github.com/${repo}/tar.gz/${encodeURIComponent(r)}`;
|
|
131
|
+
const res = await fetch(url, { redirect: 'follow' });
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
if (res.status === 404) {
|
|
134
|
+
throw new Error(`GitHub repo or ref not found: ${repo}${ref ? `#${ref}` : ''} (is the repo public?)`);
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`Download failed (${res.status}) for ${repo}${ref ? `#${ref}` : ''}.`);
|
|
137
|
+
}
|
|
138
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
139
|
+
if (buf.length > 50 * 1024 * 1024)
|
|
140
|
+
throw new Error('Plugin tarball exceeds 50 MB — refusing to install.');
|
|
141
|
+
fs.writeFileSync(destFile, buf);
|
|
142
|
+
}
|
|
143
|
+
function extractTarball(tarFile, destDir) {
|
|
144
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
145
|
+
(0, child_process_1.execFileSync)('tar', ['-xzf', tarFile, '-C', destDir], { stdio: 'pipe' });
|
|
146
|
+
// Tarball contains a single top-level dir: <repo>-<sha>/
|
|
147
|
+
const entries = fs.readdirSync(destDir).filter((e) => {
|
|
148
|
+
try {
|
|
149
|
+
return fs.statSync(path.join(destDir, e)).isDirectory();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
if (entries.length !== 1)
|
|
156
|
+
throw new Error('Unexpected tarball layout.');
|
|
157
|
+
return path.join(destDir, entries[0]);
|
|
158
|
+
}
|
|
159
|
+
// ─── Copy (sanitised) ─────────────────────────────────────────────────────────
|
|
160
|
+
/** Recursively copy a plugin dir, skipping VCS/junk and anything huge. */
|
|
161
|
+
function copyPluginDir(src, dest) {
|
|
162
|
+
const SKIP = new Set(['.git', 'node_modules', '.github', '.DS_Store', RECEIPT_FILE]);
|
|
163
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
164
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
165
|
+
if (SKIP.has(entry.name))
|
|
166
|
+
continue;
|
|
167
|
+
const s = path.join(src, entry.name);
|
|
168
|
+
const d = path.join(dest, entry.name);
|
|
169
|
+
if (entry.isSymbolicLink())
|
|
170
|
+
continue; // never follow symlinks out of the tree
|
|
171
|
+
if (entry.isDirectory()) {
|
|
172
|
+
copyPluginDir(s, d);
|
|
173
|
+
}
|
|
174
|
+
else if (entry.isFile()) {
|
|
175
|
+
fs.copyFileSync(s, d);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function pluginsRoot(scope, workDir) {
|
|
180
|
+
return scope === 'project'
|
|
181
|
+
? path.join(workDir || process.cwd(), '.nexrall', 'plugins')
|
|
182
|
+
: path.join(os.homedir(), '.nexrall', 'plugins');
|
|
183
|
+
}
|
|
184
|
+
async function installPlugin(spec, opts = {}) {
|
|
185
|
+
const source = parsePluginSource(spec);
|
|
186
|
+
const scope = opts.scope ?? 'global';
|
|
187
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'nexrall-plugin-'));
|
|
188
|
+
try {
|
|
189
|
+
// 1. Materialise the source into tmp
|
|
190
|
+
let repoRoot;
|
|
191
|
+
if (source.kind === 'local') {
|
|
192
|
+
if (!fs.existsSync(source.repo) || !fs.statSync(source.repo).isDirectory()) {
|
|
193
|
+
throw new Error(`Local plugin path not found: ${source.repo}`);
|
|
194
|
+
}
|
|
195
|
+
repoRoot = source.repo;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const tarFile = path.join(tmpBase, 'plugin.tar.gz');
|
|
199
|
+
await downloadTarball(source.repo, source.ref, tarFile);
|
|
200
|
+
repoRoot = extractTarball(tarFile, path.join(tmpBase, 'x'));
|
|
201
|
+
}
|
|
202
|
+
// 2. Locate the plugin root
|
|
203
|
+
let pluginRoot = source.subdir ? path.join(repoRoot, source.subdir) : repoRoot;
|
|
204
|
+
pluginRoot = path.resolve(pluginRoot);
|
|
205
|
+
if (!path.resolve(pluginRoot).startsWith(path.resolve(repoRoot))) {
|
|
206
|
+
throw new Error('Plugin subdirectory escapes the repository — refusing.');
|
|
207
|
+
}
|
|
208
|
+
if (!fs.existsSync(pluginRoot) || !fs.statSync(pluginRoot).isDirectory()) {
|
|
209
|
+
throw new Error(`Subdirectory not found in repo: ${source.subdir}`);
|
|
210
|
+
}
|
|
211
|
+
if (!looksLikePlugin(pluginRoot)) {
|
|
212
|
+
// Convention fallback: a repo may keep the plugin in ./plugin or ./.nexrall-plugin
|
|
213
|
+
const candidates = ['plugin', '.nexrall-plugin'].map((c) => path.join(pluginRoot, c));
|
|
214
|
+
const found = candidates.find((c) => fs.existsSync(c) && looksLikePlugin(c));
|
|
215
|
+
if (found) {
|
|
216
|
+
pluginRoot = found;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
throw new Error('No plugin found: expected plugin.json, commands/, agents/, hooks.json or mcp.json at the source root.');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// 3. Inspect + name
|
|
223
|
+
const inspection = inspectPluginDir(pluginRoot);
|
|
224
|
+
const name = opts.name ||
|
|
225
|
+
inspection.meta.name ||
|
|
226
|
+
(source.subdir ? path.basename(source.subdir) : undefined) ||
|
|
227
|
+
(source.kind === 'github' ? source.repo.split('/')[1] : path.basename(source.repo));
|
|
228
|
+
if (!/^[\w.-]+$/.test(name))
|
|
229
|
+
throw new Error(`Invalid plugin name "${name}".`);
|
|
230
|
+
// 4. Caller confirmation (safety gate — hooks/MCP warnings live here)
|
|
231
|
+
if (opts.confirm) {
|
|
232
|
+
const ok = await opts.confirm(inspection, name);
|
|
233
|
+
if (!ok)
|
|
234
|
+
throw new Error('Installation cancelled.');
|
|
235
|
+
}
|
|
236
|
+
// 5. Copy into place
|
|
237
|
+
const root = pluginsRoot(scope, opts.workDir);
|
|
238
|
+
const dest = path.join(root, name);
|
|
239
|
+
if (fs.existsSync(dest)) {
|
|
240
|
+
if (!opts.force)
|
|
241
|
+
throw new Error(`Plugin "${name}" is already installed. Use --force to overwrite or 'nex plugin update ${name}'.`);
|
|
242
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
243
|
+
}
|
|
244
|
+
copyPluginDir(pluginRoot, dest);
|
|
245
|
+
// 6. Receipt for `nex plugin update`
|
|
246
|
+
const receipt = {
|
|
247
|
+
source: source.raw,
|
|
248
|
+
kind: source.kind,
|
|
249
|
+
ref: source.ref,
|
|
250
|
+
subdir: source.subdir,
|
|
251
|
+
installedAt: new Date().toISOString(),
|
|
252
|
+
};
|
|
253
|
+
fs.writeFileSync(path.join(dest, RECEIPT_FILE), JSON.stringify(receipt, null, 2));
|
|
254
|
+
return { name, dir: dest, scope, inspection };
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function readReceipt(pluginDir) {
|
|
261
|
+
try {
|
|
262
|
+
const j = JSON.parse(fs.readFileSync(path.join(pluginDir, RECEIPT_FILE), 'utf-8'));
|
|
263
|
+
return typeof j.source === 'string' ? j : null;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function removePlugin(name, opts = {}) {
|
|
270
|
+
if (!/^[\w.-]+$/.test(name))
|
|
271
|
+
throw new Error(`Invalid plugin name "${name}".`);
|
|
272
|
+
// Search both scopes when none specified (project first — mirrors loader precedence)
|
|
273
|
+
const scopes = opts.scope ? [opts.scope] : ['project', 'global'];
|
|
274
|
+
for (const s of scopes) {
|
|
275
|
+
const dir = path.join(pluginsRoot(s, opts.workDir), name);
|
|
276
|
+
if (fs.existsSync(dir)) {
|
|
277
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
278
|
+
return dir;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
throw new Error(`Plugin "${name}" is not installed.`);
|
|
282
|
+
}
|
|
283
|
+
async function updatePlugin(name, opts = {}) {
|
|
284
|
+
const scopes = opts.scope ? [opts.scope] : ['project', 'global'];
|
|
285
|
+
for (const s of scopes) {
|
|
286
|
+
const dir = path.join(pluginsRoot(s, opts.workDir), name);
|
|
287
|
+
if (!fs.existsSync(dir))
|
|
288
|
+
continue;
|
|
289
|
+
const receipt = readReceipt(dir);
|
|
290
|
+
if (!receipt) {
|
|
291
|
+
throw new Error(`Plugin "${name}" has no install receipt (installed manually?) — reinstall it with 'nex plugin install <source> --force'.`);
|
|
292
|
+
}
|
|
293
|
+
if (receipt.kind === 'local') {
|
|
294
|
+
throw new Error(`Plugin "${name}" was installed from a local path — re-run 'nex plugin install ${receipt.source} --force'.`);
|
|
295
|
+
}
|
|
296
|
+
return installPlugin(receipt.source, { scope: s, workDir: opts.workDir, name, force: true, confirm: opts.confirm });
|
|
297
|
+
}
|
|
298
|
+
throw new Error(`Plugin "${name}" is not installed.`);
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=installer.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nexrall <support@nexrall.com> (https://nexrall.com)",
|