@eldrforge/kodrdriv 0.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +1 -0
  2. package/dist/application.js +25 -3
  3. package/dist/application.js.map +1 -1
  4. package/dist/arguments.js +103 -18
  5. package/dist/arguments.js.map +1 -1
  6. package/dist/commands/audio-commit.js +28 -7
  7. package/dist/commands/audio-commit.js.map +1 -1
  8. package/dist/commands/audio-review.js +28 -7
  9. package/dist/commands/audio-review.js.map +1 -1
  10. package/dist/commands/commit.js +75 -18
  11. package/dist/commands/commit.js.map +1 -1
  12. package/dist/commands/development.js +264 -0
  13. package/dist/commands/development.js.map +1 -0
  14. package/dist/commands/link.js +356 -181
  15. package/dist/commands/link.js.map +1 -1
  16. package/dist/commands/publish.js +166 -32
  17. package/dist/commands/publish.js.map +1 -1
  18. package/dist/commands/release.js +78 -13
  19. package/dist/commands/release.js.map +1 -1
  20. package/dist/commands/review.js +10 -6
  21. package/dist/commands/review.js.map +1 -1
  22. package/dist/commands/tree.js +450 -24
  23. package/dist/commands/tree.js.map +1 -1
  24. package/dist/commands/unlink.js +267 -372
  25. package/dist/commands/unlink.js.map +1 -1
  26. package/dist/commands/versions.js +224 -0
  27. package/dist/commands/versions.js.map +1 -0
  28. package/dist/constants.js +29 -10
  29. package/dist/constants.js.map +1 -1
  30. package/dist/content/diff.js.map +1 -1
  31. package/dist/content/files.js +192 -0
  32. package/dist/content/files.js.map +1 -0
  33. package/dist/content/log.js +16 -0
  34. package/dist/content/log.js.map +1 -1
  35. package/dist/main.js +0 -0
  36. package/dist/prompt/commit.js +9 -2
  37. package/dist/prompt/commit.js.map +1 -1
  38. package/dist/prompt/instructions/commit.md +20 -2
  39. package/dist/prompt/instructions/release.md +27 -10
  40. package/dist/prompt/instructions/review.md +75 -8
  41. package/dist/prompt/release.js +13 -5
  42. package/dist/prompt/release.js.map +1 -1
  43. package/dist/types.js +21 -5
  44. package/dist/types.js.map +1 -1
  45. package/dist/util/child.js +112 -26
  46. package/dist/util/child.js.map +1 -1
  47. package/dist/util/countdown.js +215 -0
  48. package/dist/util/countdown.js.map +1 -0
  49. package/dist/util/general.js +10 -2
  50. package/dist/util/general.js.map +1 -1
  51. package/dist/util/git.js +587 -0
  52. package/dist/util/git.js.map +1 -0
  53. package/dist/util/github.js +519 -3
  54. package/dist/util/github.js.map +1 -1
  55. package/dist/util/interactive.js +245 -79
  56. package/dist/util/interactive.js.map +1 -1
  57. package/dist/util/openai.js +70 -22
  58. package/dist/util/openai.js.map +1 -1
  59. package/dist/util/performance.js +1 -69
  60. package/dist/util/performance.js.map +1 -1
  61. package/dist/util/storage.js +28 -1
  62. package/dist/util/storage.js.map +1 -1
  63. package/dist/util/validation.js +1 -25
  64. package/dist/util/validation.js.map +1 -1
  65. package/package.json +10 -8
  66. package/test-multiline/cli/package.json +8 -0
  67. package/test-multiline/core/package.json +5 -0
  68. package/test-multiline/mobile/package.json +8 -0
  69. package/test-multiline/web/package.json +8 -0
  70. package/dist/util/npmOptimizations.js +0 -174
  71. package/dist/util/npmOptimizations.js.map +0 -1
@@ -1,224 +1,399 @@
1
- import path__default from 'path';
2
- import { ValidationError, CommandError } from '../error/CommandErrors.js';
3
1
  import { getLogger, getDryRunLogger } from '../logging.js';
2
+ import { run, runSecure } from '../util/child.js';
3
+ import { findAllPackageJsonFiles } from '../util/performance.js';
4
4
  import { create } from '../util/storage.js';
