@archon-research/uikit-cli 0.3.0-rohit-improve-cli.2 → 0.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/README.md +84 -32
- package/dist/cli.js +151 -443
- package/dist/cli.sh +10 -0
- package/dist/command-executor.js +41 -0
- package/dist/commands/format.js +36 -0
- package/dist/commands/link.js +162 -0
- package/dist/commands/lint.js +14 -0
- package/dist/commands/register.js +35 -0
- package/dist/commands/unlink.js +64 -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 +168 -0
- package/dist/shell-utils.js +9 -0
- package/dist/types.js +1 -0
- package/package.json +4 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Real npm command executor using execSync
|
|
4
|
+
*/
|
|
5
|
+
export class NpmCommandExecutor {
|
|
6
|
+
debugMode;
|
|
7
|
+
constructor(debugMode = false) {
|
|
8
|
+
this.debugMode = debugMode;
|
|
9
|
+
}
|
|
10
|
+
exec(command, options) {
|
|
11
|
+
const { cwd = process.cwd(), silent = false } = options || {};
|
|
12
|
+
if (this.debugMode) {
|
|
13
|
+
console.log(`[DEBUG] Executing: ${command}`);
|
|
14
|
+
console.log(`[DEBUG] CWD: ${cwd}`);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const stdout = execSync(command, {
|
|
18
|
+
cwd,
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
stdio: silent ? 'pipe' : 'inherit',
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
stdout: typeof stdout === 'string' ? stdout : '',
|
|
24
|
+
stderr: '',
|
|
25
|
+
success: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const err = error;
|
|
30
|
+
return {
|
|
31
|
+
stdout: err.stdout?.toString('utf8') || '',
|
|
32
|
+
stderr: err.stderr?.toString('utf8') || err.message || 'Unknown error',
|
|
33
|
+
success: false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
execQuiet(command, options) {
|
|
38
|
+
const result = this.exec(command, { ...options, silent: true });
|
|
39
|
+
return result.success;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { shellEscape } from '../shell-utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Format command - forwards to oxfmt with config detection
|
|
5
|
+
*/
|
|
6
|
+
export class FormatCommand {
|
|
7
|
+
executor;
|
|
8
|
+
fs;
|
|
9
|
+
constructor(executor, fs) {
|
|
10
|
+
this.executor = executor;
|
|
11
|
+
this.fs = fs;
|
|
12
|
+
}
|
|
13
|
+
execute(args) {
|
|
14
|
+
const modifiedArgs = [...args];
|
|
15
|
+
const defaultConfig = './.oxfmtrc.ts';
|
|
16
|
+
// Add default config if not specified and file exists
|
|
17
|
+
if (!this.hasConfigFlag(modifiedArgs)) {
|
|
18
|
+
const configPath = path.join(process.cwd(), '.oxfmtrc.ts');
|
|
19
|
+
if (this.fs.exists(configPath)) {
|
|
20
|
+
modifiedArgs.unshift(defaultConfig);
|
|
21
|
+
modifiedArgs.unshift('-c');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const escapedArgs = modifiedArgs.map((arg) => shellEscape(arg)).join(' ');
|
|
25
|
+
this.executor.exec(`npm exec -- oxfmt ${escapedArgs}`.trim(), { cwd: process.cwd() });
|
|
26
|
+
}
|
|
27
|
+
hasConfigFlag(args) {
|
|
28
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
29
|
+
const arg = args[i];
|
|
30
|
+
if (arg === '-c' || arg === '--config' || arg.startsWith('--config=')) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
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.loadWorkspaces(consumerRoot);
|
|
22
|
+
const uikitPackages = uikitWorkspaces.filter((ws) => String(ws.name ?? '').startsWith('@archon-research/'));
|
|
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, 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 path = dirByName.get(name);
|
|
44
|
+
if (path) {
|
|
45
|
+
linkedDirByName.set(name, path);
|
|
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 = [ws.dependencies];
|
|
55
|
+
const needed = new Set();
|
|
56
|
+
for (const depField of fields) {
|
|
57
|
+
for (const depName of Object.keys(depField)) {
|
|
58
|
+
if (supportedNames.has(depName)) {
|
|
59
|
+
needed.add(depName);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (needed.size > 0) {
|
|
64
|
+
neededByWorkspace.set(ws.location, [...needed]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return neededByWorkspace;
|
|
68
|
+
}
|
|
69
|
+
linkPackages(consumerRoot, neededByWorkspace, dirByName) {
|
|
70
|
+
const allNames = new Set();
|
|
71
|
+
for (const names of neededByWorkspace.values()) {
|
|
72
|
+
for (const name of names) {
|
|
73
|
+
allNames.add(name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Link at root level
|
|
77
|
+
const rootPackageArgs = [...allNames].map((name) => `"${name}"`).join(' ');
|
|
78
|
+
if (rootPackageArgs) {
|
|
79
|
+
this.logger.info('Linking packages at root level...');
|
|
80
|
+
this.executor.exec(`npm link ${rootPackageArgs} --package-lock=false --save=false`, { cwd: consumerRoot });
|
|
81
|
+
}
|
|
82
|
+
// Ensure symlinks point to correct targets
|
|
83
|
+
for (const name of allNames) {
|
|
84
|
+
const target = dirByName.get(name);
|
|
85
|
+
if (target) {
|
|
86
|
+
this.ensureLinkedPath(consumerRoot, name, target);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Link per workspace
|
|
90
|
+
for (const [workspace, names] of neededByWorkspace.entries()) {
|
|
91
|
+
const packageArgs = names.map((name) => `"${name}"`).join(' ');
|
|
92
|
+
if (!packageArgs)
|
|
93
|
+
continue;
|
|
94
|
+
this.logger.debug(`Linking packages for workspace: ${workspace}`);
|
|
95
|
+
this.executor.exec(`npm link ${packageArgs} --workspace "${workspace}" --package-lock=false --save=false`, { cwd: consumerRoot });
|
|
96
|
+
// Clean up shadow installs and Vite cache
|
|
97
|
+
for (const name of names) {
|
|
98
|
+
this.removeWorkspaceShadowInstall(consumerRoot, workspace, name);
|
|
99
|
+
}
|
|
100
|
+
this.clearWorkspaceViteCache(consumerRoot, workspace);
|
|
101
|
+
}
|
|
102
|
+
this.logger.info('✓ Linked local uikit packages into consumer workspaces.');
|
|
103
|
+
}
|
|
104
|
+
ensureLinkedPath(consumerRoot, packageName, expectedTarget) {
|
|
105
|
+
const packagePath = path.join(consumerRoot, 'node_modules', packageName);
|
|
106
|
+
try {
|
|
107
|
+
if (this.fs.isSymlink(packagePath) &&
|
|
108
|
+
this.fs.realpath(packagePath) === expectedTarget) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.fs.removeDir(packagePath);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Path may not exist yet
|
|
115
|
+
}
|
|
116
|
+
this.fs.createDir(path.dirname(packagePath));
|
|
117
|
+
this.fs.createSymlink(expectedTarget, packagePath);
|
|
118
|
+
}
|
|
119
|
+
removeWorkspaceShadowInstall(consumerRoot, workspace, packageName) {
|
|
120
|
+
const packagePath = path.join(consumerRoot, workspace, 'node_modules', packageName);
|
|
121
|
+
if (!this.fs.exists(packagePath)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
if (this.fs.isSymlink(packagePath)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.fs.removeDir(packagePath);
|
|
129
|
+
this.logger.info(`Removed shadow install at ${workspace}/node_modules/${packageName} to preserve local links.`);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Best effort cleanup
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
clearWorkspaceViteCache(consumerRoot, workspace) {
|
|
136
|
+
const viteCachePath = path.join(consumerRoot, workspace, 'node_modules', '.vite');
|
|
137
|
+
if (!this.fs.exists(viteCachePath)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
this.fs.removeDir(viteCachePath);
|
|
142
|
+
this.logger.debug(`Cleared Vite cache at ${workspace}/node_modules/.vite`);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Best effort cleanup
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
runVerification(consumerRoot, dirByName) {
|
|
149
|
+
this.logger.info('\nVerifying link state...');
|
|
150
|
+
const result = this.validator.validateLinkedPackages(consumerRoot, dirByName);
|
|
151
|
+
if (result.valid) {
|
|
152
|
+
this.logger.info('✓ All links valid');
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
this.logger.warn(`Found ${result.issues.length} issue(s):`);
|
|
156
|
+
for (const issue of result.issues) {
|
|
157
|
+
this.logger.warn(` ${issue.type}: ${issue.package}`);
|
|
158
|
+
this.logger.warn(` ${issue.details}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { shellEscape } from '../shell-utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lint command - forwards to oxlint
|
|
4
|
+
*/
|
|
5
|
+
export class LintCommand {
|
|
6
|
+
executor;
|
|
7
|
+
constructor(executor) {
|
|
8
|
+
this.executor = executor;
|
|
9
|
+
}
|
|
10
|
+
execute(args) {
|
|
11
|
+
const escapedArgs = args.map((arg) => shellEscape(arg)).join(' ');
|
|
12
|
+
this.executor.exec(`npm exec -- oxlint ${escapedArgs}`.trim(), { cwd: process.cwd() });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -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('@archon-research/'));
|
|
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,64 @@
|
|
|
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.loadWorkspaces(consumerRoot);
|
|
15
|
+
const uikitPackages = uikitWorkspaces.filter((ws) => String(ws.name ?? '').startsWith('@archon-research/'));
|
|
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
|
|
48
|
+
for (const workspace of workspaces) {
|
|
49
|
+
this.logger.debug(`Unlinking packages for workspace: ${workspace}`);
|
|
50
|
+
this.executor.execQuiet(`npm unlink ${packageArgs} --workspace "${workspace}" --package-lock=false --save=false`, { cwd: consumerRoot });
|
|
51
|
+
}
|
|
52
|
+
// Restore from registry
|
|
53
|
+
this.logger.info('Restoring packages from registry...');
|
|
54
|
+
const installResult = this.executor.exec('npm_config_min_release_age=0 npm install', {
|
|
55
|
+
cwd: consumerRoot,
|
|
56
|
+
});
|
|
57
|
+
if (!installResult.success) {
|
|
58
|
+
this.logger.error('Failed to restore packages from registry');
|
|
59
|
+
this.logger.warn('You may need to run `uikit-cli link` again to restore local links');
|
|
60
|
+
throw new Error('Unlink failed: could not restore registry packages');
|
|
61
|
+
}
|
|
62
|
+
this.logger.info('✓ Packages unlinked and restored from registry');
|
|
63
|
+
}
|
|
64
|
+
}
|
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
|
+
}
|