opkg 0.5.0 → 0.6.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 (272) hide show
  1. package/README.md +49 -8
  2. package/dist/commands/add.js +11 -276
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/init.js +73 -145
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/install.js +26 -668
  7. package/dist/commands/install.js.map +1 -1
  8. package/dist/commands/pack.js +10 -137
  9. package/dist/commands/pack.js.map +1 -1
  10. package/dist/commands/push.js +14 -8
  11. package/dist/commands/push.js.map +1 -1
  12. package/dist/commands/save.js +12 -167
  13. package/dist/commands/save.js.map +1 -1
  14. package/dist/commands/status.js +2 -2
  15. package/dist/commands/status.js.map +1 -1
  16. package/dist/commands/uninstall.js +5 -5
  17. package/dist/commands/uninstall.js.map +1 -1
  18. package/dist/constants/index.js +18 -45
  19. package/dist/constants/index.js.map +1 -1
  20. package/dist/constants/workspace.js +9 -0
  21. package/dist/constants/workspace.js.map +1 -0
  22. package/dist/core/add/add-conflict-handler.js +68 -0
  23. package/dist/core/add/add-conflict-handler.js.map +1 -0
  24. package/dist/core/add/add-pipeline.js +137 -0
  25. package/dist/core/add/add-pipeline.js.map +1 -0
  26. package/dist/core/add/package-index-updater.js +62 -33
  27. package/dist/core/add/package-index-updater.js.map +1 -1
  28. package/dist/core/add/platform-path-transformer.js +47 -0
  29. package/dist/core/add/platform-path-transformer.js.map +1 -0
  30. package/dist/core/add/source-collector.js +57 -0
  31. package/dist/core/add/source-collector.js.map +1 -0
  32. package/dist/core/dependency-resolver.js +3 -1
  33. package/dist/core/dependency-resolver.js.map +1 -1
  34. package/dist/core/directory.js +2 -2
  35. package/dist/core/directory.js.map +1 -1
  36. package/dist/core/discovery/file-discovery.js +55 -54
  37. package/dist/core/discovery/file-discovery.js.map +1 -1
  38. package/dist/core/discovery/platform-files-discovery.js +32 -17
  39. package/dist/core/discovery/platform-files-discovery.js.map +1 -1
  40. package/dist/core/install/bulk-install-pipeline.js +199 -0
  41. package/dist/core/install/bulk-install-pipeline.js.map +1 -0
  42. package/dist/core/install/canonical-plan.js +123 -0
  43. package/dist/core/install/canonical-plan.js.map +1 -0
  44. package/dist/core/install/dry-run.js +2 -2
  45. package/dist/core/install/dry-run.js.map +1 -1
  46. package/dist/core/install/index.js +3 -0
  47. package/dist/core/install/index.js.map +1 -0
  48. package/dist/core/install/install-errors.js +41 -0
  49. package/dist/core/install/install-errors.js.map +1 -0
  50. package/dist/core/install/install-flow.js +2 -5
  51. package/dist/core/install/install-flow.js.map +1 -1
  52. package/dist/core/install/install-pipeline.js +228 -0
  53. package/dist/core/install/install-pipeline.js.map +1 -0
  54. package/dist/core/install/install-reporting.js +99 -0
  55. package/dist/core/install/install-reporting.js.map +1 -0
  56. package/dist/core/install/platform-resolution.js +6 -6
  57. package/dist/core/install/platform-resolution.js.map +1 -1
  58. package/dist/core/install/remote-flow.js +67 -1
  59. package/dist/core/install/remote-flow.js.map +1 -1
  60. package/dist/core/openpackage.js +16 -8
  61. package/dist/core/openpackage.js.map +1 -1
  62. package/dist/core/package-context.js +246 -0
  63. package/dist/core/package-context.js.map +1 -0
  64. package/dist/core/package.js +3 -2
  65. package/dist/core/package.js.map +1 -1
  66. package/dist/core/platforms.js +126 -217
  67. package/dist/core/platforms.js.map +1 -1
  68. package/dist/core/registry/registry-rename.js +2 -1
  69. package/dist/core/registry/registry-rename.js.map +1 -1
  70. package/dist/core/registry.js +10 -3
  71. package/dist/core/registry.js.map +1 -1
  72. package/dist/core/remote-pull.js +2 -1
  73. package/dist/core/remote-pull.js.map +1 -1
  74. package/dist/core/save/constants.js +4 -0
  75. package/dist/core/save/constants.js.map +1 -1
  76. package/dist/core/save/name-resolution.js +31 -0
  77. package/dist/core/save/name-resolution.js.map +1 -0
  78. package/dist/core/save/package-detection.js +147 -0
  79. package/dist/core/save/package-detection.js.map +1 -0
  80. package/dist/core/save/package-saver.js +46 -43
  81. package/dist/core/save/package-saver.js.map +1 -1
  82. package/dist/core/save/package-yml-generator.js +50 -71
  83. package/dist/core/save/package-yml-generator.js.map +1 -1
  84. package/dist/core/save/root-save-candidates.js.map +1 -1
  85. package/dist/core/save/save-candidate-loader.js +89 -0
  86. package/dist/core/save/save-candidate-loader.js.map +1 -0
  87. package/dist/core/save/save-conflict-resolution.js +72 -410
  88. package/dist/core/save/save-conflict-resolution.js.map +1 -1
  89. package/dist/core/save/save-conflict-resolver.js +277 -0
  90. package/dist/core/save/save-conflict-resolver.js.map +1 -0
  91. package/dist/core/save/save-pipeline.js +151 -0
  92. package/dist/core/save/save-pipeline.js.map +1 -0
  93. package/dist/core/save/save-types.js +2 -0
  94. package/dist/core/save/save-types.js.map +1 -0
  95. package/dist/core/save/save-yml-resolution.js +77 -39
  96. package/dist/core/save/save-yml-resolution.js.map +1 -1
  97. package/dist/core/save/workspace-rename.js +6 -6
  98. package/dist/core/save/workspace-rename.js.map +1 -1
  99. package/dist/core/scoping/package-scoping.js +13 -1
  100. package/dist/core/scoping/package-scoping.js.map +1 -1
  101. package/dist/core/status/status-file-discovery.js +12 -30
  102. package/dist/core/status/status-file-discovery.js.map +1 -1
  103. package/dist/core/sync/platform-sync.js +7 -4
  104. package/dist/core/sync/platform-sync.js.map +1 -1
  105. package/dist/core/sync/root-files-sync.js +59 -1
  106. package/dist/core/sync/root-files-sync.js.map +1 -1
  107. package/dist/core/uninstall/uninstall-file-discovery.js +1 -2
  108. package/dist/core/uninstall/uninstall-file-discovery.js.map +1 -1
  109. package/dist/types/index.js.map +1 -1
  110. package/dist/utils/file-processing.js +5 -58
  111. package/dist/utils/file-processing.js.map +1 -1
  112. package/dist/utils/index-based-installer.js +15 -33
  113. package/dist/utils/index-based-installer.js.map +1 -1
  114. package/dist/utils/install-file-discovery.js +3 -3
  115. package/dist/utils/install-file-discovery.js.map +1 -1
  116. package/dist/utils/install-orchestrator.js +21 -21
  117. package/dist/utils/install-orchestrator.js.map +1 -1
  118. package/dist/utils/jsonc.js +44 -0
  119. package/dist/utils/jsonc.js.map +1 -0
  120. package/dist/utils/package-copy.js +199 -0
  121. package/dist/utils/package-copy.js.map +1 -0
  122. package/dist/utils/package-filters.js +125 -0
  123. package/dist/utils/package-filters.js.map +1 -0
  124. package/dist/utils/package-index-yml.js +15 -10
  125. package/dist/utils/package-index-yml.js.map +1 -1
  126. package/dist/utils/package-installation.js +4 -113
  127. package/dist/utils/package-installation.js.map +1 -1
  128. package/dist/utils/package-local-files.js +2 -35
  129. package/dist/utils/package-local-files.js.map +1 -1
  130. package/dist/utils/package-management.js +59 -37
  131. package/dist/utils/package-management.js.map +1 -1
  132. package/dist/utils/package-yml.js +24 -0
  133. package/dist/utils/package-yml.js.map +1 -1
  134. package/dist/utils/path-normalization.js +8 -53
  135. package/dist/utils/path-normalization.js.map +1 -1
  136. package/dist/utils/paths.js +17 -9
  137. package/dist/utils/paths.js.map +1 -1
  138. package/dist/utils/platform-file.js +16 -59
  139. package/dist/utils/platform-file.js.map +1 -1
  140. package/dist/utils/platform-mapper.js +29 -41
  141. package/dist/utils/platform-mapper.js.map +1 -1
  142. package/dist/utils/platform-specific-paths.js.map +1 -1
  143. package/dist/utils/platform-utils.js +28 -139
  144. package/dist/utils/platform-utils.js.map +1 -1
  145. package/dist/utils/platform-yaml-merge.js +13 -6
  146. package/dist/utils/platform-yaml-merge.js.map +1 -1
  147. package/dist/utils/registry-entry-filter.js +38 -24
  148. package/dist/utils/registry-entry-filter.js.map +1 -1
  149. package/dist/utils/registry-paths.js +10 -0
  150. package/dist/utils/registry-paths.js.map +1 -0
  151. package/dist/utils/root-file-installer.js +19 -0
  152. package/dist/utils/root-file-installer.js.map +1 -1
  153. package/dist/utils/root-file-registry.js.map +1 -1
  154. package/package.json +3 -2
  155. package/platforms.jsonc +178 -0
  156. package/specs/package/README.md +60 -0
  157. package/specs/package/nested-packages-and-parent-packages.md +79 -0
  158. package/specs/package/package-index-yml.md +171 -0
  159. package/specs/package/package-root-layout.md +78 -0
  160. package/specs/package/registry-payload-and-copy.md +77 -0
  161. package/specs/package/universal-content.md +144 -0
  162. package/specs/platforms.md +193 -0
  163. package/specs/save/README.md +40 -0
  164. package/specs/save/save-conflict-resolution.md +146 -0
  165. package/specs/save/save-file-discovery.md +101 -0
  166. package/specs/save/save-frontmatter-overrides.md +81 -0
  167. package/specs/save/save-modes-inputs.md +53 -0
  168. package/specs/save/save-naming-scoping.md +93 -0
  169. package/specs/save/save-package-detection.md +60 -0
  170. package/specs/save/save-registry-sync.md +126 -0
  171. package/dist/commands/release.js +0 -33
  172. package/dist/commands/release.js.map +0 -1
  173. package/dist/commands/tag.js +0 -311
  174. package/dist/commands/tag.js.map +0 -1
  175. package/dist/commands/update.js +0 -30
  176. package/dist/commands/update.js.map +0 -1
  177. package/dist/core/add/formula-index-updater.js +0 -290
  178. package/dist/core/add/formula-index-updater.js.map +0 -1
  179. package/dist/core/discovery/ai-files-discovery.js +0 -2
  180. package/dist/core/discovery/ai-files-discovery.js.map +0 -1
  181. package/dist/core/discovery/formula-files-discovery.js +0 -14
  182. package/dist/core/discovery/formula-files-discovery.js.map +0 -1
  183. package/dist/core/discovery/index-files-discovery.js +0 -91
  184. package/dist/core/discovery/index-files-discovery.js.map +0 -1
  185. package/dist/core/discovery/md-files-discovery.js +0 -82
  186. package/dist/core/discovery/md-files-discovery.js.map +0 -1
  187. package/dist/core/discovery/package-files-discovery.js +0 -14
  188. package/dist/core/discovery/package-files-discovery.js.map +0 -1
  189. package/dist/core/discovery/platform-discovery.js +0 -84
  190. package/dist/core/discovery/platform-discovery.js.map +0 -1
  191. package/dist/core/discovery/root-files-discovery.js +0 -2
  192. package/dist/core/discovery/root-files-discovery.js.map +0 -1
  193. package/dist/core/formula.js +0 -170
  194. package/dist/core/formula.js.map +0 -1
  195. package/dist/core/git-registry.js +0 -46
  196. package/dist/core/git-registry.js.map +0 -1
  197. package/dist/core/groundzero.js +0 -277
  198. package/dist/core/groundzero.js.map +0 -1
  199. package/dist/core/install/scenario.js +0 -11
  200. package/dist/core/install/scenario.js.map +0 -1
  201. package/dist/core/package-sync.js +0 -219
  202. package/dist/core/package-sync.js.map +0 -1
  203. package/dist/core/save/formula-file-generator.js +0 -167
  204. package/dist/core/save/formula-file-generator.js.map +0 -1
  205. package/dist/core/save/formula-saver.js +0 -52
  206. package/dist/core/save/formula-saver.js.map +0 -1
  207. package/dist/core/save/formula-yml-generator.js +0 -89
  208. package/dist/core/save/formula-yml-generator.js.map +0 -1
  209. package/dist/core/save/formula-yml-versioning.js +0 -108
  210. package/dist/core/save/formula-yml-versioning.js.map +0 -1
  211. package/dist/core/save/generic-file-sync.js +0 -38
  212. package/dist/core/save/generic-file-sync.js.map +0 -1
  213. package/dist/core/save/md-files-sync.js +0 -33
  214. package/dist/core/save/md-files-sync.js.map +0 -1
  215. package/dist/core/save/package-yml-versioning.js +0 -108
  216. package/dist/core/save/package-yml-versioning.js.map +0 -1
  217. package/dist/core/save/platform-sync.js +0 -95
  218. package/dist/core/save/platform-sync.js.map +0 -1
  219. package/dist/core/save/root-files-sync.js +0 -140
  220. package/dist/core/save/root-files-sync.js.map +0 -1
  221. package/dist/core/save/save-candidate-types.js +0 -2
  222. package/dist/core/save/save-candidate-types.js.map +0 -1
  223. package/dist/core/save/save-conflict-types.js +0 -2
  224. package/dist/core/save/save-conflict-types.js.map +0 -1
  225. package/dist/core/save/save-file-discovery.js +0 -5
  226. package/dist/core/save/save-file-discovery.js.map +0 -1
  227. package/dist/core/status-file-discovery.js +0 -175
  228. package/dist/core/status-file-discovery.js.map +0 -1
  229. package/dist/utils/discovery/file-processing.js +0 -156
  230. package/dist/utils/discovery/file-processing.js.map +0 -1
  231. package/dist/utils/discovery/formula-discovery.js +0 -211
  232. package/dist/utils/discovery/formula-discovery.js.map +0 -1
  233. package/dist/utils/discovery/platform-discovery.js +0 -2
  234. package/dist/utils/discovery/platform-discovery.js.map +0 -1
  235. package/dist/utils/formula-discovery.js +0 -102
  236. package/dist/utils/formula-discovery.js.map +0 -1
  237. package/dist/utils/formula-index-yml.js +0 -122
  238. package/dist/utils/formula-index-yml.js.map +0 -1
  239. package/dist/utils/formula-installation.js +0 -110
  240. package/dist/utils/formula-installation.js.map +0 -1
  241. package/dist/utils/formula-local-files.js +0 -38
  242. package/dist/utils/formula-local-files.js.map +0 -1
  243. package/dist/utils/formula-management.js +0 -191
  244. package/dist/utils/formula-management.js.map +0 -1
  245. package/dist/utils/formula-name.js +0 -97
  246. package/dist/utils/formula-name.js.map +0 -1
  247. package/dist/utils/formula-versioning.js +0 -109
  248. package/dist/utils/formula-versioning.js.map +0 -1
  249. package/dist/utils/formula-yml.js +0 -82
  250. package/dist/utils/formula-yml.js.map +0 -1
  251. package/dist/utils/git.js +0 -54
  252. package/dist/utils/git.js.map +0 -1
  253. package/dist/utils/id-based-discovery.js +0 -126
  254. package/dist/utils/id-based-discovery.js.map +0 -1
  255. package/dist/utils/id-based-installer.js +0 -249
  256. package/dist/utils/id-based-installer.js.map +0 -1
  257. package/dist/utils/index-yml-based-installer.js +0 -375
  258. package/dist/utils/index-yml-based-installer.js.map +0 -1
  259. package/dist/utils/index-yml.js +0 -124
  260. package/dist/utils/index-yml.js.map +0 -1
  261. package/dist/utils/md-frontmatter.js +0 -3
  262. package/dist/utils/md-frontmatter.js.map +0 -1
  263. package/dist/utils/package-link-yml.js +0 -92
  264. package/dist/utils/package-link-yml.js.map +0 -1
  265. package/dist/utils/platform-discovery.js +0 -2
  266. package/dist/utils/platform-discovery.js.map +0 -1
  267. package/dist/utils/platform-frontmatter-split.js +0 -15
  268. package/dist/utils/platform-frontmatter-split.js.map +0 -1
  269. package/dist/utils/timestamp-encoder.js +0 -13
  270. package/dist/utils/timestamp-encoder.js.map +0 -1
  271. package/dist/utils/wip-versioning.js +0 -24
  272. package/dist/utils/wip-versioning.js.map +0 -1