5
- import { PerformanceTimer, findAllPackageJsonFiles, scanDirectoryForPackages } from '../util/performance.js';
6
- import { smartNpmInstall } from '../util/npmOptimizations.js';
5
+ import { safeJsonParse, validatePackageJson } from '../util/validation.js';
6
+ import fs__default from 'fs/promises';
7
+ import path__default from 'path';
7
8
 
8
- const findPackagesToLink = async (scopeRoots, storage)=>{
9
- const logger = getLogger();
10
- const timer = PerformanceTimer.start(logger, 'Finding packages to link');
11
- const packagesToLink = new Map();
12
- logger.silly(`Finding packages to link from scope roots: ${JSON.stringify(scopeRoots)}`);
13
- // Scan all scope roots to build a comprehensive map of packages that can be linked
14
- const scopeTimer = PerformanceTimer.start(logger, 'Scanning all scope roots for linkable packages');
15
- const allScopePackages = new Map(); // packageName -> relativePath
16
- // Process all scopes in parallel for better performance
17
- const scopePromises = Object.entries(scopeRoots).map(async ([scope, rootDir])=>{
18
- logger.verbose(`Scanning scope ${scope} at root directory: ${rootDir}`);
19
- const scopePackages = await scanDirectoryForPackages(rootDir, storage);
20
- // Add packages from this scope to the overall map
21
- const scopeResults = [];
22
- for (const [packageName, packagePath] of scopePackages){
23
- if (packageName.startsWith(scope)) {
24
- scopeResults.push([
25
- packageName,
26
- packagePath
27
- ]);
28
- logger.debug(`Linkable package: ${packageName} -> ${packagePath}`);
9
+ // Helper function to create symbolic links manually
10
+ const createSymbolicLink = async (packageName, sourcePath, targetDir, logger, isDryRun = false)=>{
11
+ try {
12
+ // Parse package name to get scope and name parts
13
+ const [scope, name] = packageName.startsWith('@') ? packageName.split('/') : [
14
+ null,
15
+ packageName
16
+ ];
17
+ // Create the target path structure
18
+ const nodeModulesPath = path__default.join(targetDir, 'node_modules');
19
+ let targetPath;
20
+ if (scope) {
21
+ // Scoped package: node_modules/@scope/name
22
+ const scopeDir = path__default.join(nodeModulesPath, scope);
23
+ targetPath = path__default.join(scopeDir, name);
24
+ if (!isDryRun) {
25
+ // Ensure scope directory exists
26
+ await fs__default.mkdir(scopeDir, {
27
+ recursive: true
28
+ });
29
+ }
30
+ } else {
31
+ // Unscoped package: node_modules/name
32
+ targetPath = path__default.join(nodeModulesPath, name);
33
+ if (!isDryRun) {
34
+ // Ensure node_modules directory exists
35
+ await fs__default.mkdir(nodeModulesPath, {
36
+ recursive: true
37
+ });
29
38
  }
30
39
  }
31
- return scopeResults;
32
- });
33
- const allScopeResults = await Promise.all(scopePromises);
34
- // Flatten results
35
- for (const scopeResults of allScopeResults){
36
- for (const [packageName, packagePath] of scopeResults){
37
- allScopePackages.set(packageName, packagePath);
40
+ if (isDryRun) {
41
+ logger.verbose(`DRY RUN: Would create symlink: ${targetPath} -> ${sourcePath}`);
42
+ return true;
43
+ }
44
+ // Create the symbolic link using relative path for better portability
45
+ const relativePath = path__default.relative(path__default.dirname(targetPath), sourcePath);
46
+ // Check if something already exists at the target path
47
+ try {
48
+ const stats = await fs__default.lstat(targetPath); // Use lstat to not follow symlinks
49
+ if (stats.isSymbolicLink()) {
50
+ // It's a symlink, check if it points to the correct target
51
+ const existingLink = await fs__default.readlink(targetPath);
52
+ if (existingLink === relativePath) {
53
+ logger.verbose(`Symlink already exists and points to correct target: ${targetPath} -> ${relativePath}`);
54
+ return true;
55
+ } else {
56
+ logger.info(`🔧 Fixing symlink: ${targetPath} (was pointing to ${existingLink}, now pointing to ${relativePath})`);
57
+ await fs__default.unlink(targetPath);
58
+ await fs__default.symlink(relativePath, targetPath, 'dir');
59
+ logger.info(`✅ Fixed symlink: ${targetPath} -> ${relativePath}`);
60
+ return true;
61
+ }
62
+ } else if (stats.isDirectory()) {
63
+ // It's a directory, remove it
64
+ logger.warn(`⚠️ Removing existing directory to create symlink: ${targetPath}`);
65
+ await fs__default.rm(targetPath, {
66
+ recursive: true,
67
+ force: true
68
+ });
69
+ await fs__default.symlink(relativePath, targetPath, 'dir');
70
+ logger.info(`✅ Created symlink: ${targetPath} -> ${relativePath}`);
71
+ return true;
72
+ } else {
73
+ // It's a file, remove it
74
+ logger.warn(`⚠️ Removing existing file to create symlink: ${targetPath}`);
75
+ await fs__default.unlink(targetPath);
76
+ await fs__default.symlink(relativePath, targetPath, 'dir');
77
+ logger.info(`✅ Created symlink: ${targetPath} -> ${relativePath}`);
78
+ return true;
79
+ }
80
+ } catch (error) {
81
+ if (error.code === 'ENOENT') {
82
+ // Nothing exists at target path, create the symlink
83
+ await fs__default.symlink(relativePath, targetPath, 'dir');
84
+ logger.verbose(`Created symlink: ${targetPath} -> ${relativePath}`);
85
+ return true;
86
+ } else {
87
+ throw error; // Re-throw unexpected errors
88
+ }
38
89
  }
90
+ } catch (error) {
91
+ logger.warn(`Failed to create symlink for ${packageName}: ${error.message}`);
92
+ return false;
39
93
  }
40
- scopeTimer.end(`Scanned ${Object.keys(scopeRoots).length} scope roots, found ${allScopePackages.size} packages`);
41
- // Now we have all scope packages, we can resolve the ones we want to link
42
- for (const [packageName, packagePath] of allScopePackages){
43
- packagesToLink.set(packageName, packagePath);
94
+ };
95
+ // Helper function to parse package names and scopes
96
+ const parsePackageArgument = (packageArg)=>{
97
+ if (packageArg.startsWith('@')) {
98
+ const parts = packageArg.split('/');
99
+ if (parts.length === 1) {
100
+ // Just a scope like "@fjell"
101
+ return {
102
+ scope: parts[0]
103
+ };
104
+ } else {
105
+ // Full package name like "@fjell/core"
106
+ return {
107
+ scope: parts[0],
108
+ packageName: packageArg
109
+ };
110
+ }
111
+ } else {
112
+ throw new Error(`Package argument must start with @ (scope): ${packageArg}`);
44
113
  }
45
- timer.end(`Found ${packagesToLink.size} packages to link`);
46
- return packagesToLink;
47
114
  };
48
- const readLinkBackup = async (storage, logger)=>{
49
- const backupPath = path__default.join(process.cwd(), '.kodrdriv-link-backup.json');
50
- if (await storage.exists(backupPath)) {
115
+ // Find packages in the workspace that match the given scope or package name
116
+ const findMatchingPackages = async (targetDirectories, scope, storage, logger, packageName)=>{
117
+ const matchingPackages = [];
118
+ // Find all package.json files in target directories
119
+ let allPackageJsonFiles = [];
120
+ for (const targetDirectory of targetDirectories){
121
+ const packageJsonFiles = await findAllPackageJsonFiles(targetDirectory, storage);
122
+ allPackageJsonFiles = allPackageJsonFiles.concat(packageJsonFiles);
123
+ }
124
+ for (const packageJsonLocation of allPackageJsonFiles){
125
+ const packageDir = packageJsonLocation.path.replace('/package.json', '');
51
126
  try {
52
- const content = await storage.readFile(backupPath, 'utf-8');
53
- return JSON.parse(content);
54
- } catch (error) {
55
- // Log warning but continue with empty backup instead of throwing
56
- if (logger) {
57
- logger.warn(`Failed to parse link backup file: ${error}`);
127
+ const packageJsonContent = await storage.readFile(packageJsonLocation.path, 'utf-8');
128
+ const parsed = safeJsonParse(packageJsonContent, packageJsonLocation.path);
129
+ const packageJson = validatePackageJson(parsed, packageJsonLocation.path);
130
+ if (!packageJson.name) continue;
131
+ const isInScope = packageJson.name.startsWith(scope + '/');
132
+ const isExactMatch = packageName && packageJson.name === packageName;
133
+ if (isInScope || isExactMatch) {
134
+ matchingPackages.push({
135
+ name: packageJson.name,
136
+ path: packageDir,
137
+ isSource: packageName ? packageJson.name === packageName : isInScope
138
+ });
58
139
  }
59
- return {};
140
+ } catch (error) {
141
+ logger.warn(`Failed to parse ${packageJsonLocation.path}: ${error.message}`);
60
142
  }
61
143
  }
62
- return {};
63
- };
64
- const writeLinkBackup = async (backup, storage)=>{
65
- const backupPath = path__default.join(process.cwd(), '.kodrdriv-link-backup.json');
66
- await storage.writeFile(backupPath, JSON.stringify(backup, null, 2), 'utf-8');
144
+ return matchingPackages;
67
145
  };
68
- const updatePackageJson = async (packageJsonLocation, packagesToLink, backup, storage)=>{
69
- const logger = getLogger();
70
- let linkedCount = 0;
71
- const { packageJson, path: packageJsonPath, relativePath } = packageJsonLocation;
72
- // Process dependencies, devDependencies, and peerDependencies
73
- const depTypes = [
74
- 'dependencies',
75
- 'devDependencies',
76
- 'peerDependencies'
77
- ];
78
- for (const depType of depTypes){
79
- const dependencies = packageJson[depType];
80
- if (!dependencies) continue;
81
- for (const [packageName, targetPath] of packagesToLink){
82
- if (dependencies[packageName]) {
83
- // Backup original version before linking
84
- const backupKey = `${relativePath}:${packageName}`;
85
- if (!backup[backupKey]) {
86
- backup[backupKey] = {
87
- originalVersion: dependencies[packageName],
88
- dependencyType: depType,
89
- relativePath
90
- };
91
- }
92
- // Update to file: dependency
93
- const targetAbsolutePath = path__default.resolve(process.cwd(), targetPath);
94
- const fileReferencePath = path__default.relative(path__default.dirname(packageJsonPath), targetAbsolutePath);
95
- dependencies[packageName] = `file:${fileReferencePath}`;
96
- linkedCount++;
97
- logger.verbose(`Linked ${relativePath}/${depType}.${packageName}: ${backup[backupKey].originalVersion} -> file:${fileReferencePath}`);
146
+ // Find packages that depend on the target package
147
+ const findConsumingPackages = async (targetDirectories, targetPackageName, storage, logger)=>{
148
+ const consumingPackages = [];
149
+ // Find all package.json files in target directories
150
+ let allPackageJsonFiles = [];
151
+ for (const targetDirectory of targetDirectories){
152
+ const packageJsonFiles = await findAllPackageJsonFiles(targetDirectory, storage);
153
+ allPackageJsonFiles = allPackageJsonFiles.concat(packageJsonFiles);
154
+ }
155
+ for (const packageJsonLocation of allPackageJsonFiles){
156
+ const packageDir = packageJsonLocation.path.replace('/package.json', '');
157
+ try {
158
+ const packageJsonContent = await storage.readFile(packageJsonLocation.path, 'utf-8');
159
+ const parsed = safeJsonParse(packageJsonContent, packageJsonLocation.path);
160
+ const packageJson = validatePackageJson(parsed, packageJsonLocation.path);
161
+ if (!packageJson.name) continue;
162
+ // Check if this package depends on the target package
163
+ const dependencyTypes = [
164
+ 'dependencies',
165
+ 'devDependencies',
166
+ 'peerDependencies',
167
+ 'optionalDependencies'
168
+ ];
169
+ const hasDependency = dependencyTypes.some((depType)=>packageJson[depType] && packageJson[depType][targetPackageName]);
170
+ if (hasDependency && packageJson.name !== targetPackageName) {
171
+ consumingPackages.push({
172
+ name: packageJson.name,
173
+ path: packageDir
174
+ });
98
175
  }
176
+ } catch (error) {
177
+ logger.warn(`Failed to parse ${packageJsonLocation.path}: ${error.message}`);
99
178
  }
100
179
  }
101
- // NOTE: Don't write the file here - let the caller handle all modifications
102
- return linkedCount;
180
+ return consumingPackages;
103
181
  };
104
- const executeInternal = async (runConfig)=>{
105
- var _runConfig_link, _runConfig_link1;
182
+ const executeInternal = async (runConfig, packageArgument)=>{
183
+ var _runConfig_link, _runConfig_tree;
106
184
  const isDryRun = runConfig.dryRun || ((_runConfig_link = runConfig.link) === null || _runConfig_link === void 0 ? void 0 : _runConfig_link.dryRun) || false;
107
185
  const logger = getDryRunLogger(isDryRun);
108
- const overallTimer = PerformanceTimer.start(logger, 'Link command execution');
109
186
  const storage = create({
110
187
  log: logger.info
111
188
  });
112
- logger.info('🔗 Linking workspace packages...');
113
- // Get configuration
114
- const configTimer = PerformanceTimer.start(logger, 'Reading configuration');
115
- const scopeRoots = ((_runConfig_link1 = runConfig.link) === null || _runConfig_link1 === void 0 ? void 0 : _runConfig_link1.scopeRoots) || {};
116
- configTimer.end('Configuration loaded');
117
- if (Object.keys(scopeRoots).length === 0) {
118
- logger.info('No scope roots configured. Skipping link management.');
119
- overallTimer.end('Link command (no scope roots)');
120
- return 'No scope roots configured. Skipping link management.';
121
- }
122
- // Find all package.json files in current directory tree
123
- const packageJsonFiles = await findAllPackageJsonFiles(process.cwd(), storage);
124
- if (packageJsonFiles.length === 0) {
125
- overallTimer.end('Link command (no package.json files)');
126
- throw new ValidationError('No package.json files found in current directory or subdirectories.');
127
- }
128
- logger.info(`Found ${packageJsonFiles.length} package.json file(s) to process`);
129
- logger.info(`Scanning ${Object.keys(scopeRoots).length} scope root(s): ${Object.keys(scopeRoots).join(', ')}`);
130
- // Check if any package.json files already have file: dependencies (safety check)
131
- const safetyTimer = PerformanceTimer.start(logger, 'Safety check for existing file: dependencies');
132
- // checkForFileDependencies(packageJsonFiles); // This function is no longer imported
133
- safetyTimer.end('Safety check completed');
134
- // Collect all dependencies from all package.json files using optimized function
135
- // const allDependencies = collectAllDependencies(packageJsonFiles); // This function is no longer imported
136
- // logger.verbose(`Found ${Object.keys(allDependencies).length} total unique dependencies across all package.json files`);
137
- // Find matching sibling packages
138
- const packagesToLink = await findPackagesToLink(scopeRoots, storage);
139
- if (packagesToLink.size === 0) {
140
- logger.info('✅ No matching sibling packages found for linking.');
141
- overallTimer.end('Link command (no packages to link)');
142
- return 'No matching sibling packages found for linking.';
189
+ // Get target directories from config, default to current directory
190
+ const targetDirectories = ((_runConfig_tree = runConfig.tree) === null || _runConfig_tree === void 0 ? void 0 : _runConfig_tree.directories) || [
191
+ process.cwd()
192
+ ];
193
+ if (targetDirectories.length === 1) {
194
+ logger.info(`Analyzing workspace at: ${targetDirectories[0]}`);
195
+ } else {
196
+ logger.info(`Analyzing workspaces at: ${targetDirectories.join(', ')}`);
143
197
  }
144
- logger.info(`Found ${packagesToLink.size} package(s) to link: ${[
145
- ...packagesToLink.keys()
146
- ].join(', ')}`);
147
- // Read existing backup
148
- const backupTimer = PerformanceTimer.start(logger, 'Reading link backup');
149
- const backup = await readLinkBackup(storage, logger);
150
- backupTimer.end('Link backup loaded');
151
- if (isDryRun) {
152
- logger.info('Would update package.json files with file: dependencies and run npm install');
153
- for (const { relativePath } of packageJsonFiles){
154
- logger.verbose(`Would process ${relativePath}/package.json`);
198
+ // If no package argument provided, use new smart same-scope linking behavior
199
+ if (!packageArgument) {
200
+ logger.info('🔗 Smart linking current project...');
201
+ // Work in current directory only - read the package.json
202
+ const currentDir = process.cwd();
203
+ const packageJsonPath = `${currentDir}/package.json`;
204
+ let currentPackageJson;
205
+ try {
206
+ const packageJsonContent = await storage.readFile(packageJsonPath, 'utf-8');
207
+ const parsed = safeJsonParse(packageJsonContent, packageJsonPath);
208
+ currentPackageJson = validatePackageJson(parsed, packageJsonPath);
209
+ } catch (error) {
210
+ const message = `No valid package.json found in current directory: ${error.message}`;
211
+ logger.error(message);
212
+ return message;
155
213
  }
156
- for (const [packageName, packagePath] of packagesToLink.entries()){
157
- logger.verbose(`Would link ${packageName} -> file:${packagePath}`);
214
+ if (!currentPackageJson.name) {
215
+ const message = 'package.json must have a name field';
216
+ logger.error(message);
217
+ return message;
158
218
  }
159
- overallTimer.end('Link command (dry run)');
160
- return `DRY RUN: Would link ${packagesToLink.size} packages across ${packageJsonFiles.length} package.json files`;
161
- } else {
162
- // Update all package.json files with file: dependencies
163
- const updateTimer = PerformanceTimer.start(logger, 'Updating package.json files');
164
- let totalLinksCreated = 0;
165
- for (const packageJsonLocation of packageJsonFiles){
166
- const linksCreated = await updatePackageJson(packageJsonLocation, packagesToLink, backup);
167
- totalLinksCreated += linksCreated;
168
- // Write the modified package.json file to disk
169
- if (linksCreated > 0) {
170
- await storage.writeFile(packageJsonLocation.path, JSON.stringify(packageJsonLocation.packageJson, null, 2) + '\n', 'utf-8');
171
- logger.verbose(`Updated ${packageJsonLocation.relativePath}/package.json with ${linksCreated} file: dependencies`);
219
+ // Extract the scope from the current package name
220
+ const currentScope = currentPackageJson.name.startsWith('@') ? currentPackageJson.name.split('/')[0] : null;
221
+ if (!currentScope) {
222
+ const message = 'Current package must have a scoped name (e.g., @scope/package) for smart linking';
223
+ logger.warn(message);
224
+ return message;
225
+ }
226
+ logger.info(`Current package: ${currentPackageJson.name} (scope: ${currentScope})`);
227
+ // Step 1: Link the current package globally
228
+ try {
229
+ if (isDryRun) {
230
+ logger.info(`DRY RUN: Would run 'npm link' in current directory`);
231
+ } else {
232
+ logger.verbose(`Running 'npm link' to register ${currentPackageJson.name} globally...`);
233
+ await run('npm link');
234
+ logger.info(`✅ Self-linked: ${currentPackageJson.name}`);
172
235
  }
236
+ } catch (error) {
237
+ logger.error(`❌ Failed to self-link ${currentPackageJson.name}: ${error.message}`);
238
+ throw new Error(`Failed to self-link ${currentPackageJson.name}: ${error.message}`);
173
239
  }
174
- updateTimer.end(`Updated ${packageJsonFiles.length} package.json files, created ${totalLinksCreated} links`);
175
- if (totalLinksCreated === 0) {
176
- logger.info('✅ No dependencies were linked (packages may not be referenced).');
177
- overallTimer.end('Link command (no links created)');
178
- return 'No dependencies were linked.';
240
+ // Step 2: Find same-scope dependencies in current package
241
+ const allDependencies = {
242
+ ...currentPackageJson.dependencies,
243
+ ...currentPackageJson.devDependencies
244
+ };
245
+ const sameScopeDependencies = Object.keys(allDependencies).filter((depName)=>depName.startsWith(currentScope + '/'));
246
+ if (sameScopeDependencies.length === 0) {
247
+ logger.info(`No same-scope dependencies found for ${currentScope}`);
248
+ if (isDryRun) {
249
+ return `DRY RUN: Would self-link, no same-scope dependencies found to link`;
250
+ } else {
251
+ return `Self-linked ${currentPackageJson.name}, no same-scope dependencies to link`;
252
+ }
179
253
  }
180
- // Save backup after all changes
181
- const saveTimer = PerformanceTimer.start(logger, 'Saving link backup');
182
- await writeLinkBackup(backup, storage);
183
- saveTimer.end('Link backup saved');
184
- logger.info(`Updated ${packageJsonFiles.length} package.json file(s) with file: dependencies`);
185
- // Run optimized npm install to create symlinks
186
- logger.info('⏳ Installing dependencies to create symlinks...');
254
+ // Step 3: Get globally linked packages directories (only if we have same-scope dependencies)
255
+ let globallyLinkedPackages = {};
187
256
  try {
188
- const installResult = await smartNpmInstall({
189
- skipIfNotNeeded: false,
190
- preferCi: false,
191
- verbose: false
192
- });
193
- if (installResult.skipped) {
194
- logger.info(`⚡ Dependencies were up to date (${installResult.method})`);
257
+ if (isDryRun) {
258
+ logger.info(`DRY RUN: Would run 'npm ls --link -g -p' to discover linked package directories`);
259
+ logger.info(`DRY RUN: Would attempt to link same-scope dependencies: ${sameScopeDependencies.join(', ')}`);
260
+ return `DRY RUN: Would self-link and attempt to link ${sameScopeDependencies.length} same-scope dependencies`;
195
261
  } else {
196
- logger.info(`✅ Links applied successfully using ${installResult.method} (${installResult.duration}ms)`);
262
+ logger.verbose(`Discovering globally linked package directories...`);
263
+ const result = await run('npm ls --link -g -p');
264
+ const resultStr = typeof result === 'string' ? result : result.stdout;
265
+ // Parse the directory paths - each line is a directory path
266
+ const directoryPaths = resultStr.trim().split('\n').filter((line)=>line.trim() !== '');
267
+ // Extract package names from directory paths and build a map
268
+ for (const dirPath of directoryPaths){
269
+ try {
270
+ // Read the package.json to get the actual package name
271
+ const packageJsonPath = `${dirPath.trim()}/package.json`;
272
+ const packageJsonContent = await storage.readFile(packageJsonPath, 'utf-8');
273
+ const parsed = safeJsonParse(packageJsonContent, packageJsonPath);
274
+ const packageJson = validatePackageJson(parsed, packageJsonPath);
275
+ if (packageJson.name) {
276
+ globallyLinkedPackages[packageJson.name] = dirPath.trim();
277
+ }
278
+ } catch (packageError) {
279
+ logger.verbose(`Could not read package.json from ${dirPath}: ${packageError.message}`);
280
+ }
281
+ }
282
+ const linkedCount = Object.keys(globallyLinkedPackages).length;
283
+ logger.verbose(`Found ${linkedCount} globally linked package(s)`);
197
284
  }
198
285
  } catch (error) {
199
- logger.warn(`Failed to install dependencies: ${error}. You may need to run 'npm install' manually.`);
286
+ logger.warn(`Failed to get globally linked packages (continuing anyway): ${error.message}`);
287
+ globallyLinkedPackages = {};
288
+ }
289
+ logger.info(`Found ${sameScopeDependencies.length} same-scope dependencies: ${sameScopeDependencies.join(', ')}`);
290
+ // Step 4: Link same-scope dependencies that are available globally using manual symlinks
291
+ const linkedDependencies = [];
292
+ for (const depName of sameScopeDependencies){
293
+ const sourcePath = globallyLinkedPackages[depName];
294
+ if (sourcePath) {
295
+ try {
296
+ logger.verbose(`Linking same-scope dependency: ${depName} from ${sourcePath}`);
297
+ // Create the symbolic link manually using the directory path directly
298
+ const success = await createSymbolicLink(depName, sourcePath, currentDir, logger, isDryRun);
299
+ if (success) {
300
+ logger.info(`✅ Linked dependency: ${depName}`);
301
+ linkedDependencies.push(depName);
302
+ } else {
303
+ logger.warn(`⚠️ Failed to link ${depName}`);
304
+ }
305
+ } catch (error) {
306
+ logger.warn(`⚠️ Failed to link ${depName}: ${error.message}`);
307
+ }
308
+ } else {
309
+ logger.verbose(`Skipping ${depName} (not globally linked)`);
310
+ }
200
311
  }
201
- const summary = `Successfully linked ${totalLinksCreated} dependency reference(s) across ${packageJsonFiles.length} package.json file(s):\n${[
202
- ...packagesToLink.entries()
203
- ].map(([name, path])=>` - ${name}: file:${path}`).join('\n')}`;
204
- overallTimer.end('Link command execution completed');
312
+ const summary = linkedDependencies.length > 0 ? `Self-linked ${currentPackageJson.name} and linked ${linkedDependencies.length} same-scope dependencies: ${linkedDependencies.join(', ')}` : `Self-linked ${currentPackageJson.name}, no same-scope dependencies were available to link`;
313
+ logger.info(summary);
205
314
  return summary;
206
315
  }
316
+ // New scope-based linking behavior
317
+ logger.info(`🔗 Linking scope/package: ${packageArgument}`);
318
+ const { scope, packageName } = parsePackageArgument(packageArgument);
319
+ logger.verbose(`Parsed scope: ${scope}, package: ${packageName || 'all packages in scope'}`);
320
+ // Find matching packages in the workspace
321
+ const matchingPackages = await findMatchingPackages(targetDirectories, scope, storage, logger, packageName);
322
+ if (matchingPackages.length === 0) {
323
+ const message = packageName ? `No package found matching: ${packageName}` : `No packages found in scope: ${scope}`;
324
+ logger.warn(message);
325
+ return message;
326
+ }
327
+ logger.info(`Found ${matchingPackages.length} matching package(s)`);
328
+ const linkedPackages = [];
329
+ // If specific package name provided, use that; otherwise link all packages in scope
330
+ const packagesToLink = packageName ? matchingPackages.filter((pkg)=>pkg.name === packageName) : matchingPackages;
331
+ for (const pkg of packagesToLink){
332
+ logger.info(`Processing package: ${pkg.name}`);
333
+ // Step A: Run 'npm link' in the source package directory
334
+ try {
335
+ const originalCwd = process.cwd();
336
+ process.chdir(pkg.path);
337
+ try {
338
+ if (isDryRun) {
339
+ logger.info(`DRY RUN: Would run 'npm link' in: ${pkg.path}`);
340
+ } else {
341
+ logger.verbose(`Running 'npm link' in source: ${pkg.path}`);
342
+ await run('npm link');
343
+ logger.info(`✅ Source linked: ${pkg.name}`);
344
+ }
345
+ } finally{
346
+ process.chdir(originalCwd);
347
+ }
348
+ // Step B: Find all packages that depend on this package and link them
349
+ const consumingPackages = await findConsumingPackages(targetDirectories, pkg.name, storage, logger);
350
+ if (consumingPackages.length === 0) {
351
+ logger.info(`No consuming packages found for: ${pkg.name}`);
352
+ } else {
353
+ logger.info(`Found ${consumingPackages.length} consuming package(s) for: ${pkg.name}`);
354
+ for (const consumer of consumingPackages){
355
+ try {
356
+ const consumerOriginalCwd = process.cwd();
357
+ process.chdir(consumer.path);
358
+ try {
359
+ if (isDryRun) {
360
+ logger.info(`DRY RUN: Would run 'npm link ${pkg.name}' in: ${consumer.path}`);
361
+ } else {
362
+ logger.verbose(`Running 'npm link ${pkg.name}' in consumer: ${consumer.path}`);
363
+ await runSecure('npm', [
364
+ 'link',
365
+ pkg.name
366
+ ]);
367
+ logger.info(`✅ Consumer linked: ${consumer.name} -> ${pkg.name}`);
368
+ }
369
+ } finally{
370
+ process.chdir(consumerOriginalCwd);
371
+ }
372
+ } catch (error) {
373
+ logger.error(`❌ Failed to link ${pkg.name} in ${consumer.name}: ${error.message}`);
374
+ throw new Error(`Failed to link ${pkg.name} in consumer ${consumer.name}: ${error.message}`);
375
+ }
376
+ }
377
+ }
378
+ linkedPackages.push(pkg.name);
379
+ } catch (error) {
380
+ logger.error(`❌ Failed to link source package ${pkg.name}: ${error.message}`);
381
+ throw new Error(`Failed to link source package ${pkg.name}: ${error.message}`);
382
+ }
383
+ }
384
+ const summary = `Successfully linked ${linkedPackages.length} package(s): ${linkedPackages.join(', ')}`;
385
+ logger.info(summary);
386
+ return summary;
207
387
  };
208
- const execute = async (runConfig)=>{
388
+ const execute = async (runConfig, packageArgument)=>{
209
389
  try {
210
- return await executeInternal(runConfig);
390
+ var _runConfig_link;
391
+ // Use packageArgument from runConfig if not provided as parameter
392
+ const finalPackageArgument = packageArgument || ((_runConfig_link = runConfig.link) === null || _runConfig_link === void 0 ? void 0 : _runConfig_link.packageArgument);
393
+ return await executeInternal(runConfig, finalPackageArgument);
211
394
  } catch (error) {
212
395
  const logger = getLogger();
213
- if (error instanceof ValidationError || error instanceof CommandError) {
214
- logger.error(`link failed: ${error.message}`);
215
- if (error.cause) {
216
- logger.debug(`Caused by: ${error.cause.message}`);
217
- }
218
- throw error;
219
- }
220
- // Unexpected errors
221
- logger.error(`link encountered unexpected error: ${error.message}`);
396
+ logger.error(`link failed: ${error.message}`);
222
397
  throw error;
223
398
  }
224
399
  };