@alex-dbs/coderuler 0.0.0-stage → 1.3.1
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/LICENSE +175 -0
- package/README.md +218 -2
- package/dist/adapter/cli/commands/check.d.ts +3 -0
- package/dist/adapter/cli/commands/check.js +93 -0
- package/dist/adapter/cli/commands/deps.d.ts +3 -0
- package/dist/adapter/cli/commands/deps.js +4 -0
- package/dist/adapter/cli/commands/inspect.d.ts +3 -0
- package/dist/adapter/cli/commands/inspect.js +59 -0
- package/dist/adapter/cli/commands/rules.d.ts +10 -0
- package/dist/adapter/cli/commands/rules.js +17 -0
- package/dist/adapter/cli/commands/skill.d.ts +4 -0
- package/dist/adapter/cli/commands/skill.js +7 -0
- package/dist/adapter/cli/commands/test.d.ts +3 -0
- package/dist/adapter/cli/commands/test.js +78 -0
- package/dist/adapter/cli/commands/update.d.ts +3 -0
- package/dist/adapter/cli/commands/update.js +7 -0
- package/dist/adapter/cli/commands/validate.d.ts +3 -0
- package/dist/adapter/cli/commands/validate.js +12 -0
- package/dist/adapter/cli/exit.d.ts +7 -0
- package/dist/adapter/cli/exit.js +10 -0
- package/dist/adapter/cli/inspect-json.d.ts +9 -0
- package/dist/adapter/cli/inspect-json.js +160 -0
- package/dist/adapter/cli/inspect-usage.d.ts +9 -0
- package/dist/adapter/cli/inspect-usage.js +14 -0
- package/dist/adapter/cli/program.d.ts +4 -0
- package/dist/adapter/cli/program.js +216 -0
- package/dist/adapter/cli/progress.d.ts +22 -0
- package/dist/adapter/cli/progress.js +79 -0
- package/dist/adapter/cli/reports.d.ts +47 -0
- package/dist/adapter/cli/reports.js +735 -0
- package/dist/adapter/cli/self-update.d.ts +1 -0
- package/dist/adapter/cli/self-update.js +89 -0
- package/dist/adapter/cli/services.d.ts +101 -0
- package/dist/adapter/cli/services.js +1 -0
- package/dist/adapter/cli/text.d.ts +5 -0
- package/dist/adapter/cli/text.js +36 -0
- package/dist/adapter/cli/version.d.ts +1 -0
- package/dist/adapter/cli/version.js +3 -0
- package/dist/adapter/decision/jev.d.ts +11 -0
- package/dist/adapter/decision/jev.js +138 -0
- package/dist/adapter/filesystem/dependencies.d.ts +26 -0
- package/dist/adapter/filesystem/dependencies.js +577 -0
- package/dist/adapter/filesystem/dependency-installation.d.ts +11 -0
- package/dist/adapter/filesystem/dependency-installation.js +373 -0
- package/dist/adapter/filesystem/project.d.ts +14 -0
- package/dist/adapter/filesystem/project.js +387 -0
- package/dist/adapter/filesystem/rule-resolution.d.ts +4 -0
- package/dist/adapter/filesystem/rule-resolution.js +516 -0
- package/dist/adapter/filesystem/rule-test-worker.d.ts +17 -0
- package/dist/adapter/filesystem/rule-test-worker.js +47 -0
- package/dist/adapter/filesystem/rule-tests.d.ts +3 -0
- package/dist/adapter/filesystem/rule-tests.js +264 -0
- package/dist/adapter/filesystem/rules.d.ts +13 -0
- package/dist/adapter/filesystem/rules.js +242 -0
- package/dist/adapter/git/changes.d.ts +1 -0
- package/dist/adapter/git/changes.js +54 -0
- package/dist/adapter/git/environment.d.ts +1 -0
- package/dist/adapter/git/environment.js +10 -0
- package/dist/adapter/github/api.d.ts +5 -0
- package/dist/adapter/github/api.js +40 -0
- package/dist/adapter/github/comments.d.ts +3 -0
- package/dist/adapter/github/comments.js +235 -0
- package/dist/adapter/github/pull-request.d.ts +16 -0
- package/dist/adapter/github/pull-request.js +139 -0
- package/dist/adapter/scope/astgrep-languages.d.ts +5 -0
- package/dist/adapter/scope/astgrep-languages.js +119 -0
- package/dist/adapter/scope/astgrep.d.ts +14 -0
- package/dist/adapter/scope/astgrep.js +183 -0
- package/dist/adapter/scope/csv.d.ts +4 -0
- package/dist/adapter/scope/csv.js +158 -0
- package/dist/adapter/scope/file.d.ts +2 -0
- package/dist/adapter/scope/file.js +4 -0
- package/dist/adapter/scope/json.d.ts +7 -0
- package/dist/adapter/scope/json.js +132 -0
- package/dist/adapter/scope/lines.d.ts +10 -0
- package/dist/adapter/scope/lines.js +42 -0
- package/dist/adapter/scope/protobuf-options.d.ts +29 -0
- package/dist/adapter/scope/protobuf-options.js +64 -0
- package/dist/adapter/scope/protobuf-parser.d.ts +30 -0
- package/dist/adapter/scope/protobuf-parser.js +772 -0
- package/dist/adapter/scope/protobuf.d.ts +5 -0
- package/dist/adapter/scope/protobuf.js +53 -0
- package/dist/adapter/scope/structured.d.ts +10 -0
- package/dist/adapter/scope/structured.js +84 -0
- package/dist/adapter/scope/yaml.d.ts +7 -0
- package/dist/adapter/scope/yaml.js +224 -0
- package/dist/adapter/skills/cli.d.ts +10 -0
- package/dist/adapter/skills/cli.js +77 -0
- package/dist/app/concurrency.d.ts +1 -0
- package/dist/app/concurrency.js +44 -0
- package/dist/app/evaluators.d.ts +14 -0
- package/dist/app/evaluators.js +175 -0
- package/dist/app/execute.d.ts +2 -0
- package/dist/app/execute.js +427 -0
- package/dist/app/jsonpath.d.ts +1 -0
- package/dist/app/jsonpath.js +8 -0
- package/dist/app/ports.d.ts +165 -0
- package/dist/app/ports.js +1 -0
- package/dist/app/rule-tests.d.ts +20 -0
- package/dist/app/rule-tests.js +11 -0
- package/dist/app/rule.d.ts +121 -0
- package/dist/app/rule.js +24 -0
- package/dist/app/selection.d.ts +2 -0
- package/dist/app/selection.js +23 -0
- package/dist/app/usage.d.ts +2 -0
- package/dist/app/usage.js +13 -0
- package/dist/app/validate.d.ts +11 -0
- package/dist/app/validate.js +415 -0
- package/dist/app/violation-comments.d.ts +10 -0
- package/dist/app/violation-comments.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +11 -0
- package/dist/infra/runtime.d.ts +3 -0
- package/dist/infra/runtime.js +168 -0
- package/dist/infra/settings.d.ts +5 -0
- package/dist/infra/settings.js +91 -0
- package/package.json +86 -4
- package/skills/coderuler-new-rule/SKILL.md +141 -0
- package/skills/coderuler-new-rule/reference.md +447 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { Agent } from 'node:https';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
8
|
+
import { currentEnv } from 'npm-install-checks';
|
|
9
|
+
import npa from 'npm-package-arg';
|
|
10
|
+
import { parse as parseIntegrity } from 'ssri';
|
|
11
|
+
import { prepareRuleDependencySdk, publishRuleDependencyInstallation, withRuleDependencyDirectoryLock } from './dependency-installation.js';
|
|
12
|
+
const registry = 'https://registry.npmjs.org/';
|
|
13
|
+
const lockName = 'coderuler.lock';
|
|
14
|
+
const sdkName = '@alex-dbs/coderuler';
|
|
15
|
+
const markerName = '.coderuler-install.json';
|
|
16
|
+
const rootIdentity = { name: 'coderuler-rules', version: '1.0.0' };
|
|
17
|
+
const forbiddenRootFields = ['scripts', 'workspaces', 'overrides', 'devDependencies', 'optionalDependencies', 'peerDependencies', 'peerDependenciesMeta'];
|
|
18
|
+
const dependencyFields = ['dependencies', 'optionalDependencies', 'peerDependencies'];
|
|
19
|
+
const compatibilityFields = ['engines', 'os', 'cpu', 'libc'];
|
|
20
|
+
function code(error) {
|
|
21
|
+
return error !== null && typeof error === 'object' && 'code' in error ? String(error.code) : undefined;
|
|
22
|
+
}
|
|
23
|
+
function object(value, label) {
|
|
24
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
25
|
+
throw new Error(`${label} must be an object`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function packageName(name) {
|
|
29
|
+
const parsed = npa(name);
|
|
30
|
+
if (parsed.name !== name || !parsed.registry || parsed.rawSpec !== '*')
|
|
31
|
+
throw new Error(`Invalid dependency name ${JSON.stringify(name)}`);
|
|
32
|
+
}
|
|
33
|
+
function exactVersion(name, value) {
|
|
34
|
+
if (typeof value !== 'string')
|
|
35
|
+
throw new Error(`${name}: an exact package version is required`);
|
|
36
|
+
const spec = npa.resolve(name, value);
|
|
37
|
+
if (spec.type !== 'version' || spec.fetchSpec !== value)
|
|
38
|
+
throw new Error(`${name}: an exact package version is required`);
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function hasBundledDependencies(value) {
|
|
42
|
+
return value !== undefined && value !== false && (!Array.isArray(value) || value.length > 0);
|
|
43
|
+
}
|
|
44
|
+
function dependencyMap(manifest, field, label) {
|
|
45
|
+
const value = manifest[field];
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
return {};
|
|
48
|
+
const result = Object.create(null);
|
|
49
|
+
for (const [name, specifier] of Object.entries(object(value, `${label} ${field}`))) {
|
|
50
|
+
packageName(name);
|
|
51
|
+
if (typeof specifier !== 'string')
|
|
52
|
+
throw new Error(`${label}: invalid ${field} specification for ${name}`);
|
|
53
|
+
const parsed = npa.resolve(name, specifier);
|
|
54
|
+
if (!parsed.registry || !['version', 'range', 'tag'].includes(parsed.type)) {
|
|
55
|
+
throw new Error(`${label}: unsupported dependency source for ${name}; only public npm registry packages are supported`);
|
|
56
|
+
}
|
|
57
|
+
result[name] = specifier;
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function locationName(location) {
|
|
62
|
+
const parts = location.split('/');
|
|
63
|
+
let name = '';
|
|
64
|
+
for (let index = 0; index < parts.length;) {
|
|
65
|
+
if (parts[index++] !== 'node_modules')
|
|
66
|
+
throw new Error(`Invalid lock package location ${JSON.stringify(location)}`);
|
|
67
|
+
const part = parts[index++];
|
|
68
|
+
if (!part)
|
|
69
|
+
throw new Error(`Invalid lock package location ${JSON.stringify(location)}`);
|
|
70
|
+
name = part.startsWith('@') ? `${part}/${parts[index++] ?? ''}` : part;
|
|
71
|
+
packageName(name);
|
|
72
|
+
}
|
|
73
|
+
if (!name || location.includes('\\'))
|
|
74
|
+
throw new Error(`Invalid lock package location ${JSON.stringify(location)}`);
|
|
75
|
+
return name;
|
|
76
|
+
}
|
|
77
|
+
function validateIntegrity(value, label) {
|
|
78
|
+
if (typeof value !== 'string')
|
|
79
|
+
throw new Error(`${label}: missing SHA-512 integrity`);
|
|
80
|
+
const parsed = parseIntegrity(value, { strict: true });
|
|
81
|
+
if (!parsed?.sha512?.length || parsed.sha512.some(hash => Buffer.from(hash.digest, 'base64').length !== 64)) {
|
|
82
|
+
throw new Error(`${label}: invalid SHA-512 integrity`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function validateUrl(value, label) {
|
|
86
|
+
if (typeof value !== 'string')
|
|
87
|
+
throw new Error(`${label}: missing locked npm tarball URL`);
|
|
88
|
+
let url;
|
|
89
|
+
try {
|
|
90
|
+
url = new URL(value);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw new Error(`${label}: invalid locked npm tarball URL`);
|
|
94
|
+
}
|
|
95
|
+
if (url.origin !== 'https://registry.npmjs.org' || url.username || url.password || url.hash) {
|
|
96
|
+
throw new Error(`${label}: tarballs must use https://registry.npmjs.org without credentials or fragments`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function validateLock(text, directory) {
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = object(JSON.parse(text), 'Rule dependency lock');
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw new Error('Invalid coderuler.lock JSON; regenerate it with coderuler deps add', { cause: error });
|
|
106
|
+
}
|
|
107
|
+
if (parsed.lockfileVersion !== 3)
|
|
108
|
+
throw new Error('coderuler.lock must use npm lockfileVersion 3');
|
|
109
|
+
const packages = object(parsed.packages, 'coderuler.lock packages');
|
|
110
|
+
const root = object(packages[''], 'coderuler.lock root package');
|
|
111
|
+
if (parsed.name !== rootIdentity.name || parsed.version !== rootIdentity.version || root.name !== rootIdentity.name || root.version !== rootIdentity.version) {
|
|
112
|
+
throw new Error('coderuler.lock must use the coderuler-rules@1.0.0 root identity');
|
|
113
|
+
}
|
|
114
|
+
for (const field of forbiddenRootFields) {
|
|
115
|
+
if (root[field] !== undefined || parsed[field] !== undefined)
|
|
116
|
+
throw new Error(`coderuler.lock root ${field} is not supported`);
|
|
117
|
+
}
|
|
118
|
+
for (const field of Object.keys(root)) {
|
|
119
|
+
if (!['name', 'version', 'dependencies'].includes(field))
|
|
120
|
+
throw new Error(`coderuler.lock root ${field} is not supported`);
|
|
121
|
+
}
|
|
122
|
+
const direct = dependencyMap(root, 'dependencies', 'coderuler.lock root');
|
|
123
|
+
for (const [name, version] of Object.entries(direct)) {
|
|
124
|
+
if (name === sdkName)
|
|
125
|
+
throw new Error(`${sdkName}: the SDK comes from the selected CodeRuler installation`);
|
|
126
|
+
exactVersion(name, version);
|
|
127
|
+
}
|
|
128
|
+
for (const [location, unknownEntry] of Object.entries(packages)) {
|
|
129
|
+
if (location === '')
|
|
130
|
+
continue;
|
|
131
|
+
const name = locationName(location);
|
|
132
|
+
const entry = object(unknownEntry, `coderuler.lock ${location}`);
|
|
133
|
+
exactVersion(name, entry.version);
|
|
134
|
+
if (entry.name !== undefined && entry.name !== name)
|
|
135
|
+
throw new Error(`${name}: package aliases are unsupported`);
|
|
136
|
+
for (const field of ['acceptDependencies', 'overrides', 'workspaces', 'packageExtensions', 'packageExtensionsHash', 'npmExtensionHash']) {
|
|
137
|
+
if (entry[field] !== undefined)
|
|
138
|
+
throw new Error(`${name}: unsupported graph metadata ${field}`);
|
|
139
|
+
}
|
|
140
|
+
if (entry.link || entry.inBundle || hasBundledDependencies(entry.bundleDependencies) || hasBundledDependencies(entry.bundledDependencies) || entry.hasShrinkwrap || entry.patched || entry.packageExtensionsApplied || entry.npmExtensionApplied) {
|
|
141
|
+
throw new Error(`${name}: linked, bundled and shrinkwrapped packages are unsupported`);
|
|
142
|
+
}
|
|
143
|
+
if (entry.hasInstallScript)
|
|
144
|
+
throw new Error(`${name}: install scripts are unsupported; choose a package without install-time build artifacts`);
|
|
145
|
+
validateIntegrity(entry.integrity, name);
|
|
146
|
+
validateUrl(entry.resolved, name);
|
|
147
|
+
for (const field of dependencyFields)
|
|
148
|
+
dependencyMap(entry, field, name);
|
|
149
|
+
if (entry.peerDependenciesMeta !== undefined) {
|
|
150
|
+
for (const [peer, metadata] of Object.entries(object(entry.peerDependenciesMeta, `${name} peerDependenciesMeta`))) {
|
|
151
|
+
packageName(peer);
|
|
152
|
+
const peerMetadata = object(metadata, `${name} peer metadata for ${peer}`);
|
|
153
|
+
if (peerMetadata.optional !== undefined && typeof peerMetadata.optional !== 'boolean')
|
|
154
|
+
throw new Error(`${name}: invalid optional peer metadata for ${peer}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (entry.engines !== undefined) {
|
|
158
|
+
if (Object.values(object(entry.engines, `${name} engines`)).some(value => typeof value !== 'string'))
|
|
159
|
+
throw new Error(`${name}: invalid engines metadata`);
|
|
160
|
+
}
|
|
161
|
+
for (const field of ['os', 'cpu', 'libc']) {
|
|
162
|
+
const value = entry[field];
|
|
163
|
+
if (value !== undefined && typeof value !== 'string' && (!Array.isArray(value) || value.some(item => typeof item !== 'string')))
|
|
164
|
+
throw new Error(`${name}: invalid ${field} metadata`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { text, lock: { lockfileVersion: 3, packages }, direct, key: ruleDependencyLockKey(parsed, directory) };
|
|
168
|
+
}
|
|
169
|
+
function canonicalJson(value) {
|
|
170
|
+
if (Array.isArray(value))
|
|
171
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
172
|
+
if (value && typeof value === 'object') {
|
|
173
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(',')}}`;
|
|
174
|
+
}
|
|
175
|
+
return JSON.stringify(value) ?? 'undefined';
|
|
176
|
+
}
|
|
177
|
+
export function ruleDependencyLockKey(parsed, directory) {
|
|
178
|
+
if (parsed === null)
|
|
179
|
+
return 'empty';
|
|
180
|
+
const packages = object(parsed.packages, 'coderuler.lock packages');
|
|
181
|
+
const root = object(packages[''], 'coderuler.lock root package');
|
|
182
|
+
if (Object.keys(packages).length === 1 && Object.keys(dependencyMap(root, 'dependencies', 'coderuler.lock root')).length === 0)
|
|
183
|
+
return 'empty';
|
|
184
|
+
return createHash('sha256').update(JSON.stringify([
|
|
185
|
+
canonicalJson(parsed), directory, 1, process.version, process.platform, process.arch, currentEnv.libc(process.platform) ?? '',
|
|
186
|
+
])).digest('hex');
|
|
187
|
+
}
|
|
188
|
+
export async function readRuleDependencyLock(directory) {
|
|
189
|
+
const path = join(directory, lockName);
|
|
190
|
+
let handle;
|
|
191
|
+
try {
|
|
192
|
+
const stat = await lstat(path);
|
|
193
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
194
|
+
throw new Error(`${path}: dependency lock must be a regular, non-symlinked file`);
|
|
195
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
196
|
+
if (!(await handle.stat()).isFile())
|
|
197
|
+
throw new Error(`${path}: dependency lock must be a regular file`);
|
|
198
|
+
return await handle.readFile('utf8');
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (code(error) === 'ENOENT')
|
|
202
|
+
return null;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
await handle?.close();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function ensureDependencyIgnore(directory) {
|
|
210
|
+
const path = join(directory, '.gitignore');
|
|
211
|
+
try {
|
|
212
|
+
const stat = await lstat(path);
|
|
213
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
214
|
+
throw new Error(`${path}: dependency ignore must be a regular, non-symlinked file`);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (code(error) !== 'ENOENT')
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
const handle = await open(path, constants.O_RDWR | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
221
|
+
try {
|
|
222
|
+
if (!(await handle.stat()).isFile())
|
|
223
|
+
throw new Error(`${path}: dependency ignore must be a regular file`);
|
|
224
|
+
const text = await handle.readFile('utf8');
|
|
225
|
+
if (/^\/?node_modules\/?\r?$/m.test(text))
|
|
226
|
+
return;
|
|
227
|
+
const newline = text.includes('\r\n') ? '\r\n' : '\n';
|
|
228
|
+
await handle.writeFile(`${text.length > 0 && !text.endsWith('\n') ? newline : ''}/node_modules/${newline}`);
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
await handle.close();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
export async function dependencyDirectory(ruleFile) {
|
|
235
|
+
const initial = dirname(await realpath(ruleFile));
|
|
236
|
+
let directory = initial;
|
|
237
|
+
for (;;) {
|
|
238
|
+
if (await readRuleDependencyLock(directory) !== null || basename(directory) === '.coderuler')
|
|
239
|
+
return directory;
|
|
240
|
+
const parent = dirname(directory);
|
|
241
|
+
if (parent === directory)
|
|
242
|
+
return initial;
|
|
243
|
+
directory = parent;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
export class RegistryHttpsAgent extends Agent {
|
|
247
|
+
addRequest(request, options) {
|
|
248
|
+
const host = options.hostname ?? options.host;
|
|
249
|
+
if (host !== 'registry.npmjs.org' || (options.protocol !== undefined && options.protocol !== 'https:') || (options.port !== undefined && String(options.port) !== '443')) {
|
|
250
|
+
throw new Error('Rule dependency transport permits only https://registry.npmjs.org:443');
|
|
251
|
+
}
|
|
252
|
+
const prototype = Agent.prototype;
|
|
253
|
+
prototype.addRequest.call(this, request, options);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const agent = new RegistryHttpsAgent();
|
|
257
|
+
const policy = {
|
|
258
|
+
registry, allowGit: 'none', allowRemote: 'none', allowFile: 'none', allowDirectory: 'none', allowRegistry: 'all',
|
|
259
|
+
ignoreScripts: true, strictSSL: true, replaceRegistryHost: 'never', agent, proxy: null, noProxy: '*',
|
|
260
|
+
binLinks: false, audit: false, fund: false, workspacesEnabled: false, lockfileVersion: 3,
|
|
261
|
+
};
|
|
262
|
+
function rootPackage(direct) {
|
|
263
|
+
return { ...rootIdentity, dependencies: Object.fromEntries(Object.entries(direct).sort(([a], [b]) => a.localeCompare(b))) };
|
|
264
|
+
}
|
|
265
|
+
function emptyLock() {
|
|
266
|
+
return `${JSON.stringify({ ...rootIdentity, lockfileVersion: 3, requires: true, packages: { '': rootPackage({}) } }, null, 2)}\n`;
|
|
267
|
+
}
|
|
268
|
+
async function stageLock(path, snapshot) {
|
|
269
|
+
await writeFile(join(path, 'package.json'), `${JSON.stringify(rootPackage(snapshot.direct), null, 2)}\n`);
|
|
270
|
+
await writeFile(join(path, 'package-lock.json'), snapshot.text);
|
|
271
|
+
}
|
|
272
|
+
async function inspectArtifacts(directory, label) {
|
|
273
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
274
|
+
const path = join(directory, entry.name);
|
|
275
|
+
if (entry.name === 'node_modules') {
|
|
276
|
+
throw new Error(`${label}: unsupported package artifact node_modules; bundled dependencies are unsupported`);
|
|
277
|
+
}
|
|
278
|
+
if (entry.name === 'binding.gyp' || entry.name.endsWith('.node') || entry.name === 'npm-shrinkwrap.json') {
|
|
279
|
+
throw new Error(`${label}: unsupported package artifact ${entry.name}; install-time builds, native addons, bundled dependencies and shrinkwraps are unsupported`);
|
|
280
|
+
}
|
|
281
|
+
if (entry.isDirectory())
|
|
282
|
+
await inspectArtifacts(path, label);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function comparable(manifest, field) {
|
|
286
|
+
if (dependencyFields.includes(field) || field === 'peerDependenciesMeta')
|
|
287
|
+
return manifest[field] ?? {};
|
|
288
|
+
const value = manifest[field];
|
|
289
|
+
if (value && typeof value === 'object' && Object.keys(value).length === 0)
|
|
290
|
+
return undefined;
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
async function validateExtracted(directory, location, locked) {
|
|
294
|
+
const name = locationName(location);
|
|
295
|
+
let raw;
|
|
296
|
+
try {
|
|
297
|
+
raw = object(JSON.parse(await readFile(join(directory, 'package.json'), 'utf8')), `${name} package.json`);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
throw new Error(`${name}: invalid extracted package.json`, { cause: error });
|
|
301
|
+
}
|
|
302
|
+
const scripts = raw.scripts === undefined ? {} : object(raw.scripts, `${name} scripts`);
|
|
303
|
+
if (['preinstall', 'install', 'postinstall'].some(script => Object.hasOwn(scripts, script))) {
|
|
304
|
+
throw new Error(`${name}: install scripts are unsupported; choose a package without install-time build artifacts`);
|
|
305
|
+
}
|
|
306
|
+
if (hasBundledDependencies(raw.bundleDependencies) || hasBundledDependencies(raw.bundledDependencies))
|
|
307
|
+
throw new Error(`${name}: bundled dependencies are unsupported`);
|
|
308
|
+
await inspectArtifacts(directory, name);
|
|
309
|
+
// A static import would load package validation tooling for every CLI invocation.
|
|
310
|
+
const { default: PackageJson } = await import('@npmcli/package-json');
|
|
311
|
+
const normalized = await new PackageJson().fromContent(structuredClone(raw)).normalize({ steps: PackageJson.normalizeSteps.filter(step => step !== '_attributes') });
|
|
312
|
+
const manifest = normalized.content;
|
|
313
|
+
for (const field of dependencyFields)
|
|
314
|
+
dependencyMap(manifest, field, name);
|
|
315
|
+
if (manifest.name !== name || manifest.version !== locked.version)
|
|
316
|
+
throw new Error(`${name}: extracted package name or version does not match coderuler.lock`);
|
|
317
|
+
for (const field of [...dependencyFields, 'peerDependenciesMeta', ...compatibilityFields]) {
|
|
318
|
+
if (!isDeepStrictEqual(comparable(manifest, field), comparable(locked, field)))
|
|
319
|
+
throw new Error(`${name}: extracted ${field} does not match coderuler.lock`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function regularDirectory(path) {
|
|
323
|
+
const stat = await lstat(path);
|
|
324
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
325
|
+
}
|
|
326
|
+
async function installedLocally(directory, snapshot) {
|
|
327
|
+
try {
|
|
328
|
+
const modules = join(directory, 'node_modules');
|
|
329
|
+
if (!await regularDirectory(modules))
|
|
330
|
+
return false;
|
|
331
|
+
const markerPath = join(modules, markerName);
|
|
332
|
+
const stat = await lstat(markerPath);
|
|
333
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
334
|
+
return false;
|
|
335
|
+
const marker = object(JSON.parse(await readFile(markerPath, 'utf8')), 'Rule dependency installation marker');
|
|
336
|
+
if (marker.version !== 1 || marker.key !== snapshot.key || !Array.isArray(marker.installedLocations))
|
|
337
|
+
return false;
|
|
338
|
+
const locations = new Set();
|
|
339
|
+
for (const location of marker.installedLocations) {
|
|
340
|
+
if (typeof location !== 'string' || !location || !snapshot.lock.packages[location] || locations.has(location))
|
|
341
|
+
return false;
|
|
342
|
+
locations.add(location);
|
|
343
|
+
let path = directory;
|
|
344
|
+
for (const segment of location.split('/')) {
|
|
345
|
+
path = join(path, segment);
|
|
346
|
+
if (!await regularDirectory(path))
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
const packagePath = join(path, 'package.json');
|
|
350
|
+
const packageStat = await lstat(packagePath);
|
|
351
|
+
if (!packageStat.isFile() || packageStat.isSymbolicLink())
|
|
352
|
+
return false;
|
|
353
|
+
const manifest = object(JSON.parse(await readFile(packagePath, 'utf8')), packagePath);
|
|
354
|
+
if (manifest.name !== locationName(location) || manifest.version !== snapshot.lock.packages[location].version)
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
// Completion metadata cannot hide a missing direct or required transitive package.
|
|
358
|
+
for (const location of ['', ...locations]) {
|
|
359
|
+
const manifest = snapshot.lock.packages[location];
|
|
360
|
+
const optional = manifest.optionalDependencies ?? {};
|
|
361
|
+
const required = new Set(Object.keys(manifest.dependencies ?? {}).filter(name => !Object.hasOwn(optional, name)));
|
|
362
|
+
const peerMetadata = manifest.peerDependenciesMeta;
|
|
363
|
+
for (const name of Object.keys(manifest.peerDependencies ?? {})) {
|
|
364
|
+
if (!peerMetadata?.[name]?.optional)
|
|
365
|
+
required.add(name);
|
|
366
|
+
}
|
|
367
|
+
for (const name of required) {
|
|
368
|
+
let parent = location;
|
|
369
|
+
let found = false;
|
|
370
|
+
for (;;) {
|
|
371
|
+
if (locations.has(`${parent ? `${parent}/` : ''}node_modules/${name}`)) {
|
|
372
|
+
found = true;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
if (!parent)
|
|
376
|
+
break;
|
|
377
|
+
const split = parent.lastIndexOf('/node_modules/');
|
|
378
|
+
parent = split < 0 ? '' : parent.slice(0, split);
|
|
379
|
+
}
|
|
380
|
+
if (!found)
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
if (code(error) && !['ENOENT', 'ENOTDIR'].includes(code(error)))
|
|
388
|
+
throw error;
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async function install(directory, cacheRoot, previous, request) {
|
|
393
|
+
const stage = await realpath(await mkdtemp(join(directory, '.coderuler-deps-stage-')));
|
|
394
|
+
try {
|
|
395
|
+
let snapshot = previous;
|
|
396
|
+
let expected = [];
|
|
397
|
+
// An explicitly empty lock needs only an SDK installation, not npm graph work.
|
|
398
|
+
if (request || previous.key !== 'empty') {
|
|
399
|
+
await stageLock(stage, previous);
|
|
400
|
+
// A static import would load npm's graph stack during ordinary rule loading.
|
|
401
|
+
const { default: Arborist } = await import('@npmcli/arborist');
|
|
402
|
+
const arborist = new Arborist({ ...policy, engineStrict: true, savePrefix: '', path: stage, cache: join(cacheRoot, 'tarballs') });
|
|
403
|
+
const tree = await arborist.buildIdealTree(request);
|
|
404
|
+
const exact = Object.create(null);
|
|
405
|
+
for (const name of Object.keys(dependencyMap(tree.package, 'dependencies', 'resolved root')).sort()) {
|
|
406
|
+
const edge = tree.edgesOut.get(name);
|
|
407
|
+
if (!edge?.to || !edge.valid)
|
|
408
|
+
throw new Error(`${name}: resolution did not produce a compatible direct package`);
|
|
409
|
+
exact[name] = exactVersion(name, edge.to.version);
|
|
410
|
+
if (!request && exact[name] !== previous.direct[name])
|
|
411
|
+
throw new Error(`${name}: repair changed an exact direct dependency`);
|
|
412
|
+
}
|
|
413
|
+
tree.package = rootPackage(exact);
|
|
414
|
+
tree.meta.add(tree);
|
|
415
|
+
const serialized = `${JSON.stringify(JSON.parse(tree.meta.toString()), null, 2)}\n`;
|
|
416
|
+
snapshot = validateLock(isDeepStrictEqual(JSON.parse(previous.text), JSON.parse(serialized)) ? previous.text : serialized, directory);
|
|
417
|
+
const nodes = [...tree.inventory.values()].filter(node => node.location).map(node => ({ location: node.location, omitted: node.inert }));
|
|
418
|
+
expected = nodes.filter(node => !node.omitted).map(node => node.location);
|
|
419
|
+
for (const node of tree.inventory.values()) {
|
|
420
|
+
for (const edge of node.edgesOut.values()) {
|
|
421
|
+
if (!edge.valid && !(edge.peer && edge.optional && !edge.to)) {
|
|
422
|
+
throw new Error(`${node.location || 'coderuler.lock root'}: package resolution failed for dependency ${edge.name}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
for (const node of nodes) {
|
|
427
|
+
const entry = snapshot.lock.packages[node.location];
|
|
428
|
+
const audit = await mkdtemp(join(stage, '.audit-'));
|
|
429
|
+
try {
|
|
430
|
+
// Inspect each raw artifact in isolation, including npm's omitted optional branches.
|
|
431
|
+
// A static import would load registry extraction tooling during ordinary checks.
|
|
432
|
+
const { default: pacote } = await import('pacote');
|
|
433
|
+
await pacote.extract(entry.resolved, audit, { ...policy, allowRemote: 'all', integrity: entry.integrity, cache: join(cacheRoot, 'tarballs') });
|
|
434
|
+
await validateExtracted(audit, node.location, entry);
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
await rm(audit, { recursive: true, force: true });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
await writeFile(join(stage, 'package.json'), `${JSON.stringify(rootPackage(exact), null, 2)}\n`);
|
|
441
|
+
const actual = await arborist.reify({ save: false });
|
|
442
|
+
for (const location of expected) {
|
|
443
|
+
if (!actual.inventory.get(location) || !await pathExists(join(stage, location))) {
|
|
444
|
+
throw new Error(`${locationName(location)}: locked package acquisition failed; run coderuler deps sync after correcting the package or cache`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const modules = join(stage, 'node_modules');
|
|
449
|
+
await mkdir(modules, { recursive: true });
|
|
450
|
+
const sdkTarget = await prepareRuleDependencySdk(directory, modules);
|
|
451
|
+
await writeFile(join(modules, markerName), `${JSON.stringify({
|
|
452
|
+
version: 1, key: snapshot.key, installedLocations: expected.sort(),
|
|
453
|
+
...(sdkTarget === undefined ? {} : { sdkTarget }),
|
|
454
|
+
}, null, 2)}\n`);
|
|
455
|
+
return { snapshot, stage };
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
await rm(stage, { recursive: true, force: true });
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function prepared(directory, snapshot) {
|
|
463
|
+
return {
|
|
464
|
+
directory,
|
|
465
|
+
directDependencies: Object.keys(snapshot?.direct ?? {}).sort(), key: snapshot?.key ?? 'empty',
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function parsedPackages(operation, packages) {
|
|
469
|
+
if (packages.length === 0)
|
|
470
|
+
throw new Error(`coderuler deps ${operation} requires at least one package`);
|
|
471
|
+
const names = new Set();
|
|
472
|
+
return packages.map(value => {
|
|
473
|
+
const parsed = npa(value);
|
|
474
|
+
if (!parsed.name || !parsed.registry || !['version', 'range', 'tag'].includes(parsed.type))
|
|
475
|
+
throw new Error(`${JSON.stringify(value)}: only public npm package names, versions, ranges and tags are supported`);
|
|
476
|
+
const name = parsed.name;
|
|
477
|
+
if (operation === 'remove' && value !== name)
|
|
478
|
+
throw new Error(`${JSON.stringify(value)}: remove requires an unversioned direct package name`);
|
|
479
|
+
if (name === sdkName)
|
|
480
|
+
throw new Error(`${sdkName}: the SDK comes from the selected CodeRuler installation`);
|
|
481
|
+
if (names.has(name))
|
|
482
|
+
throw new Error(`${name}: duplicate package in dependency command`);
|
|
483
|
+
names.add(name);
|
|
484
|
+
return value;
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
async function pathExists(path) {
|
|
488
|
+
try {
|
|
489
|
+
await lstat(path);
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
if (code(error) === 'ENOENT')
|
|
494
|
+
return false;
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
export function createRuleDependencyManager(options = {}) {
|
|
499
|
+
const cacheRoot = resolve(options.cacheRoot ?? join(homedir(), '.cache', 'coderuler', 'dependencies'));
|
|
500
|
+
const snapshotAt = async (directory) => {
|
|
501
|
+
const text = await readRuleDependencyLock(directory);
|
|
502
|
+
try {
|
|
503
|
+
return text === null ? null : validateLock(text, directory);
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
const detail = error instanceof Error ? error.message : 'invalid lock contents';
|
|
507
|
+
throw new Error(`${join(directory, lockName)}: ${detail}; use coderuler deps add to regenerate the lock`, { cause: error });
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
const publish = async (directory, previousLock, installation) => {
|
|
511
|
+
try {
|
|
512
|
+
if (await readRuleDependencyLock(directory) !== previousLock)
|
|
513
|
+
throw new Error(`${join(directory, lockName)}: dependency lock changed during installation; rerun coderuler deps sync`);
|
|
514
|
+
await ensureDependencyIgnore(directory);
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
await rm(installation.stage, { recursive: true, force: true });
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
// Publication takes ownership of staging, including any rollback backup.
|
|
521
|
+
await publishRuleDependencyInstallation({
|
|
522
|
+
directory, stage: installation.stage, previousLock, nextLock: installation.snapshot.text,
|
|
523
|
+
...(options.onWarning === undefined ? {} : { onWarning: options.onWarning }),
|
|
524
|
+
});
|
|
525
|
+
return prepared(directory, installation.snapshot);
|
|
526
|
+
};
|
|
527
|
+
const prepare = async (input, explicit) => {
|
|
528
|
+
const directory = await realpath(input);
|
|
529
|
+
if (!explicit && !await pathExists(join(directory, '.coderuler-deps-write'))) {
|
|
530
|
+
const snapshot = await snapshotAt(directory);
|
|
531
|
+
if ((!snapshot || snapshot.key === 'empty' || await installedLocally(directory, snapshot))
|
|
532
|
+
&& !await pathExists(join(directory, '.coderuler-deps-write'))
|
|
533
|
+
&& await readRuleDependencyLock(directory) === (snapshot?.text ?? null))
|
|
534
|
+
return prepared(directory, snapshot);
|
|
535
|
+
}
|
|
536
|
+
return withRuleDependencyDirectoryLock(directory, async () => {
|
|
537
|
+
const snapshot = await snapshotAt(directory);
|
|
538
|
+
if (!snapshot) {
|
|
539
|
+
if (explicit)
|
|
540
|
+
await prepareRuleDependencySdk(directory, join(directory, 'node_modules'));
|
|
541
|
+
return prepared(directory, snapshot);
|
|
542
|
+
}
|
|
543
|
+
if (snapshot.key === 'empty' && !explicit)
|
|
544
|
+
return prepared(directory, snapshot);
|
|
545
|
+
if (await installedLocally(directory, snapshot)) {
|
|
546
|
+
if (explicit) {
|
|
547
|
+
await prepareRuleDependencySdk(directory, join(directory, 'node_modules'));
|
|
548
|
+
await ensureDependencyIgnore(directory);
|
|
549
|
+
}
|
|
550
|
+
return prepared(directory, snapshot);
|
|
551
|
+
}
|
|
552
|
+
return publish(directory, snapshot.text, await install(directory, cacheRoot, snapshot));
|
|
553
|
+
}, options.onWarning);
|
|
554
|
+
};
|
|
555
|
+
return {
|
|
556
|
+
prepare: directory => prepare(directory, false),
|
|
557
|
+
sync: directory => prepare(directory, true),
|
|
558
|
+
async change(input, operation, requested) {
|
|
559
|
+
const packages = parsedPackages(operation, requested);
|
|
560
|
+
if (operation === 'add')
|
|
561
|
+
await mkdir(input, { recursive: true });
|
|
562
|
+
const directory = await realpath(input);
|
|
563
|
+
return withRuleDependencyDirectoryLock(directory, async () => {
|
|
564
|
+
const previous = await readRuleDependencyLock(directory);
|
|
565
|
+
const snapshot = validateLock(previous ?? emptyLock(), directory);
|
|
566
|
+
if (operation === 'remove') {
|
|
567
|
+
for (const name of packages)
|
|
568
|
+
if (!Object.hasOwn(snapshot.direct, name))
|
|
569
|
+
throw new Error(`${name}: not a declared direct dependency`);
|
|
570
|
+
}
|
|
571
|
+
const installation = await install(directory, cacheRoot, snapshot, operation === 'add' ? { add: packages } : { rm: packages });
|
|
572
|
+
const result = await publish(directory, previous, installation);
|
|
573
|
+
return { ...result, directVersions: installation.snapshot.direct };
|
|
574
|
+
}, options.onWarning);
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function withRuleDependencyDirectoryLock<T>(ownerDirectory: string, action: () => Promise<T>, onWarning?: (message: string) => void): Promise<T>;
|
|
2
|
+
/** Called under the owner mutex. Only the SDK may be a generated package link. */
|
|
3
|
+
export declare function prepareRuleDependencySdk(ownerDirectory: string, nodeModules: string): Promise<string | undefined>;
|
|
4
|
+
/** Takes ownership of stage. The caller holds the owner mutex throughout publication. */
|
|
5
|
+
export declare function publishRuleDependencyInstallation(options: {
|
|
6
|
+
readonly directory: string;
|
|
7
|
+
readonly stage: string;
|
|
8
|
+
readonly previousLock: string | null;
|
|
9
|
+
readonly nextLock: string;
|
|
10
|
+
readonly onWarning?: (message: string) => void;
|
|
11
|
+
}): Promise<void>;
|