@@ -1,688 +1,48 @@
1
- import * as semver from 'semver';
2
- import { ensureRegistryDirectories, listPackageVersions } from '../core/directory.js';
3
- import { displayDependencyTree } from '../core/dependency-resolver.js';
4
- import { exists } from '../utils/fs.js';
5
- import { logger } from '../utils/logger.js';
6
- import { withErrorHandling, UserCancellationError, PackageNotFoundError } from '../utils/errors.js';
7
- import { createPlatformDirectories } from '../core/platforms.js';
1
+ import { DIR_PATTERNS, PACKAGE_PATHS } from '../constants/index.js';
2
+ import { runBulkInstallPipeline } from '../core/install/bulk-install-pipeline.js';
3
+ import { runInstallPipeline, determineResolutionMode } from '../core/install/install-pipeline.js';
4
+ import { withErrorHandling } from '../utils/errors.js';
8
5
  import { normalizePlatforms } from '../utils/platform-mapper.js';
9
- import { resolvePlatforms } from '../core/install/platform-resolution.js';
10
- import { prepareInstallEnvironment, resolveDependenciesForInstall, processConflictResolution, performIndexBasedInstallationPhases, VersionResolutionAbortError } from '../core/install/install-flow.js';
11
- import { getLocalPackageYmlPath, getAIDir, isRootPackage } from '../utils/paths.js';
12
- import { createBasicPackageYml, addPackageToYml, writeLocalPackageFromRegistry } from '../utils/package-management.js';
13
- import { displayInstallationSummary, displayInstallationResults, } from '../utils/package-installation.js';
14
- import { planConflictsForPackage } from '../utils/index-based-installer.js';
15
- import { withOperationErrorHandling, } from '../utils/error-handling.js';
16
- import { extractPackagesFromConfig } from '../utils/install-helpers.js';
17
- import { parsePackageYml } from '../utils/package-yml.js';
18
- import { parsePackageInput, arePackageNamesEquivalent } from '../utils/package-name.js';
19
- import { safePrompts } from '../utils/prompts.js';
20
- import { createCaretRange, isExactVersion, parseVersionRange, resolveVersionRange } from '../utils/version-ranges.js';
21
- import { aggregateRecursiveDownloads } from '../core/remote-pull.js';
22
- import { computeMissingDownloadKeys } from '../core/install/download-keys.js';
23
- import { fetchMissingDependencyMetadata, pullMissingDependencies } from '../core/install/remote-flow.js';
24
- import { recordBatchOutcome } from '../core/install/remote-reporting.js';
25
- import { handleDryRunMode } from '../core/install/dry-run.js';
26
- import { extractRemoteErrorReason } from '../utils/error-reasons.js';
27
- import { selectInstallVersionUnified } from '../core/install/version-selection.js';
28
- export function determineResolutionMode(options) {
29
- if (options.resolutionMode) {
30
- return options.resolutionMode;
31
- }
32
- if (options.remote) {
33
- return 'remote-primary';
6
+ import { parsePackageInput } from '../utils/package-name.js';
7
+ import { logger } from '../utils/logger.js';
8
+ import { normalizePathForProcessing } from '../utils/path-normalization.js';
9
+ function assertTargetDirOutsideMetadata(targetDir) {
10
+ const normalized = normalizePathForProcessing(targetDir ?? '.');
11
+ if (!normalized || normalized === '.') {
12
+ return; // default install root
34
13
  }
35
- if (options.local) {
36
- return 'local-only';
14
+ if (normalized === DIR_PATTERNS.OPENPACKAGE ||
15
+ normalized.startsWith(`${DIR_PATTERNS.OPENPACKAGE}/`)) {
16
+ throw new Error(`Installation target '${targetDir}' cannot point inside ${DIR_PATTERNS.OPENPACKAGE} (reserved for metadata like ${PACKAGE_PATHS.INDEX_RELATIVE}). Choose a workspace path outside metadata.`);
37
17
  }
38
- return 'default';
39
18
  }
40
19
  export function validateResolutionFlags(options) {
41
20
  if (options.remote && options.local) {
42
21
  throw new Error('--remote and --local cannot be used together. Choose one resolution mode.');
43
22
  }
44
23
  }
45
- /**
46
- * Install all packages from CWD package.yml file
47
- * @param targetDir - Target directory for installation
48
- * @param options - Installation options including dev flag
49
- * @returns Command result with installation summary
50
- */
51
- async function installAllPackagesCommand(targetDir, options) {
52
- const cwd = process.cwd();
53
- logger.info(`Installing all packages from package.yml to: ${getAIDir(cwd)}`, { options });
54
- await ensureRegistryDirectories();
55
- // Auto-create basic package.yml if it doesn't exist
56
- await createBasicPackageYml(cwd);
57
- const packageYmlPath = getLocalPackageYmlPath(cwd);
58
- const cwdConfig = await withOperationErrorHandling(() => parsePackageYml(packageYmlPath), 'parse package.yml', packageYmlPath);
59
- const allPackagesToInstall = extractPackagesFromConfig(cwdConfig);
60
- // Filter out any packages that match the root package name
61
- const packagesToInstall = [];
62
- const skippedRootPackages = [];
63
- for (const pkg of allPackagesToInstall) {
64
- if (await isRootPackage(cwd, pkg.name)) {
65
- skippedRootPackages.push(pkg);
66
- console.log(`⚠️ Skipping ${pkg.name} - it matches your project's root package name`);
67
- }
68
- else {
69
- packagesToInstall.push(pkg);
70
- }
71
- }
72
- if (packagesToInstall.length === 0) {
73
- if (skippedRootPackages.length > 0) {
74
- console.log('✓ All packages in package.yml were skipped (matched root package)');
75
- console.log('\nTips:');
76
- console.log('• Root packages cannot be installed as dependencies');
77
- console.log('• Use "opkg install <package-name>" to install external packages');
78
- console.log('• Use "opkg save" to create a WIP copy of your root package in the registry');
79
- }
80
- else {
81
- console.log('⚠️ No packages found in package.yml');
82
- console.log('\nTips:');
83
- console.log('• Add packages to the "packages" array in package.yml');
84
- console.log('• Add development packages to the "dev-packages" array in package.yml');
85
- console.log('• Use "opkg install <package-name>" to install a specific package');
86
- }
87
- return { success: true, data: { installed: 0, skipped: skippedRootPackages.length } };
88
- }
89
- console.log(`✓ Installing ${packagesToInstall.length} packages from package.yml:`);
90
- packagesToInstall.forEach(pkg => {
91
- const prefix = pkg.isDev ? '[dev] ' : '';
92
- const label = pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name;
93
- console.log(` • ${prefix}${label}`);
94
- });
95
- if (skippedRootPackages.length > 0) {
96
- console.log(` • ${skippedRootPackages.length} packages skipped (matched root package)`);
97
- }
98
- console.log('');
99
- const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
100
- const normalizedPlatforms = normalizePlatforms(options.platforms);
101
- const resolvedPlatforms = await resolvePlatforms(cwd, normalizedPlatforms, { interactive });
102
- // Install packages sequentially to avoid conflicts
103
- let totalInstalled = 0;
104
- let totalSkipped = 0;
105
- const results = [];
106
- const aggregateWarnings = new Set();
107
- for (const pkg of packagesToInstall) {
108
- try {
109
- const label = pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name;
110
- const baseConflictDecisions = options.conflictDecisions
111
- ? { ...options.conflictDecisions }
112
- : undefined;
113
- const installOptions = {
114
- ...options,
115
- dev: pkg.isDev,
116
- resolvedPlatforms,
117
- conflictDecisions: baseConflictDecisions
118
- };
119
- let conflictPlanningVersion = pkg.version;
120
- if (pkg.version && !isExactVersion(pkg.version)) {
121
- try {
122
- const localVersions = await listPackageVersions(pkg.name);
123
- conflictPlanningVersion = resolveVersionRange(pkg.version, localVersions) ?? undefined;
124
- }
125
- catch {
126
- conflictPlanningVersion = undefined;
127
- }
128
- }
129
- if (conflictPlanningVersion) {
130
- try {
131
- const conflicts = await planConflictsForPackage(cwd, pkg.name, conflictPlanningVersion, resolvedPlatforms);
132
- if (conflicts.length > 0) {
133
- const shouldPrompt = interactive && (!installOptions.conflictStrategy || installOptions.conflictStrategy === 'ask');
134
- if (shouldPrompt) {
135
- console.log(`\n⚠️ Detected ${conflicts.length} potential file conflict${conflicts.length === 1 ? '' : 's'} for ${label}.`);
136
- const preview = conflicts.slice(0, 5);
137
- for (const conflict of preview) {
138
- const ownerInfo = conflict.ownerPackage ? `owned by ${conflict.ownerPackage}` : 'already exists locally';
139
- console.log(` • ${conflict.relPath} (${ownerInfo})`);
140
- }
141
- if (conflicts.length > preview.length) {
142
- console.log(` • ... and ${conflicts.length - preview.length} more`);
143
- }
144
- const selection = await safePrompts({
145
- type: 'select',
146
- name: 'strategy',
147
- message: `Choose conflict handling for ${label}:`,
148
- choices: [
149
- { title: 'Keep both (rename existing files)', value: 'keep-both' },
150
- { title: 'Overwrite existing files', value: 'overwrite' },
151
- { title: 'Skip conflicting files', value: 'skip' },
152
- { title: 'Review individually', value: 'ask' }
153
- ],
154
- initial: installOptions.conflictStrategy === 'ask' ? 3 : 0
155
- });
156
- const chosenStrategy = selection.strategy;
157
- installOptions.conflictStrategy = chosenStrategy;
158
- if (chosenStrategy === 'ask') {
159
- const decisions = {};
160
- for (const conflict of conflicts) {
161
- const detail = await safePrompts({
162
- type: 'select',
163
- name: 'decision',
164
- message: `${conflict.relPath}${conflict.ownerPackage ? ` (owned by ${conflict.ownerPackage})` : ''}:`,
165
- choices: [
166
- { title: 'Keep both (rename existing)', value: 'keep-both' },
167
- { title: 'Overwrite existing', value: 'overwrite' },
168
- { title: 'Skip (keep existing)', value: 'skip' }
169
- ],
170
- initial: 0
171
- });
172
- const decisionValue = detail.decision;
173
- decisions[conflict.relPath] = decisionValue;
174
- }
175
- installOptions.conflictDecisions = decisions;
176
- }
177
- }
178
- else if (!interactive && (!installOptions.conflictStrategy || installOptions.conflictStrategy === 'ask')) {
179
- logger.warn(`Detected ${conflicts.length} potential conflict${conflicts.length === 1 ? '' : 's'} for ${label}, but running in non-interactive mode. Conflicting files will be skipped unless '--conflicts' is provided.`);
180
- }
181
- }
182
- }
183
- catch (planError) {
184
- logger.warn(`Failed to evaluate conflicts for ${label}: ${planError}`);
185
- }
186
- }
187
- console.log(`\n🔧 Installing ${pkg.isDev ? '[dev] ' : ''}${label}...`);
188
- const result = await installPackageCommand(pkg.name, targetDir, installOptions, pkg.version);
189
- if (result.success) {
190
- totalInstalled++;
191
- results.push({ name: pkg.name, success: true });
192
- console.log(`✓ Successfully installed ${pkg.name}`);
193
- if (result.warnings && result.warnings.length > 0) {
194
- result.warnings.forEach(warning => aggregateWarnings.add(warning));
195
- }
196
- }
197
- else {
198
- totalSkipped++;
199
- results.push({ name: pkg.name, success: false, error: result.error });
200
- console.log(`❌ Failed to install ${pkg.name}: ${result.error}`);
201
- }
202
- }
203
- catch (error) {
204
- if (error instanceof UserCancellationError) {
205
- throw error; // Re-throw to allow clean exit
206
- }
207
- totalSkipped++;
208
- results.push({ name: pkg.name, success: false, error: String(error) });
209
- console.log(`❌ Failed to install ${pkg.name}: ${error}`);
210
- }
211
- }
212
- displayInstallationSummary(totalInstalled, totalSkipped, packagesToInstall.length, results);
213
- if (aggregateWarnings.size > 0) {
214
- console.log('\n⚠️ Warnings during installation:');
215
- aggregateWarnings.forEach(warning => {
216
- console.log(` • ${warning}`);
217
- });
218
- }
219
- const allSuccessful = totalSkipped === 0;
220
- return {
221
- success: allSuccessful,
222
- data: {
223
- installed: totalInstalled,
224
- skipped: totalSkipped,
225
- results
226
- },
227
- error: allSuccessful ? undefined : `${totalSkipped} packages failed to install`,
228
- warnings: totalSkipped > 0 ? [`${totalSkipped} packages failed to install`] : undefined
229
- };
230
- }
231
- /**
232
- * Install package command implementation with recursive dependency resolution
233
- * @param packageName - Name of the package to install
234
- * @param targetDir - Target directory for installation
235
- * @param options - Installation options including force, dry-run, and dev flags
236
- * @param version - Specific version to install (optional)
237
- * @returns Command result with detailed installation information
238
- */
239
- async function installPackageCommand(packageName, targetDir, options, version) {
240
- const cwd = process.cwd();
241
- const resolutionMode = options.resolutionMode ?? determineResolutionMode(options);
242
- // 1) Validate root package and early return
243
- if (await isRootPackage(cwd, packageName)) {
244
- console.log(`⚠️ Cannot install ${packageName} - it matches your project's root package name`);
245
- console.log(` This would create a circular dependency.`);
246
- console.log(`💡 Tip: Use 'opkg install' without specifying a package name to install all packages`);
247
- console.log(` referenced in your .openpackage/package.yml file.`);
248
- return {
249
- success: true,
250
- data: { skipped: true, reason: 'root package' }
251
- };
252
- }
253
- logger.debug(`Installing package '${packageName}' with dependencies to: ${getAIDir(cwd)}`, { options });
254
- const dryRun = !!options.dryRun;
255
- const forceRemote = resolutionMode === 'remote-primary';
256
- const warnings = [];
257
- const warnedPackages = new Set();
258
- const canonicalPlan = await determineCanonicalInstallPlan({
259
- cwd,
260
- packageName,
261
- cliSpec: version,
262
- devFlag: options.dev ?? false
263
- });
264
- if (canonicalPlan.compatibilityMessage) {
265
- console.log(`ℹ️ ${canonicalPlan.compatibilityMessage}`);
266
- }
267
- let versionConstraint = canonicalPlan.effectiveRange;
268
- const selectionOptions = options.stable ? { preferStable: true } : undefined;
269
- const preselection = await selectInstallVersionUnified({
270
- packageName,
271
- constraint: versionConstraint,
272
- mode: resolutionMode,
273
- selectionOptions,
274
- profile: options.profile,
275
- apiKey: options.apiKey
276
- });
277
- if (preselection.sources.warnings.length > 0) {
278
- preselection.sources.warnings.forEach(message => {
279
- warnings.push(message);
280
- console.log(`⚠️ ${message}`);
281
- });
282
- }
283
- const selectedRootVersion = preselection.selectedVersion;
284
- if (!selectedRootVersion) {
285
- const constraintLabel = canonicalPlan.dependencyState === 'existing' && canonicalPlan.canonicalRange
286
- ? canonicalPlan.canonicalRange
287
- : versionConstraint;
288
- throw buildNoVersionFoundError(packageName, constraintLabel, preselection.selection, resolutionMode);
289
- }
290
- const source = preselection.resolutionSource ?? 'local';
291
- console.log(formatSelectionSummary(source, packageName, selectedRootVersion));
292
- // 2) Prepare env via prepareInstallEnvironment
293
- const { specifiedPlatforms } = await prepareInstallEnvironment(cwd, options);
294
- let resolvedPackages = [];
295
- let missingPackages = [];
296
- const resolveDependenciesOutcome = async () => {
297
- try {
298
- const data = await resolveDependenciesForInstall(packageName, cwd, versionConstraint, options);
299
- if (data.warnings && data.warnings.length > 0) {
300
- data.warnings.forEach(message => {
301
- warnings.push(message);
302
- // Surface resolver warnings (including circular dependency notices)
303
- // directly to the user for better visibility.
304
- console.log(`⚠️ ${message}`);
305
- const match = message.match(/Remote pull failed for `([^`]+)`/);
306
- if (match) {
307
- warnedPackages.add(match[1]);
308
- }
309
- });
310
- }
311
- return { success: true, data };
312
- }
313
- catch (error) {
314
- if (error instanceof VersionResolutionAbortError) {
315
- return { success: false, commandResult: { success: false, error: error.message } };
316
- }
317
- if (error instanceof PackageNotFoundError ||
318
- (error instanceof Error && (error.message.includes('not available in local registry') ||
319
- (error.message.includes('Package') && error.message.includes('not found'))))) {
320
- console.log('❌ Package not found');
321
- return { success: false, commandResult: { success: false, error: 'Package not found' } };
322
- }
323
- throw error;
324
- }
325
- };
326
- // 3) Resolve dependencies
327
- const initialResolution = await resolveDependenciesOutcome();
328
- if (!initialResolution.success) {
329
- return initialResolution.commandResult;
330
- }
331
- resolvedPackages = initialResolution.data.resolvedPackages;
332
- missingPackages = initialResolution.data.missingPackages;
333
- const remoteOutcomes = {
334
- ...(initialResolution.data.remoteOutcomes ?? {})
335
- };
336
- const computeRetryEligibleMissing = (names) => {
337
- return names.filter(name => {
338
- const outcome = remoteOutcomes[name];
339
- if (!outcome) {
340
- return true;
341
- }
342
- return outcome.reason !== 'not-found' && outcome.reason !== 'access-denied';
343
- });
344
- };
345
- let retryEligibleMissing = computeRetryEligibleMissing(missingPackages);
346
- // Track packages that were already warned about during resolution
347
- // to avoid duplicate warnings when fetching metadata
348
- const pullMissingFromRemote = async () => {
349
- if (retryEligibleMissing.length === 0) {
350
- return null;
351
- }
352
- const metadataResults = await fetchMissingDependencyMetadata(retryEligibleMissing, resolvedPackages, {
353
- dryRun,
354
- profile: options.profile,
355
- apiKey: options.apiKey,
356
- alreadyWarnedPackages: warnedPackages,
357
- onFailure: (name, failure) => {
358
- remoteOutcomes[name] = {
359
- name,
360
- reason: failure.reason,
361
- message: failure.message
362
- };
363
- }
364
- });
365
- if (metadataResults.length === 0) {
366
- return null;
367
- }
368
- const keysToDownload = new Set();
369
- for (const metadata of metadataResults) {
370
- const aggregated = aggregateRecursiveDownloads([metadata.response]);
371
- const missingKeys = await computeMissingDownloadKeys(aggregated);
372
- missingKeys.forEach((key) => keysToDownload.add(key));
373
- }
374
- const batchResults = await pullMissingDependencies(metadataResults, keysToDownload, {
375
- dryRun,
376
- profile: options.profile,
377
- apiKey: options.apiKey
378
- });
379
- for (const batchResult of batchResults) {
380
- recordBatchOutcome('Pulled dependencies', batchResult, warnings, dryRun);
381
- updateRemoteOutcomesFromBatch(batchResult, remoteOutcomes);
382
- }
383
- const refreshedResolution = await resolveDependenciesOutcome();
384
- if (!refreshedResolution.success) {
385
- return refreshedResolution.commandResult;
386
- }
387
- resolvedPackages = refreshedResolution.data.resolvedPackages;
388
- missingPackages = refreshedResolution.data.missingPackages;
389
- if (refreshedResolution.data.remoteOutcomes) {
390
- Object.assign(remoteOutcomes, refreshedResolution.data.remoteOutcomes);
391
- }
392
- retryEligibleMissing = computeRetryEligibleMissing(missingPackages);
393
- return null;
394
- };
395
- if (missingPackages.length > 0) {
396
- if (resolutionMode === 'local-only') {
397
- logger.info('Local-only mode: missing dependencies will not be pulled from remote', {
398
- missingPackages: Array.from(new Set(missingPackages))
399
- });
400
- }
401
- else if (retryEligibleMissing.length > 0) {
402
- const pullResult = await pullMissingFromRemote();
403
- if (pullResult) {
404
- return pullResult;
405
- }
406
- }
407
- }
408
- // 7) Warn if still missing
409
- if (missingPackages.length > 0) {
410
- const missingSummary = `Missing packages: ${Array.from(new Set(missingPackages)).join(', ')}`;
411
- console.log(`⚠️ ${missingSummary}`);
412
- warnings.push(missingSummary);
413
- }
414
- // 8) Process conflicts
415
- const conflictProcessing = await processConflictResolution(resolvedPackages, options);
416
- if ('cancelled' in conflictProcessing) {
417
- console.log(`Installation cancelled by user`);
418
- return {
419
- success: true,
420
- data: {
421
- packageName,
422
- targetDir: getAIDir(cwd),
423
- resolvedPackages: [],
424
- totalPackages: 0,
425
- installed: 0,
426
- skipped: 1,
427
- totalGroundzeroFiles: 0
428
- }
429
- };
430
- }
431
- const { finalResolvedPackages, conflictResult } = conflictProcessing;
432
- displayDependencyTree(finalResolvedPackages, true);
433
- const packageYmlPath = getLocalPackageYmlPath(cwd);
434
- const packageYmlExists = await exists(packageYmlPath);
435
- // 9) If dryRun, delegate to handleDryRunMode and return
436
- if (options.dryRun) {
437
- return await handleDryRunMode(finalResolvedPackages, packageName, targetDir, options, packageYmlExists);
438
- }
439
- // 10) Resolve platforms, create dirs, perform phases, write metadata, update package.yml, display results, return
440
- const canPromptForPlatforms = Boolean(process.stdin.isTTY && process.stdout.isTTY);
441
- const finalPlatforms = options.resolvedPlatforms && options.resolvedPlatforms.length > 0
442
- ? options.resolvedPlatforms
443
- : await resolvePlatforms(cwd, specifiedPlatforms, { interactive: canPromptForPlatforms });
444
- const createdDirs = await createPlatformDirectories(cwd, finalPlatforms);
445
- const mainPackage = finalResolvedPackages.find((f) => f.isRoot);
446
- const installationOutcome = await performIndexBasedInstallationPhases({
447
- cwd,
448
- packages: finalResolvedPackages,
449
- platforms: finalPlatforms,
450
- conflictResult,
451
- options,
452
- targetDir
453
- });
454
- for (const resolved of finalResolvedPackages) {
455
- await writeLocalPackageFromRegistry(cwd, resolved.name, resolved.version);
456
- }
457
- if (packageYmlExists && mainPackage) {
458
- const persistTarget = resolvePersistRange(canonicalPlan.persistDecision, mainPackage.version);
459
- if (persistTarget) {
460
- await addPackageToYml(cwd, packageName, mainPackage.version, persistTarget.target === 'dev-packages', persistTarget.range, true);
461
- }
462
- }
463
- displayInstallationResults(packageName, finalResolvedPackages, { platforms: finalPlatforms, created: createdDirs }, options, mainPackage, installationOutcome.allAddedFiles, installationOutcome.allUpdatedFiles, installationOutcome.rootFileResults, missingPackages, remoteOutcomes);
464
- return {
465
- success: true,
466
- data: {
467
- packageName,
468
- targetDir: getAIDir(cwd),
469
- resolvedPackages: finalResolvedPackages,
470
- totalPackages: finalResolvedPackages.length,
471
- installed: installationOutcome.installedCount,
472
- skipped: installationOutcome.skippedCount,
473
- totalGroundzeroFiles: installationOutcome.totalGroundzeroFiles
474
- },
475
- warnings: warnings.length > 0 ? Array.from(new Set(warnings)) : undefined
476
- };
477
- }
478
- export function formatSelectionSummary(source, packageName, version) {
479
- const packageSpecifier = packageName.startsWith('@') ? packageName : `@${packageName}`;
480
- return `✓ Selected ${source} ${packageSpecifier}@${version}`;
481
- }
482
- function updateRemoteOutcomesFromBatch(batchResult, remoteOutcomes) {
483
- if (!batchResult.failed || batchResult.failed.length === 0) {
484
- return;
485
- }
486
- for (const failure of batchResult.failed) {
487
- const reasonLabel = extractRemoteErrorReason(failure.error ?? 'Unknown error');
488
- const reasonTag = mapReasonLabelToOutcome(reasonLabel);
489
- const packageName = failure.name;
490
- const message = `Remote pull failed for \`${packageName}\` (reason: ${reasonLabel})`;
491
- remoteOutcomes[packageName] = {
492
- name: packageName,
493
- reason: reasonTag,
494
- message
495
- };
496
- }
497
- }
498
- function mapReasonLabelToOutcome(reasonLabel) {
499
- switch (reasonLabel) {
500
- case 'not found in remote registry':
501
- case 'not found in registry':
502
- return 'not-found';
503
- case 'access denied':
504
- return 'access-denied';
505
- case 'network error':
506
- return 'network';
507
- case 'integrity check failed':
508
- return 'integrity';
509
- default:
510
- return 'unknown';
511
- }
512
- }
513
- async function determineCanonicalInstallPlan(args) {
514
- const normalizedCliSpec = args.cliSpec?.trim() || undefined;
515
- const existing = await findCanonicalDependency(args.cwd, args.packageName);
516
- const target = args.devFlag ? 'dev-packages' : 'packages';
517
- if (existing) {
518
- const canonicalConstraint = parseConstraintOrThrow('package', existing.range, args.packageName);
519
- if (normalizedCliSpec) {
520
- const cliConstraint = parseConstraintOrThrow('cli', normalizedCliSpec, args.packageName);
521
- if (!isRangeSubset(cliConstraint.resolverRange, canonicalConstraint.resolverRange)) {
522
- throw buildCanonicalConflictError(args.packageName, cliConstraint.displayRange, existing.range);
523
- }
524
- return {
525
- effectiveRange: canonicalConstraint.resolverRange,
526
- dependencyState: 'existing',
527
- canonicalRange: existing.range,
528
- canonicalTarget: existing.target,
529
- persistDecision: { type: 'none' },
530
- compatibilityMessage: `Using version range from package.yml (${existing.range}); CLI spec '${cliConstraint.displayRange}' is compatible.`
531
- };
532
- }
533
- return {
534
- effectiveRange: canonicalConstraint.resolverRange,
535
- dependencyState: 'existing',
536
- canonicalRange: existing.range,
537
- canonicalTarget: existing.target,
538
- persistDecision: { type: 'none' }
539
- };
540
- }
541
- if (normalizedCliSpec) {
542
- const cliConstraint = parseConstraintOrThrow('cli', normalizedCliSpec, args.packageName);
543
- return {
544
- effectiveRange: cliConstraint.resolverRange,
545
- dependencyState: 'fresh',
546
- persistDecision: {
547
- type: 'explicit',
548
- target,
549
- range: cliConstraint.displayRange
550
- }
551
- };
552
- }
553
- return {
554
- effectiveRange: '*',
555
- dependencyState: 'fresh',
556
- persistDecision: {
557
- type: 'derive',
558
- target,
559
- mode: 'caret-or-exact'
560
- }
561
- };
562
- }
563
- async function findCanonicalDependency(cwd, packageName) {
564
- const packageYmlPath = getLocalPackageYmlPath(cwd);
565
- if (!(await exists(packageYmlPath))) {
566
- return null;
567
- }
568
- try {
569
- const config = await parsePackageYml(packageYmlPath);
570
- const match = locateDependencyInArray(config.packages, packageName, 'packages') ||
571
- locateDependencyInArray(config['dev-packages'], packageName, 'dev-packages');
572
- return match;
573
- }
574
- catch (error) {
575
- const detail = error instanceof Error ? error.message : String(error);
576
- throw new Error(`Failed to parse ${packageYmlPath}: ${detail}`);
577
- }
578
- }
579
- function locateDependencyInArray(deps, packageName, target) {
580
- if (!deps) {
581
- return null;
582
- }
583
- const entry = deps.find(dep => arePackageNamesEquivalent(dep.name, packageName));
584
- if (!entry) {
585
- return null;
586
- }
587
- if (!entry.version || !entry.version.trim()) {
588
- throw new Error(`Dependency '${packageName}' in .openpackage/package.yml must declare a version range. Edit the file and try again.`);
589
- }
590
- return {
591
- range: entry.version.trim(),
592
- target
593
- };
594
- }
595
- function parseConstraintOrThrow(source, raw, packageName) {
596
- try {
597
- const parsed = parseVersionRange(raw);
598
- return { resolverRange: parsed.range, displayRange: parsed.original };
599
- }
600
- catch (error) {
601
- const message = error instanceof Error ? error.message : String(error);
602
- if (source === 'cli') {
603
- throw new Error(`Invalid version spec '${raw}' provided via CLI for '${packageName}'. ${message}. Adjust the CLI input and try again.`);
604
- }
605
- throw new Error(`Dependency '${packageName}' in .openpackage/package.yml has invalid version '${raw}'. ${message}. Edit the file and try again.`);
606
- }
607
- }
608
- function isRangeSubset(candidate, canonical) {
609
- try {
610
- return semver.subset(candidate, canonical, { includePrerelease: true });
611
- }
612
- catch {
613
- return false;
614
- }
615
- }
616
- function buildCanonicalConflictError(packageName, cliSpec, canonicalRange) {
617
- return new Error(`Requested '${packageName}@${cliSpec}', but .openpackage/package.yml declares '${packageName}' with range '${canonicalRange}'. Edit package.yml to change the dependency line, then re-run opkg install.`);
618
- }
619
- function resolvePersistRange(decision, selectedVersion) {
620
- if (decision.type === 'none') {
621
- return null;
622
- }
623
- if (decision.type === 'explicit') {
624
- return { range: decision.range, target: decision.target };
625
- }
626
- // Create caret range from the selected version
627
- const derivedRange = createCaretRange(selectedVersion);
628
- return { range: derivedRange, target: decision.target };
629
- }
630
- function buildNoVersionFoundError(packageName, constraint, selection, mode) {
631
- const stableList = formatVersionList(selection.availableStable);
632
- const prereleaseList = formatVersionList(selection.availablePrerelease);
633
- const suggestions = [
634
- 'Edit .openpackage/package.yml or adjust the CLI range, then retry.',
635
- 'Use opkg save/pack to create a compatible version in the local registry.'
636
- ];
637
- if (mode === 'local-only') {
638
- suggestions.push('Re-run without --local to include remote versions in resolution.');
639
- }
640
- const message = [
641
- `No version of '${packageName}' satisfies '${constraint}'.`,
642
- `Available stable versions: ${stableList}`,
643
- `Available WIP/pre-release versions: ${prereleaseList}`,
644
- 'Suggested next steps:',
645
- ...suggestions.map(suggestion => ` • ${suggestion}`)
646
- ].join('\n');
647
- return new Error(message);
648
- }
649
- function formatVersionList(versions) {
650
- if (!versions || versions.length === 0) {
651
- return 'none';
652
- }
653
- return versions.join(', ');
654
- }
655
- /**
656
- * Main install command router - handles both individual and bulk install
657
- * @param packageName - Name of package to install (optional, installs all if not provided)
658
- * @param targetDir - Target directory for installation
659
- * @param options - Installation options
660
- * @returns Command result with installation status and data
661
- */
662
24
  async function installCommand(packageName, targetDir, options) {
663
- const mode = determineResolutionMode(options);
664
- options.resolutionMode = mode;
665
- logger.debug('Install resolution mode selected', { mode });
666
- // If no package name provided, install all from package.yml
25
+ assertTargetDirOutsideMetadata(targetDir);
26
+ options.resolutionMode = determineResolutionMode(options);
27
+ logger.debug('Install resolution mode selected', { mode: options.resolutionMode });
667
28
  if (!packageName) {
668
- return await installAllPackagesCommand(targetDir, options);
29
+ return await runBulkInstallPipeline(targetDir, options);
669
30
  }
670
- // Parse package name and version from input
671
- const { name, version: inputVersion } = parsePackageInput(packageName);
672
- // Install the specific package with version
673
- return await installPackageCommand(name, targetDir, options, inputVersion);
31
+ const { name, version } = parsePackageInput(packageName);
32
+ return await runInstallPipeline({
33
+ ...options,
34
+ packageName: name,
35
+ version,
36
+ targetDir
37
+ });
674
38
  }
675
- /**
676
- * Setup the install command
677
- * @param program - Commander program instance to register the command with
678
- */
679
39
  export function setupInstallCommand(program) {
680
40
  program
681
41
  .command('install')
682
42
  .alias('i')
683
43
  .description('Install packages from the local (and optional remote) registry into this workspace. Works with WIP copies from `opkg save` and stable releases from `opkg pack`.')
684
44
  .argument('[package-name]', 'name of the package to install (optional - installs all from package.yml if not specified). Supports package@version syntax.')
685
- .argument('[target-dir]', 'target directory relative to cwd/ai for /ai files only (defaults to ai root)', '.')
45
+ .argument('[target-dir]', 'target directory relative to the workspace install root (defaults to ./ai)', '.')
686
46
  .option('--dry-run', 'preview changes without applying them')
687
47
  .option('--force', 'overwrite existing files')
688
48
  .option('--conflicts <strategy>', 'conflict handling strategy: keep-both, overwrite, skip, or ask')
@@ -694,7 +54,6 @@ export function setupInstallCommand(program) {
694
54
  .option('--profile <profile>', 'profile to use for authentication')
695
55
  .option('--api-key <key>', 'API key for authentication (overrides profile)')
696
56
  .action(withErrorHandling(async (packageName, targetDir, options) => {
697
- // Normalize platforms option early for downstream logic
698
57
  options.platforms = normalizePlatforms(options.platforms);
699
58
  const commandOptions = options;
700
59
  const rawConflictStrategy = commandOptions.conflicts ?? options.conflictStrategy;
@@ -711,7 +70,6 @@ export function setupInstallCommand(program) {
711
70
  const result = await installCommand(packageName, targetDir, options);
712
71
  if (!result.success) {
713
72
  if (result.error === 'Package not found') {
714
- // Handled case: already printed minimal message, do not bubble to global handler
715
73
  return;
716
74
  }
717
75
  throw new Error(result.error || 'Installation operation failed');