@archon-research/uikit-cli 0.2.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.
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,168 @@
1
+ import path from 'node:path';
2
+ /**
3
+ * Package and workspace discovery for monorepos
4
+ */
5
+ export class PackageDiscovery {
6
+ fs;
7
+ constructor(fs) {
8
+ this.fs = fs;
9
+ }
10
+ /**
11
+ * Find consumer workspace root by walking up looking for package.json with workspaces
12
+ */
13
+ findConsumerRoot(startDir) {
14
+ let current = startDir;
15
+ while (true) {
16
+ const pkgPath = path.join(current, 'package.json');
17
+ if (this.fs.exists(pkgPath)) {
18
+ try {
19
+ const pkg = this.fs.readJson(pkgPath);
20
+ if (pkg.workspaces) {
21
+ return current;
22
+ }
23
+ }
24
+ catch {
25
+ // Continue searching if parse fails
26
+ }
27
+ }
28
+ const parent = path.dirname(current);
29
+ if (parent === current) {
30
+ throw new Error(`Could not find consumer workspace root (no package.json with workspaces field)\n` +
31
+ `Searched from: ${startDir}\n` +
32
+ `Suggestion: Run from inside a workspace-based monorepo`);
33
+ }
34
+ current = parent;
35
+ }
36
+ }
37
+ /**
38
+ * Find uikit root by walking up looking for valid uikit monorepo structure
39
+ */
40
+ findUIKitRoot(startDir) {
41
+ let current = startDir;
42
+ while (true) {
43
+ if (this.isValidUIKitRoot(current)) {
44
+ return current;
45
+ }
46
+ const parent = path.dirname(current);
47
+ if (parent === current) {
48
+ return null;
49
+ }
50
+ current = parent;
51
+ }
52
+ }
53
+ /**
54
+ * Find uikit root from consumer by looking for sibling "uikit" directory
55
+ */
56
+ findUIKitRootFromConsumer(consumerRoot) {
57
+ const candidateNames = ['uikit'];
58
+ let current = consumerRoot;
59
+ while (true) {
60
+ for (const candidateName of candidateNames) {
61
+ const candidate = path.join(current, candidateName);
62
+ if (this.isValidUIKitRoot(candidate)) {
63
+ return candidate;
64
+ }
65
+ }
66
+ const parent = path.dirname(current);
67
+ if (parent === current) {
68
+ return null;
69
+ }
70
+ current = parent;
71
+ }
72
+ }
73
+ /**
74
+ * Check if directory is a valid uikit root (has design-system package)
75
+ */
76
+ isValidUIKitRoot(rootDir) {
77
+ if (!this.fs.exists(rootDir)) {
78
+ return false;
79
+ }
80
+ try {
81
+ const workspaces = this.loadWorkspaces(rootDir);
82
+ return workspaces.some((ws) => ws.name === '@archon-research/design-system');
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ /**
89
+ * Load all workspaces from a monorepo root
90
+ */
91
+ loadWorkspaces(rootDir) {
92
+ const patterns = this.getWorkspacePatterns(rootDir);
93
+ const workspaces = [];
94
+ if (process.env.UIKIT_DEBUG) {
95
+ console.log('[DEBUG loadWorkspaces] rootDir:', rootDir, 'patterns:', patterns);
96
+ }
97
+ for (const pattern of patterns) {
98
+ const resolved = this.resolveWorkspacePattern(rootDir, pattern);
99
+ if (process.env.UIKIT_DEBUG) {
100
+ console.log('[DEBUG loadWorkspaces] pattern:', pattern, 'resolved to:', resolved);
101
+ }
102
+ for (const dir of resolved) {
103
+ const pkgPath = path.join(dir, 'package.json');
104
+ if (!this.fs.exists(pkgPath)) {
105
+ if (process.env.UIKIT_DEBUG) {
106
+ console.log('[DEBUG loadWorkspaces] Skipping (no package.json):', dir);
107
+ }
108
+ continue;
109
+ }
110
+ try {
111
+ const pkg = this.fs.readJson(pkgPath);
112
+ workspaces.push({
113
+ name: pkg.name ?? null,
114
+ location: path.relative(rootDir, dir),
115
+ path: dir,
116
+ dependencies: pkg.dependencies ?? {},
117
+ });
118
+ }
119
+ catch {
120
+ // Skip invalid package.json
121
+ }
122
+ }
123
+ }
124
+ return workspaces.filter((ws) => Boolean(ws.name));
125
+ }
126
+ getWorkspacePatterns(rootDir) {
127
+ const pkgPath = path.join(rootDir, 'package.json');
128
+ if (!this.fs.exists(pkgPath)) {
129
+ return [];
130
+ }
131
+ try {
132
+ const pkg = this.fs.readJson(pkgPath);
133
+ if (Array.isArray(pkg.workspaces)) {
134
+ return pkg.workspaces;
135
+ }
136
+ if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) {
137
+ return pkg.workspaces.packages;
138
+ }
139
+ }
140
+ catch {
141
+ // Return empty if parse fails
142
+ }
143
+ return [];
144
+ }
145
+ resolveWorkspacePattern(rootDir, pattern) {
146
+ // Handle simple glob patterns like "packages/*"
147
+ if (pattern.includes('*')) {
148
+ // Only support "dir/*" pattern for now
149
+ if (pattern.endsWith('/*')) {
150
+ const baseDir = pattern.slice(0, -2);
151
+ const basePath = path.join(rootDir, baseDir);
152
+ if (!this.fs.exists(basePath) || !this.fs.isDirectory(basePath)) {
153
+ return [];
154
+ }
155
+ const entries = this.fs.readDir(basePath);
156
+ return entries
157
+ .filter((entry) => !entry.startsWith('.') && entry !== 'node_modules')
158
+ .map((entry) => path.join(basePath, entry))
159
+ .filter((p) => this.fs.isDirectory(p));
160
+ }
161
+ // Unsupported pattern
162
+ return [];
163
+ }
164
+ // Static pattern, return as-is if it's a directory
165
+ const fullPath = path.join(rootDir, pattern);
166
+ return this.fs.isDirectory(fullPath) ? [fullPath] : [];
167
+ }
168
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shell argument escaping utility
3
+ */
4
+ export function shellEscape(arg) {
5
+ if (/^[a-zA-Z0-9_\-./:=]+$/.test(arg)) {
6
+ return arg;
7
+ }
8
+ return `'${arg.replace(/'/g, "'\\''")}'`;
9
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};