reverseport 1.4.0 → 1.5.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/bin/reverseport.mjs +1 -1
- package/dist/generatedPackageMetadata.js +1 -1
- package/native/windows/build/manifest.json +1 -1
- package/package.json +3 -3
- package/port/cli.mjs +16 -0
- package/port/src/graph.mjs +28 -6
- package/port/src/skills.mjs +41 -0
- package/port/src/symbols.mjs +22 -1
- package/port/src/tools.mjs +2 -2
- package/port/src/util.mjs +18 -2
- package/skills/reverse-engineer-anything/SKILL.md +1 -1
package/bin/reverseport.mjs
CHANGED
|
@@ -17,7 +17,7 @@ const VERSION = JSON.parse(readFileSync(path.join(REPO, 'package.json'), 'utf8')
|
|
|
17
17
|
const PORT_COMMANDS = new Set([
|
|
18
18
|
'scan', 'graph', 'symbols', 'scene', 'binary', 'bin', 'archive', 'pck', 'probe', 'extract', 'carve', 'diff',
|
|
19
19
|
'compare-builds', 'report', 'lookup', 'recon', 'web', 'blueprint', 'rebuild', 'map', 'convert', 'plan', 'unit',
|
|
20
|
-
'write', 'port-scene', 'copy-assets', 'status', 'verify', 'case', 'proof', 'tools', 'serve', 'describe', 'feedback', 'connect',
|
|
20
|
+
'write', 'port-scene', 'copy-assets', 'status', 'verify', 'case', 'proof', 'tools', 'serve', 'describe', 'feedback', 'connect', 'skills',
|
|
21
21
|
]);
|
|
22
22
|
const ALWAYS_ENGINE = ['setup', 'doctor', 'update', 'uninstall', 'mcp', 'capabilities', 'providers', 'help'];
|
|
23
23
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Generated from package.json and package-lock.json; do not edit. */
|
|
2
2
|
export const PACKAGE_METADATA = {
|
|
3
3
|
name: "reverseport",
|
|
4
|
-
version: "1.
|
|
4
|
+
version: "1.5.0", // x-release-please-version
|
|
5
5
|
serverSdkVersion: "2.3.1",
|
|
6
6
|
clientSdkVersion: "2.3.1",
|
|
7
7
|
coreSdkVersion: "2.3.1",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reverseport",
|
|
3
3
|
"mcpName": "io.github.laaielichter/reverseport",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.0",
|
|
5
5
|
"description": "Reverse engineer anything. Port it anywhere. One CLI and MCP server that lets coding agents take apart apps, games, binaries and websites — decompile, trace, compare builds, recon without files — and rebuild or port them to Godot, Unity, Unreal, three.js or any language.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -301,8 +301,8 @@
|
|
|
301
301
|
},
|
|
302
302
|
"lint-staged": {
|
|
303
303
|
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
|
|
304
|
-
"oxfmt",
|
|
305
|
-
"oxlint --fix"
|
|
304
|
+
"oxfmt --no-error-on-unmatched-pattern",
|
|
305
|
+
"oxlint --fix --no-error-on-unmatched-pattern"
|
|
306
306
|
],
|
|
307
307
|
"*.{json,md,yml,yaml}": "oxfmt --no-error-on-unmatched-pattern"
|
|
308
308
|
},
|
package/port/cli.mjs
CHANGED
|
@@ -118,6 +118,8 @@ Proof (evidence ledger)
|
|
|
118
118
|
Other
|
|
119
119
|
tools [--all] [--format f] list the tools (--all adds the engine's when built); formats: ${FORMATS.join(', ')}
|
|
120
120
|
describe <tool> one tool's description and input schema (engine tools too, when built)
|
|
121
|
+
skills [list] the skills bundled with this version
|
|
122
|
+
skills get <name> one skill's full guide, matching this version (--json for JSON)
|
|
121
123
|
feedback notes agents left with share_feedback (stays on this computer)
|
|
122
124
|
connect [client] config snippet for any agent: MCP clients, HTTP/OpenAPI, OpenAI, Anthropic, Gemini, shell
|
|
123
125
|
serve HTTP API on http://127.0.0.1:32147 (REVERSEPORT_PORT)
|
|
@@ -153,6 +155,20 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
153
155
|
print({ name: t.name, title: t.title, description: t.description, inputSchema: t.inputSchema, annotations: t.annotations, cli: `reverseport ${t.name} ${(POSITIONAL[t.name] ?? []).map(p => `<${p}>`).join(' ')} [--key value] [--args '{…}']`.replace(/\s+/g, ' ').trim() });
|
|
154
156
|
return 0;
|
|
155
157
|
}
|
|
158
|
+
if (cmd === 'skills') {
|
|
159
|
+
const { listSkills, getSkill } = await import('./src/skills.mjs');
|
|
160
|
+
const [sub, name] = rest.filter(x => !x.startsWith('--'));
|
|
161
|
+
try {
|
|
162
|
+
if (!sub || sub === 'list') { print({ version: VERSION, skills: await listSkills() }); return 0; }
|
|
163
|
+
if (sub === 'get') {
|
|
164
|
+
const s = await getSkill(name);
|
|
165
|
+
if (argv.includes('--json')) print({ version: VERSION, name: s.name, description: s.description, body: s.body });
|
|
166
|
+
else process.stdout.write(`<!-- ReversePort ${VERSION} · skill ${s.name} -->\n${s.text.endsWith('\n') ? s.text : `${s.text}\n`}`);
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
return printError(new RPError('INVALID_ARGUMENT', `Unknown skills command "${sub}". Use: reverseport skills list | reverseport skills get <name>`));
|
|
170
|
+
} catch (e) { return printError(e); }
|
|
171
|
+
}
|
|
156
172
|
if (cmd === 'feedback') { print(await readFeedback()); return 0; }
|
|
157
173
|
if (cmd === 'connect') {
|
|
158
174
|
const { connect } = await import('./src/connect.mjs');
|
package/port/src/graph.mjs
CHANGED
|
@@ -103,10 +103,27 @@ export async function buildGraph(dir, { maxFiles = 30000 } = {}) {
|
|
|
103
103
|
for (const b of ['', path.posix.dirname(from)]) for (const c of [`${p}.lua`, `${p}/init.lua`]) { const hit = exists(path.posix.normalize(b ? `${b}/${c}` : c)); if (hit) return hit; }
|
|
104
104
|
return null;
|
|
105
105
|
};
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
// Rust: a file's child modules live in `<stem>/` (or next to mod.rs/lib.rs/main.rs); `crate::` starts at the
|
|
107
|
+
// crate's src/, and a workspace crate is reachable by its package name (`-` reads as `_`).
|
|
108
|
+
const rustModuleDir = from => { const dir = path.posix.dirname(from), stem = path.posix.basename(from, '.rs'); return ['mod', 'main', 'lib'].includes(stem) ? dir : path.posix.join(dir, stem); };
|
|
109
|
+
const rustModuleFile = dir => exists(`${dir}.rs`) ?? exists(`${dir}/mod.rs`) ?? exists(`${dir}/lib.rs`) ?? exists(`${dir}/main.rs`);
|
|
110
|
+
const resolveRustMod = (from, name) => { const base = rustModuleDir(from); return exists(path.posix.normalize(`${base}/${name}.rs`)) ?? exists(path.posix.normalize(`${base}/${name}/mod.rs`)); };
|
|
111
|
+
const rustCrates = new Map();
|
|
112
|
+
for (const f of files) if (/(^|\/)Cargo\.toml$/.test(f.rel)) {
|
|
113
|
+
const name = (await fs.readFile(f.abs, 'utf8').catch(() => '')).match(/^\[package\][^[]*?^\s*name\s*=\s*"([^"]+)"/ms)?.[1];
|
|
114
|
+
if (name) rustCrates.set(name.replace(/-/g, '_'), path.posix.join(path.posix.dirname(f.rel), 'src').replace(/^\.\//, ''));
|
|
115
|
+
}
|
|
116
|
+
const rustCrateSrc = from => { let dir = path.posix.dirname(from); while (dir && dir !== '.') { if (path.posix.basename(dir) === 'src' && exists(path.posix.join(path.posix.dirname(dir), 'Cargo.toml'))) return dir; dir = path.posix.dirname(dir); } return null; };
|
|
117
|
+
const resolveRustUse = (from, spec) => {
|
|
118
|
+
const segs = spec.split('::').filter(Boolean);
|
|
119
|
+
let base;
|
|
120
|
+
if (segs[0] === 'crate') { base = rustCrateSrc(from); segs.shift(); }
|
|
121
|
+
else if (segs[0] === 'self') { base = rustModuleDir(from); segs.shift(); }
|
|
122
|
+
else if (segs[0] === 'super') { base = rustModuleDir(from); while (segs[0] === 'super') { segs.shift(); base = path.posix.dirname(base); } }
|
|
123
|
+
else if (rustCrates.has(segs[0])) base = rustCrates.get(segs.shift());
|
|
124
|
+
if (!base || base === '.') return null;
|
|
125
|
+
for (let i = segs.length; i >= 1; i--) { const p = `${base}/${segs.slice(0, i).join('/')}`; const hit = exists(`${p}.rs`) ?? exists(`${p}/mod.rs`); if (hit) return hit; }
|
|
126
|
+
return rustModuleFile(base);
|
|
110
127
|
};
|
|
111
128
|
|
|
112
129
|
for (const n of nodes.values()) {
|
|
@@ -134,7 +151,7 @@ export async function buildGraph(dir, { maxFiles = 30000 } = {}) {
|
|
|
134
151
|
case 'python': hit = resolvePy(n.file, d.spec); if (!hit && !d.spec.startsWith('.')) external(d.spec.split('.')[0]); break;
|
|
135
152
|
case 'c': case 'c-header': case 'cpp': case 'objc': if (d.kind === 'system') external(`<${d.spec}>`); else { hit = resolveInclude(n.file, d.spec); if (!hit) external(`"${d.spec}"`); } break;
|
|
136
153
|
case 'lua': hit = resolveLua(n.file, d.spec); if (!hit) external(d.spec); break;
|
|
137
|
-
case 'rust': if (d.kind === 'mod') hit = resolveRustMod(n.file, d.spec); else if (!/^(crate|self|super)
|
|
154
|
+
case 'rust': if (d.kind === 'mod') hit = resolveRustMod(n.file, d.spec); else { hit = resolveRustUse(n.file, d.spec); if (!hit && !/^(crate|self|super)(::|$)/.test(d.spec)) external(d.spec.split('::')[0]); } break;
|
|
138
155
|
case 'go':
|
|
139
156
|
if (goModule && d.spec.startsWith(`${goModule}/`)) {
|
|
140
157
|
const pkgDir = d.spec.slice(goModule.length + 1);
|
|
@@ -239,9 +256,14 @@ export function tarjanOrder(keys, out) {
|
|
|
239
256
|
return { sccs, order: sccs.flat() };
|
|
240
257
|
}
|
|
241
258
|
|
|
259
|
+
/** Test files: under a test folder, or named *.test.*, *.spec.*, *_test.*, test_*. */
|
|
260
|
+
export const isTestFile = rel => /(^|\/)(tests?|__tests__|specs?|testing)\//.test(rel) || /(\.(test|spec)\.\w+|_tests?\.\w+)$/.test(rel) || /(^|\/)test_[^/]+$/.test(rel);
|
|
261
|
+
|
|
242
262
|
export function graphMermaid(graph, max = 60) {
|
|
243
263
|
const score = k => (graph.inDegree.get(k) ?? 0) + (graph.outEdges.get(k)?.length ?? 0);
|
|
244
|
-
|
|
264
|
+
// Tests depend on many files, so by degree alone they would crowd out the modules a reader wants to see.
|
|
265
|
+
const ranked = [...graph.nodes.keys()].sort((a, b) => score(b) - score(a));
|
|
266
|
+
const keep = [...ranked.filter(k => !isTestFile(k)), ...ranked.filter(isTestFile)].slice(0, max);
|
|
245
267
|
const id = new Map(keep.map((k, i) => [k, `f${i}`]));
|
|
246
268
|
const lines = ['graph LR'];
|
|
247
269
|
for (const k of keep) lines.push(` ${id.get(k)}["${path.posix.basename(k).replace(/"/g, "'")}"]`);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The skills that ship with this exact ReversePort version, served by the CLI.
|
|
2
|
+
// An agent that runs `reverseport skills get <name>` always reads the guide that matches the binary it calls,
|
|
3
|
+
// even when a copied SKILL.md in its own skills folder is older.
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { RPError } from './util.mjs';
|
|
8
|
+
|
|
9
|
+
const SKILLS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'skills');
|
|
10
|
+
|
|
11
|
+
function frontmatter(text) {
|
|
12
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
13
|
+
const meta = {};
|
|
14
|
+
if (m) for (const line of m[1].split(/\r?\n/)) { const kv = line.match(/^([a-zA-Z_-]+):\s*(.*)$/); if (kv) meta[kv[1]] = kv[2].replace(/^["']|["']$/g, ''); }
|
|
15
|
+
return { meta, body: m ? text.slice(m[0].length) : text };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function load(dir) {
|
|
19
|
+
let folders = [];
|
|
20
|
+
try { folders = (await fs.readdir(dir, { withFileTypes: true })).filter(d => d.isDirectory()).map(d => d.name).sort(); } catch { return []; }
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const folder of folders) {
|
|
23
|
+
const file = path.join(dir, folder, 'SKILL.md');
|
|
24
|
+
let text;
|
|
25
|
+
try { text = await fs.readFile(file, 'utf8'); } catch { continue; } // a folder without SKILL.md is not a skill
|
|
26
|
+
const { meta, body } = frontmatter(text);
|
|
27
|
+
out.push({ name: meta.name || folder, description: meta.description ?? '', file, body: body.trim(), text });
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function listSkills({ dir = SKILLS_DIR } = {}) {
|
|
33
|
+
return (await load(dir)).map(({ name, description }) => ({ name, description }));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function getSkill(name, { dir = SKILLS_DIR } = {}) {
|
|
37
|
+
const all = await load(dir);
|
|
38
|
+
const hit = all.find(s => s.name === name);
|
|
39
|
+
if (!hit) throw new RPError('INVALID_ARGUMENT', `${name ? `Unknown skill "${name}".` : 'Name a skill.'} Skills: ${all.map(s => s.name).join(', ') || '(none bundled)'}.`);
|
|
40
|
+
return hit;
|
|
41
|
+
}
|
package/port/src/symbols.mjs
CHANGED
|
@@ -55,6 +55,25 @@ function lineIndex(text) {
|
|
|
55
55
|
|
|
56
56
|
const all = (re, text) => [...text.matchAll(re)];
|
|
57
57
|
|
|
58
|
+
/** `a::{b, c::{self, D}, *}` → `a::b`, `a::c`, `a::c::D`, `a`. Aliases (`as X`) and leading `::` are dropped. */
|
|
59
|
+
export function expandRustUse(spec) {
|
|
60
|
+
const text = spec.replace(/\s+as\s+\w+/g, '').replace(/\s+/g, '').replace(/^::/, '');
|
|
61
|
+
const out = [];
|
|
62
|
+
const walk = (prefix, s) => {
|
|
63
|
+
const open = s.indexOf('{');
|
|
64
|
+
if (open < 0) { const p = `${prefix}${s}`.replace(/::(self|\*)$/, '').replace(/^(self|\*)$/, ''); if (p) out.push(p); return; }
|
|
65
|
+
let depth = 0, close = open;
|
|
66
|
+
for (; close < s.length; close++) { if (s[close] === '{') depth++; else if (s[close] === '}' && --depth === 0) break; }
|
|
67
|
+
const items = [];
|
|
68
|
+
let start = open + 1, level = 0;
|
|
69
|
+
for (let i = open + 1; i < close; i++) { if (s[i] === '{') level++; else if (s[i] === '}') level--; else if (s[i] === ',' && !level) { items.push(s.slice(start, i)); start = i + 1; } }
|
|
70
|
+
items.push(s.slice(start, close));
|
|
71
|
+
for (const item of items) if (item) walk(prefix + s.slice(0, open), item);
|
|
72
|
+
};
|
|
73
|
+
walk('', text);
|
|
74
|
+
return [...new Set(out)];
|
|
75
|
+
}
|
|
76
|
+
|
|
58
77
|
export function extractSymbols(text, language) {
|
|
59
78
|
const code = stripComments(text, language);
|
|
60
79
|
const line = lineIndex(code);
|
|
@@ -150,7 +169,9 @@ export function extractSymbols(text, language) {
|
|
|
150
169
|
for (const m of all(/\bfn\s+(\w+)/g, code)) fn(m[1], m);
|
|
151
170
|
for (const m of all(/\b(struct|enum|trait|union)\s+(\w+)/g, code)) cls(m[2], m, { kind: m[1] });
|
|
152
171
|
for (const m of all(/^\s*(?:pub(?:\([^)]*\))?\s+)?mod\s+(\w+)\s*;/gm, code)) dep(m[1], 'mod');
|
|
153
|
-
|
|
172
|
+
// `use a::{b, c::{self, D}};` names every leaf; a `use super::…` that is indented sits in an inline
|
|
173
|
+
// `mod tests { … }`, whose parent is this file, so one `super` becomes `self`.
|
|
174
|
+
for (const m of all(/^([ \t]*)(?:pub(?:\([^)]*\))?\s+)?use\s+([^;]+);/gm, code)) for (const spec of expandRustUse(m[2])) dep(m[1] ? spec.replace(/^super(?=::|$)/, 'self') : spec, 'use');
|
|
154
175
|
if (/\bfn\s+main\s*\(/.test(code)) out.entry = true;
|
|
155
176
|
break;
|
|
156
177
|
}
|
package/port/src/tools.mjs
CHANGED
|
@@ -125,8 +125,8 @@ function validate(schema, args, where = 'arguments') {
|
|
|
125
125
|
throw new RPError('INVALID_ARGUMENT', `Unknown argument ${hints.join(', ')}${where === 'arguments' ? '' : ` in ${where}`}. Allowed: ${allowed.join(', ')}.`);
|
|
126
126
|
}
|
|
127
127
|
for (const r of schema.required ?? []) if (args[r] === undefined || args[r] === null || args[r] === '') {
|
|
128
|
-
const d = schema.properties?.[r]
|
|
129
|
-
throw new RPError('INVALID_ARGUMENT', `Missing ${r}${d ? ` (${d.replace(/\.$/, '')})` : ''}.`);
|
|
128
|
+
const { description: d, enum: choices } = schema.properties?.[r] ?? {};
|
|
129
|
+
throw new RPError('INVALID_ARGUMENT', `Missing ${r}${d ? ` (${d.replace(/\.$/, '')})` : ''}.${choices ? ` One of: ${choices.join(', ')}.` : ''}`);
|
|
130
130
|
}
|
|
131
131
|
for (const [k, v] of Object.entries(args)) if (v !== undefined && v !== null && schema.properties?.[k]) validate(schema.properties[k], v, k);
|
|
132
132
|
return;
|
package/port/src/util.mjs
CHANGED
|
@@ -15,8 +15,22 @@ export const IGNORED_DIRS = new Set([
|
|
|
15
15
|
'.git', '.svn', '.hg', 'node_modules', '.godot', '.import', 'Library', 'Temp', 'Logs', 'UserSettings', 'obj',
|
|
16
16
|
'__pycache__', '.venv', 'venv', '.gradle', '.idea', '.vs', '.vscode', 'DerivedDataCache', 'Intermediate', 'Saved',
|
|
17
17
|
'.mono', '.reverseport', '.cache', '.next', '.nuxt', '.parcel-cache', '.turbo', 'Pods', '.dart_tool',
|
|
18
|
+
'.pytest_cache', '.mypy_cache', '.ruff_cache', '.tox', 'zig-cache', '.zig-cache', 'zig-out', 'DerivedData',
|
|
19
|
+
'.svelte-kit', '.angular', '.vite', '.expo', '.terraform', 'bower_components', 'jspm_packages',
|
|
18
20
|
]);
|
|
19
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Folders that are build output or a package store only next to a marker file: `target/` is Rust or Maven output
|
|
24
|
+
* beside Cargo.toml/pom.xml but may be real source elsewhere; `vendor/` holds copied dependencies beside go.mod,
|
|
25
|
+
* composer.json or a Gemfile.
|
|
26
|
+
*/
|
|
27
|
+
export const IGNORED_DIRS_BESIDE = {
|
|
28
|
+
target: ['Cargo.toml', 'pom.xml', 'build.sbt'],
|
|
29
|
+
vendor: ['go.mod', 'composer.json', 'Gemfile'],
|
|
30
|
+
'.build': ['Package.swift'],
|
|
31
|
+
'.gradle': [],
|
|
32
|
+
};
|
|
33
|
+
|
|
20
34
|
export async function statOrNull(file) { try { return await fs.stat(file); } catch { return null; } }
|
|
21
35
|
|
|
22
36
|
export async function requireFile(file, what = 'file') {
|
|
@@ -88,7 +102,7 @@ export function isText(buf) {
|
|
|
88
102
|
export const toPosix = p => p.split(path.sep).join('/');
|
|
89
103
|
|
|
90
104
|
/** Walk a folder. Returns files with posix relative paths; skips IGNORED_DIRS and symlinks. */
|
|
91
|
-
export async function walk(root, { maxFiles = 50000, ignore = IGNORED_DIRS, maxDepth = 40 } = {}) {
|
|
105
|
+
export async function walk(root, { maxFiles = 50000, ignore = IGNORED_DIRS, ignoreBeside = ignore === IGNORED_DIRS ? IGNORED_DIRS_BESIDE : {}, maxDepth = 40 } = {}) {
|
|
92
106
|
const files = [];
|
|
93
107
|
const skipped = new Set();
|
|
94
108
|
const unreadable = [];
|
|
@@ -98,12 +112,14 @@ export async function walk(root, { maxFiles = 50000, ignore = IGNORED_DIRS, maxD
|
|
|
98
112
|
let entries;
|
|
99
113
|
try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch (e) { if (unreadable.length < 50) unreadable.push({ path: toPosix(path.relative(root, dir)) || '.', code: e.code ?? 'ERROR' }); return; }
|
|
100
114
|
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
115
|
+
const names = new Set(entries.map(e => e.name));
|
|
116
|
+
const buildOutput = name => ignoreBeside[name]?.some(marker => names.has(marker));
|
|
101
117
|
for (const e of entries) {
|
|
102
118
|
if (truncated) return;
|
|
103
119
|
const abs = path.join(dir, e.name);
|
|
104
120
|
if (e.isSymbolicLink()) continue;
|
|
105
121
|
if (e.isDirectory()) {
|
|
106
|
-
if (ignore.has(e.name)) { skipped.add(toPosix(path.relative(root, abs))); continue; }
|
|
122
|
+
if (ignore.has(e.name) || buildOutput(e.name)) { skipped.add(toPosix(path.relative(root, abs))); continue; }
|
|
107
123
|
await visit(abs, depth + 1);
|
|
108
124
|
} else if (e.isFile()) {
|
|
109
125
|
if (files.length >= maxFiles) { truncated = true; return; }
|
|
@@ -34,7 +34,7 @@ original task after repair. Client approval, shell PATH, registration and engine
|
|
|
34
34
|
availability have different remedies; avoid repeating setup for all of them.
|
|
35
35
|
|
|
36
36
|
Prefer an existing `reverseport` CLI. When none is installed and ReversePort package acquisition
|
|
37
|
-
is within the authorized scope, use `npx -y reverseport@1.
|
|
37
|
+
is within the authorized scope, use `npx -y reverseport@1.5.0` in its place.
|
|
38
38
|
Configuration repair starts with a read-only setup plan; show paths, backups and
|
|
39
39
|
skill replacement before applying authorized changes. Preserve existing approval
|
|
40
40
|
and target-execution boundaries. ReversePort setup never installs or upgrades unrelated
|