opkg 0.6.1 → 0.7.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.
- package/README.md +109 -186
- package/assets/openpackage_ascii_dark.png +0 -0
- package/assets/openpackage_ascii_light.png +0 -0
- package/dist/commands/add.js +34 -10
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/apply.js +16 -0
- package/dist/commands/apply.js.map +1 -0
- package/dist/commands/delete.js +1 -1
- package/dist/commands/delete.js.map +1 -1
- package/dist/commands/install.js +177 -8
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/list.js +2 -2
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.js +1 -1
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/new.js +125 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/pack.js +7 -13
- package/dist/commands/pack.js.map +1 -1
- package/dist/commands/pull.js +1 -1
- package/dist/commands/pull.js.map +1 -1
- package/dist/commands/push.js +1 -1
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/remove.js +63 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/save.js +11 -17
- package/dist/commands/save.js.map +1 -1
- package/dist/commands/set.js +33 -0
- package/dist/commands/set.js.map +1 -0
- package/dist/commands/show.js +16 -94
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/status.js +26 -701
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/uninstall.js +14 -427
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/constants/index.js +72 -16
- package/dist/constants/index.js.map +1 -1
- package/dist/core/add/add-conflict-handler.js +1 -8
- package/dist/core/add/add-conflict-handler.js.map +1 -1
- package/dist/core/add/add-pipeline.js +12 -10
- package/dist/core/add/add-pipeline.js.map +1 -1
- package/dist/core/add/add-to-source-pipeline.js +123 -0
- package/dist/core/add/add-to-source-pipeline.js.map +1 -0
- package/dist/core/add/package-index-updater.js +77 -78
- package/dist/core/add/package-index-updater.js.map +1 -1
- package/dist/core/add/platform-path-transformer.js +6 -4
- package/dist/core/add/platform-path-transformer.js.map +1 -1
- package/dist/core/add/source-collector.js +2 -3
- package/dist/core/add/source-collector.js.map +1 -1
- package/dist/core/apply/apply-pipeline.js +110 -0
- package/dist/core/apply/apply-pipeline.js.map +1 -0
- package/dist/core/dependency-resolver.js +263 -21
- package/dist/core/dependency-resolver.js.map +1 -1
- package/dist/core/discovery/file-discovery.js +1 -2
- package/dist/core/discovery/file-discovery.js.map +1 -1
- package/dist/core/discovery/platform-files-discovery.js +33 -18
- package/dist/core/discovery/platform-files-discovery.js.map +1 -1
- package/dist/core/flows/flow-executor.js +974 -0
- package/dist/core/flows/flow-executor.js.map +1 -0
- package/dist/core/flows/flow-inverter.js +442 -0
- package/dist/core/flows/flow-inverter.js.map +1 -0
- package/dist/core/flows/flow-key-extractor.js +101 -0
- package/dist/core/flows/flow-key-extractor.js.map +1 -0
- package/dist/core/flows/flow-key-mapper.js +382 -0
- package/dist/core/flows/flow-key-mapper.js.map +1 -0
- package/dist/core/flows/flow-transforms.js +632 -0
- package/dist/core/flows/flow-transforms.js.map +1 -0
- package/dist/core/flows/map-pipeline/context.js +73 -0
- package/dist/core/flows/map-pipeline/context.js.map +1 -0
- package/dist/core/flows/map-pipeline/index.js +156 -0
- package/dist/core/flows/map-pipeline/index.js.map +1 -0
- package/dist/core/flows/map-pipeline/operations/copy.js +104 -0
- package/dist/core/flows/map-pipeline/operations/copy.js.map +1 -0
- package/dist/core/flows/map-pipeline/operations/pipe.js +70 -0
- package/dist/core/flows/map-pipeline/operations/pipe.js.map +1 -0
- package/dist/core/flows/map-pipeline/operations/rename.js +102 -0
- package/dist/core/flows/map-pipeline/operations/rename.js.map +1 -0
- package/dist/core/flows/map-pipeline/operations/set.js +50 -0
- package/dist/core/flows/map-pipeline/operations/set.js.map +1 -0
- package/dist/core/flows/map-pipeline/operations/switch.js +79 -0
- package/dist/core/flows/map-pipeline/operations/switch.js.map +1 -0
- package/dist/core/flows/map-pipeline/operations/transform.js +543 -0
- package/dist/core/flows/map-pipeline/operations/transform.js.map +1 -0
- package/dist/core/flows/map-pipeline/operations/unset.js +65 -0
- package/dist/core/flows/map-pipeline/operations/unset.js.map +1 -0
- package/dist/core/flows/map-pipeline/types.js +8 -0
- package/dist/core/flows/map-pipeline/types.js.map +1 -0
- package/dist/core/flows/map-pipeline/utils.js +278 -0
- package/dist/core/flows/map-pipeline/utils.js.map +1 -0
- package/dist/core/flows/platform-converter.js +328 -0
- package/dist/core/flows/platform-converter.js.map +1 -0
- package/dist/core/flows/source-resolver.js +192 -0
- package/dist/core/flows/source-resolver.js.map +1 -0
- package/dist/core/flows/toml-domain-transforms.js +23 -0
- package/dist/core/flows/toml-domain-transforms.js.map +1 -0
- package/dist/core/install/bulk-install-pipeline.js +68 -7
- package/dist/core/install/bulk-install-pipeline.js.map +1 -1
- package/dist/core/install/canonical-plan.js +3 -3
- package/dist/core/install/canonical-plan.js.map +1 -1
- package/dist/core/install/dry-run.js +3 -3
- package/dist/core/install/dry-run.js.map +1 -1
- package/dist/core/install/flow-based-installer.js +1158 -0
- package/dist/core/install/flow-based-installer.js.map +1 -0
- package/dist/core/install/flow-workspace-tracker.js +111 -0
- package/dist/core/install/flow-workspace-tracker.js.map +1 -0
- package/dist/core/install/format-detector.js +228 -0
- package/dist/core/install/format-detector.js.map +1 -0
- package/dist/core/install/git-package-loader.js +20 -0
- package/dist/core/install/git-package-loader.js.map +1 -0
- package/dist/core/install/install-errors.js +1 -1
- package/dist/core/install/install-errors.js.map +1 -1
- package/dist/core/install/install-flow.js +34 -14
- package/dist/core/install/install-flow.js.map +1 -1
- package/dist/core/install/install-pipeline.js +52 -17
- package/dist/core/install/install-pipeline.js.map +1 -1
- package/dist/core/install/install-reporting.js +26 -8
- package/dist/core/install/install-reporting.js.map +1 -1
- package/dist/core/install/local-source-resolution.js +103 -0
- package/dist/core/install/local-source-resolution.js.map +1 -0
- package/dist/core/install/marketplace-handler.js +221 -0
- package/dist/core/install/marketplace-handler.js.map +1 -0
- package/dist/core/install/path-install-pipeline.js +241 -0
- package/dist/core/install/path-install-pipeline.js.map +1 -0
- package/dist/core/install/path-package-loader.js +116 -0
- package/dist/core/install/path-package-loader.js.map +1 -0
- package/dist/core/install/plugin-detector.js +72 -0
- package/dist/core/install/plugin-detector.js.map +1 -0
- package/dist/core/install/plugin-to-universal-converter.js +218 -0
- package/dist/core/install/plugin-to-universal-converter.js.map +1 -0
- package/dist/core/install/plugin-transformer.js +191 -0
- package/dist/core/install/plugin-transformer.js.map +1 -0
- package/dist/core/install/version-selection.js +1 -1
- package/dist/core/install/version-selection.js.map +1 -1
- package/dist/core/openpackage.js +40 -22
- package/dist/core/openpackage.js.map +1 -1
- package/dist/core/pack/pack-output.js +62 -0
- package/dist/core/pack/pack-output.js.map +1 -0
- package/dist/core/pack/pack-pipeline.js +186 -0
- package/dist/core/pack/pack-pipeline.js.map +1 -0
- package/dist/core/package-context.js +45 -70
- package/dist/core/package-context.js.map +1 -1
- package/dist/core/package-creation.js +203 -0
- package/dist/core/package-creation.js.map +1 -0
- package/dist/core/package.js +20 -6
- package/dist/core/package.js.map +1 -1
- package/dist/core/platforms.js +665 -209
- package/dist/core/platforms.js.map +1 -1
- package/dist/core/push/push-context.js +1 -1
- package/dist/core/push/push-context.js.map +1 -1
- package/dist/core/push/push-upload.js +2 -2
- package/dist/core/push/push-upload.js.map +1 -1
- package/dist/core/registry.js +6 -6
- package/dist/core/registry.js.map +1 -1
- package/dist/core/remote-pull.js +2 -2
- package/dist/core/remote-pull.js.map +1 -1
- package/dist/core/remove/removal-collector.js +52 -0
- package/dist/core/remove/removal-collector.js.map +1 -0
- package/dist/core/remove/removal-confirmation.js +39 -0
- package/dist/core/remove/removal-confirmation.js.map +1 -0
- package/dist/core/remove/remove-from-source-pipeline.js +173 -0
- package/dist/core/remove/remove-from-source-pipeline.js.map +1 -0
- package/dist/core/save/constants.js +3 -3
- package/dist/core/save/constants.js.map +1 -1
- package/dist/core/save/flow-based-saver.js +270 -0
- package/dist/core/save/flow-based-saver.js.map +1 -0
- package/dist/core/save/name-resolution.js +1 -1
- package/dist/core/save/name-resolution.js.map +1 -1
- package/dist/core/save/package-yml-generator.js +4 -5
- package/dist/core/save/package-yml-generator.js.map +1 -1
- package/dist/core/save/save-candidate-builder.js +215 -0
- package/dist/core/save/save-candidate-builder.js.map +1 -0
- package/dist/core/save/save-candidate-loader.js +12 -11
- package/dist/core/save/save-candidate-loader.js.map +1 -1
- package/dist/core/save/save-conflict-analyzer.js +150 -0
- package/dist/core/save/save-conflict-analyzer.js.map +1 -0
- package/dist/core/save/save-conflict-resolution.js +28 -14
- package/dist/core/save/save-conflict-resolution.js.map +1 -1
- package/dist/core/save/save-conflict-resolver.js +31 -275
- package/dist/core/save/save-conflict-resolver.js.map +1 -1
- package/dist/core/save/save-group-builder.js +52 -0
- package/dist/core/save/save-group-builder.js.map +1 -0
- package/dist/core/save/save-interactive-resolver.js +190 -0
- package/dist/core/save/save-interactive-resolver.js.map +1 -0
- package/dist/core/save/save-pipeline.js +58 -34
- package/dist/core/save/save-pipeline.js.map +1 -1
- package/dist/core/save/save-platform-handler.js +53 -0
- package/dist/core/save/save-platform-handler.js.map +1 -0
- package/dist/core/save/save-resolution-executor.js +145 -0
- package/dist/core/save/save-resolution-executor.js.map +1 -0
- package/dist/core/save/save-result-reporter.js +167 -0
- package/dist/core/save/save-result-reporter.js.map +1 -0
- package/dist/core/save/save-to-source-pipeline.js +154 -0
- package/dist/core/save/save-to-source-pipeline.js.map +1 -0
- package/dist/core/save/save-versioning.js +4 -4
- package/dist/core/save/save-versioning.js.map +1 -1
- package/dist/core/save/save-write-coordinator.js +204 -0
- package/dist/core/save/save-write-coordinator.js.map +1 -0
- package/dist/core/save/save-yml-resolution.js +28 -216
- package/dist/core/save/save-yml-resolution.js.map +1 -1
- package/dist/core/save/workspace-rename.js +7 -8
- package/dist/core/save/workspace-rename.js.map +1 -1
- package/dist/core/set/set-output.js +72 -0
- package/dist/core/set/set-output.js.map +1 -0
- package/dist/core/set/set-pipeline.js +361 -0
- package/dist/core/set/set-pipeline.js.map +1 -0
- package/dist/core/set/set-types.js +5 -0
- package/dist/core/set/set-types.js.map +1 -0
- package/dist/core/show/package-resolver.js +257 -0
- package/dist/core/show/package-resolver.js.map +1 -0
- package/dist/core/show/scope-discovery.js +165 -0
- package/dist/core/show/scope-discovery.js.map +1 -0
- package/dist/core/show/show-output.js +168 -0
- package/dist/core/show/show-output.js.map +1 -0
- package/dist/core/show/show-pipeline.js +113 -0
- package/dist/core/show/show-pipeline.js.map +1 -0
- package/dist/core/show/show-types.js +5 -0
- package/dist/core/show/show-types.js.map +1 -0
- package/dist/core/source-resolution/dependency-graph.js +104 -0
- package/dist/core/source-resolution/dependency-graph.js.map +1 -0
- package/dist/core/source-resolution/resolve-mutable-source.js +109 -0
- package/dist/core/source-resolution/resolve-mutable-source.js.map +1 -0
- package/dist/core/source-resolution/resolve-package-source.js +29 -0
- package/dist/core/source-resolution/resolve-package-source.js.map +1 -0
- package/dist/core/source-resolution/resolve-registry-version.js +35 -0
- package/dist/core/source-resolution/resolve-registry-version.js.map +1 -0
- package/dist/core/source-resolution/types.js.map +1 -0
- package/dist/core/status/status-file-discovery.js +23 -12
- package/dist/core/status/status-file-discovery.js.map +1 -1
- package/dist/core/status/status-pipeline.js +134 -0
- package/dist/core/status/status-pipeline.js.map +1 -0
- package/dist/core/sync/platform-sync-summary.js +27 -0
- package/dist/core/sync/platform-sync-summary.js.map +1 -0
- package/dist/core/uninstall/flow-aware-uninstaller.js +189 -0
- package/dist/core/uninstall/flow-aware-uninstaller.js.map +1 -0
- package/dist/core/uninstall/uninstall-file-discovery.js +11 -6
- package/dist/core/uninstall/uninstall-file-discovery.js.map +1 -1
- package/dist/core/uninstall/uninstall-pipeline.js +141 -0
- package/dist/core/uninstall/uninstall-pipeline.js.map +1 -0
- package/dist/core/universal-patterns.js +64 -0
- package/dist/core/universal-patterns.js.map +1 -0
- package/dist/index.js +99 -6
- package/dist/index.js.map +1 -1
- package/dist/types/flows.js +8 -0
- package/dist/types/flows.js.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/platform-flows.js +8 -0
- package/dist/types/platform-flows.js.map +1 -0
- package/dist/types/workspace-index.js +6 -0
- package/dist/types/workspace-index.js.map +1 -0
- package/dist/utils/custom-path-resolution.js +160 -0
- package/dist/utils/custom-path-resolution.js.map +1 -0
- package/dist/utils/dependency-coverage.js +1 -1
- package/dist/utils/dependency-coverage.js.map +1 -1
- package/dist/utils/file-processing.js +1 -1
- package/dist/utils/flow-index-installer.js +209 -0
- package/dist/utils/flow-index-installer.js.map +1 -0
- package/dist/utils/formatters.js +47 -1
- package/dist/utils/formatters.js.map +1 -1
- package/dist/utils/fs.js +17 -0
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/git-clone-registry.js +88 -0
- package/dist/utils/git-clone-registry.js.map +1 -0
- package/dist/utils/git-clone.js +69 -0
- package/dist/utils/git-clone.js.map +1 -0
- package/dist/utils/git-spec.js +96 -0
- package/dist/utils/git-spec.js.map +1 -0
- package/dist/utils/http-client.js +7 -0
- package/dist/utils/http-client.js.map +1 -1
- package/dist/utils/index-based-installer.js +356 -163
- package/dist/utils/index-based-installer.js.map +1 -1
- package/dist/utils/install-conflict-handler.js +2 -2
- package/dist/utils/install-conflict-handler.js.map +1 -1
- package/dist/utils/install-file-discovery.js +18 -13
- package/dist/utils/install-file-discovery.js.map +1 -1
- package/dist/utils/install-helpers.js +43 -20
- package/dist/utils/install-helpers.js.map +1 -1
- package/dist/utils/jsonc.js +23 -1
- package/dist/utils/jsonc.js.map +1 -1
- package/dist/utils/manifest-paths.js +1 -1
- package/dist/utils/manifest-paths.js.map +1 -1
- package/dist/utils/markdown-frontmatter.js +46 -0
- package/dist/utils/markdown-frontmatter.js.map +1 -1
- package/dist/utils/package-copy.js +5 -103
- package/dist/utils/package-copy.js.map +1 -1
- package/dist/utils/package-filters.js +9 -105
- package/dist/utils/package-filters.js.map +1 -1
- package/dist/utils/package-index-yml.js +27 -6
- package/dist/utils/package-index-yml.js.map +1 -1
- package/dist/utils/package-input.js +98 -0
- package/dist/utils/package-input.js.map +1 -0
- package/dist/utils/package-management.js +80 -28
- package/dist/utils/package-management.js.map +1 -1
- package/dist/utils/package-name-resolution.js +327 -0
- package/dist/utils/package-name-resolution.js.map +1 -0
- package/dist/utils/package-name.js +18 -16
- package/dist/utils/package-name.js.map +1 -1
- package/dist/utils/package-versioning.js +2 -33
- package/dist/utils/package-versioning.js.map +1 -1
- package/dist/utils/package-yml.js +19 -28
- package/dist/utils/package-yml.js.map +1 -1
- package/dist/utils/path-resolution.js +102 -0
- package/dist/utils/path-resolution.js.map +1 -0
- package/dist/utils/paths.js +6 -6
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/platform-file.js +36 -24
- package/dist/utils/platform-file.js.map +1 -1
- package/dist/utils/platform-mapper.js +222 -68
- package/dist/utils/platform-mapper.js.map +1 -1
- package/dist/utils/platform-root-files.js +44 -0
- package/dist/utils/platform-root-files.js.map +1 -0
- package/dist/utils/platform-utils.js +35 -54
- package/dist/utils/platform-utils.js.map +1 -1
- package/dist/utils/platform-yaml-merge.js +20 -140
- package/dist/utils/platform-yaml-merge.js.map +1 -1
- package/dist/utils/prompts.js +92 -7
- package/dist/utils/prompts.js.map +1 -1
- package/dist/utils/registry-entry-filter.js +50 -27
- package/dist/utils/registry-entry-filter.js.map +1 -1
- package/dist/utils/registry-paths.js +5 -4
- package/dist/utils/registry-paths.js.map +1 -1
- package/dist/utils/scope-resolution.js +156 -0
- package/dist/utils/scope-resolution.js.map +1 -0
- package/dist/utils/source-mutability.js +15 -0
- package/dist/utils/source-mutability.js.map +1 -0
- package/dist/utils/tarball.js +29 -4
- package/dist/utils/tarball.js.map +1 -1
- package/dist/utils/version-ranges.js +1 -32
- package/dist/utils/version-ranges.js.map +1 -1
- package/dist/utils/workspace-index-helpers.js +28 -0
- package/dist/utils/workspace-index-helpers.js.map +1 -0
- package/dist/utils/workspace-index-ownership.js +100 -0
- package/dist/utils/workspace-index-ownership.js.map +1 -0
- package/dist/utils/workspace-index-yml.js +173 -0
- package/dist/utils/workspace-index-yml.js.map +1 -0
- package/examples/custom-subdirs-platform.jsonc +157 -0
- package/package.json +7 -2
- package/platforms.jsonc +531 -84
- package/schemas/map-pipeline-v1.json +256 -0
- package/schemas/platforms-v1.json +400 -0
- package/specs/README.md +88 -0
- package/specs/add/README.md +166 -0
- package/specs/agents-claude.md +570 -0
- package/specs/agents-opencode.md +622 -0
- package/specs/apply/README.md +21 -0
- package/specs/apply/apply-behavior.md +58 -0
- package/specs/apply/apply-command.md +51 -0
- package/specs/apply/conflicts.md +41 -0
- package/specs/apply/index-effects.md +81 -0
- package/specs/architecture.md +107 -0
- package/specs/auth/README.md +17 -0
- package/specs/auth/auth-http-contract.md +25 -0
- package/specs/auth/cli/credentials.md +39 -0
- package/specs/auth/cli/login.md +32 -0
- package/specs/auth/cli/logout.md +16 -0
- package/specs/claude-mcp.md +1065 -0
- package/specs/claude-plugins-marketplace.md +363 -0
- package/specs/claude-plugins.md +413 -0
- package/specs/cli-options.md +52 -0
- package/specs/codex-mcp.md +114 -0
- package/specs/commands-overview.md +175 -0
- package/specs/directory-layout.md +95 -0
- package/specs/install/README.md +12 -4
- package/specs/install/git-sources.md +230 -0
- package/specs/install/install-behavior.md +483 -73
- package/specs/install/package-yml-canonical.md +67 -35
- package/specs/install/version-resolution.md +69 -115
- package/specs/new/README.md +769 -0
- package/specs/new/SUMMARY.md +310 -0
- package/specs/new/scope-behavior.md +793 -0
- package/specs/pack/README.md +77 -0
- package/specs/pack/package-name-resolution.md +330 -0
- package/specs/package/README.md +18 -17
- package/specs/package/nested-packages-and-parent-packages.md +32 -31
- package/specs/package/package-index-yml.md +95 -101
- package/specs/package/package-root-layout.md +64 -46
- package/specs/package/registry-payload-and-copy.md +50 -44
- package/specs/package/universal-content.md +33 -56
- package/specs/package-sources.md +248 -0
- package/specs/platforms/README.md +52 -0
- package/specs/platforms/configuration.md +571 -0
- package/specs/platforms/detection.md +552 -0
- package/specs/platforms/directory-layout.md +599 -0
- package/specs/platforms/examples.md +1146 -0
- package/specs/platforms/flow-reference.md +1240 -0
- package/specs/platforms/flows.md +1488 -0
- package/specs/platforms/map-pipeline.md +801 -0
- package/specs/platforms/overview.md +349 -0
- package/specs/platforms/specification.md +700 -0
- package/specs/platforms/troubleshooting.md +697 -0
- package/specs/platforms/universal-converter.md +520 -0
- package/specs/push/README.md +1 -0
- package/specs/push/push-behavior.md +11 -3
- package/specs/push/push-remote-upload.md +1 -1
- package/specs/push/push-scoping.md +1 -1
- package/specs/push/push-version-selection.md +1 -1
- package/specs/registry.md +111 -0
- package/specs/remove/README.md +257 -0
- package/specs/save/README.md +21 -17
- package/specs/save/save-conflict-resolution.md +205 -83
- package/specs/save/save-file-discovery.md +6 -4
- package/specs/save/save-frontmatter-overrides.md +11 -15
- package/specs/save/save-modes-inputs.md +9 -39
- package/specs/save/save-naming-scoping.md +4 -4
- package/specs/save/save-package-detection.md +13 -13
- package/specs/save/save-registry-sync.md +16 -106
- package/specs/save/save-versioning.md +80 -0
- package/specs/scope-management.md +92 -0
- package/specs/set/README.md +520 -0
- package/specs/set/set-behavior.md +563 -0
- package/specs/show/README.md +483 -0
- package/specs/show/show-remote.md +494 -0
- package/specs/status/README.md +38 -0
- package/specs/uninstall/README.md +231 -0
- package/dist/commands/duplicate.js +0 -69
- package/dist/commands/duplicate.js.map +0 -1
- package/dist/commands/init.js +0 -117
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/prune.js +0 -357
- package/dist/commands/prune.js.map +0 -1
- package/dist/commands/tui.js +0 -61
- package/dist/commands/tui.js.map +0 -1
- package/dist/core/install/index.js +0 -3
- package/dist/core/install/index.js.map +0 -1
- package/dist/core/push/push-single-file.js +0 -56
- package/dist/core/push/push-single-file.js.map +0 -1
- package/dist/core/save/package-detection.js +0 -147
- package/dist/core/save/package-detection.js.map +0 -1
- package/dist/core/save/save-single-file.js +0 -124
- package/dist/core/save/save-single-file.js.map +0 -1
- package/dist/core/token-store.js +0 -73
- package/dist/core/token-store.js.map +0 -1
- package/dist/tui/app.js +0 -95
- package/dist/tui/app.js.map +0 -1
- package/dist/tui/components/package-list.js +0 -73
- package/dist/tui/components/package-list.js.map +0 -1
- package/dist/tui/controller.js +0 -365
- package/dist/tui/controller.js.map +0 -1
- package/dist/tui/index.js +0 -12
- package/dist/tui/index.js.map +0 -1
- package/dist/tui/services/file-index.js +0 -64
- package/dist/tui/services/file-index.js.map +0 -1
- package/dist/tui/services/packages.js +0 -18
- package/dist/tui/services/packages.js.map +0 -1
- package/dist/tui/services/save.js +0 -21
- package/dist/tui/services/save.js.map +0 -1
- package/dist/tui/state/app-state.js +0 -15
- package/dist/tui/state/app-state.js.map +0 -1
- package/dist/tui/state.js +0 -17
- package/dist/tui/state.js.map +0 -1
- package/dist/tui/types.js.map +0 -1
- package/dist/tui/views/add-file-modal.js +0 -129
- package/dist/tui/views/add-file-modal.js.map +0 -1
- package/dist/tui/views/file-preview.js +0 -44
- package/dist/tui/views/file-preview.js.map +0 -1
- package/dist/tui/views/list-packages.js +0 -73
- package/dist/tui/views/list-packages.js.map +0 -1
- package/dist/tui/views/main-menu.js +0 -29
- package/dist/tui/views/main-menu.js.map +0 -1
- package/dist/tui/views/manage-view.js +0 -81
- package/dist/tui/views/manage-view.js.map +0 -1
- package/dist/tui/views/package-hub.js +0 -120
- package/dist/tui/views/package-hub.js.map +0 -1
- package/dist/tui/views/placeholder.js +0 -24
- package/dist/tui/views/placeholder.js.map +0 -1
- package/dist/utils/bun-bootstrap.js +0 -72
- package/dist/utils/bun-bootstrap.js.map +0 -1
- package/dist/utils/entity-id.js +0 -19
- package/dist/utils/entity-id.js.map +0 -1
- package/dist/utils/package-local-files.js +0 -5
- package/dist/utils/package-local-files.js.map +0 -1
- package/dist/utils/path-matching.js +0 -74
- package/dist/utils/path-matching.js.map +0 -1
- package/dist/utils/root-file-operations.js +0 -39
- package/dist/utils/root-file-operations.js.map +0 -1
- package/dist/utils/root-file-transformer.js +0 -27
- package/dist/utils/root-file-transformer.js.map +0 -1
- package/dist/utils/yaml-frontmatter.js +0 -25
- package/dist/utils/yaml-frontmatter.js.map +0 -1
- package/specs/auth/auth-device-flow.md +0 -70
- package/specs/login/login-device-flow.md +0 -70
- package/specs/platforms.md +0 -193
- package/specs/save-pack-versioning.md +0 -224
- package/specs/save-pack.md +0 -68
- /package/dist/{tui → core/source-resolution}/types.js +0 -0
|
@@ -0,0 +1,1158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow-Based Installer Module
|
|
3
|
+
*
|
|
4
|
+
* Handles installation of package files using the declarative flow system.
|
|
5
|
+
* Integrates with the existing install pipeline to execute flow transformations
|
|
6
|
+
* for each package file, with multi-package composition and priority-based merging.
|
|
7
|
+
*/
|
|
8
|
+
import { join, dirname, basename, relative, extname } from 'path';
|
|
9
|
+
import { promises as fs } from 'fs';
|
|
10
|
+
import { getPlatformDefinition, getGlobalExportFlows, platformUsesFlows, getAllPlatforms, isPlatformId } from '../platforms.js';
|
|
11
|
+
import { createFlowExecutor } from '../flows/flow-executor.js';
|
|
12
|
+
import { exists, ensureDir } from '../../utils/fs.js';
|
|
13
|
+
import { logger } from '../../utils/logger.js';
|
|
14
|
+
import { toTildePath } from '../../utils/path-resolution.js';
|
|
15
|
+
import { minimatch } from 'minimatch';
|
|
16
|
+
import { parseUniversalPath } from '../../utils/platform-file.js';
|
|
17
|
+
import { detectPackageFormat, shouldInstallDirectly, shouldUsePathMappingOnly, needsConversion } from '../install/format-detector.js';
|
|
18
|
+
import { createPlatformConverter } from '../flows/platform-converter.js';
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Helpers
|
|
21
|
+
// ============================================================================
|
|
22
|
+
/**
|
|
23
|
+
* Get the first pattern from a flow's from field
|
|
24
|
+
* For array patterns, returns the first pattern; for string, returns as-is
|
|
25
|
+
*/
|
|
26
|
+
function getFirstFromPattern(from) {
|
|
27
|
+
return Array.isArray(from) ? from[0] : from;
|
|
28
|
+
}
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Platform Suffix Detection Helpers
|
|
31
|
+
// ============================================================================
|
|
32
|
+
/**
|
|
33
|
+
* Extract platform suffix from filename (e.g., "mcp.claude.jsonc" -> "claude")
|
|
34
|
+
* Works for both root-level files and files in subdirectories
|
|
35
|
+
*/
|
|
36
|
+
function extractPlatformSuffixFromFilename(filename) {
|
|
37
|
+
const knownPlatforms = getAllPlatforms({ includeDisabled: true });
|
|
38
|
+
const baseName = basename(filename);
|
|
39
|
+
const parts = baseName.split('.');
|
|
40
|
+
// Need at least 3 parts: name.platform.ext
|
|
41
|
+
if (parts.length >= 3) {
|
|
42
|
+
const possiblePlatform = parts[parts.length - 2];
|
|
43
|
+
if (isPlatformId(possiblePlatform)) {
|
|
44
|
+
return possiblePlatform;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Strip platform suffix from filename (e.g., "mcp.claude.jsonc" -> "mcp.jsonc")
|
|
51
|
+
*/
|
|
52
|
+
function stripPlatformSuffixFromFilename(filename) {
|
|
53
|
+
const platformSuffix = extractPlatformSuffixFromFilename(filename);
|
|
54
|
+
if (!platformSuffix) {
|
|
55
|
+
return filename;
|
|
56
|
+
}
|
|
57
|
+
const dir = dirname(filename);
|
|
58
|
+
const baseName = basename(filename);
|
|
59
|
+
const parts = baseName.split('.');
|
|
60
|
+
// Remove platform suffix (second-to-last part)
|
|
61
|
+
const strippedParts = [...parts.slice(0, -2), parts[parts.length - 1]];
|
|
62
|
+
const strippedBaseName = strippedParts.join('.');
|
|
63
|
+
return dir === '.' ? strippedBaseName : join(dir, strippedBaseName);
|
|
64
|
+
}
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Flow Discovery
|
|
67
|
+
// ============================================================================
|
|
68
|
+
/**
|
|
69
|
+
* Get applicable flows for a platform, including global flows
|
|
70
|
+
*/
|
|
71
|
+
function getApplicableFlows(platform, cwd) {
|
|
72
|
+
const flows = [];
|
|
73
|
+
// Add global export flows first (applied before platform-specific)
|
|
74
|
+
const globalExportFlows = getGlobalExportFlows(cwd);
|
|
75
|
+
if (globalExportFlows && globalExportFlows.length > 0) {
|
|
76
|
+
flows.push(...globalExportFlows);
|
|
77
|
+
}
|
|
78
|
+
// Add platform-specific export flows
|
|
79
|
+
const definition = getPlatformDefinition(platform, cwd);
|
|
80
|
+
if (definition.export && definition.export.length > 0) {
|
|
81
|
+
flows.push(...definition.export);
|
|
82
|
+
}
|
|
83
|
+
return flows;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Discover source files that match flow patterns
|
|
87
|
+
* Resolves {name} placeholders and glob patterns
|
|
88
|
+
*/
|
|
89
|
+
async function discoverFlowSources(flows, packageRoot, context) {
|
|
90
|
+
const flowSources = new Map();
|
|
91
|
+
for (const flow of flows) {
|
|
92
|
+
const firstPattern = getFirstFromPattern(flow.from);
|
|
93
|
+
const sourcePattern = resolvePattern(firstPattern, context);
|
|
94
|
+
const sourcePaths = await matchPattern(sourcePattern, packageRoot);
|
|
95
|
+
flowSources.set(flow, sourcePaths);
|
|
96
|
+
}
|
|
97
|
+
return flowSources;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve pattern placeholders like {name}
|
|
101
|
+
* Note: {name} is reserved for pattern matching and is NOT replaced
|
|
102
|
+
* unless explicitly provided in the context variables
|
|
103
|
+
*/
|
|
104
|
+
function resolvePattern(pattern, context, capturedName) {
|
|
105
|
+
return pattern.replace(/{(\w+)}/g, (match, key) => {
|
|
106
|
+
// If capturedName is provided and this is {name}, use the captured value
|
|
107
|
+
if (key === 'name' && capturedName !== undefined) {
|
|
108
|
+
return capturedName;
|
|
109
|
+
}
|
|
110
|
+
// Otherwise, reserve {name} for pattern matching - don't substitute it
|
|
111
|
+
if (key === 'name') {
|
|
112
|
+
return match;
|
|
113
|
+
}
|
|
114
|
+
if (key in context.variables) {
|
|
115
|
+
return String(context.variables[key]);
|
|
116
|
+
}
|
|
117
|
+
return match;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Extract the captured {name} value from a source path that matched a pattern
|
|
122
|
+
* For example: sourcePath="rules/typescript.md", pattern="rules/{name}.md" → "typescript"
|
|
123
|
+
*/
|
|
124
|
+
function extractCapturedName(sourcePath, pattern) {
|
|
125
|
+
// Convert pattern to regex with capture group for {name}
|
|
126
|
+
const regexPattern = pattern
|
|
127
|
+
.replace(/\{name\}/g, '([^/]+)')
|
|
128
|
+
.replace(/\*/g, '.*')
|
|
129
|
+
.replace(/\./g, '\\.');
|
|
130
|
+
const regex = new RegExp('^' + regexPattern + '$');
|
|
131
|
+
const match = sourcePath.match(regex);
|
|
132
|
+
if (match && match[1]) {
|
|
133
|
+
return match[1];
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Match files against a pattern
|
|
139
|
+
* Supports simple patterns with {name} placeholders and * wildcards
|
|
140
|
+
* Also discovers platform-specific variant files (e.g., mcp.claude.jsonc for mcp.jsonc)
|
|
141
|
+
*/
|
|
142
|
+
async function matchPattern(pattern, baseDir) {
|
|
143
|
+
const matches = [];
|
|
144
|
+
// Fast path: no wildcards/placeholders, check exact file and platform-specific variants
|
|
145
|
+
if (!pattern.includes('*') && !pattern.includes('{')) {
|
|
146
|
+
const exactPath = join(baseDir, pattern);
|
|
147
|
+
// Check for exact match
|
|
148
|
+
if (await exists(exactPath)) {
|
|
149
|
+
matches.push(relative(baseDir, exactPath));
|
|
150
|
+
}
|
|
151
|
+
// Also check for platform-specific variants (e.g., mcp.claude.jsonc, mcp.cursor.jsonc)
|
|
152
|
+
const dirPath = dirname(exactPath);
|
|
153
|
+
const fileName = basename(exactPath);
|
|
154
|
+
const nameParts = fileName.split('.');
|
|
155
|
+
if (nameParts.length >= 2 && await exists(dirPath)) {
|
|
156
|
+
// Get all known platforms
|
|
157
|
+
const knownPlatforms = getAllPlatforms({ includeDisabled: true });
|
|
158
|
+
// For each platform, check if a platform-specific variant exists
|
|
159
|
+
// Pattern: name.platform.ext (e.g., mcp.claude.jsonc)
|
|
160
|
+
const ext = nameParts[nameParts.length - 1];
|
|
161
|
+
const baseName = nameParts.slice(0, -1).join('.');
|
|
162
|
+
for (const platform of knownPlatforms) {
|
|
163
|
+
const platformFileName = `${baseName}.${platform}.${ext}`;
|
|
164
|
+
const platformPath = join(dirPath, platformFileName);
|
|
165
|
+
if (await exists(platformPath)) {
|
|
166
|
+
matches.push(relative(baseDir, platformPath));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return matches;
|
|
171
|
+
}
|
|
172
|
+
// Globs: reuse a minimatch-based recursive walk similar to flow-executor.ts
|
|
173
|
+
const parts = pattern.split('/');
|
|
174
|
+
const globPart = parts.findIndex(p => p.includes('*'));
|
|
175
|
+
// No glob segment (e.g. {name}.md): scan the parent dir and filter
|
|
176
|
+
if (globPart === -1) {
|
|
177
|
+
const dirRel = dirname(pattern);
|
|
178
|
+
const filePattern = basename(pattern);
|
|
179
|
+
const searchDir = join(baseDir, dirRel);
|
|
180
|
+
if (!(await exists(searchDir)))
|
|
181
|
+
return [];
|
|
182
|
+
const entries = await fs.readdir(searchDir, { withFileTypes: true });
|
|
183
|
+
const regex = new RegExp('^' +
|
|
184
|
+
filePattern
|
|
185
|
+
.replace(/\{name\}/g, '([^/]+)')
|
|
186
|
+
.replace(/\./g, '\\.')
|
|
187
|
+
.replace(/\*/g, '.*') +
|
|
188
|
+
'$');
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
if (!entry.isFile())
|
|
191
|
+
continue;
|
|
192
|
+
if (!regex.test(entry.name))
|
|
193
|
+
continue;
|
|
194
|
+
matches.push(relative(baseDir, join(searchDir, entry.name)));
|
|
195
|
+
}
|
|
196
|
+
return matches;
|
|
197
|
+
}
|
|
198
|
+
const dirPath = join(baseDir, ...parts.slice(0, globPart));
|
|
199
|
+
const filePattern = parts.slice(globPart).join('/');
|
|
200
|
+
if (!(await exists(dirPath))) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
await findMatchingFiles(dirPath, filePattern, baseDir, matches);
|
|
204
|
+
return matches;
|
|
205
|
+
}
|
|
206
|
+
async function findMatchingFiles(dir, pattern, baseDir, matches) {
|
|
207
|
+
try {
|
|
208
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
const fullPath = join(dir, entry.name);
|
|
211
|
+
const rel = relative(baseDir, fullPath);
|
|
212
|
+
if (entry.isDirectory()) {
|
|
213
|
+
await findMatchingFiles(fullPath, pattern, baseDir, matches);
|
|
214
|
+
}
|
|
215
|
+
else if (entry.isFile()) {
|
|
216
|
+
if (minimatch(rel, pattern, { dot: false })) {
|
|
217
|
+
matches.push(rel);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// ignore
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function resolveTargetFromGlob(sourceAbsPath, fromPattern, toPattern, context) {
|
|
227
|
+
const sourceRelFromPackage = relative(context.packageRoot, sourceAbsPath);
|
|
228
|
+
// If 'to' pattern has glob, map the structure
|
|
229
|
+
if (toPattern.includes('*')) {
|
|
230
|
+
// Handle ** recursive patterns
|
|
231
|
+
if (fromPattern.includes('**') && toPattern.includes('**')) {
|
|
232
|
+
const fromParts = fromPattern.split('**');
|
|
233
|
+
const toParts = toPattern.split('**');
|
|
234
|
+
const fromBase = fromParts[0].replace(/\/$/, '');
|
|
235
|
+
const toBase = toParts[0].replace(/\/$/, '');
|
|
236
|
+
const fromSuffix = fromParts[1] || '';
|
|
237
|
+
const toSuffix = toParts[1] || '';
|
|
238
|
+
let relativeSubpath = sourceRelFromPackage;
|
|
239
|
+
if (fromBase) {
|
|
240
|
+
relativeSubpath = sourceRelFromPackage.startsWith(fromBase + '/')
|
|
241
|
+
? sourceRelFromPackage.slice(fromBase.length + 1)
|
|
242
|
+
: sourceRelFromPackage;
|
|
243
|
+
}
|
|
244
|
+
// Handle extension mapping if suffixes specify extensions: /**/*.md -> /**/*.mdc
|
|
245
|
+
if (fromSuffix && toSuffix) {
|
|
246
|
+
const fromExt = fromSuffix.replace(/^\/?\*+/, '');
|
|
247
|
+
const toExt = toSuffix.replace(/^\/?\*+/, '');
|
|
248
|
+
if (fromExt && toExt && fromExt !== toExt) {
|
|
249
|
+
relativeSubpath = relativeSubpath.replace(new RegExp(fromExt.replace('.', '\\.') + '$'), toExt);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const targetPath = toBase ? join(toBase, relativeSubpath) : relativeSubpath;
|
|
253
|
+
return join(context.workspaceRoot, targetPath);
|
|
254
|
+
}
|
|
255
|
+
// Single-level * patterns
|
|
256
|
+
const sourceExt = extname(sourceAbsPath);
|
|
257
|
+
const sourceBase = basename(sourceAbsPath, sourceExt);
|
|
258
|
+
const toParts = toPattern.split('*');
|
|
259
|
+
const toPrefix = toParts[0];
|
|
260
|
+
const toSuffix = toParts[1] || '';
|
|
261
|
+
const targetExt = toSuffix.startsWith('.') ? toSuffix : (sourceExt + toSuffix);
|
|
262
|
+
const targetFileName = sourceBase + targetExt;
|
|
263
|
+
return join(context.workspaceRoot, toPrefix + targetFileName);
|
|
264
|
+
}
|
|
265
|
+
// No glob in target - use as-is
|
|
266
|
+
return join(context.workspaceRoot, toPattern);
|
|
267
|
+
}
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// Flow Execution
|
|
270
|
+
// ============================================================================
|
|
271
|
+
/**
|
|
272
|
+
* Execute flows for a single package installation with format detection and conversion
|
|
273
|
+
*/
|
|
274
|
+
export async function installPackageWithFlows(installContext, options) {
|
|
275
|
+
const { packageName, packageRoot, workspaceRoot, platform, packageVersion, priority, dryRun } = installContext;
|
|
276
|
+
const result = {
|
|
277
|
+
success: true,
|
|
278
|
+
filesProcessed: 0,
|
|
279
|
+
filesWritten: 0,
|
|
280
|
+
conflicts: [],
|
|
281
|
+
errors: [],
|
|
282
|
+
targetPaths: [],
|
|
283
|
+
fileMapping: {}
|
|
284
|
+
};
|
|
285
|
+
try {
|
|
286
|
+
// Check if platform uses flows
|
|
287
|
+
if (!platformUsesFlows(platform, workspaceRoot)) {
|
|
288
|
+
// Fall back to subdirs-based installation
|
|
289
|
+
logger.debug(`Platform ${platform} does not use flows, skipping flow-based installation`);
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
// Phase 1: Get or detect package format
|
|
293
|
+
const packageFormat = installContext.packageFormat || await detectPackageFormatFromDirectory(packageRoot);
|
|
294
|
+
logger.info('Package format determination', {
|
|
295
|
+
providedFormat: installContext.packageFormat ? 'yes' : 'no',
|
|
296
|
+
providedType: installContext.packageFormat?.type,
|
|
297
|
+
providedPlatform: installContext.packageFormat?.platform,
|
|
298
|
+
finalType: packageFormat.type,
|
|
299
|
+
finalPlatform: packageFormat.platform
|
|
300
|
+
});
|
|
301
|
+
logger.debug('Package format', {
|
|
302
|
+
package: packageName,
|
|
303
|
+
type: packageFormat.type,
|
|
304
|
+
platform: packageFormat.platform,
|
|
305
|
+
confidence: packageFormat.confidence,
|
|
306
|
+
isNativeFormat: packageFormat.isNativeFormat,
|
|
307
|
+
nativePlatform: packageFormat.nativePlatform,
|
|
308
|
+
targetPlatform: platform,
|
|
309
|
+
source: installContext.packageFormat ? 'provided' : 'detected'
|
|
310
|
+
});
|
|
311
|
+
// Phase 2: Check if path-mapping-only installation (native format)
|
|
312
|
+
if (shouldUsePathMappingOnly(packageFormat, platform)) {
|
|
313
|
+
logger.info(`Installing ${packageName} for ${platform} with path mapping only (native format, no content transforms)`);
|
|
314
|
+
return await installWithPathMappingOnly(installContext, packageFormat, options);
|
|
315
|
+
}
|
|
316
|
+
// Phase 3: Check if direct installation (no conversion, no path mapping)
|
|
317
|
+
if (shouldInstallDirectly(packageFormat, platform)) {
|
|
318
|
+
logger.info(`Installing ${packageName} AS-IS for ${platform} platform (matching format)`);
|
|
319
|
+
return await installDirectly(installContext, packageFormat);
|
|
320
|
+
}
|
|
321
|
+
// Phase 5: Check if conversion needed
|
|
322
|
+
if (needsConversion(packageFormat, platform)) {
|
|
323
|
+
logger.info(`Converting ${packageName} from ${packageFormat.platform} to ${platform} format`);
|
|
324
|
+
return await installWithConversion(installContext, packageFormat, options);
|
|
325
|
+
}
|
|
326
|
+
// Phase 7: Standard flow-based installation (universal format)
|
|
327
|
+
// This is the original behavior for universal packages
|
|
328
|
+
logger.debug(`Standard flow-based installation for ${packageName}`);
|
|
329
|
+
// Get applicable flows
|
|
330
|
+
const flows = getApplicableFlows(platform, workspaceRoot);
|
|
331
|
+
if (flows.length === 0) {
|
|
332
|
+
logger.debug(`No flows defined for platform ${platform}`);
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
// Create flow executor
|
|
336
|
+
const executor = createFlowExecutor();
|
|
337
|
+
// Get platform definition for accessing rootFile and other metadata
|
|
338
|
+
const platformDef = getPlatformDefinition(platform, workspaceRoot);
|
|
339
|
+
// Build flow context
|
|
340
|
+
const flowContext = {
|
|
341
|
+
workspaceRoot,
|
|
342
|
+
packageRoot,
|
|
343
|
+
platform,
|
|
344
|
+
packageName,
|
|
345
|
+
direction: 'install',
|
|
346
|
+
variables: {
|
|
347
|
+
name: packageName,
|
|
348
|
+
version: packageVersion,
|
|
349
|
+
priority,
|
|
350
|
+
rootFile: platformDef.rootFile,
|
|
351
|
+
rootDir: platformDef.rootDir
|
|
352
|
+
},
|
|
353
|
+
dryRun
|
|
354
|
+
};
|
|
355
|
+
// Discover source files for each flow
|
|
356
|
+
const flowSources = await discoverFlowSources(flows, packageRoot, flowContext);
|
|
357
|
+
// Build a map of base paths to platforms that have override files
|
|
358
|
+
// This allows universal files to exclude platforms that have platform-specific overrides
|
|
359
|
+
const overridesByBasePath = new Map();
|
|
360
|
+
for (const [flow, sources] of flowSources) {
|
|
361
|
+
for (const sourceRel of sources) {
|
|
362
|
+
const parsed = parseUniversalPath(sourceRel, { allowPlatformSuffix: true });
|
|
363
|
+
const platformSuffix = parsed?.platformSuffix || extractPlatformSuffixFromFilename(sourceRel);
|
|
364
|
+
if (platformSuffix) {
|
|
365
|
+
// For universal subdir files, use the parsed baseKey
|
|
366
|
+
// For root-level files, use the stripped filename as the baseKey
|
|
367
|
+
const baseKey = parsed
|
|
368
|
+
? `${parsed.universalSubdir}/${parsed.relPath}`
|
|
369
|
+
: stripPlatformSuffixFromFilename(sourceRel);
|
|
370
|
+
if (!overridesByBasePath.has(baseKey)) {
|
|
371
|
+
overridesByBasePath.set(baseKey, new Set());
|
|
372
|
+
}
|
|
373
|
+
overridesByBasePath.get(baseKey).add(platformSuffix);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// Execute flows per *concrete source file* (avoid re-expanding globs inside executor)
|
|
378
|
+
for (const [flow, sources] of flowSources) {
|
|
379
|
+
for (const sourceRel of sources) {
|
|
380
|
+
const sourceAbs = join(packageRoot, sourceRel);
|
|
381
|
+
// Check for platform-specific file suffix (e.g., commands/foo.claude.md or mcp.claude.jsonc)
|
|
382
|
+
// Parse with allowPlatformSuffix to detect and strip platform suffix
|
|
383
|
+
const parsed = parseUniversalPath(sourceRel, { allowPlatformSuffix: true });
|
|
384
|
+
// For files without universal subdir prefix, check platform suffix directly from filename
|
|
385
|
+
const platformSuffix = parsed?.platformSuffix || extractPlatformSuffixFromFilename(sourceRel);
|
|
386
|
+
const isUniversalSubdirFile = parsed !== null;
|
|
387
|
+
// If file has platform suffix, only process for that specific platform
|
|
388
|
+
if (platformSuffix) {
|
|
389
|
+
const filePlatform = platformSuffix;
|
|
390
|
+
if (filePlatform !== platform) {
|
|
391
|
+
// This file is for a different platform, skip it
|
|
392
|
+
logger.debug(`Skipping ${sourceRel} for platform ${platform} (file is for ${filePlatform})`);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
// File is for current platform - continue processing
|
|
396
|
+
}
|
|
397
|
+
else if (isUniversalSubdirFile && parsed) {
|
|
398
|
+
// Universal file with subdir: check if there's a platform-specific override for current platform
|
|
399
|
+
const baseKey = `${parsed.universalSubdir}/${parsed.relPath}`;
|
|
400
|
+
const overridePlatforms = overridesByBasePath.get(baseKey);
|
|
401
|
+
if (overridePlatforms && overridePlatforms.has(platform)) {
|
|
402
|
+
// This universal file is overridden by a platform-specific file for this platform
|
|
403
|
+
logger.debug(`Skipping universal file ${sourceRel} for platform ${platform} (overridden by platform-specific file)`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
// Root-level file without platform suffix: check if there's a platform-specific override
|
|
409
|
+
const strippedFileName = stripPlatformSuffixFromFilename(sourceRel);
|
|
410
|
+
// Check if any file in sources is a platform-specific override for this file
|
|
411
|
+
const hasOverrideForPlatform = sources.some(s => {
|
|
412
|
+
const sSuffix = extractPlatformSuffixFromFilename(s);
|
|
413
|
+
const sStripped = stripPlatformSuffixFromFilename(s);
|
|
414
|
+
return sSuffix === platform && sStripped === strippedFileName;
|
|
415
|
+
});
|
|
416
|
+
if (hasOverrideForPlatform) {
|
|
417
|
+
// This universal file is overridden by a platform-specific file for this platform
|
|
418
|
+
logger.debug(`Skipping universal file ${sourceRel} for platform ${platform} (overridden by platform-specific file)`);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
// Use suffix-stripped path if available, otherwise use original
|
|
424
|
+
// This is the path used for flow pattern matching and target path resolution
|
|
425
|
+
const sourceRelForMapping = parsed ? `${parsed.universalSubdir}/${parsed.relPath}` : sourceRel;
|
|
426
|
+
const sourceAbsForMapping = parsed ? join(packageRoot, sourceRelForMapping) : sourceAbs;
|
|
427
|
+
const firstPattern = getFirstFromPattern(flow.from);
|
|
428
|
+
const capturedName = extractCapturedName(sourceRelForMapping, firstPattern);
|
|
429
|
+
const sourceContext = {
|
|
430
|
+
...flowContext,
|
|
431
|
+
variables: {
|
|
432
|
+
...flowContext.variables,
|
|
433
|
+
sourcePath: sourceRelForMapping,
|
|
434
|
+
sourceDir: dirname(sourceRelForMapping),
|
|
435
|
+
sourceFile: basename(sourceRelForMapping),
|
|
436
|
+
...(capturedName ? { capturedName } : {})
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
// Resolve a concrete target path so flow-executor doesn't need glob expansion.
|
|
440
|
+
// Use the suffix-stripped source path for target resolution
|
|
441
|
+
const rawToPattern = typeof flow.to === 'string' ? flow.to : Object.keys(flow.to)[0] ?? '';
|
|
442
|
+
const resolvedToPattern = resolvePattern(rawToPattern, sourceContext, capturedName);
|
|
443
|
+
const targetAbs = resolveTargetFromGlob(sourceAbsForMapping, firstPattern, resolvedToPattern, sourceContext);
|
|
444
|
+
const targetRel = relative(workspaceRoot, targetAbs);
|
|
445
|
+
// Create a concrete flow using the original source path for file reading
|
|
446
|
+
// but with target path computed from the suffix-stripped source
|
|
447
|
+
// The flow executor will resolve flow.from relative to packageRoot
|
|
448
|
+
const concreteFlow = {
|
|
449
|
+
...flow,
|
|
450
|
+
from: sourceRel, // Original source path (may have platform suffix) for file reading
|
|
451
|
+
to: targetRel // Target path computed from stripped source
|
|
452
|
+
};
|
|
453
|
+
const flowResult = await executor.executeFlow(concreteFlow, sourceContext);
|
|
454
|
+
const wasSkipped = flowResult.warnings?.includes('Flow skipped due to condition');
|
|
455
|
+
if (!wasSkipped) {
|
|
456
|
+
result.filesProcessed++;
|
|
457
|
+
}
|
|
458
|
+
if (flowResult.success && !wasSkipped) {
|
|
459
|
+
const target = typeof flowResult.target === 'string' ? flowResult.target : flowResult.target;
|
|
460
|
+
if (typeof target === 'string') {
|
|
461
|
+
result.targetPaths.push(target);
|
|
462
|
+
const targetRelFromWorkspace = relative(workspaceRoot, target);
|
|
463
|
+
if (!result.fileMapping[sourceRel])
|
|
464
|
+
result.fileMapping[sourceRel] = [];
|
|
465
|
+
const normalizedTargetRel = targetRelFromWorkspace.replace(/\\/g, '/');
|
|
466
|
+
const isKeyTrackedMerge = (flowResult.merge === 'deep' || flowResult.merge === 'shallow') &&
|
|
467
|
+
Array.isArray(flowResult.keys);
|
|
468
|
+
if (isKeyTrackedMerge) {
|
|
469
|
+
result.fileMapping[sourceRel].push({
|
|
470
|
+
target: normalizedTargetRel,
|
|
471
|
+
merge: flowResult.merge,
|
|
472
|
+
keys: flowResult.keys
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
result.fileMapping[sourceRel].push(normalizedTargetRel);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (!dryRun) {
|
|
480
|
+
result.filesWritten++;
|
|
481
|
+
}
|
|
482
|
+
if (flowResult.conflicts && flowResult.conflicts.length > 0) {
|
|
483
|
+
for (const conflict of flowResult.conflicts) {
|
|
484
|
+
const packages = [];
|
|
485
|
+
packages.push({ packageName: conflict.winner, priority: 0, chosen: true });
|
|
486
|
+
for (const loser of conflict.losers) {
|
|
487
|
+
packages.push({ packageName: loser, priority: 0, chosen: false });
|
|
488
|
+
}
|
|
489
|
+
result.conflicts.push({
|
|
490
|
+
targetPath: conflict.path,
|
|
491
|
+
packages,
|
|
492
|
+
message: `Conflict in ${conflict.path}: ${conflict.winner} overwrites ${conflict.losers.join(', ')}`
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else if (!flowResult.success) {
|
|
498
|
+
result.success = false;
|
|
499
|
+
result.errors.push({
|
|
500
|
+
flow,
|
|
501
|
+
sourcePath: sourceRel,
|
|
502
|
+
error: flowResult.error || new Error('Unknown error'),
|
|
503
|
+
message: `Failed to execute flow for ${sourceRel}: ${flowResult.error?.message || 'Unknown error'}`
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
result.success = false;
|
|
509
|
+
result.errors.push({
|
|
510
|
+
flow,
|
|
511
|
+
sourcePath: sourceRel,
|
|
512
|
+
error: error,
|
|
513
|
+
message: `Error processing ${sourceRel}: ${error.message}`
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Log results
|
|
519
|
+
if (result.filesProcessed > 0) {
|
|
520
|
+
logger.info(`Processed ${result.filesProcessed} files for ${packageName} on platform ${platform}` +
|
|
521
|
+
(dryRun ? ' (dry run)' : `, wrote ${result.filesWritten} files`));
|
|
522
|
+
}
|
|
523
|
+
// Log conflicts
|
|
524
|
+
if (result.conflicts.length > 0) {
|
|
525
|
+
logger.warn(`Detected ${result.conflicts.length} conflicts during installation`);
|
|
526
|
+
for (const conflict of result.conflicts) {
|
|
527
|
+
const winner = conflict.packages.find(p => p.chosen);
|
|
528
|
+
logger.warn(` ${toTildePath(conflict.targetPath)}: ${winner?.packageName} (priority ${winner?.priority}) overwrites ` +
|
|
529
|
+
`${conflict.packages.find(p => !p.chosen)?.packageName}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Log errors
|
|
533
|
+
if (result.errors.length > 0) {
|
|
534
|
+
logger.error(`Encountered ${result.errors.length} errors during installation`);
|
|
535
|
+
for (const error of result.errors) {
|
|
536
|
+
logger.error(` ${error.sourcePath}: ${error.message}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
result.success = false;
|
|
542
|
+
logger.error(`Failed to install package ${packageName} with flows: ${error.message}`);
|
|
543
|
+
}
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Execute flows for multiple packages with priority-based merging
|
|
548
|
+
*/
|
|
549
|
+
export async function installPackagesWithFlows(packages, workspaceRoot, platform, options) {
|
|
550
|
+
const aggregatedResult = {
|
|
551
|
+
success: true,
|
|
552
|
+
filesProcessed: 0,
|
|
553
|
+
filesWritten: 0,
|
|
554
|
+
conflicts: [],
|
|
555
|
+
errors: [],
|
|
556
|
+
targetPaths: [],
|
|
557
|
+
fileMapping: {}
|
|
558
|
+
};
|
|
559
|
+
const dryRun = options?.dryRun ?? false;
|
|
560
|
+
// Sort packages by priority (LOWER priority first, so higher priority writes last and wins)
|
|
561
|
+
const sortedPackages = [...packages].sort((a, b) => a.priority - b.priority);
|
|
562
|
+
// Track files written by each package for conflict detection
|
|
563
|
+
const fileTargets = new Map();
|
|
564
|
+
// Install each package
|
|
565
|
+
for (const pkg of sortedPackages) {
|
|
566
|
+
const installContext = {
|
|
567
|
+
packageName: pkg.packageName,
|
|
568
|
+
packageRoot: pkg.packageRoot,
|
|
569
|
+
workspaceRoot,
|
|
570
|
+
platform,
|
|
571
|
+
packageVersion: pkg.packageVersion,
|
|
572
|
+
priority: pkg.priority,
|
|
573
|
+
dryRun
|
|
574
|
+
};
|
|
575
|
+
// Get flows and discover target files to track conflicts
|
|
576
|
+
const flows = getApplicableFlows(platform, workspaceRoot);
|
|
577
|
+
const flowContext = {
|
|
578
|
+
workspaceRoot,
|
|
579
|
+
packageRoot: pkg.packageRoot,
|
|
580
|
+
platform,
|
|
581
|
+
packageName: pkg.packageName,
|
|
582
|
+
direction: 'install',
|
|
583
|
+
variables: {
|
|
584
|
+
name: pkg.packageName,
|
|
585
|
+
version: pkg.packageVersion,
|
|
586
|
+
priority: pkg.priority
|
|
587
|
+
},
|
|
588
|
+
dryRun
|
|
589
|
+
};
|
|
590
|
+
// Discover target paths for this package
|
|
591
|
+
const flowSources = await discoverFlowSources(flows, pkg.packageRoot, flowContext);
|
|
592
|
+
for (const [flow, sources] of flowSources) {
|
|
593
|
+
if (sources.length > 0) {
|
|
594
|
+
// Determine target path from flow
|
|
595
|
+
const targetPath = typeof flow.to === 'string'
|
|
596
|
+
? resolvePattern(flow.to, flowContext)
|
|
597
|
+
: Object.keys(flow.to)[0]; // For multi-target, use first target
|
|
598
|
+
// Track this package writing to this target
|
|
599
|
+
if (!fileTargets.has(targetPath)) {
|
|
600
|
+
fileTargets.set(targetPath, []);
|
|
601
|
+
}
|
|
602
|
+
fileTargets.get(targetPath).push({
|
|
603
|
+
packageName: pkg.packageName,
|
|
604
|
+
priority: pkg.priority
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const result = await installPackageWithFlows(installContext, options);
|
|
609
|
+
// Aggregate results
|
|
610
|
+
aggregatedResult.filesProcessed += result.filesProcessed;
|
|
611
|
+
aggregatedResult.filesWritten += result.filesWritten;
|
|
612
|
+
aggregatedResult.errors.push(...result.errors);
|
|
613
|
+
aggregatedResult.targetPaths.push(...(result.targetPaths ?? []));
|
|
614
|
+
for (const [source, targets] of Object.entries(result.fileMapping ?? {})) {
|
|
615
|
+
const existing = aggregatedResult.fileMapping[source] ?? [];
|
|
616
|
+
aggregatedResult.fileMapping[source] = Array.from(new Set([...existing, ...targets])).sort();
|
|
617
|
+
}
|
|
618
|
+
if (!result.success) {
|
|
619
|
+
aggregatedResult.success = false;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// Detect conflicts: files written by multiple packages
|
|
623
|
+
for (const [targetPath, writers] of fileTargets) {
|
|
624
|
+
if (writers.length > 1) {
|
|
625
|
+
// Sort by priority to determine winner
|
|
626
|
+
const sortedWriters = [...writers].sort((a, b) => b.priority - a.priority);
|
|
627
|
+
const winner = sortedWriters[0];
|
|
628
|
+
aggregatedResult.conflicts.push({
|
|
629
|
+
targetPath,
|
|
630
|
+
packages: sortedWriters.map((w, i) => ({
|
|
631
|
+
packageName: w.packageName,
|
|
632
|
+
priority: w.priority,
|
|
633
|
+
chosen: i === 0 // First in sorted list (highest priority) is chosen
|
|
634
|
+
})),
|
|
635
|
+
message: `Conflict in ${targetPath}: ${winner.packageName} (priority ${winner.priority}) overwrites ${sortedWriters.slice(1).map(w => w.packageName).join(', ')}`
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return aggregatedResult;
|
|
640
|
+
}
|
|
641
|
+
// ============================================================================
|
|
642
|
+
// Format Detection and Conversion Helpers
|
|
643
|
+
// ============================================================================
|
|
644
|
+
/**
|
|
645
|
+
* Detect package format from directory by reading files
|
|
646
|
+
*/
|
|
647
|
+
async function detectPackageFormatFromDirectory(packageRoot) {
|
|
648
|
+
const files = [];
|
|
649
|
+
// Read all files in package directory
|
|
650
|
+
try {
|
|
651
|
+
for await (const fullPath of walkFiles(packageRoot)) {
|
|
652
|
+
const relativePath = relative(packageRoot, fullPath);
|
|
653
|
+
// Skip git metadata and junk files
|
|
654
|
+
if (relativePath.startsWith('.git/') || relativePath === '.git') {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
const pathParts = relativePath.split('/');
|
|
658
|
+
const isJunk = await import('junk').then(m => m.isJunk);
|
|
659
|
+
if (pathParts.some(part => isJunk(part))) {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
files.push({
|
|
663
|
+
path: relativePath,
|
|
664
|
+
content: '' // We only need paths for format detection
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
logger.error('Failed to read package directory for format detection', { packageRoot, error });
|
|
670
|
+
}
|
|
671
|
+
return detectPackageFormat(files);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Helper to walk files in directory
|
|
675
|
+
*/
|
|
676
|
+
async function* walkFiles(dir) {
|
|
677
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
678
|
+
for (const entry of entries) {
|
|
679
|
+
const fullPath = join(dir, entry.name);
|
|
680
|
+
if (entry.isDirectory()) {
|
|
681
|
+
yield* walkFiles(fullPath);
|
|
682
|
+
}
|
|
683
|
+
else if (entry.isFile()) {
|
|
684
|
+
yield fullPath;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Install package directly without flow transformations (AS-IS installation)
|
|
690
|
+
* Used when source platform = target platform
|
|
691
|
+
*/
|
|
692
|
+
async function installDirectly(installContext, packageFormat) {
|
|
693
|
+
const { packageName, packageRoot, workspaceRoot, platform, dryRun } = installContext;
|
|
694
|
+
const result = {
|
|
695
|
+
success: true,
|
|
696
|
+
filesProcessed: 0,
|
|
697
|
+
filesWritten: 0,
|
|
698
|
+
conflicts: [],
|
|
699
|
+
errors: [],
|
|
700
|
+
targetPaths: [],
|
|
701
|
+
fileMapping: {}
|
|
702
|
+
};
|
|
703
|
+
logger.info(`Installing ${packageName} directly for ${platform} (no transformations)`);
|
|
704
|
+
try {
|
|
705
|
+
// Copy files AS-IS from package to workspace
|
|
706
|
+
for await (const sourcePath of walkFiles(packageRoot)) {
|
|
707
|
+
const relativePath = relative(packageRoot, sourcePath);
|
|
708
|
+
// Skip metadata files
|
|
709
|
+
if (relativePath.startsWith('.openpackage/') || relativePath === 'openpackage.yml') {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const targetPath = join(workspaceRoot, relativePath);
|
|
713
|
+
result.filesProcessed++;
|
|
714
|
+
if (!dryRun) {
|
|
715
|
+
await ensureDir(dirname(targetPath));
|
|
716
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
717
|
+
result.filesWritten++;
|
|
718
|
+
}
|
|
719
|
+
result.targetPaths.push(targetPath);
|
|
720
|
+
// Track file mapping for uninstall
|
|
721
|
+
if (!result.fileMapping[relativePath]) {
|
|
722
|
+
result.fileMapping[relativePath] = [];
|
|
723
|
+
}
|
|
724
|
+
result.fileMapping[relativePath].push(relativePath);
|
|
725
|
+
}
|
|
726
|
+
logger.info(`Direct installation complete: ${result.filesProcessed} files processed`);
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
logger.error('Direct installation failed', { packageName, error });
|
|
730
|
+
result.success = false;
|
|
731
|
+
result.errors.push({
|
|
732
|
+
flow: { from: packageRoot, to: workspaceRoot },
|
|
733
|
+
sourcePath: packageRoot,
|
|
734
|
+
error: error,
|
|
735
|
+
message: `Failed to install directly: ${error.message}`
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return result;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Install package with path mapping only (no content transformations)
|
|
742
|
+
*
|
|
743
|
+
* Used for native format packages where content is already correct for the target
|
|
744
|
+
* platform (e.g., Claude plugin with Claude-format frontmatter), but file paths
|
|
745
|
+
* need to be mapped from universal subdirs to platform subdirs.
|
|
746
|
+
*
|
|
747
|
+
* Strategy:
|
|
748
|
+
* 1. Get platform flows for target platform
|
|
749
|
+
* 2. Strip all content transformations (map, pipe operations)
|
|
750
|
+
* 3. Execute flows with path mapping only
|
|
751
|
+
*
|
|
752
|
+
* Example:
|
|
753
|
+
* Source: commands/test.md (Claude plugin root)
|
|
754
|
+
* Target: .claude/commands/test.md (workspace)
|
|
755
|
+
* Content: Unchanged (already in Claude format)
|
|
756
|
+
*/
|
|
757
|
+
async function installWithPathMappingOnly(installContext, packageFormat, options) {
|
|
758
|
+
const { packageName, packageRoot, workspaceRoot, platform, packageVersion, priority, dryRun } = installContext;
|
|
759
|
+
const result = {
|
|
760
|
+
success: true,
|
|
761
|
+
filesProcessed: 0,
|
|
762
|
+
filesWritten: 0,
|
|
763
|
+
conflicts: [],
|
|
764
|
+
errors: [],
|
|
765
|
+
targetPaths: [],
|
|
766
|
+
fileMapping: {}
|
|
767
|
+
};
|
|
768
|
+
logger.info(`Installing ${packageName} with path mapping only for ${platform} (native format)`);
|
|
769
|
+
try {
|
|
770
|
+
// Check if platform uses flows
|
|
771
|
+
if (!platformUsesFlows(platform, workspaceRoot)) {
|
|
772
|
+
logger.warn(`Platform ${platform} does not use flows, falling back to direct installation`);
|
|
773
|
+
return await installDirectly(installContext, packageFormat);
|
|
774
|
+
}
|
|
775
|
+
// Get platform flows
|
|
776
|
+
let flows = getApplicableFlows(platform, workspaceRoot);
|
|
777
|
+
if (flows.length === 0) {
|
|
778
|
+
logger.warn(`No flows defined for platform ${platform}, falling back to direct installation`);
|
|
779
|
+
return await installDirectly(installContext, packageFormat);
|
|
780
|
+
}
|
|
781
|
+
// Strip content transformations, keeping only path mappings
|
|
782
|
+
flows = stripContentTransformations(flows);
|
|
783
|
+
logger.debug(`Using ${flows.length} path-mapping-only flows for ${platform}`);
|
|
784
|
+
// Create flow executor
|
|
785
|
+
const executor = createFlowExecutor();
|
|
786
|
+
// Get platform definition
|
|
787
|
+
const platformDef = getPlatformDefinition(platform, workspaceRoot);
|
|
788
|
+
// Build flow context
|
|
789
|
+
const flowContext = {
|
|
790
|
+
workspaceRoot,
|
|
791
|
+
packageRoot,
|
|
792
|
+
platform,
|
|
793
|
+
packageName,
|
|
794
|
+
direction: 'install',
|
|
795
|
+
variables: {
|
|
796
|
+
name: packageName,
|
|
797
|
+
version: packageVersion,
|
|
798
|
+
priority,
|
|
799
|
+
rootFile: platformDef.rootFile,
|
|
800
|
+
rootDir: platformDef.rootDir
|
|
801
|
+
},
|
|
802
|
+
dryRun
|
|
803
|
+
};
|
|
804
|
+
// Discover source files for each flow
|
|
805
|
+
const flowSources = await discoverFlowSources(flows, packageRoot, flowContext);
|
|
806
|
+
// Build override map for platform-specific files
|
|
807
|
+
const overridesByBasePath = new Map();
|
|
808
|
+
for (const [flow, sources] of flowSources) {
|
|
809
|
+
for (const sourceRel of sources) {
|
|
810
|
+
const parsed = parseUniversalPath(sourceRel, { allowPlatformSuffix: true });
|
|
811
|
+
const platformSuffix = parsed?.platformSuffix || extractPlatformSuffixFromFilename(sourceRel);
|
|
812
|
+
if (platformSuffix) {
|
|
813
|
+
const baseKey = parsed
|
|
814
|
+
? `${parsed.universalSubdir}/${parsed.relPath}`
|
|
815
|
+
: stripPlatformSuffixFromFilename(sourceRel);
|
|
816
|
+
if (!overridesByBasePath.has(baseKey)) {
|
|
817
|
+
overridesByBasePath.set(baseKey, new Set());
|
|
818
|
+
}
|
|
819
|
+
overridesByBasePath.get(baseKey).add(platformSuffix);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
// Execute flows per source file
|
|
824
|
+
for (const [flow, sources] of flowSources) {
|
|
825
|
+
for (const sourceRel of sources) {
|
|
826
|
+
const sourceAbs = join(packageRoot, sourceRel);
|
|
827
|
+
// Check for platform-specific file suffix
|
|
828
|
+
const parsed = parseUniversalPath(sourceRel, { allowPlatformSuffix: true });
|
|
829
|
+
const platformSuffix = parsed?.platformSuffix || extractPlatformSuffixFromFilename(sourceRel);
|
|
830
|
+
const isUniversalSubdirFile = parsed !== null;
|
|
831
|
+
// Skip files not meant for this platform
|
|
832
|
+
if (platformSuffix) {
|
|
833
|
+
const filePlatform = platformSuffix;
|
|
834
|
+
if (filePlatform !== platform) {
|
|
835
|
+
logger.debug(`Skipping ${sourceRel} for platform ${platform} (file is for ${filePlatform})`);
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
else if (isUniversalSubdirFile && parsed) {
|
|
840
|
+
const baseKey = `${parsed.universalSubdir}/${parsed.relPath}`;
|
|
841
|
+
const overridePlatforms = overridesByBasePath.get(baseKey);
|
|
842
|
+
if (overridePlatforms && overridePlatforms.has(platform)) {
|
|
843
|
+
logger.debug(`Skipping universal file ${sourceRel} for platform ${platform} (overridden by platform-specific file)`);
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
const strippedFileName = stripPlatformSuffixFromFilename(sourceRel);
|
|
849
|
+
const hasOverrideForPlatform = sources.some(s => {
|
|
850
|
+
const sSuffix = extractPlatformSuffixFromFilename(s);
|
|
851
|
+
const sStripped = stripPlatformSuffixFromFilename(s);
|
|
852
|
+
return sSuffix === platform && sStripped === strippedFileName;
|
|
853
|
+
});
|
|
854
|
+
if (hasOverrideForPlatform) {
|
|
855
|
+
logger.debug(`Skipping universal file ${sourceRel} for platform ${platform} (overridden by platform-specific file)`);
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
// Use suffix-stripped path for flow pattern matching
|
|
861
|
+
const sourceRelForMapping = parsed ? `${parsed.universalSubdir}/${parsed.relPath}` : sourceRel;
|
|
862
|
+
const sourceAbsForMapping = parsed ? join(packageRoot, sourceRelForMapping) : sourceAbs;
|
|
863
|
+
const firstPattern = getFirstFromPattern(flow.from);
|
|
864
|
+
const capturedName = extractCapturedName(sourceRelForMapping, firstPattern);
|
|
865
|
+
const sourceContext = {
|
|
866
|
+
...flowContext,
|
|
867
|
+
variables: {
|
|
868
|
+
...flowContext.variables,
|
|
869
|
+
sourcePath: sourceRelForMapping,
|
|
870
|
+
sourceDir: dirname(sourceRelForMapping),
|
|
871
|
+
sourceFile: basename(sourceRelForMapping),
|
|
872
|
+
...(capturedName ? { capturedName } : {})
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
// Resolve target path
|
|
876
|
+
const rawToPattern = typeof flow.to === 'string' ? flow.to : Object.keys(flow.to)[0] ?? '';
|
|
877
|
+
const resolvedToPattern = resolvePattern(rawToPattern, sourceContext, capturedName);
|
|
878
|
+
const targetAbs = resolveTargetFromGlob(sourceAbsForMapping, firstPattern, resolvedToPattern, sourceContext);
|
|
879
|
+
const targetRel = relative(workspaceRoot, targetAbs);
|
|
880
|
+
// Create concrete flow
|
|
881
|
+
const concreteFlow = {
|
|
882
|
+
...flow,
|
|
883
|
+
from: sourceRel,
|
|
884
|
+
to: targetRel
|
|
885
|
+
};
|
|
886
|
+
const flowResult = await executor.executeFlow(concreteFlow, sourceContext);
|
|
887
|
+
const wasSkipped = flowResult.warnings?.includes('Flow skipped due to condition');
|
|
888
|
+
if (!wasSkipped) {
|
|
889
|
+
result.filesProcessed++;
|
|
890
|
+
}
|
|
891
|
+
if (flowResult.success && !wasSkipped) {
|
|
892
|
+
const target = typeof flowResult.target === 'string' ? flowResult.target : flowResult.target;
|
|
893
|
+
if (typeof target === 'string') {
|
|
894
|
+
result.targetPaths.push(target);
|
|
895
|
+
const targetRelFromWorkspace = relative(workspaceRoot, target);
|
|
896
|
+
if (!result.fileMapping[sourceRel])
|
|
897
|
+
result.fileMapping[sourceRel] = [];
|
|
898
|
+
const normalizedTargetRel = targetRelFromWorkspace.replace(/\\/g, '/');
|
|
899
|
+
const isKeyTrackedMerge = (flowResult.merge === 'deep' || flowResult.merge === 'shallow') &&
|
|
900
|
+
Array.isArray(flowResult.keys);
|
|
901
|
+
if (isKeyTrackedMerge) {
|
|
902
|
+
result.fileMapping[sourceRel].push({
|
|
903
|
+
target: normalizedTargetRel,
|
|
904
|
+
merge: flowResult.merge,
|
|
905
|
+
keys: flowResult.keys
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
result.fileMapping[sourceRel].push(normalizedTargetRel);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (!dryRun) {
|
|
913
|
+
result.filesWritten++;
|
|
914
|
+
}
|
|
915
|
+
if (flowResult.conflicts && flowResult.conflicts.length > 0) {
|
|
916
|
+
for (const conflict of flowResult.conflicts) {
|
|
917
|
+
const packages = [];
|
|
918
|
+
packages.push({ packageName: conflict.winner, priority: 0, chosen: true });
|
|
919
|
+
for (const loser of conflict.losers) {
|
|
920
|
+
packages.push({ packageName: loser, priority: 0, chosen: false });
|
|
921
|
+
}
|
|
922
|
+
result.conflicts.push({
|
|
923
|
+
targetPath: conflict.path,
|
|
924
|
+
packages,
|
|
925
|
+
message: `Conflict in ${conflict.path}: ${conflict.winner} overwrites ${conflict.losers.join(', ')}`
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
else if (!flowResult.success) {
|
|
931
|
+
result.success = false;
|
|
932
|
+
result.errors.push({
|
|
933
|
+
flow,
|
|
934
|
+
sourcePath: sourceRel,
|
|
935
|
+
error: flowResult.error || new Error('Unknown error'),
|
|
936
|
+
message: `Failed to execute flow for ${sourceRel}: ${flowResult.error?.message || 'Unknown error'}`
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
941
|
+
result.success = false;
|
|
942
|
+
result.errors.push({
|
|
943
|
+
flow,
|
|
944
|
+
sourcePath: sourceRel,
|
|
945
|
+
error: error,
|
|
946
|
+
message: `Error processing ${sourceRel}: ${error.message}`
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
// Log results
|
|
952
|
+
if (result.filesProcessed > 0) {
|
|
953
|
+
logger.info(`Processed ${result.filesProcessed} files for ${packageName} on platform ${platform}` +
|
|
954
|
+
(dryRun ? ' (dry run)' : `, wrote ${result.filesWritten} files`));
|
|
955
|
+
}
|
|
956
|
+
// Log conflicts
|
|
957
|
+
if (result.conflicts.length > 0) {
|
|
958
|
+
logger.warn(`Detected ${result.conflicts.length} conflicts during installation`);
|
|
959
|
+
for (const conflict of result.conflicts) {
|
|
960
|
+
const winner = conflict.packages.find(p => p.chosen);
|
|
961
|
+
logger.warn(` ${toTildePath(conflict.targetPath)}: ${winner?.packageName} (priority ${winner?.priority}) overwrites ` +
|
|
962
|
+
`${conflict.packages.find(p => !p.chosen)?.packageName}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
// Log errors
|
|
966
|
+
if (result.errors.length > 0) {
|
|
967
|
+
logger.error(`Encountered ${result.errors.length} errors during installation`);
|
|
968
|
+
for (const error of result.errors) {
|
|
969
|
+
logger.error(` ${error.sourcePath}: ${error.message}`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
catch (error) {
|
|
974
|
+
result.success = false;
|
|
975
|
+
logger.error(`Failed to install package ${packageName} with path mapping: ${error.message}`);
|
|
976
|
+
result.errors.push({
|
|
977
|
+
flow: { from: packageRoot, to: workspaceRoot },
|
|
978
|
+
sourcePath: packageRoot,
|
|
979
|
+
error: error,
|
|
980
|
+
message: `Failed to install with path mapping: ${error.message}`
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
return result;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Strip content transformations from flows, keeping only path mappings
|
|
987
|
+
*
|
|
988
|
+
* Removes:
|
|
989
|
+
* - map operations (frontmatter transformations)
|
|
990
|
+
* - pipe operations (except format converters needed for file type changes)
|
|
991
|
+
*
|
|
992
|
+
* Keeps:
|
|
993
|
+
* - from/to path patterns (the core path mapping)
|
|
994
|
+
* - merge strategies (for multi-package composition)
|
|
995
|
+
* - when conditions (for conditional flows)
|
|
996
|
+
*/
|
|
997
|
+
function stripContentTransformations(flows) {
|
|
998
|
+
return flows.map(flow => {
|
|
999
|
+
const strippedFlow = {
|
|
1000
|
+
from: flow.from,
|
|
1001
|
+
to: flow.to
|
|
1002
|
+
};
|
|
1003
|
+
// Keep merge strategy if defined
|
|
1004
|
+
if (flow.merge) {
|
|
1005
|
+
strippedFlow.merge = flow.merge;
|
|
1006
|
+
}
|
|
1007
|
+
// Keep when conditions
|
|
1008
|
+
if (flow.when) {
|
|
1009
|
+
strippedFlow.when = flow.when;
|
|
1010
|
+
}
|
|
1011
|
+
// Note: pipe transforms are now handled within the map pipeline via $pipe operation
|
|
1012
|
+
// Explicitly skip map transformations (commented for clarity)
|
|
1013
|
+
// strippedFlow.map = undefined;
|
|
1014
|
+
return strippedFlow;
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Install package with format conversion
|
|
1019
|
+
* Converts from source platform format → universal → target platform format
|
|
1020
|
+
*/
|
|
1021
|
+
async function installWithConversion(installContext, packageFormat, options) {
|
|
1022
|
+
const { packageName, packageRoot, workspaceRoot, platform, dryRun } = installContext;
|
|
1023
|
+
const result = {
|
|
1024
|
+
success: true,
|
|
1025
|
+
filesProcessed: 0,
|
|
1026
|
+
filesWritten: 0,
|
|
1027
|
+
conflicts: [],
|
|
1028
|
+
errors: [],
|
|
1029
|
+
targetPaths: [],
|
|
1030
|
+
fileMapping: {}
|
|
1031
|
+
};
|
|
1032
|
+
try {
|
|
1033
|
+
// Step 1: Load package files
|
|
1034
|
+
const { readTextFile } = await import('../../utils/fs.js');
|
|
1035
|
+
const packageFiles = [];
|
|
1036
|
+
for await (const sourcePath of walkFiles(packageRoot)) {
|
|
1037
|
+
const relativePath = relative(packageRoot, sourcePath);
|
|
1038
|
+
// Skip metadata
|
|
1039
|
+
if (relativePath.startsWith('.openpackage/') || relativePath === 'openpackage.yml') {
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
const content = await readTextFile(sourcePath);
|
|
1043
|
+
packageFiles.push({ path: relativePath, content, encoding: 'utf8' });
|
|
1044
|
+
}
|
|
1045
|
+
// Step 2: Create package object
|
|
1046
|
+
const pkg = {
|
|
1047
|
+
metadata: {
|
|
1048
|
+
name: packageName,
|
|
1049
|
+
version: installContext.packageVersion
|
|
1050
|
+
},
|
|
1051
|
+
files: packageFiles,
|
|
1052
|
+
_format: packageFormat
|
|
1053
|
+
};
|
|
1054
|
+
// Step 3: Convert from source platform format to universal format
|
|
1055
|
+
const converter = createPlatformConverter(workspaceRoot);
|
|
1056
|
+
const conversionResult = await converter.convert(pkg, platform, { dryRun });
|
|
1057
|
+
if (!conversionResult.success || !conversionResult.convertedPackage) {
|
|
1058
|
+
logger.error('Package conversion failed', {
|
|
1059
|
+
package: packageName,
|
|
1060
|
+
stages: conversionResult.stages
|
|
1061
|
+
});
|
|
1062
|
+
result.success = false;
|
|
1063
|
+
result.errors.push({
|
|
1064
|
+
flow: { from: packageRoot, to: workspaceRoot },
|
|
1065
|
+
sourcePath: packageRoot,
|
|
1066
|
+
error: new Error('Conversion failed'),
|
|
1067
|
+
message: 'Failed to convert package format'
|
|
1068
|
+
});
|
|
1069
|
+
return result;
|
|
1070
|
+
}
|
|
1071
|
+
logger.info(`Conversion to universal format complete (${conversionResult.stages.length} stages), now applying ${platform} platform flows`);
|
|
1072
|
+
// Step 4: Write converted (universal format) files to temporary directory
|
|
1073
|
+
const { mkdtemp, rm } = await import('fs/promises');
|
|
1074
|
+
const { tmpdir } = await import('os');
|
|
1075
|
+
const { writeTextFile, ensureDir: ensureDirUtil } = await import('../../utils/fs.js');
|
|
1076
|
+
let tempPackageRoot = null;
|
|
1077
|
+
try {
|
|
1078
|
+
tempPackageRoot = await mkdtemp(join(tmpdir(), 'opkg-converted-'));
|
|
1079
|
+
// Write all converted files to temp directory
|
|
1080
|
+
for (const file of conversionResult.convertedPackage.files) {
|
|
1081
|
+
const filePath = join(tempPackageRoot, file.path);
|
|
1082
|
+
await ensureDirUtil(dirname(filePath));
|
|
1083
|
+
await writeTextFile(filePath, file.content);
|
|
1084
|
+
}
|
|
1085
|
+
logger.debug(`Wrote ${conversionResult.convertedPackage.files.length} converted files to temp directory`, {
|
|
1086
|
+
tempPackageRoot
|
|
1087
|
+
});
|
|
1088
|
+
// Step 5: Install from temp directory using standard flow-based installation
|
|
1089
|
+
// This will apply the target platform flows to the now-universal-format content
|
|
1090
|
+
const convertedInstallContext = {
|
|
1091
|
+
...installContext,
|
|
1092
|
+
packageRoot: tempPackageRoot,
|
|
1093
|
+
// Important: Clear packageFormat so it gets re-detected as universal format
|
|
1094
|
+
packageFormat: undefined
|
|
1095
|
+
};
|
|
1096
|
+
// Recursively call installPackageWithFlows, but with converted package root
|
|
1097
|
+
// This will apply standard platform flows (Universal → Target Platform)
|
|
1098
|
+
const installResult = await installPackageWithFlows(convertedInstallContext, options);
|
|
1099
|
+
// Cleanup temp directory
|
|
1100
|
+
if (tempPackageRoot) {
|
|
1101
|
+
await rm(tempPackageRoot, { recursive: true, force: true });
|
|
1102
|
+
}
|
|
1103
|
+
return installResult;
|
|
1104
|
+
}
|
|
1105
|
+
catch (error) {
|
|
1106
|
+
// Cleanup on error
|
|
1107
|
+
if (tempPackageRoot) {
|
|
1108
|
+
try {
|
|
1109
|
+
await rm(tempPackageRoot, { recursive: true, force: true });
|
|
1110
|
+
}
|
|
1111
|
+
catch (cleanupError) {
|
|
1112
|
+
logger.warn('Failed to cleanup temp directory after error', { tempPackageRoot, cleanupError });
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
logger.error('Failed to install converted package', { packageName, error });
|
|
1116
|
+
result.success = false;
|
|
1117
|
+
result.errors.push({
|
|
1118
|
+
flow: { from: packageRoot, to: workspaceRoot },
|
|
1119
|
+
sourcePath: packageRoot,
|
|
1120
|
+
error: error,
|
|
1121
|
+
message: `Failed to install converted package: ${error.message}`
|
|
1122
|
+
});
|
|
1123
|
+
return result;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
catch (error) {
|
|
1127
|
+
logger.error('Conversion installation failed', { packageName, error });
|
|
1128
|
+
result.success = false;
|
|
1129
|
+
result.errors.push({
|
|
1130
|
+
flow: { from: packageRoot, to: workspaceRoot },
|
|
1131
|
+
sourcePath: packageRoot,
|
|
1132
|
+
error: error,
|
|
1133
|
+
message: `Failed to install with conversion: ${error.message}`
|
|
1134
|
+
});
|
|
1135
|
+
return result;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// ============================================================================
|
|
1139
|
+
// Helper Functions
|
|
1140
|
+
// ============================================================================
|
|
1141
|
+
/**
|
|
1142
|
+
* Check if a file should be processed with flows
|
|
1143
|
+
*/
|
|
1144
|
+
export function shouldUseFlows(platform, cwd) {
|
|
1145
|
+
return platformUsesFlows(platform, cwd);
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Get flow statistics for reporting
|
|
1149
|
+
*/
|
|
1150
|
+
export function getFlowStatistics(result) {
|
|
1151
|
+
return {
|
|
1152
|
+
total: result.filesProcessed,
|
|
1153
|
+
written: result.filesWritten,
|
|
1154
|
+
conflicts: result.conflicts.length,
|
|
1155
|
+
errors: result.errors.length
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
//# sourceMappingURL=flow-based-installer.js.map
|