@r0hitsharma/uikit-cli 0.12.0-rohit-fork-ci.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/README.md +286 -0
- package/dist/cli.js +225 -0
- package/dist/cli.sh +10 -0
- package/dist/command-executor.js +41 -0
- package/dist/commands/doctor.js +518 -0
- package/dist/commands/format.js +34 -0
- package/dist/commands/link.js +252 -0
- package/dist/commands/lint.js +43 -0
- package/dist/commands/register.js +35 -0
- package/dist/commands/unlink.js +68 -0
- package/dist/fs-utils.js +56 -0
- package/dist/link-validator.js +80 -0
- package/dist/logger.js +43 -0
- package/dist/package-discovery.js +218 -0
- package/dist/shell-utils.js +9 -0
- package/dist/tool-binaries.js +16 -0
- package/dist/types.js +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Link command - links uikit packages into consumer workspaces
|
|
4
|
+
*/
|
|
5
|
+
export class LinkCommand {
|
|
6
|
+
discovery;
|
|
7
|
+
executor;
|
|
8
|
+
validator;
|
|
9
|
+
fs;
|
|
10
|
+
logger;
|
|
11
|
+
constructor(discovery, executor, validator, fs, logger) {
|
|
12
|
+
this.discovery = discovery;
|
|
13
|
+
this.executor = executor;
|
|
14
|
+
this.validator = validator;
|
|
15
|
+
this.fs = fs;
|
|
16
|
+
this.logger = logger;
|
|
17
|
+
}
|
|
18
|
+
execute(consumerRoot, uikitRoot, verify = false) {
|
|
19
|
+
this.logger.debug('Starting link command', { consumerRoot, uikitRoot });
|
|
20
|
+
const uikitWorkspaces = this.discovery.loadWorkspaces(uikitRoot);
|
|
21
|
+
const consumerWorkspaces = this.discovery.loadConsumerWorkspaces(consumerRoot);
|
|
22
|
+
const uikitPackages = uikitWorkspaces.filter((ws) => String(ws.name ?? '').startsWith('@r0hitsharma/'));
|
|
23
|
+
const dirByName = new Map(uikitPackages.map((pkg) => [pkg.name ?? '', pkg.path]));
|
|
24
|
+
dirByName.delete('');
|
|
25
|
+
const supportedNames = new Set(dirByName.keys());
|
|
26
|
+
const neededByWorkspace = this.collectWorkspaceRequirements(consumerWorkspaces, supportedNames);
|
|
27
|
+
if (neededByWorkspace.size === 0) {
|
|
28
|
+
this.logger.info('No local uikit packages referenced by consumer workspaces.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Collect all packages that were actually linked
|
|
32
|
+
const linkedNames = new Set();
|
|
33
|
+
for (const names of neededByWorkspace.values()) {
|
|
34
|
+
for (const name of names) {
|
|
35
|
+
linkedNames.add(name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
this.linkPackages(consumerRoot, uikitRoot, neededByWorkspace, dirByName);
|
|
39
|
+
if (verify) {
|
|
40
|
+
// Only verify packages that were actually linked
|
|
41
|
+
const linkedDirByName = new Map();
|
|
42
|
+
for (const name of linkedNames) {
|
|
43
|
+
const packageDir = dirByName.get(name);
|
|
44
|
+
if (packageDir) {
|
|
45
|
+
linkedDirByName.set(name, packageDir);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
this.runVerification(consumerRoot, linkedDirByName);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
collectWorkspaceRequirements(workspaces, supportedNames) {
|
|
52
|
+
const neededByWorkspace = new Map();
|
|
53
|
+
for (const ws of workspaces) {
|
|
54
|
+
const fields = [
|
|
55
|
+
ws.dependencies,
|
|
56
|
+
ws.devDependencies,
|
|
57
|
+
ws.peerDependencies,
|
|
58
|
+
ws.optionalDependencies,
|
|
59
|
+
];
|
|
60
|
+
const needed = new Set();
|
|
61
|
+
for (const depField of fields) {
|
|
62
|
+
for (const depName of Object.keys(depField)) {
|
|
63
|
+
if (supportedNames.has(depName)) {
|
|
64
|
+
needed.add(depName);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (needed.size > 0) {
|
|
69
|
+
neededByWorkspace.set(ws.location, [...needed]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return neededByWorkspace;
|
|
73
|
+
}
|
|
74
|
+
linkPackages(consumerRoot, uikitRoot, neededByWorkspace, dirByName) {
|
|
75
|
+
const allNames = new Set();
|
|
76
|
+
for (const names of neededByWorkspace.values()) {
|
|
77
|
+
for (const name of names) {
|
|
78
|
+
allNames.add(name);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Track link steps that failed so we never report a partial link as success.
|
|
82
|
+
const failures = [];
|
|
83
|
+
// Link at root level
|
|
84
|
+
const rootPackageArgs = [...allNames].map((name) => `"${name}"`).join(' ');
|
|
85
|
+
if (rootPackageArgs) {
|
|
86
|
+
this.logger.info('Linking packages at root level...');
|
|
87
|
+
const result = this.executor.exec(`npm link ${rootPackageArgs} --package-lock=false --save=false`, {
|
|
88
|
+
cwd: consumerRoot,
|
|
89
|
+
});
|
|
90
|
+
if (!result.success) {
|
|
91
|
+
failures.push(`root: ${[...allNames].join(', ')}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Ensure symlinks point to correct targets
|
|
95
|
+
for (const name of allNames) {
|
|
96
|
+
const target = dirByName.get(name);
|
|
97
|
+
if (target) {
|
|
98
|
+
this.ensureLinkedPath(consumerRoot, name, target);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Link per workspace
|
|
102
|
+
for (const [workspace, names] of neededByWorkspace.entries()) {
|
|
103
|
+
const packageArgs = names.map((name) => `"${name}"`).join(' ');
|
|
104
|
+
if (!packageArgs)
|
|
105
|
+
continue;
|
|
106
|
+
// A single-package consumer surfaces as the root itself (location '').
|
|
107
|
+
// The root-level `npm link` above already linked it; there is no separate
|
|
108
|
+
// workspace node_modules to fix up, and `--workspace ""` is invalid.
|
|
109
|
+
if (workspace === '' || workspace === '.')
|
|
110
|
+
continue;
|
|
111
|
+
this.logger.debug(`Linking packages for workspace: ${workspace}`);
|
|
112
|
+
const result = this.executor.exec(`npm link ${packageArgs} --workspace "${workspace}" --package-lock=false --save=false`, { cwd: consumerRoot });
|
|
113
|
+
if (!result.success) {
|
|
114
|
+
failures.push(`${workspace}: ${names.join(', ')}`);
|
|
115
|
+
}
|
|
116
|
+
// Clean up shadow installs and Vite cache
|
|
117
|
+
for (const name of names) {
|
|
118
|
+
this.removeWorkspaceShadowInstall(consumerRoot, workspace, name);
|
|
119
|
+
}
|
|
120
|
+
this.clearWorkspaceViteCache(consumerRoot, workspace);
|
|
121
|
+
}
|
|
122
|
+
// Ensure linked packages can resolve external runtime deps when preserve-symlinks is enabled.
|
|
123
|
+
// Run this last because workspace linking can mutate root node_modules.
|
|
124
|
+
this.linkMissingRuntimeDependencies(consumerRoot, uikitRoot, allNames, dirByName);
|
|
125
|
+
if (failures.length > 0) {
|
|
126
|
+
this.logger.warn('Some packages did not link — the local link is PARTIAL, and those ' +
|
|
127
|
+
'names may resolve to a stale registry version instead:');
|
|
128
|
+
for (const failure of failures) {
|
|
129
|
+
this.logger.warn(` ${failure}`);
|
|
130
|
+
}
|
|
131
|
+
this.logger.warn('A consumer .npmrc with `min-release-age` can reject a fresh prerelease ' +
|
|
132
|
+
'with ETARGET; re-run with an override (e.g. --min-release-age=0) or ' +
|
|
133
|
+
'link those packages manually.');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.logger.info('✓ Linked local uikit packages into consumer workspaces.');
|
|
137
|
+
}
|
|
138
|
+
linkMissingRuntimeDependencies(consumerRoot, uikitRoot, linkedNames, dirByName) {
|
|
139
|
+
const depsToLink = this.collectExternalDependencyClosure(uikitRoot, linkedNames, dirByName);
|
|
140
|
+
if (depsToLink.size === 0) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
for (const depName of depsToLink) {
|
|
144
|
+
const sourcePath = path.join(uikitRoot, 'node_modules', depName);
|
|
145
|
+
const consumerPath = path.join(consumerRoot, 'node_modules', depName);
|
|
146
|
+
if (!this.fs.exists(sourcePath)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
// Respect existing consumer installations to avoid overriding deliberate versions.
|
|
150
|
+
if (this.fs.exists(consumerPath)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
this.fs.createDir(path.dirname(consumerPath));
|
|
154
|
+
this.fs.createSymlink(sourcePath, consumerPath);
|
|
155
|
+
this.logger.debug(`Linked runtime dependency ${depName}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
collectExternalDependencyClosure(uikitRoot, linkedNames, dirByName) {
|
|
159
|
+
const result = new Set();
|
|
160
|
+
const queue = [];
|
|
161
|
+
for (const linkedName of linkedNames) {
|
|
162
|
+
const packageDir = dirByName.get(linkedName);
|
|
163
|
+
if (!packageDir)
|
|
164
|
+
continue;
|
|
165
|
+
const packageJsonPath = path.join(packageDir, 'package.json');
|
|
166
|
+
if (!this.fs.exists(packageJsonPath))
|
|
167
|
+
continue;
|
|
168
|
+
const packageJson = this.fs.readJson(packageJsonPath);
|
|
169
|
+
for (const depName of Object.keys(packageJson.dependencies ?? {})) {
|
|
170
|
+
if (!depName.startsWith('@r0hitsharma/')) {
|
|
171
|
+
queue.push(depName);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
while (queue.length > 0) {
|
|
176
|
+
const depName = queue.shift();
|
|
177
|
+
if (!depName || result.has(depName)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const depPackageJsonPath = path.join(uikitRoot, 'node_modules', depName, 'package.json');
|
|
181
|
+
if (!this.fs.exists(depPackageJsonPath)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
result.add(depName);
|
|
185
|
+
const depPackageJson = this.fs.readJson(depPackageJsonPath);
|
|
186
|
+
for (const nestedDep of Object.keys(depPackageJson.dependencies ?? {})) {
|
|
187
|
+
if (!nestedDep.startsWith('@r0hitsharma/')) {
|
|
188
|
+
queue.push(nestedDep);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
ensureLinkedPath(consumerRoot, packageName, expectedTarget) {
|
|
195
|
+
const packagePath = path.join(consumerRoot, 'node_modules', packageName);
|
|
196
|
+
try {
|
|
197
|
+
if (this.fs.isSymlink(packagePath) &&
|
|
198
|
+
this.fs.realpath(packagePath) === expectedTarget) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.fs.removeDir(packagePath);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Path may not exist yet
|
|
205
|
+
}
|
|
206
|
+
this.fs.createDir(path.dirname(packagePath));
|
|
207
|
+
this.fs.createSymlink(expectedTarget, packagePath);
|
|
208
|
+
}
|
|
209
|
+
removeWorkspaceShadowInstall(consumerRoot, workspace, packageName) {
|
|
210
|
+
const packagePath = path.join(consumerRoot, workspace, 'node_modules', packageName);
|
|
211
|
+
if (!this.fs.exists(packagePath)) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
if (this.fs.isSymlink(packagePath)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.fs.removeDir(packagePath);
|
|
219
|
+
this.logger.info(`Removed shadow install at ${workspace}/node_modules/${packageName} to preserve local links.`);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Best effort cleanup
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
clearWorkspaceViteCache(consumerRoot, workspace) {
|
|
226
|
+
const viteCachePath = path.join(consumerRoot, workspace, 'node_modules', '.vite');
|
|
227
|
+
if (!this.fs.exists(viteCachePath)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
this.fs.removeDir(viteCachePath);
|
|
232
|
+
this.logger.debug(`Cleared Vite cache at ${workspace}/node_modules/.vite`);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Best effort cleanup
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
runVerification(consumerRoot, dirByName) {
|
|
239
|
+
this.logger.info('\nVerifying link state...');
|
|
240
|
+
const result = this.validator.validateLinkedPackages(consumerRoot, dirByName);
|
|
241
|
+
if (result.valid) {
|
|
242
|
+
this.logger.info('✓ All links valid');
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
this.logger.warn(`Found ${result.issues.length} issue(s):`);
|
|
246
|
+
for (const issue of result.issues) {
|
|
247
|
+
this.logger.warn(` ${issue.type}: ${issue.package}`);
|
|
248
|
+
this.logger.warn(` ${issue.details}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { shellEscape } from '../shell-utils.js';
|
|
2
|
+
import { resolveCliBinary } from '../tool-binaries.js';
|
|
3
|
+
/**
|
|
4
|
+
* Lint command - forwards to oxlint
|
|
5
|
+
*
|
|
6
|
+
* Deliberately does *not* inject `-c`, unlike `format`. oxfmt only
|
|
7
|
+
* auto-discovers the `.json` form of its config, so `format` has to point at
|
|
8
|
+
* `.oxfmtrc.ts` itself; oxlint resolves `oxlint.config.ts` per target file by
|
|
9
|
+
* walking up from it, so every package in a multi-target run gets its own
|
|
10
|
+
* config with no help. Injecting `-c ./oxlint.config.ts` would *replace* that
|
|
11
|
+
* per-package resolution with whatever config happens to sit in cwd — in the
|
|
12
|
+
* git-hook shape (cwd at the repo root, targets in several packages) that
|
|
13
|
+
* silently drops the packages' own rules.
|
|
14
|
+
*/
|
|
15
|
+
export class LintCommand {
|
|
16
|
+
executor;
|
|
17
|
+
constructor(executor) {
|
|
18
|
+
this.executor = executor;
|
|
19
|
+
}
|
|
20
|
+
execute(args) {
|
|
21
|
+
const modifiedArgs = [...args];
|
|
22
|
+
// oxlint exits 0 on warnings. The shared presets set `correctness` and
|
|
23
|
+
// `suspicious` to `warn`, so without this every violation they describe is
|
|
24
|
+
// reported and then ignored. Callers who want warnings to stay advisory can
|
|
25
|
+
// pass their own `--max-warnings`/`--deny-warnings` and keep it.
|
|
26
|
+
if (!this.hasWarningLimitFlag(modifiedArgs)) {
|
|
27
|
+
modifiedArgs.push('--max-warnings=0');
|
|
28
|
+
}
|
|
29
|
+
const escapedArgs = modifiedArgs.map((arg) => shellEscape(arg)).join(' ');
|
|
30
|
+
const oxlintBinary = shellEscape(resolveCliBinary('oxlint', 'bin/oxlint'));
|
|
31
|
+
return this.executor.exec(`${oxlintBinary} ${escapedArgs}`.trim(), {
|
|
32
|
+
cwd: process.cwd(),
|
|
33
|
+
}).success;
|
|
34
|
+
}
|
|
35
|
+
// Only the two flags that govern the *exit code* count here. `-W`/`--warn`
|
|
36
|
+
// looks related but sets a rule's severity, and must not suppress the
|
|
37
|
+
// default.
|
|
38
|
+
hasWarningLimitFlag(args) {
|
|
39
|
+
return args.some((arg) => arg === '--max-warnings' ||
|
|
40
|
+
arg.startsWith('--max-warnings=') ||
|
|
41
|
+
arg === '--deny-warnings');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register command - globally links uikit packages via npm link
|
|
3
|
+
*/
|
|
4
|
+
export class RegisterCommand {
|
|
5
|
+
discovery;
|
|
6
|
+
executor;
|
|
7
|
+
logger;
|
|
8
|
+
constructor(discovery, executor, logger) {
|
|
9
|
+
this.discovery = discovery;
|
|
10
|
+
this.executor = executor;
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
}
|
|
13
|
+
execute(uikitRoot, supportedNames) {
|
|
14
|
+
this.logger.debug('Starting register command', { uikitRoot });
|
|
15
|
+
const uikitWorkspaces = this.discovery.loadWorkspaces(uikitRoot);
|
|
16
|
+
const uikitPackages = uikitWorkspaces.filter((ws) => String(ws.name ?? '').startsWith('@r0hitsharma/'));
|
|
17
|
+
const packagesToRegister = supportedNames
|
|
18
|
+
? uikitPackages.filter((pkg) => supportedNames.has(pkg.name ?? ''))
|
|
19
|
+
: uikitPackages;
|
|
20
|
+
this.logger.info(`Registering ${packagesToRegister.length} local uikit packages...`);
|
|
21
|
+
for (const pkg of packagesToRegister) {
|
|
22
|
+
if (!pkg.name)
|
|
23
|
+
continue;
|
|
24
|
+
this.logger.debug(`Registering ${pkg.name}`, { path: pkg.path });
|
|
25
|
+
const result = this.executor.exec(`npm link`, { cwd: pkg.path });
|
|
26
|
+
if (!result.success) {
|
|
27
|
+
this.logger.error(`Failed to register ${pkg.name}`, {
|
|
28
|
+
error: result.stderr,
|
|
29
|
+
});
|
|
30
|
+
throw new Error(`Failed to register ${pkg.name}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
this.logger.info('✓ All packages registered successfully');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unlink command - simplified version without registry checks
|
|
3
|
+
*/
|
|
4
|
+
export class UnlinkCommand {
|
|
5
|
+
executor;
|
|
6
|
+
logger;
|
|
7
|
+
constructor(executor, logger) {
|
|
8
|
+
this.executor = executor;
|
|
9
|
+
this.logger = logger;
|
|
10
|
+
}
|
|
11
|
+
execute(consumerRoot, uikitRoot, discovery) {
|
|
12
|
+
// Load workspaces and determine requirements
|
|
13
|
+
const uikitWorkspaces = discovery.loadWorkspaces(uikitRoot);
|
|
14
|
+
const consumerWorkspaces = discovery.loadConsumerWorkspaces(consumerRoot);
|
|
15
|
+
const uikitPackages = uikitWorkspaces.filter((ws) => String(ws.name ?? '').startsWith('@r0hitsharma/'));
|
|
16
|
+
const supportedNames = new Set(uikitPackages.map((pkg) => pkg.name ?? '').filter((name) => name !== ''));
|
|
17
|
+
// Collect workspace requirements
|
|
18
|
+
const neededByWorkspace = new Map();
|
|
19
|
+
const allPackages = new Set();
|
|
20
|
+
for (const ws of consumerWorkspaces) {
|
|
21
|
+
const needed = new Set();
|
|
22
|
+
for (const depName of Object.keys(ws.dependencies)) {
|
|
23
|
+
if (supportedNames.has(depName)) {
|
|
24
|
+
needed.add(depName);
|
|
25
|
+
allPackages.add(depName);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (needed.size > 0) {
|
|
29
|
+
neededByWorkspace.set(ws.location, [...needed]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const packages = [...allPackages];
|
|
33
|
+
const workspaces = [...neededByWorkspace.keys()];
|
|
34
|
+
this.logger.debug('Starting unlink command', {
|
|
35
|
+
consumerRoot,
|
|
36
|
+
packageCount: packages.length,
|
|
37
|
+
});
|
|
38
|
+
// Unlink at root level
|
|
39
|
+
this.logger.info('Unlinking packages at root level...');
|
|
40
|
+
const packageArgs = packages.map((name) => `"${name}"`).join(' ');
|
|
41
|
+
if (packageArgs) {
|
|
42
|
+
const result = this.executor.execQuiet(`npm unlink ${packageArgs} --package-lock=false --save=false`, { cwd: consumerRoot });
|
|
43
|
+
if (!result) {
|
|
44
|
+
this.logger.warn('Root unlink had issues, continuing...');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Unlink per workspace. The single-package consumer surfaces as the root
|
|
48
|
+
// (location ''); the root-level unlink above already covered it and
|
|
49
|
+
// `--workspace ""` is invalid.
|
|
50
|
+
for (const workspace of workspaces) {
|
|
51
|
+
if (workspace === '' || workspace === '.')
|
|
52
|
+
continue;
|
|
53
|
+
this.logger.debug(`Unlinking packages for workspace: ${workspace}`);
|
|
54
|
+
this.executor.execQuiet(`npm unlink ${packageArgs} --workspace "${workspace}" --package-lock=false --save=false`, { cwd: consumerRoot });
|
|
55
|
+
}
|
|
56
|
+
// Restore from registry
|
|
57
|
+
this.logger.info('Restoring packages from registry...');
|
|
58
|
+
const installResult = this.executor.exec('npm_config_min_release_age=0 npm install', {
|
|
59
|
+
cwd: consumerRoot,
|
|
60
|
+
});
|
|
61
|
+
if (!installResult.success) {
|
|
62
|
+
this.logger.error('Failed to restore packages from registry');
|
|
63
|
+
this.logger.warn('You may need to run `uikit-cli link` again to restore local links');
|
|
64
|
+
throw new Error('Unlink failed: could not restore registry packages');
|
|
65
|
+
}
|
|
66
|
+
this.logger.info('✓ Packages unlinked and restored from registry');
|
|
67
|
+
}
|
|
68
|
+
}
|
package/dist/fs-utils.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, symlinkSync, } from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Real file system implementation
|
|
4
|
+
*/
|
|
5
|
+
export class RealFileSystem {
|
|
6
|
+
exists(path) {
|
|
7
|
+
return existsSync(path);
|
|
8
|
+
}
|
|
9
|
+
readFile(path) {
|
|
10
|
+
return readFileSync(path, 'utf8');
|
|
11
|
+
}
|
|
12
|
+
readJson(path) {
|
|
13
|
+
try {
|
|
14
|
+
if (process.env.UIKIT_DEBUG) {
|
|
15
|
+
console.log('[DEBUG readJson]', path);
|
|
16
|
+
}
|
|
17
|
+
const content = this.readFile(path);
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error('[readJson ERROR] Failed to read:', path);
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
realpath(path) {
|
|
26
|
+
return realpathSync(path);
|
|
27
|
+
}
|
|
28
|
+
isSymlink(path) {
|
|
29
|
+
try {
|
|
30
|
+
return lstatSync(path).isSymbolicLink();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
removeDir(path, options) {
|
|
37
|
+
rmSync(path, { recursive: true, force: true, ...options });
|
|
38
|
+
}
|
|
39
|
+
createSymlink(target, path) {
|
|
40
|
+
symlinkSync(target, path, 'dir');
|
|
41
|
+
}
|
|
42
|
+
createDir(path, options) {
|
|
43
|
+
mkdirSync(path, { recursive: true, ...options });
|
|
44
|
+
}
|
|
45
|
+
readDir(path) {
|
|
46
|
+
return readdirSync(path);
|
|
47
|
+
}
|
|
48
|
+
isDirectory(path) {
|
|
49
|
+
try {
|
|
50
|
+
return statSync(path).isDirectory();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Validates link state without npm registry checks
|
|
4
|
+
*/
|
|
5
|
+
export class LinkValidator {
|
|
6
|
+
fs;
|
|
7
|
+
logger;
|
|
8
|
+
constructor(fs, logger) {
|
|
9
|
+
this.fs = fs;
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Validate all expected links in consumer
|
|
14
|
+
*/
|
|
15
|
+
validateLinkedPackages(consumerRoot, expectedLinks) {
|
|
16
|
+
const issues = [];
|
|
17
|
+
this.logger.debug('Validating linked packages', {
|
|
18
|
+
consumerRoot,
|
|
19
|
+
packageCount: expectedLinks.size,
|
|
20
|
+
});
|
|
21
|
+
for (const [packageName, expectedTarget] of expectedLinks) {
|
|
22
|
+
const linkPath = path.join(consumerRoot, 'node_modules', packageName);
|
|
23
|
+
const status = this.validateSymlink(linkPath, expectedTarget);
|
|
24
|
+
if (status !== 'valid') {
|
|
25
|
+
issues.push({
|
|
26
|
+
type: status,
|
|
27
|
+
package: packageName,
|
|
28
|
+
details: this.getStatusDetails(linkPath, expectedTarget, status),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
valid: issues.length === 0,
|
|
34
|
+
issues,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Validate a single symlink
|
|
39
|
+
*/
|
|
40
|
+
validateSymlink(linkPath, expectedTarget) {
|
|
41
|
+
if (!this.fs.exists(linkPath)) {
|
|
42
|
+
return 'missing';
|
|
43
|
+
}
|
|
44
|
+
if (!this.fs.isSymlink(linkPath)) {
|
|
45
|
+
// Not a symlink, might be a regular install
|
|
46
|
+
return 'missing';
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const actualTarget = this.fs.realpath(linkPath);
|
|
50
|
+
const normalizedExpected = path.resolve(expectedTarget);
|
|
51
|
+
const normalizedActual = path.resolve(actualTarget);
|
|
52
|
+
if (normalizedActual === normalizedExpected) {
|
|
53
|
+
return 'valid';
|
|
54
|
+
}
|
|
55
|
+
return 'wrong-target';
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return 'broken';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
getStatusDetails(linkPath, expectedTarget, status) {
|
|
62
|
+
switch (status) {
|
|
63
|
+
case 'missing':
|
|
64
|
+
return `Link does not exist at ${linkPath}`;
|
|
65
|
+
case 'broken':
|
|
66
|
+
return `Symlink exists but points to non-existent location`;
|
|
67
|
+
case 'wrong-target': {
|
|
68
|
+
try {
|
|
69
|
+
const actualTarget = this.fs.realpath(linkPath);
|
|
70
|
+
return `Expected: ${expectedTarget}\nActual: ${actualTarget}`;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return `Could not read symlink target`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
return '';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console logger with optional debug mode
|
|
3
|
+
*/
|
|
4
|
+
export class ConsoleLogger {
|
|
5
|
+
debugMode;
|
|
6
|
+
constructor(debugMode = false) {
|
|
7
|
+
this.debugMode = debugMode;
|
|
8
|
+
}
|
|
9
|
+
info(message, context) {
|
|
10
|
+
if (context) {
|
|
11
|
+
console.log(message, context);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
console.log(message);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
warn(message, context) {
|
|
18
|
+
if (context) {
|
|
19
|
+
console.warn(`⚠️ ${message}`, context);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.warn(`⚠️ ${message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
error(message, context) {
|
|
26
|
+
if (context) {
|
|
27
|
+
console.error(`❌ ${message}`, context);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.error(`❌ ${message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
debug(message, context) {
|
|
34
|
+
if (!this.debugMode)
|
|
35
|
+
return;
|
|
36
|
+
if (context) {
|
|
37
|
+
console.log(`[DEBUG] ${message}`, context);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.log(`[DEBUG] ${message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|