@prodero/curator 0.0.0-stage → 0.2.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/ARCHITECTURE.md +298 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +9 -0
- package/LICENSE +21 -0
- package/README.md +93 -2
- package/SECURITY.md +7 -0
- package/bin/prodero +7 -0
- package/dist/cli.js +266 -0
- package/dist/context-instructions.js +2 -0
- package/dist/contracts.js +51 -0
- package/dist/conversation-presentation.js +297 -0
- package/dist/core/agent-instructions.js +52 -0
- package/dist/core/applicability-label.js +11 -0
- package/dist/core/applicability.js +143 -0
- package/dist/core/bootstrap.js +129 -0
- package/dist/core/consolidation.js +315 -0
- package/dist/core/constants.js +21 -0
- package/dist/core/context-allocation.js +54 -0
- package/dist/core/context-snapshot.js +12 -0
- package/dist/core/context.js +489 -0
- package/dist/core/curation-clauses.js +68 -0
- package/dist/core/curation-evidence.js +98 -0
- package/dist/core/curation-graphical.js +117 -0
- package/dist/core/curation-question.js +28 -0
- package/dist/core/curation-review-presentation.js +4 -0
- package/dist/core/curation-review-state.js +62 -0
- package/dist/core/curation-review.js +573 -0
- package/dist/core/curation-revision.js +106 -0
- package/dist/core/curation-semantic-contract.js +242 -0
- package/dist/core/curation-semantic-jobs.js +340 -0
- package/dist/core/diagnostics.js +17 -0
- package/dist/core/format.js +114 -0
- package/dist/core/ids.js +6 -0
- package/dist/core/imports.js +169 -0
- package/dist/core/index.js +93 -0
- package/dist/core/instruction-review.js +86 -0
- package/dist/core/integrity-files.js +174 -0
- package/dist/core/intelligence.js +95 -0
- package/dist/core/knowledge-analysis.js +46 -0
- package/dist/core/knowledge-authority.js +143 -0
- package/dist/core/knowledge-curation.js +316 -0
- package/dist/core/knowledge-engine.js +475 -0
- package/dist/core/knowledge-graph.js +192 -0
- package/dist/core/knowledge-lifecycle.js +212 -0
- package/dist/core/knowledge-management.js +113 -0
- package/dist/core/knowledge-migration.js +84 -0
- package/dist/core/knowledge-policy.js +38 -0
- package/dist/core/knowledge-provenance.js +90 -0
- package/dist/core/knowledge-write-policy.js +85 -0
- package/dist/core/language.js +14 -0
- package/dist/core/local-preferences.js +76 -0
- package/dist/core/matching.js +6 -0
- package/dist/core/memory-index.js +241 -0
- package/dist/core/memory-intelligence.js +288 -0
- package/dist/core/memory-link-repair.js +70 -0
- package/dist/core/memory-operations.js +122 -0
- package/dist/core/memory-pressure.js +212 -0
- package/dist/core/memory-sync-curator.js +82 -0
- package/dist/core/memory-sync.js +558 -0
- package/dist/core/memory-transformation-bundle.js +163 -0
- package/dist/core/memory-transformation-proposal.js +135 -0
- package/dist/core/memory-transformation.js +108 -0
- package/dist/core/memory.js +186 -0
- package/dist/core/migrations.js +569 -0
- package/dist/core/npm-update.js +289 -0
- package/dist/core/path-scope.js +27 -0
- package/dist/core/plugin-files.js +116 -0
- package/dist/core/project-root.js +39 -0
- package/dist/core/project.js +634 -0
- package/dist/core/promotion.js +279 -0
- package/dist/core/reconciliation.js +61 -0
- package/dist/core/secrets.js +67 -0
- package/dist/core/semantic-safety.js +33 -0
- package/dist/core/session-index.js +116 -0
- package/dist/core/session-review.js +150 -0
- package/dist/core/sessions.js +289 -0
- package/dist/core/settings.js +94 -0
- package/dist/core/skill-curation.js +239 -0
- package/dist/core/skill-polish.js +41 -0
- package/dist/core/skills.js +387 -0
- package/dist/core/storage.js +462 -0
- package/dist/core/sync-transaction.js +290 -0
- package/dist/core/telemetry.js +64 -0
- package/dist/core/templates.js +53 -0
- package/dist/core/types.js +2 -0
- package/dist/core/update.js +326 -0
- package/dist/curation-review-adapter.js +92 -0
- package/dist/diagnostic-boundary.js +55 -0
- package/dist/filesystem/permissions.js +39 -0
- package/dist/hosts/adapters.js +119 -0
- package/dist/hosts/files.js +133 -0
- package/dist/hosts/manager.js +150 -0
- package/dist/hosts/public-entry.js +47 -0
- package/dist/hosts/runtime-command.js +66 -0
- package/dist/hosts/runtime.js +139 -0
- package/dist/hosts/wizard.js +114 -0
- package/dist/knowledge-table.js +38 -0
- package/dist/mcp-development.js +69 -0
- package/dist/mcp-workspace.js +141 -0
- package/dist/memory-presentation.js +182 -0
- package/dist/memory-sync-operation.js +88 -0
- package/dist/plugins/api.js +2 -0
- package/dist/plugins/context.js +20 -0
- package/dist/plugins/mcp.js +72 -0
- package/dist/plugins/registry.js +106 -0
- package/dist/presentation-cards.js +32 -0
- package/dist/presentation-server.js +94 -0
- package/dist/presentation-trace.js +174 -0
- package/dist/presentation.js +443 -0
- package/dist/providers/mcp-sampling.js +45 -0
- package/dist/server.js +664 -0
- package/dist/settings-definition.js +22 -0
- package/dist/skill-instructions.js +2 -0
- package/dist/status-presentation.js +20 -0
- package/dist/text-layout.js +278 -0
- package/dist/update-command.js +53 -0
- package/dist/update-instructions.js +4 -0
- package/dist-ui/index.html +216 -0
- package/docs/APPLICABILITY.md +151 -0
- package/docs/COMPATIBILITY.md +68 -0
- package/docs/CONTEXT-SELECTION-V2.md +61 -0
- package/docs/CURATION-APP-REVIEW.md +156 -0
- package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
- package/docs/IMPORTER-CURATION.md +142 -0
- package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
- package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
- package/docs/MIGRATIONS.md +103 -0
- package/docs/PRIVACY.md +28 -0
- package/docs/RECOVERY.md +58 -0
- package/docs/RELEASING.md +98 -0
- package/docs/hosts.md +152 -0
- package/docs/images/prodero-context-ready-cropped.png +0 -0
- package/docs/images/prodero-overview-cropped.png +0 -0
- package/docs/images/prodero-session-learned-cropped.png +0 -0
- package/docs/plugins.md +83 -0
- package/docs/ui-themes.md +49 -0
- package/instructions/AGENTS.md +39 -0
- package/package.json +92 -4
- package/plugins/migrate_ai_studio/README.md +101 -0
- package/plugins/migrate_ai_studio/index.cjs +396 -0
- package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
- package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
- package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
- package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
- package/plugins/migrate_ai_studio/plugin.json +11 -0
- package/plugins/migrate_ai_studio/schema.cjs +20 -0
- package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
- package/prodero.release.json +5 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/** Terminal-owned npm update. Every controller import is a Node builtin and is
|
|
2
|
+
* loaded before npm may replace this package; no project or host writers. */
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { gunzip } from 'node:zlib';
|
|
10
|
+
import { promisify } from 'node:util';
|
|
11
|
+
const unzip = promisify(gunzip);
|
|
12
|
+
const NAME = '@prodero/curator', REGISTRY = 'https://registry.npmjs.org/';
|
|
13
|
+
const stable = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
14
|
+
const MAX_TARBALL = 32 * 1024 * 1024, MAX_UNPACKED = 128 * 1024 * 1024;
|
|
15
|
+
export class NpmUpdateError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
exitCode;
|
|
18
|
+
constructor(code, message, exitCode = 1) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.exitCode = exitCode;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function fail(code, message) { throw new NpmUpdateError(code, message); }
|
|
25
|
+
// The registry normalizes away ./; allow only these equivalent exact paths.
|
|
26
|
+
const supportedBin = (value) => !!value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 1 && Object.hasOwn(value, 'prodero') && ['bin/prodero', './bin/prodero'].includes(value.prodero);
|
|
27
|
+
const comparison = (a, b) => { const x = a.split('.').map(Number), y = b.split('.').map(Number); return x[0] - y[0] || x[1] - y[1] || x[2] - y[2]; };
|
|
28
|
+
const supportedEngine = (engine, node) => {
|
|
29
|
+
// The initial allowlist intentionally accepts only the project's published
|
|
30
|
+
// engine contract. Unknown range syntax is unsupported, never guessed.
|
|
31
|
+
return engine === '>=22.14.0 <23' && stable.test(node) && comparison(node, '22.14.0') >= 0 && comparison(node, '23.0.0') < 0;
|
|
32
|
+
};
|
|
33
|
+
async function bounded(response, max) {
|
|
34
|
+
if (!response.ok || !response.body)
|
|
35
|
+
fail('NPM_UPDATE_REGISTRY', 'The public npm artifact or metadata is unavailable. No install started.');
|
|
36
|
+
const length = Number(response.headers.get('content-length'));
|
|
37
|
+
if (length > max)
|
|
38
|
+
fail('NPM_UPDATE_ARTIFACT', 'The npm response exceeds the supported size.');
|
|
39
|
+
const reader = response.body.getReader(), parts = [];
|
|
40
|
+
let bytes = 0;
|
|
41
|
+
try {
|
|
42
|
+
while (true) {
|
|
43
|
+
const chunk = await reader.read();
|
|
44
|
+
if (chunk.done)
|
|
45
|
+
break;
|
|
46
|
+
bytes += chunk.value.length;
|
|
47
|
+
if (bytes > max)
|
|
48
|
+
fail('NPM_UPDATE_ARTIFACT', 'The npm response exceeds the supported size.');
|
|
49
|
+
parts.push(Buffer.from(chunk.value));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await reader.cancel().catch(() => { });
|
|
54
|
+
}
|
|
55
|
+
return Buffer.concat(parts);
|
|
56
|
+
}
|
|
57
|
+
function childEnv(temp, node) {
|
|
58
|
+
return { HOME: path.join(temp, 'home'), TMPDIR: path.join(temp, 'tmp'), PATH: path.dirname(node), LANG: 'C', LC_ALL: 'C', NO_COLOR: '1', npm_config_update_notifier: 'false' };
|
|
59
|
+
}
|
|
60
|
+
/** Wait for actual exit, but never wait indefinitely for descendants holding
|
|
61
|
+
* inherited output pipes. Only this command's direct child is signalled. */
|
|
62
|
+
export async function runNpmCommand(node, args, options) {
|
|
63
|
+
return new Promise(resolve => {
|
|
64
|
+
let stdout = '', stderr = '', timedOut = false, done = false, exited = false, code = null, signal = null;
|
|
65
|
+
const child = spawn(node, args, { cwd: options.cwd, env: options.env, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
66
|
+
let hardTimer, drainTimer, finalTimer;
|
|
67
|
+
const finish = (outputIncomplete) => { if (done)
|
|
68
|
+
return; done = true; clearTimeout(deadline); clearTimeout(hardTimer); clearTimeout(drainTimer); clearTimeout(finalTimer); child.stdout.destroy(); child.stderr.destroy(); child.unref(); resolve({ code, stdout, stderr, timedOut, exited, outputIncomplete, signal }); };
|
|
69
|
+
child.stdout.on('data', data => { stdout = (stdout + String(data)).slice(-65536); });
|
|
70
|
+
child.stderr.on('data', data => { stderr = (stderr + String(data)).slice(-65536); });
|
|
71
|
+
const deadline = setTimeout(() => { timedOut = true; child.kill('SIGTERM'); hardTimer = setTimeout(() => { child.kill('SIGKILL'); finalTimer = setTimeout(() => finish(true), 250); }, 5000); }, options.timeout);
|
|
72
|
+
child.once('error', () => finish(true));
|
|
73
|
+
child.once('exit', (exitCode, exitSignal) => { exited = true; code = exitCode; signal = exitSignal; clearTimeout(deadline); clearTimeout(hardTimer); drainTimer = setTimeout(() => finish(true), 250); });
|
|
74
|
+
child.once('close', () => finish(false));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function flags(temp, prefix) { return ['--prefix', prefix, '--userconfig', path.join(temp, 'user.npmrc'), '--globalconfig', path.join(temp, 'global.npmrc'), '--cache', path.join(temp, 'cache'), '--registry', REGISTRY]; }
|
|
78
|
+
async function owned(target, directory = false) {
|
|
79
|
+
const stat = await fs.lstat(target);
|
|
80
|
+
if (stat.isSymbolicLink() || (directory ? !stat.isDirectory() : !stat.isFile()) || (process.getuid && stat.uid !== process.getuid()) || (stat.mode & 0o022))
|
|
81
|
+
fail('NPM_UPDATE_LAYOUT', 'This updater requires an ordinary same-user npm installation without writable shared paths. Use your package manager for this layout.');
|
|
82
|
+
if (directory && !(stat.mode & 0o200))
|
|
83
|
+
fail('NPM_UPDATE_PERMISSION', 'This npm installation is not writable. Repair permissions with its package manager; this updater does not use sudo.');
|
|
84
|
+
if (directory)
|
|
85
|
+
await fs.access(target, fs.constants.W_OK).catch(() => fail('NPM_UPDATE_PERMISSION', 'This npm installation is not writable. Repair permissions with its package manager; this updater does not use sudo.'));
|
|
86
|
+
return stat;
|
|
87
|
+
}
|
|
88
|
+
async function layout(deps, temp) {
|
|
89
|
+
const selectedRoot = path.resolve(deps.packageRoot ?? fileURLToPath(new URL('../..', import.meta.url))), root = await fs.realpath(selectedRoot), prefix = path.resolve(root, '../../../..');
|
|
90
|
+
if (selectedRoot !== root)
|
|
91
|
+
fail('NPM_UPDATE_LAYOUT', 'Linked package roots are unsupported. Invoke this installation’s own public executable.');
|
|
92
|
+
const entry = path.resolve(deps.entryPath ?? process.argv[1] ?? '');
|
|
93
|
+
const publicEntries = [path.join(prefix, 'bin/prodero'), path.join(root, 'bin/prodero')], internalCli = path.join(root, 'dist/cli.js');
|
|
94
|
+
if (!publicEntries.includes(entry) && entry !== internalCli || await fs.realpath(entry) !== (entry === internalCli ? internalCli : path.join(root, 'bin/prodero')))
|
|
95
|
+
fail('NPM_UPDATE_LAYOUT', 'The invoked executable does not belong to this exact npm installation. Linked aliases cannot select a different prefix.');
|
|
96
|
+
if (root !== path.join(prefix, 'lib/node_modules/@prodero/curator') || await fs.stat(path.join(root, 'src/cli.ts')).catch(() => null))
|
|
97
|
+
fail('NPM_UPDATE_LAYOUT', 'Only an installed global npm @prodero/curator package is supported. Linked packages and source checkouts need their package manager.');
|
|
98
|
+
for (const dir of [prefix, path.join(prefix, 'bin'), path.join(prefix, 'lib'), path.join(prefix, 'lib/node_modules'), path.join(prefix, 'lib/node_modules/@prodero'), root])
|
|
99
|
+
await owned(dir, true);
|
|
100
|
+
await owned(path.join(root, 'package.json'));
|
|
101
|
+
await owned(path.join(root, 'bin/prodero'));
|
|
102
|
+
const pkg = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'));
|
|
103
|
+
if (pkg.name !== NAME || !stable.test(pkg.version) || !supportedBin(pkg.bin))
|
|
104
|
+
fail('NPM_UPDATE_LAYOUT', 'The installed package identity or public executable is unsupported.');
|
|
105
|
+
const publicBin = path.join(prefix, 'bin/prodero');
|
|
106
|
+
const bin = await fs.lstat(publicBin);
|
|
107
|
+
if (!bin.isSymbolicLink() || process.getuid && bin.uid !== process.getuid() || await fs.realpath(publicBin) !== path.join(root, 'bin/prodero'))
|
|
108
|
+
fail('NPM_UPDATE_LAYOUT', 'The public executable does not belong to this npm package. No other installation will be changed.');
|
|
109
|
+
const node = await fs.realpath(deps.nodePath ?? process.execPath);
|
|
110
|
+
await fs.access(node, fs.constants.X_OK);
|
|
111
|
+
const npm = await fs.realpath(path.join(path.dirname(node), 'npm')).catch(() => fail('NPM_UPDATE_TOOLCHAIN', 'Cannot find npm beside the current Node executable. Use this installation’s package manager.'));
|
|
112
|
+
const npmRoot = path.resolve(path.dirname(npm), '..'), npmPkg = JSON.parse(await fs.readFile(path.join(npmRoot, 'package.json'), 'utf8'));
|
|
113
|
+
if (npm !== path.join(npmRoot, 'bin/npm-cli.js') || npmPkg.name !== 'npm' || npmPkg.bin?.npm !== 'bin/npm-cli.js' || !/^10\.9\.\d+$/.test(npmPkg.version))
|
|
114
|
+
fail('NPM_UPDATE_TOOLCHAIN', 'This updater supports npm 10.9.x with Node 22.14–22.x. Other toolchains need their package manager.');
|
|
115
|
+
if (!supportedEngine('>=22.14.0 <23', process.versions.node))
|
|
116
|
+
fail('NPM_UPDATE_TOOLCHAIN', 'This updater requires Node 22.14–22.x.');
|
|
117
|
+
for (const file of [node, npm, path.join(npmRoot, 'package.json')]) {
|
|
118
|
+
const s = await fs.lstat(file);
|
|
119
|
+
if (!s.isFile() || s.mode & 0o022 || process.getuid && s.uid !== process.getuid() && s.uid !== 0)
|
|
120
|
+
fail('NPM_UPDATE_TOOLCHAIN', 'The Node/npm toolchain ownership or permissions are unsupported.');
|
|
121
|
+
}
|
|
122
|
+
if (node.startsWith(root + path.sep) || npm.startsWith(root + path.sep))
|
|
123
|
+
fail('NPM_UPDATE_TOOLCHAIN', 'Node and npm must be outside the package being replaced.');
|
|
124
|
+
const run = deps.run ?? runNpmCommand, options = { cwd: temp, env: childEnv(temp, node), timeout: 15000 }, fixed = flags(temp, prefix);
|
|
125
|
+
const npmRootResult = await run(node, [npm, 'root', '--global', ...fixed], options), listing = await run(node, [npm, 'ls', '--global', '--parseable', '--depth=0', NAME, ...fixed], options);
|
|
126
|
+
if (npmRootResult.code !== 0 || npmRootResult.timedOut || npmRootResult.outputIncomplete || npmRootResult.exited === false || npmRootResult.stdout.trim() !== path.join(prefix, 'lib/node_modules') || listing.code !== 0 || listing.timedOut || listing.outputIncomplete || listing.exited === false || !listing.stdout.trim().split(/\r?\n/).includes(root))
|
|
127
|
+
fail('NPM_UPDATE_LAYOUT', 'The verified npm does not recognize this exact global installation. No package was changed.');
|
|
128
|
+
const identities = await Promise.all([entry, node, npm, path.join(npmRoot, 'package.json'), path.join(root, 'package.json'), path.join(root, 'bin/prodero')].map(async (file) => { const s = await fs.stat(file); return [file, s.dev, s.ino, s.size, s.mtimeMs, s.uid, s.mode]; }));
|
|
129
|
+
return { root, prefix, node, npm, version: pkg.version, identity: JSON.stringify(identities) };
|
|
130
|
+
}
|
|
131
|
+
function manifest(value, version) {
|
|
132
|
+
const v = value;
|
|
133
|
+
if (!v || v.name !== NAME || !stable.test(v.version) || version && v.version !== version || !supportedBin(v.bin) || !supportedEngine(v.engines?.node, process.versions.node))
|
|
134
|
+
fail('NPM_UPDATE_TARGET', 'The npm target identity, executable or Node engine contract is unsupported.');
|
|
135
|
+
if (v.scripts && (typeof v.scripts !== 'object' || Array.isArray(v.scripts)))
|
|
136
|
+
fail('NPM_UPDATE_TARGET', 'Invalid npm target script metadata.');
|
|
137
|
+
if (v.scripts && ['preinstall', 'install', 'postinstall'].some(k => v.scripts[k]))
|
|
138
|
+
fail('NPM_UPDATE_SCRIPTS', 'The target requires install lifecycle hooks, which this updater does not run.');
|
|
139
|
+
return v;
|
|
140
|
+
}
|
|
141
|
+
/** Inspect the verified archive before npm can create bins or install anything.
|
|
142
|
+
* Normal ustar and npm PAX headers are supported; links/escaping paths are not. */
|
|
143
|
+
async function archiveManifest(compressed) {
|
|
144
|
+
const tar = await unzip(compressed, { maxOutputLength: MAX_UNPACKED }), files = new Set();
|
|
145
|
+
let pkg, paxPath;
|
|
146
|
+
for (let offset = 0; offset + 512 <= tar.length;) {
|
|
147
|
+
const h = tar.subarray(offset, offset + 512);
|
|
148
|
+
if (h.every(b => b === 0))
|
|
149
|
+
break;
|
|
150
|
+
const field = (start, size) => h.subarray(start, start + size).toString('utf8').replace(/\0.*$/s, '');
|
|
151
|
+
const sizeText = field(124, 12).trim();
|
|
152
|
+
if (!/^[0-7]+$/.test(sizeText))
|
|
153
|
+
fail('NPM_UPDATE_ARTIFACT', 'Unsupported npm archive size field.');
|
|
154
|
+
const size = parseInt(sizeText, 8), end = offset + 512 + size;
|
|
155
|
+
if (!Number.isSafeInteger(size) || end > tar.length)
|
|
156
|
+
fail('NPM_UPDATE_ARTIFACT', 'Incomplete npm archive.');
|
|
157
|
+
const checksum = parseInt(field(148, 8).trim(), 8), sum = h.reduce((a, b, i) => a + (i >= 148 && i < 156 ? 32 : b), 0);
|
|
158
|
+
if (checksum !== sum)
|
|
159
|
+
fail('NPM_UPDATE_ARTIFACT', 'Invalid npm archive header.');
|
|
160
|
+
const type = field(156, 1), body = tar.subarray(offset + 512, end);
|
|
161
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
162
|
+
if (type === 'x' || type === 'g') {
|
|
163
|
+
let at = 0;
|
|
164
|
+
while (at < body.length) {
|
|
165
|
+
const space = body.indexOf(32, at), length = Number(body.subarray(at, space).toString());
|
|
166
|
+
if (space < at || !Number.isSafeInteger(length) || length <= space - at + 1 || at + length > body.length)
|
|
167
|
+
fail('NPM_UPDATE_ARTIFACT', 'Invalid npm archive metadata.');
|
|
168
|
+
const record = body.subarray(space + 1, at + length - 1).toString(), equal = record.indexOf('=');
|
|
169
|
+
if (record.slice(0, equal) === 'path')
|
|
170
|
+
paxPath = record.slice(equal + 1);
|
|
171
|
+
if (['linkpath', 'size'].includes(record.slice(0, equal)))
|
|
172
|
+
fail('NPM_UPDATE_ARTIFACT', 'Unsupported npm archive metadata.');
|
|
173
|
+
at += length;
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const name = paxPath ?? [field(345, 155), field(0, 100)].filter(Boolean).join('/');
|
|
178
|
+
paxPath = undefined;
|
|
179
|
+
if (!name.startsWith('package/') || name.includes('\\') || name.split('/').includes('..') || path.posix.normalize(name).replace(/\/$/, '') !== name.replace(/\/$/, ''))
|
|
180
|
+
fail('NPM_UPDATE_ARTIFACT', 'Unsafe npm archive path.');
|
|
181
|
+
if (type === '5')
|
|
182
|
+
continue;
|
|
183
|
+
if (type !== '' && type !== '0')
|
|
184
|
+
fail('NPM_UPDATE_ARTIFACT', 'Linked or unsupported npm archive entries are not supported.');
|
|
185
|
+
const relative = name.slice(8);
|
|
186
|
+
if (files.has(relative))
|
|
187
|
+
fail('NPM_UPDATE_ARTIFACT', 'Duplicate npm archive entry.');
|
|
188
|
+
files.add(relative);
|
|
189
|
+
if (relative.endsWith('.node') || path.posix.basename(relative) === 'binding.gyp')
|
|
190
|
+
fail('NPM_UPDATE_SCRIPTS', 'A target native build is unsupported with install scripts disabled.');
|
|
191
|
+
if (relative === 'package.json') {
|
|
192
|
+
if (size > 1024 * 1024)
|
|
193
|
+
fail('NPM_UPDATE_ARTIFACT', 'The package manifest is too large.');
|
|
194
|
+
try {
|
|
195
|
+
pkg = JSON.parse(body.toString('utf8'));
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
fail('NPM_UPDATE_ARTIFACT', 'Invalid package manifest in npm archive.');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!pkg || !files.has('bin/prodero') || !files.has('dist/cli.js'))
|
|
203
|
+
fail('NPM_UPDATE_ARTIFACT', 'The target is missing its prebuilt public executable or CLI.');
|
|
204
|
+
return { pkg, files };
|
|
205
|
+
}
|
|
206
|
+
export async function runNpmUpdate(options = {}, deps = {}) {
|
|
207
|
+
if (options.targetVersion && !stable.test(options.targetVersion))
|
|
208
|
+
fail('NPM_UPDATE_VERSION', 'Use one exact stable version (for example 0.2.0), without ranges, tags or URLs.');
|
|
209
|
+
if (!deps.interactive && !(options.yes && options.targetVersion))
|
|
210
|
+
fail('NPM_UPDATE_CONFIRMATION', 'Use an interactive terminal, or explicitly run prodero update <exact-version> --yes after ending work and closing Codex and Claude.');
|
|
211
|
+
const write = deps.write ?? console.log, temp = await fs.mkdtemp(path.join(os.tmpdir(), 'prodero-npm-update-'));
|
|
212
|
+
let started = false;
|
|
213
|
+
try {
|
|
214
|
+
await Promise.all(['home', 'tmp', 'cache'].map(dir => fs.mkdir(path.join(temp, dir), { mode: 0o700 })));
|
|
215
|
+
await Promise.all(['user.npmrc', 'global.npmrc'].map(file => fs.writeFile(path.join(temp, file), '', { mode: 0o600 })));
|
|
216
|
+
const current = await layout(deps, temp), request = deps.request ?? fetch;
|
|
217
|
+
let target;
|
|
218
|
+
try {
|
|
219
|
+
const data = await bounded(await request(`${REGISTRY}@prodero%2fcurator/${options.targetVersion ?? 'latest'}`, { signal: AbortSignal.timeout(15000), redirect: 'error' }), 1024 * 1024);
|
|
220
|
+
target = manifest(JSON.parse(data.toString()), options.targetVersion);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof NpmUpdateError)
|
|
224
|
+
throw error;
|
|
225
|
+
fail('NPM_UPDATE_REGISTRY', 'Cannot verify the public npm target. No install started.');
|
|
226
|
+
}
|
|
227
|
+
if (comparison(target.version, current.version) < 0)
|
|
228
|
+
fail('NPM_UPDATE_VERSION', 'Downgrades are not supported. No package was changed.');
|
|
229
|
+
if (target.version === current.version)
|
|
230
|
+
return { ok: true, status: 'current', installedVersion: current.version, targetVersion: target.version, prefix: current.prefix, restartRequired: false, message: 'This exact npm version is already installed. No package was changed.', exitCode: 0 };
|
|
231
|
+
let url;
|
|
232
|
+
try {
|
|
233
|
+
url = new URL(target.dist.tarball);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
fail('NPM_UPDATE_TARGET', 'The npm artifact URL is missing or invalid.');
|
|
237
|
+
}
|
|
238
|
+
if (url.protocol !== 'https:' || url.hostname !== 'registry.npmjs.org' || url.port || url.username || url.password || url.search || url.hash || url.pathname !== `/@prodero/curator/-/curator-${target.version}.tgz` || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(target.dist.integrity))
|
|
239
|
+
fail('NPM_UPDATE_TARGET', 'Only a verified standard npm registry artifact is supported.');
|
|
240
|
+
let tarball;
|
|
241
|
+
try {
|
|
242
|
+
tarball = await bounded(await request(url.href, { signal: AbortSignal.timeout(30000), redirect: 'error' }), MAX_TARBALL);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (error instanceof NpmUpdateError)
|
|
246
|
+
throw error;
|
|
247
|
+
fail('NPM_UPDATE_REGISTRY', 'Cannot download the verified npm artifact. No install started.');
|
|
248
|
+
}
|
|
249
|
+
if (`sha512-${crypto.createHash('sha512').update(tarball).digest('base64')}` !== target.dist.integrity)
|
|
250
|
+
fail('NPM_UPDATE_INTEGRITY', 'The downloaded npm artifact does not match its approved integrity. No install started.');
|
|
251
|
+
const archived = manifest((await archiveManifest(tarball)).pkg, target.version);
|
|
252
|
+
if (archived.engines.node !== target.engines.node)
|
|
253
|
+
fail('NPM_UPDATE_TARGET', 'The artifact engine metadata differs from the registry.');
|
|
254
|
+
write(`Update ${NAME}: ${current.version} → ${target.version}\nDestination: ${current.prefix}\nInstall scripts are disabled. Standard npm production dependencies will be resolved from the public registry.\nFinish work and close Codex and Claude before continuing. This command does not detect or close running applications.`);
|
|
255
|
+
if (options.yes)
|
|
256
|
+
write('--yes confirms that you have finished work and closed Codex and Claude.');
|
|
257
|
+
if (!options.yes && !await deps.confirm?.('Have you finished work and closed Codex and Claude? Continue with this update? [y/N] '))
|
|
258
|
+
return { ok: false, status: 'cancelled', installedVersion: current.version, targetVersion: target.version, prefix: current.prefix, restartRequired: false, message: 'Update cancelled. No package was changed.', exitCode: 1 };
|
|
259
|
+
if ((await layout(deps, temp)).identity !== current.identity)
|
|
260
|
+
fail('NPM_UPDATE_CHANGED', 'The installation or toolchain changed during confirmation. Run the update again to review it.');
|
|
261
|
+
const artifact = path.join(temp, 'curator.tgz');
|
|
262
|
+
await fs.writeFile(artifact, tarball, { mode: 0o600 });
|
|
263
|
+
write('Installing the checked npm artifact. Keep Codex and Claude closed until verification finishes.');
|
|
264
|
+
const run = deps.run ?? runNpmCommand, execOptions = { cwd: temp, env: childEnv(temp, current.node), timeout: 180000 };
|
|
265
|
+
started = true;
|
|
266
|
+
const installed = await run(current.node, [current.npm, 'install', '--global', ...flags(temp, current.prefix), '--ignore-scripts', '--engine-strict', '--omit=dev', '--no-audit', '--no-fund', artifact], execOptions);
|
|
267
|
+
if (installed.code !== 0 || installed.timedOut || installed.outputIncomplete || installed.exited === false)
|
|
268
|
+
return { ok: false, status: 'failed', installedVersion: null, targetVersion: target.version, prefix: current.prefix, restartRequired: true, message: 'npm did not complete successfully. The installation may be partial; keep applications closed and inspect/repair this npm installation before retrying. No rollback is claimed.', exitCode: installed.code && installed.code > 0 ? installed.code : 1 };
|
|
269
|
+
// From this boundary onward use only builtin readers/child-process code already
|
|
270
|
+
// loaded above. Never import new helpers from the replaced installation.
|
|
271
|
+
const pkg = JSON.parse(await fs.readFile(path.join(current.root, 'package.json'), 'utf8'));
|
|
272
|
+
if (pkg.name !== NAME || pkg.version !== target.version || !supportedBin(pkg.bin) || await fs.realpath(path.join(current.prefix, 'bin/prodero')) !== path.join(current.root, 'bin/prodero'))
|
|
273
|
+
fail('NPM_UPDATE_VERIFY', 'npm finished, but the installed package or public executable does not match the target. Keep applications closed and repair the npm installation.');
|
|
274
|
+
await fs.access(path.join(current.prefix, 'bin/prodero'), fs.constants.X_OK);
|
|
275
|
+
const version = await run(current.node, [path.join(current.root, 'bin/prodero'), '--version'], { ...execOptions, timeout: 15000 }), help = await run(current.node, [path.join(current.root, 'bin/prodero'), '--help'], { ...execOptions, timeout: 15000 });
|
|
276
|
+
if (version.outputIncomplete || help.outputIncomplete || version.exited === false || help.exited === false || version.timedOut || help.timedOut || version.code !== 0 || version.stdout.trim() !== target.version || help.code !== 0 || !help.stdout.includes('Usage: prodero'))
|
|
277
|
+
fail('NPM_UPDATE_VERIFY', 'The installed target failed its CLI smoke checks. Keep applications closed and repair the npm installation.');
|
|
278
|
+
return { ok: true, status: 'updated', installedVersion: target.version, targetVersion: target.version, prefix: current.prefix, restartRequired: true, message: `Installed ${NAME}@${target.version} and verified its public CLI. Reopen Codex or Claude and reconnect MCP to load the new runtime. Project data and saved host/dev selections were not changed by this updater.`, exitCode: 0 };
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (error instanceof NpmUpdateError)
|
|
282
|
+
throw error;
|
|
283
|
+
throw new NpmUpdateError(started ? 'NPM_UPDATE_VERIFY' : 'NPM_UPDATE_FAILED', started ? 'The update outcome could not be verified. Keep applications closed and inspect/repair the npm installation before retrying.' : 'The npm installation or target could not be verified. No install started.');
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
await fs.rm(temp, { recursive: true, force: true }).catch(() => { });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=npm-update.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function isPathScope(item) {
|
|
3
|
+
return item.scope === 'module' && Boolean(item.scopeRef) && (item.scopeKind === 'path' || item.scopeKind !== 'topic' && /[/.]/.test(item.scopeRef));
|
|
4
|
+
}
|
|
5
|
+
export function normalizeScopePath(value) { return path.posix.normalize(value).replace(/^\.\//, '').replace(/\/$/, ''); }
|
|
6
|
+
export function pathApplies(reference, requested) {
|
|
7
|
+
const scope = normalizeScopePath(reference), target = normalizeScopePath(requested);
|
|
8
|
+
return target === scope || target.startsWith(`${scope}/`);
|
|
9
|
+
}
|
|
10
|
+
export function pathScopeRelevant(query, item) {
|
|
11
|
+
if (!isPathScope(item))
|
|
12
|
+
return true;
|
|
13
|
+
const value = query.trim();
|
|
14
|
+
if (pathApplies(item.scopeRef, value))
|
|
15
|
+
return true;
|
|
16
|
+
// A bare directory name in prose is thematic text. Quote it or name a descendant.
|
|
17
|
+
const mentions = [...value.matchAll(/`([^`]+)`|"([^"\n]+)"|(?:^|\s)(\.?\/?[\w@.-]+\/[\w@./-]*)/g)].map(m => m[1] ?? m[2] ?? m[3]);
|
|
18
|
+
return mentions.some(p => pathApplies(item.scopeRef, p));
|
|
19
|
+
}
|
|
20
|
+
export function assertPathScope(item) {
|
|
21
|
+
if (item.scopeKind !== 'path')
|
|
22
|
+
return;
|
|
23
|
+
const p = item.scopeRef;
|
|
24
|
+
if (item.scope !== 'module' || !p || p.startsWith('/') || p.includes('\\') || p.split('/').some(c => c === '..' || c === '') || p === '.')
|
|
25
|
+
throw new Error('Invalid repository path scope. Use a repository-relative file or directory.');
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=path-scope.js.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { assertCompatibleProject } from "./storage.js";
|
|
2
|
+
import { hasSecret } from "./secrets.js";
|
|
3
|
+
export { hasSecret } from "./secrets.js";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
const skipped = new Set([".git", "node_modules", ".cache", ".transactions", "dist", "dist-ui"]);
|
|
8
|
+
export function safeRelative(value) {
|
|
9
|
+
if (!value || path.isAbsolute(value) || value.includes("\\") || /[\x00-\x1f]/.test(value) || value.split("/").some(p => !p || p === ".." || p === "."))
|
|
10
|
+
throw new Error("Unsafe relative path");
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
export async function checkedPath(root, relative) {
|
|
14
|
+
await assertCompatibleProject(root);
|
|
15
|
+
if (relative !== ".")
|
|
16
|
+
safeRelative(relative);
|
|
17
|
+
let cursor = await fs.realpath(root);
|
|
18
|
+
for (const part of relative === "." ? [] : relative.split("/")) {
|
|
19
|
+
cursor = path.join(cursor, part);
|
|
20
|
+
try {
|
|
21
|
+
if ((await fs.lstat(cursor)).isSymbolicLink())
|
|
22
|
+
throw new Error("Symlink source/destination rejected");
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code !== "ENOENT")
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return cursor;
|
|
30
|
+
}
|
|
31
|
+
/** Conservative source exclusions: credential stores/config may expose more than one secret. */
|
|
32
|
+
export function secretPath(value) {
|
|
33
|
+
const normalized = value.replace(/\\/g, '/');
|
|
34
|
+
// Exclude these private configuration trees, including caches and nonstandard key names.
|
|
35
|
+
if (/(^|\/)(?:\.ssh|\.aws|\.azure)(?:\/|$)/i.test(normalized) || /(^|\/)(?:\.config\/gcloud|AppData\/Roaming\/gcloud)(?:\/|$)/i.test(normalized))
|
|
36
|
+
return true;
|
|
37
|
+
if (/(^|\/)gcloud\/(?:credentials\.db|access_tokens\.db|legacy_credentials(?:\/|$)|configurations(?:\/|$))/i.test(normalized))
|
|
38
|
+
return true;
|
|
39
|
+
if (/(^|\/)(?:ssh_config|sshd_config)(?:$|\.d(?:\/|$))/i.test(normalized))
|
|
40
|
+
return true;
|
|
41
|
+
if (/(^|\/)(?:\.kube\/config|\.docker\/config\.json)(?:\.[^/]*)?$/i.test(normalized))
|
|
42
|
+
return true;
|
|
43
|
+
const name = normalized.split('/').at(-1) ?? '';
|
|
44
|
+
if (/^(?:\.env(?:\..*)?|\.npmrc(?:\..*)?|\.git-credentials(?:\..*)?|\.netrc|_netrc|\.pypirc|\.boto|credentials(?:\..*)?|application_default_credentials\.json|id_(?:rsa|dsa|ecdsa|ed25519)(?:_sk)?|ssh_host_.*_key)$/i.test(name))
|
|
45
|
+
return true;
|
|
46
|
+
if (/\.(?:pem|key|p12|pfx|ppk)$/i.test(name))
|
|
47
|
+
return true;
|
|
48
|
+
// Credential-specific data filenames; do not blacklist ordinary auth modules or docs.
|
|
49
|
+
return /^(?:(?:[^/]+[-_.])?service[-_]?account(?:[-_]?(?:key|credentials))?(?:[-_.](?:prod|production|dev|development|staging|test))?|(?:[^/]+[-_.])?credentials|client[_-]?secret(?:[_-].*)?)\.(?:json|ya?ml|toml|ini)$/i.test(name);
|
|
50
|
+
}
|
|
51
|
+
export async function scanFiles(root, relative, limit = 2000) {
|
|
52
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 20000)
|
|
53
|
+
throw new Error("Invalid file count limit");
|
|
54
|
+
const base = await checkedPath(root, relative);
|
|
55
|
+
const files = [];
|
|
56
|
+
let truncated = false, count = 0;
|
|
57
|
+
try {
|
|
58
|
+
if (!(await fs.stat(base)).isDirectory())
|
|
59
|
+
throw new Error("Source is not a directory");
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error.code === "ENOENT")
|
|
63
|
+
return { files, truncated, exists: false };
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
async function visit(dir, prefix) {
|
|
67
|
+
for (const entry of (await fs.readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
68
|
+
if ((relative === "." && (skipped.has(entry.name) || entry.name === ".prodero")) || (relative === ".prodero" && [".cache", ".transactions"].includes(entry.name)))
|
|
69
|
+
continue;
|
|
70
|
+
if (++count > limit) {
|
|
71
|
+
truncated = true;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (entry.isSymbolicLink())
|
|
75
|
+
throw new Error("Symlink in source tree rejected");
|
|
76
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
77
|
+
if (entry.isDirectory())
|
|
78
|
+
await visit(path.join(dir, entry.name), rel);
|
|
79
|
+
else if (entry.isFile())
|
|
80
|
+
files.push({ path: rel, bytes: (await fs.stat(path.join(dir, entry.name))).size });
|
|
81
|
+
else
|
|
82
|
+
throw new Error("Special source file rejected");
|
|
83
|
+
if (truncated)
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await visit(base, relative === "." ? "" : relative);
|
|
88
|
+
return { files, truncated, exists: true };
|
|
89
|
+
}
|
|
90
|
+
export async function readSource(root, relative) {
|
|
91
|
+
if (secretPath(relative))
|
|
92
|
+
throw new Error("Secret-bearing file type excluded");
|
|
93
|
+
const target = await checkedPath(root, relative);
|
|
94
|
+
const stat = await fs.stat(target);
|
|
95
|
+
if (!stat.isFile() || stat.size > 128000)
|
|
96
|
+
throw new Error("Source file exceeds text budget");
|
|
97
|
+
const raw = await fs.readFile(target);
|
|
98
|
+
if (raw.includes(0))
|
|
99
|
+
throw new Error("Binary source excluded");
|
|
100
|
+
const content = raw.toString("utf8");
|
|
101
|
+
if (hasSecret(content))
|
|
102
|
+
throw new Error("Potential secret-bearing source excluded");
|
|
103
|
+
return { path: relative, content, sha256: crypto.createHash("sha256").update(raw).digest("hex") };
|
|
104
|
+
}
|
|
105
|
+
/** External integrity scope: bounded, path-screened, Git-ignore aware; no source text retained. */
|
|
106
|
+
export async function snapshotFiles(root, relative, options = {}) {
|
|
107
|
+
return (await (await import("./integrity-files.js")).integritySnapshot(root, relative, options)).hashes;
|
|
108
|
+
}
|
|
109
|
+
/** Core-only exception: canonical Local records/checkpoints participate in import conflict/replay checks. */
|
|
110
|
+
export async function snapshotImportState(root) {
|
|
111
|
+
return (await (await import("./integrity-files.js")).integritySnapshot(root, ".prodero", {}, true)).hashes;
|
|
112
|
+
}
|
|
113
|
+
export async function digestTree(root, relative, options = {}) {
|
|
114
|
+
return (await (await import("./integrity-files.js")).integritySnapshot(root, relative, options)).digest;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=plugin-files.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/** Validate and retain the selected boundary; only CLI implicit discovery searches ancestors. */
|
|
4
|
+
export async function explicitProjectRoot(value) {
|
|
5
|
+
if (!value.trim() || !path.isAbsolute(value))
|
|
6
|
+
throw new Error("Project root must be an absolute existing directory.");
|
|
7
|
+
const root = path.resolve(value);
|
|
8
|
+
if (!(await fs.stat(root).catch(() => null))?.isDirectory())
|
|
9
|
+
throw new Error("Project root is not an existing directory. Select an absolute existing directory.");
|
|
10
|
+
// A new project inside a Git working tree would dirty that parent on init.
|
|
11
|
+
// Existing nested Prodero projects and standalone Git/worktree roots retain their boundary.
|
|
12
|
+
if (await fs.stat(path.join(root, ".git")).catch(() => null) || await fs.stat(path.join(root, ".prodero/config.json")).catch(() => null))
|
|
13
|
+
return root;
|
|
14
|
+
let parent = path.dirname(root);
|
|
15
|
+
while (true) {
|
|
16
|
+
if (await fs.stat(path.join(parent, ".git")).catch(() => null)) {
|
|
17
|
+
throw new Error("PRODERO_NESTED_ROOT: The selected directory is inside another Git repository and is not an initialized Prodero project. Select the intended repository root explicitly or create a standalone Git repository in the selected directory. The parent was not selected or changed.");
|
|
18
|
+
}
|
|
19
|
+
const next = path.dirname(parent);
|
|
20
|
+
if (next === parent)
|
|
21
|
+
break;
|
|
22
|
+
parent = next;
|
|
23
|
+
}
|
|
24
|
+
return root;
|
|
25
|
+
}
|
|
26
|
+
export async function cliProjectRoot(value) {
|
|
27
|
+
if (value !== undefined)
|
|
28
|
+
return explicitProjectRoot(value);
|
|
29
|
+
let current = process.cwd();
|
|
30
|
+
while (true) {
|
|
31
|
+
if (await fs.stat(path.join(current, ".git")).catch(() => null) || await fs.stat(path.join(current, ".prodero/config.json")).catch(() => null))
|
|
32
|
+
return current;
|
|
33
|
+
const parent = path.dirname(current);
|
|
34
|
+
if (parent === current)
|
|
35
|
+
throw new Error("No project found. Use --root /absolute/project to select an existing directory explicitly.");
|
|
36
|
+
current = parent;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=project-root.js.map
|