@blxzer/pactile 0.5.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/CHANGELOG.md +602 -0
- package/LICENSE +235 -0
- package/README.md +115 -0
- package/README.zh-CN.md +115 -0
- package/bin/compat-warning.js +11 -0
- package/bin/cstl.js +6 -0
- package/bin/pactile.js +3 -0
- package/bin/smart-search.js +38 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +362 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/commands/capability-smoke.d.ts +24 -0
- package/dist/commands/capability-smoke.d.ts.map +1 -0
- package/dist/commands/capability-smoke.js +83 -0
- package/dist/commands/capability-smoke.js.map +1 -0
- package/dist/commands/channel/adapters/claude.d.ts +29 -0
- package/dist/commands/channel/adapters/claude.d.ts.map +1 -0
- package/dist/commands/channel/adapters/claude.js +203 -0
- package/dist/commands/channel/adapters/claude.js.map +1 -0
- package/dist/commands/channel/adapters/codex.d.ts +85 -0
- package/dist/commands/channel/adapters/codex.d.ts.map +1 -0
- package/dist/commands/channel/adapters/codex.js +505 -0
- package/dist/commands/channel/adapters/codex.js.map +1 -0
- package/dist/commands/channel/adapters/index.d.ts +84 -0
- package/dist/commands/channel/adapters/index.d.ts.map +1 -0
- package/dist/commands/channel/adapters/index.js +115 -0
- package/dist/commands/channel/adapters/index.js.map +1 -0
- package/dist/commands/channel/adapters/types.d.ts +33 -0
- package/dist/commands/channel/adapters/types.d.ts.map +1 -0
- package/dist/commands/channel/adapters/types.js +2 -0
- package/dist/commands/channel/adapters/types.js.map +1 -0
- package/dist/commands/channel/agent-loader.d.ts +32 -0
- package/dist/commands/channel/agent-loader.d.ts.map +1 -0
- package/dist/commands/channel/agent-loader.js +154 -0
- package/dist/commands/channel/agent-loader.js.map +1 -0
- package/dist/commands/channel/context-loader.d.ts +26 -0
- package/dist/commands/channel/context-loader.d.ts.map +1 -0
- package/dist/commands/channel/context-loader.js +290 -0
- package/dist/commands/channel/context-loader.js.map +1 -0
- package/dist/commands/channel/context.d.ts +16 -0
- package/dist/commands/channel/context.d.ts.map +1 -0
- package/dist/commands/channel/context.js +83 -0
- package/dist/commands/channel/context.js.map +1 -0
- package/dist/commands/channel/create.d.ts +27 -0
- package/dist/commands/channel/create.d.ts.map +1 -0
- package/dist/commands/channel/create.js +39 -0
- package/dist/commands/channel/create.js.map +1 -0
- package/dist/commands/channel/dev-parse-trace.d.ts +14 -0
- package/dist/commands/channel/dev-parse-trace.d.ts.map +1 -0
- package/dist/commands/channel/dev-parse-trace.js +70 -0
- package/dist/commands/channel/dev-parse-trace.js.map +1 -0
- package/dist/commands/channel/guard.d.ts +150 -0
- package/dist/commands/channel/guard.d.ts.map +1 -0
- package/dist/commands/channel/guard.js +475 -0
- package/dist/commands/channel/guard.js.map +1 -0
- package/dist/commands/channel/index.d.ts +3 -0
- package/dist/commands/channel/index.d.ts.map +1 -0
- package/dist/commands/channel/index.js +531 -0
- package/dist/commands/channel/index.js.map +1 -0
- package/dist/commands/channel/interrupt.d.ts +10 -0
- package/dist/commands/channel/interrupt.d.ts.map +1 -0
- package/dist/commands/channel/interrupt.js +22 -0
- package/dist/commands/channel/interrupt.js.map +1 -0
- package/dist/commands/channel/kill.d.ts +7 -0
- package/dist/commands/channel/kill.d.ts.map +1 -0
- package/dist/commands/channel/kill.js +121 -0
- package/dist/commands/channel/kill.js.map +1 -0
- package/dist/commands/channel/list.d.ts +17 -0
- package/dist/commands/channel/list.d.ts.map +1 -0
- package/dist/commands/channel/list.js +233 -0
- package/dist/commands/channel/list.js.map +1 -0
- package/dist/commands/channel/messages.d.ts +15 -0
- package/dist/commands/channel/messages.d.ts.map +1 -0
- package/dist/commands/channel/messages.js +245 -0
- package/dist/commands/channel/messages.js.map +1 -0
- package/dist/commands/channel/rm.d.ts +27 -0
- package/dist/commands/channel/rm.d.ts.map +1 -0
- package/dist/commands/channel/rm.js +216 -0
- package/dist/commands/channel/rm.js.map +1 -0
- package/dist/commands/channel/run.d.ts +30 -0
- package/dist/commands/channel/run.d.ts.map +1 -0
- package/dist/commands/channel/run.js +130 -0
- package/dist/commands/channel/run.js.map +1 -0
- package/dist/commands/channel/send.d.ts +11 -0
- package/dist/commands/channel/send.d.ts.map +1 -0
- package/dist/commands/channel/send.js +24 -0
- package/dist/commands/channel/send.js.map +1 -0
- package/dist/commands/channel/spawn.d.ts +40 -0
- package/dist/commands/channel/spawn.d.ts.map +1 -0
- package/dist/commands/channel/spawn.js +242 -0
- package/dist/commands/channel/spawn.js.map +1 -0
- package/dist/commands/channel/store/events.d.ts +39 -0
- package/dist/commands/channel/store/events.d.ts.map +1 -0
- package/dist/commands/channel/store/events.js +87 -0
- package/dist/commands/channel/store/events.js.map +1 -0
- package/dist/commands/channel/store/filter.d.ts +3 -0
- package/dist/commands/channel/store/filter.d.ts.map +1 -0
- package/dist/commands/channel/store/filter.js +2 -0
- package/dist/commands/channel/store/filter.js.map +1 -0
- package/dist/commands/channel/store/lock.d.ts +23 -0
- package/dist/commands/channel/store/lock.d.ts.map +1 -0
- package/dist/commands/channel/store/lock.js +99 -0
- package/dist/commands/channel/store/lock.js.map +1 -0
- package/dist/commands/channel/store/paths.d.ts +63 -0
- package/dist/commands/channel/store/paths.d.ts.map +1 -0
- package/dist/commands/channel/store/paths.js +247 -0
- package/dist/commands/channel/store/paths.js.map +1 -0
- package/dist/commands/channel/store/schema.d.ts +27 -0
- package/dist/commands/channel/store/schema.d.ts.map +1 -0
- package/dist/commands/channel/store/schema.js +34 -0
- package/dist/commands/channel/store/schema.js.map +1 -0
- package/dist/commands/channel/store/thread-state.d.ts +5 -0
- package/dist/commands/channel/store/thread-state.d.ts.map +1 -0
- package/dist/commands/channel/store/thread-state.js +16 -0
- package/dist/commands/channel/store/thread-state.js.map +1 -0
- package/dist/commands/channel/store/watch.d.ts +19 -0
- package/dist/commands/channel/store/watch.d.ts.map +1 -0
- package/dist/commands/channel/store/watch.js +146 -0
- package/dist/commands/channel/store/watch.js.map +1 -0
- package/dist/commands/channel/supervisor/idle.d.ts +46 -0
- package/dist/commands/channel/supervisor/idle.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/idle.js +72 -0
- package/dist/commands/channel/supervisor/idle.js.map +1 -0
- package/dist/commands/channel/supervisor/inbox.d.ts +30 -0
- package/dist/commands/channel/supervisor/inbox.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/inbox.js +160 -0
- package/dist/commands/channel/supervisor/inbox.js.map +1 -0
- package/dist/commands/channel/supervisor/shutdown.d.ts +68 -0
- package/dist/commands/channel/supervisor/shutdown.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/shutdown.js +146 -0
- package/dist/commands/channel/supervisor/shutdown.js.map +1 -0
- package/dist/commands/channel/supervisor/stdout.d.ts +51 -0
- package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/stdout.js +121 -0
- package/dist/commands/channel/supervisor/stdout.js.map +1 -0
- package/dist/commands/channel/supervisor/turns.d.ts +31 -0
- package/dist/commands/channel/supervisor/turns.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/turns.js +45 -0
- package/dist/commands/channel/supervisor/turns.js.map +1 -0
- package/dist/commands/channel/supervisor/warning.d.ts +48 -0
- package/dist/commands/channel/supervisor/warning.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/warning.js +77 -0
- package/dist/commands/channel/supervisor/warning.js.map +1 -0
- package/dist/commands/channel/supervisor.d.ts +59 -0
- package/dist/commands/channel/supervisor.d.ts.map +1 -0
- package/dist/commands/channel/supervisor.js +345 -0
- package/dist/commands/channel/supervisor.js.map +1 -0
- package/dist/commands/channel/text-body.d.ts +13 -0
- package/dist/commands/channel/text-body.d.ts.map +1 -0
- package/dist/commands/channel/text-body.js +47 -0
- package/dist/commands/channel/text-body.js.map +1 -0
- package/dist/commands/channel/threads.d.ts +39 -0
- package/dist/commands/channel/threads.d.ts.map +1 -0
- package/dist/commands/channel/threads.js +106 -0
- package/dist/commands/channel/threads.js.map +1 -0
- package/dist/commands/channel/title.d.ts +12 -0
- package/dist/commands/channel/title.d.ts.map +1 -0
- package/dist/commands/channel/title.js +24 -0
- package/dist/commands/channel/title.js.map +1 -0
- package/dist/commands/channel/wait.d.ts +17 -0
- package/dist/commands/channel/wait.d.ts.map +1 -0
- package/dist/commands/channel/wait.js +75 -0
- package/dist/commands/channel/wait.js.map +1 -0
- package/dist/commands/detach.d.ts +7 -0
- package/dist/commands/detach.d.ts.map +1 -0
- package/dist/commands/detach.js +31 -0
- package/dist/commands/detach.js.map +1 -0
- package/dist/commands/init.d.ts +51 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +1660 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/migrate.d.ts +10 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +38 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/purge.d.ts +9 -0
- package/dist/commands/purge.d.ts.map +1 -0
- package/dist/commands/purge.js +29 -0
- package/dist/commands/purge.js.map +1 -0
- package/dist/commands/rollback.d.ts +7 -0
- package/dist/commands/rollback.d.ts.map +1 -0
- package/dist/commands/rollback.js +27 -0
- package/dist/commands/rollback.js.map +1 -0
- package/dist/commands/rollout.d.ts +34 -0
- package/dist/commands/rollout.d.ts.map +1 -0
- package/dist/commands/rollout.js +102 -0
- package/dist/commands/rollout.js.map +1 -0
- package/dist/commands/uninstall.d.ts +23 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +68 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/update.d.ts +139 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +2459 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/upgrade.d.ts +28 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +84 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/validate-rules.d.ts +6 -0
- package/dist/commands/validate-rules.d.ts.map +1 -0
- package/dist/commands/validate-rules.js +33 -0
- package/dist/commands/validate-rules.js.map +1 -0
- package/dist/commands/workflow.d.ts +35 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +220 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/configurators/cursor.d.ts +21 -0
- package/dist/configurators/cursor.d.ts.map +1 -0
- package/dist/configurators/cursor.js +51 -0
- package/dist/configurators/cursor.js.map +1 -0
- package/dist/configurators/index.d.ts +68 -0
- package/dist/configurators/index.d.ts.map +1 -0
- package/dist/configurators/index.js +170 -0
- package/dist/configurators/index.js.map +1 -0
- package/dist/configurators/shared.d.ts +200 -0
- package/dist/configurators/shared.d.ts.map +1 -0
- package/dist/configurators/shared.js +602 -0
- package/dist/configurators/shared.js.map +1 -0
- package/dist/configurators/workflow.d.ts +45 -0
- package/dist/configurators/workflow.d.ts.map +1 -0
- package/dist/configurators/workflow.js +256 -0
- package/dist/configurators/workflow.js.map +1 -0
- package/dist/constants/paths.d.ts +93 -0
- package/dist/constants/paths.d.ts.map +1 -0
- package/dist/constants/paths.js +106 -0
- package/dist/constants/paths.js.map +1 -0
- package/dist/constants/version.d.ts +9 -0
- package/dist/constants/version.d.ts.map +1 -0
- package/dist/constants/version.js +15 -0
- package/dist/constants/version.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/index.d.ts +62 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +187 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/manifests/0.1.0.json +9 -0
- package/dist/migrations/manifests/0.1.1.json +9 -0
- package/dist/migrations/manifests/0.1.2.json +9 -0
- package/dist/migrations/manifests/0.1.3.json +9 -0
- package/dist/migrations/manifests/0.1.4.json +9 -0
- package/dist/migrations/manifests/0.2.1.json +9 -0
- package/dist/migrations/manifests/0.2.10.json +374 -0
- package/dist/migrations/manifests/0.2.2.json +9 -0
- package/dist/migrations/manifests/0.2.3.json +9 -0
- package/dist/migrations/manifests/0.2.4.json +9 -0
- package/dist/migrations/manifests/0.2.5.json +9 -0
- package/dist/migrations/manifests/0.2.6.json +9 -0
- package/dist/migrations/manifests/0.2.7.json +9 -0
- package/dist/migrations/manifests/0.2.8.json +9 -0
- package/dist/migrations/manifests/0.2.9.json +9 -0
- package/dist/migrations/manifests/0.3.0.json +89 -0
- package/dist/migrations/manifests/0.3.1.json +32 -0
- package/dist/migrations/manifests/0.3.2.json +16 -0
- package/dist/migrations/manifests/0.3.3.json +9 -0
- package/dist/migrations/manifests/0.3.4.json +9 -0
- package/dist/migrations/manifests/0.3.5.json +9 -0
- package/dist/migrations/manifests/0.3.6.json +9 -0
- package/dist/migrations/manifests/0.4.0.json +9 -0
- package/dist/migrations/manifests/0.4.1.json +9 -0
- package/dist/migrations/manifests/0.4.2.json +9 -0
- package/dist/migrations/manifests/0.4.3.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.0.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.1.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.2.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.3.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.4.json +9 -0
- package/dist/migrations/manifests/0.5.0-beta.5.json +9 -0
- package/dist/pactile/adapters/codex/index.d.ts +75 -0
- package/dist/pactile/adapters/codex/index.d.ts.map +1 -0
- package/dist/pactile/adapters/codex/index.js +269 -0
- package/dist/pactile/adapters/codex/index.js.map +1 -0
- package/dist/pactile/adapters/cursor/index.d.ts +60 -0
- package/dist/pactile/adapters/cursor/index.d.ts.map +1 -0
- package/dist/pactile/adapters/cursor/index.js +186 -0
- package/dist/pactile/adapters/cursor/index.js.map +1 -0
- package/dist/pactile/adapters/index.d.ts +4 -0
- package/dist/pactile/adapters/index.d.ts.map +1 -0
- package/dist/pactile/adapters/index.js +4 -0
- package/dist/pactile/adapters/index.js.map +1 -0
- package/dist/pactile/adoption/bindings.d.ts +20 -0
- package/dist/pactile/adoption/bindings.d.ts.map +1 -0
- package/dist/pactile/adoption/bindings.js +99 -0
- package/dist/pactile/adoption/bindings.js.map +1 -0
- package/dist/pactile/adoption/discovery.d.ts +8 -0
- package/dist/pactile/adoption/discovery.d.ts.map +1 -0
- package/dist/pactile/adoption/discovery.js +159 -0
- package/dist/pactile/adoption/discovery.js.map +1 -0
- package/dist/pactile/adoption/inventory.d.ts +28 -0
- package/dist/pactile/adoption/inventory.d.ts.map +1 -0
- package/dist/pactile/adoption/inventory.js +261 -0
- package/dist/pactile/adoption/inventory.js.map +1 -0
- package/dist/pactile/adoption/safety.d.ts +19 -0
- package/dist/pactile/adoption/safety.d.ts.map +1 -0
- package/dist/pactile/adoption/safety.js +88 -0
- package/dist/pactile/adoption/safety.js.map +1 -0
- package/dist/pactile/adoption/workflow.d.ts +31 -0
- package/dist/pactile/adoption/workflow.d.ts.map +1 -0
- package/dist/pactile/adoption/workflow.js +259 -0
- package/dist/pactile/adoption/workflow.js.map +1 -0
- package/dist/pactile/compat/cli-options.d.ts +9 -0
- package/dist/pactile/compat/cli-options.d.ts.map +1 -0
- package/dist/pactile/compat/cli-options.js +9 -0
- package/dist/pactile/compat/cli-options.js.map +1 -0
- package/dist/pactile/compat/init-context.d.ts +16 -0
- package/dist/pactile/compat/init-context.d.ts.map +1 -0
- package/dist/pactile/compat/init-context.js +87 -0
- package/dist/pactile/compat/init-context.js.map +1 -0
- package/dist/pactile/compat/legacy-migrations.d.ts +8 -0
- package/dist/pactile/compat/legacy-migrations.d.ts.map +1 -0
- package/dist/pactile/compat/legacy-migrations.js +17 -0
- package/dist/pactile/compat/legacy-migrations.js.map +1 -0
- package/dist/pactile/compat/retired-alternate-client.d.ts +42 -0
- package/dist/pactile/compat/retired-alternate-client.d.ts.map +1 -0
- package/dist/pactile/compat/retired-alternate-client.js +153 -0
- package/dist/pactile/compat/retired-alternate-client.js.map +1 -0
- package/dist/pactile/compat/update-residue.d.ts +4 -0
- package/dist/pactile/compat/update-residue.d.ts.map +1 -0
- package/dist/pactile/compat/update-residue.js +55 -0
- package/dist/pactile/compat/update-residue.js.map +1 -0
- package/dist/pactile/exit/index.d.ts +3 -0
- package/dist/pactile/exit/index.d.ts.map +1 -0
- package/dist/pactile/exit/index.js +3 -0
- package/dist/pactile/exit/index.js.map +1 -0
- package/dist/pactile/exit/receipt.d.ts +23 -0
- package/dist/pactile/exit/receipt.d.ts.map +1 -0
- package/dist/pactile/exit/receipt.js +64 -0
- package/dist/pactile/exit/receipt.js.map +1 -0
- package/dist/pactile/exit/service.d.ts +107 -0
- package/dist/pactile/exit/service.d.ts.map +1 -0
- package/dist/pactile/exit/service.js +774 -0
- package/dist/pactile/exit/service.js.map +1 -0
- package/dist/pactile/index.d.ts +26 -0
- package/dist/pactile/index.d.ts.map +1 -0
- package/dist/pactile/index.js +22 -0
- package/dist/pactile/index.js.map +1 -0
- package/dist/pactile/lifecycle/command-runner.d.ts +26 -0
- package/dist/pactile/lifecycle/command-runner.d.ts.map +1 -0
- package/dist/pactile/lifecycle/command-runner.js +129 -0
- package/dist/pactile/lifecycle/command-runner.js.map +1 -0
- package/dist/pactile/lifecycle/default-adapters.d.ts +5 -0
- package/dist/pactile/lifecycle/default-adapters.d.ts.map +1 -0
- package/dist/pactile/lifecycle/default-adapters.js +136 -0
- package/dist/pactile/lifecycle/default-adapters.js.map +1 -0
- package/dist/pactile/lifecycle/index.d.ts +6 -0
- package/dist/pactile/lifecycle/index.d.ts.map +1 -0
- package/dist/pactile/lifecycle/index.js +6 -0
- package/dist/pactile/lifecycle/index.js.map +1 -0
- package/dist/pactile/lifecycle/legacy-import.d.ts +16 -0
- package/dist/pactile/lifecycle/legacy-import.d.ts.map +1 -0
- package/dist/pactile/lifecycle/legacy-import.js +228 -0
- package/dist/pactile/lifecycle/legacy-import.js.map +1 -0
- package/dist/pactile/lifecycle/orchestrator.d.ts +109 -0
- package/dist/pactile/lifecycle/orchestrator.d.ts.map +1 -0
- package/dist/pactile/lifecycle/orchestrator.js +1030 -0
- package/dist/pactile/lifecycle/orchestrator.js.map +1 -0
- package/dist/pactile/lifecycle/project-files.d.ts +28 -0
- package/dist/pactile/lifecycle/project-files.d.ts.map +1 -0
- package/dist/pactile/lifecycle/project-files.js +297 -0
- package/dist/pactile/lifecycle/project-files.js.map +1 -0
- package/dist/pactile/middleware/index.d.ts +3 -0
- package/dist/pactile/middleware/index.d.ts.map +1 -0
- package/dist/pactile/middleware/index.js +2 -0
- package/dist/pactile/middleware/index.js.map +1 -0
- package/dist/pactile/middleware/manifest-loader.d.ts +22 -0
- package/dist/pactile/middleware/manifest-loader.d.ts.map +1 -0
- package/dist/pactile/middleware/manifest-loader.js +228 -0
- package/dist/pactile/middleware/manifest-loader.js.map +1 -0
- package/dist/pactile/migration/index.d.ts +2 -0
- package/dist/pactile/migration/index.d.ts.map +1 -0
- package/dist/pactile/migration/index.js +2 -0
- package/dist/pactile/migration/index.js.map +1 -0
- package/dist/pactile/migration/transaction.d.ts +54 -0
- package/dist/pactile/migration/transaction.d.ts.map +1 -0
- package/dist/pactile/migration/transaction.js +445 -0
- package/dist/pactile/migration/transaction.js.map +1 -0
- package/dist/pactile/projection/managed-block.d.ts +32 -0
- package/dist/pactile/projection/managed-block.d.ts.map +1 -0
- package/dist/pactile/projection/managed-block.js +78 -0
- package/dist/pactile/projection/managed-block.js.map +1 -0
- package/dist/pactile/projection/planner.d.ts +65 -0
- package/dist/pactile/projection/planner.d.ts.map +1 -0
- package/dist/pactile/projection/planner.js +349 -0
- package/dist/pactile/projection/planner.js.map +1 -0
- package/dist/pactile/projection/shared/index.d.ts +38 -0
- package/dist/pactile/projection/shared/index.d.ts.map +1 -0
- package/dist/pactile/projection/shared/index.js +186 -0
- package/dist/pactile/projection/shared/index.js.map +1 -0
- package/dist/pactile/projection/store.d.ts +72 -0
- package/dist/pactile/projection/store.d.ts.map +1 -0
- package/dist/pactile/projection/store.js +669 -0
- package/dist/pactile/projection/store.js.map +1 -0
- package/dist/pactile/projection/structured-merge.d.ts +11 -0
- package/dist/pactile/projection/structured-merge.d.ts.map +1 -0
- package/dist/pactile/projection/structured-merge.js +347 -0
- package/dist/pactile/projection/structured-merge.js.map +1 -0
- package/dist/pactile/providers/index.d.ts +3 -0
- package/dist/pactile/providers/index.d.ts.map +1 -0
- package/dist/pactile/providers/index.js +3 -0
- package/dist/pactile/providers/index.js.map +1 -0
- package/dist/pactile/providers/probes.d.ts +55 -0
- package/dist/pactile/providers/probes.d.ts.map +1 -0
- package/dist/pactile/providers/probes.js +164 -0
- package/dist/pactile/providers/probes.js.map +1 -0
- package/dist/pactile/registry.d.ts +64 -0
- package/dist/pactile/registry.d.ts.map +1 -0
- package/dist/pactile/registry.js +196 -0
- package/dist/pactile/registry.js.map +1 -0
- package/dist/pactile/retrieval/boundary.d.ts +18 -0
- package/dist/pactile/retrieval/boundary.d.ts.map +1 -0
- package/dist/pactile/retrieval/boundary.js +113 -0
- package/dist/pactile/retrieval/boundary.js.map +1 -0
- package/dist/pactile/retrieval/evidence.d.ts +9 -0
- package/dist/pactile/retrieval/evidence.d.ts.map +1 -0
- package/dist/pactile/retrieval/evidence.js +327 -0
- package/dist/pactile/retrieval/evidence.js.map +1 -0
- package/dist/pactile/retrieval/index.d.ts +4 -0
- package/dist/pactile/retrieval/index.d.ts.map +1 -0
- package/dist/pactile/retrieval/index.js +4 -0
- package/dist/pactile/retrieval/index.js.map +1 -0
- package/dist/pactile/retrieval/planner.d.ts +23 -0
- package/dist/pactile/retrieval/planner.d.ts.map +1 -0
- package/dist/pactile/retrieval/planner.js +702 -0
- package/dist/pactile/retrieval/planner.js.map +1 -0
- package/dist/pactile/retrieval/types.d.ts +108 -0
- package/dist/pactile/retrieval/types.d.ts.map +1 -0
- package/dist/pactile/retrieval/types.js +36 -0
- package/dist/pactile/retrieval/types.js.map +1 -0
- package/dist/pactile/runtime/case-fold.d.ts +2 -0
- package/dist/pactile/runtime/case-fold.d.ts.map +1 -0
- package/dist/pactile/runtime/case-fold.js +31 -0
- package/dist/pactile/runtime/case-fold.js.map +1 -0
- package/dist/pactile/runtime/index.d.ts +8 -0
- package/dist/pactile/runtime/index.d.ts.map +1 -0
- package/dist/pactile/runtime/index.js +5 -0
- package/dist/pactile/runtime/index.js.map +1 -0
- package/dist/pactile/runtime/json-api.d.ts +15 -0
- package/dist/pactile/runtime/json-api.d.ts.map +1 -0
- package/dist/pactile/runtime/json-api.js +68 -0
- package/dist/pactile/runtime/json-api.js.map +1 -0
- package/dist/pactile/runtime/paths.d.ts +59 -0
- package/dist/pactile/runtime/paths.d.ts.map +1 -0
- package/dist/pactile/runtime/paths.js +201 -0
- package/dist/pactile/runtime/paths.js.map +1 -0
- package/dist/pactile/runtime/stores.d.ts +54 -0
- package/dist/pactile/runtime/stores.d.ts.map +1 -0
- package/dist/pactile/runtime/stores.js +375 -0
- package/dist/pactile/runtime/stores.js.map +1 -0
- package/dist/pactile/runtime/unicode-nfc-data.d.ts +12 -0
- package/dist/pactile/runtime/unicode-nfc-data.d.ts.map +1 -0
- package/dist/pactile/runtime/unicode-nfc-data.js +8 -0
- package/dist/pactile/runtime/unicode-nfc-data.js.map +1 -0
- package/dist/pactile/runtime/unicode-nfc.d.ts +3 -0
- package/dist/pactile/runtime/unicode-nfc.d.ts.map +1 -0
- package/dist/pactile/runtime/unicode-nfc.js +77 -0
- package/dist/pactile/runtime/unicode-nfc.js.map +1 -0
- package/dist/pactile/tiles/catalog.d.ts +35 -0
- package/dist/pactile/tiles/catalog.d.ts.map +1 -0
- package/dist/pactile/tiles/catalog.js +235 -0
- package/dist/pactile/tiles/catalog.js.map +1 -0
- package/dist/pactile/tiles/compiler.d.ts +54 -0
- package/dist/pactile/tiles/compiler.d.ts.map +1 -0
- package/dist/pactile/tiles/compiler.js +270 -0
- package/dist/pactile/tiles/compiler.js.map +1 -0
- package/dist/pactile/tiles/content/baseline/index.d.ts +5 -0
- package/dist/pactile/tiles/content/baseline/index.d.ts.map +1 -0
- package/dist/pactile/tiles/content/baseline/index.js +115 -0
- package/dist/pactile/tiles/content/baseline/index.js.map +1 -0
- package/dist/pactile/tiles/content/index.d.ts +4 -0
- package/dist/pactile/tiles/content/index.d.ts.map +1 -0
- package/dist/pactile/tiles/content/index.js +4 -0
- package/dist/pactile/tiles/content/index.js.map +1 -0
- package/dist/pactile/tiles/content/ondemand/index.d.ts +5 -0
- package/dist/pactile/tiles/content/ondemand/index.d.ts.map +1 -0
- package/dist/pactile/tiles/content/ondemand/index.js +184 -0
- package/dist/pactile/tiles/content/ondemand/index.js.map +1 -0
- package/dist/pactile/tiles/content/source.d.ts +29 -0
- package/dist/pactile/tiles/content/source.d.ts.map +1 -0
- package/dist/pactile/tiles/content/source.js +86 -0
- package/dist/pactile/tiles/content/source.js.map +1 -0
- package/dist/pactile/tiles/loader.d.ts +31 -0
- package/dist/pactile/tiles/loader.d.ts.map +1 -0
- package/dist/pactile/tiles/loader.js +139 -0
- package/dist/pactile/tiles/loader.js.map +1 -0
- package/dist/pactile/tiles/safe-yaml.d.ts +11 -0
- package/dist/pactile/tiles/safe-yaml.d.ts.map +1 -0
- package/dist/pactile/tiles/safe-yaml.js +212 -0
- package/dist/pactile/tiles/safe-yaml.js.map +1 -0
- package/dist/templates/common/bundled-skills/pactile-check/SKILL.md +161 -0
- package/dist/templates/common/bundled-skills/pactile-check/references/fowler-12-smells.md +28 -0
- package/dist/templates/common/bundled-skills/pactile-handoff/SKILL.md +117 -0
- package/dist/templates/common/bundled-skills/pactile-meta/SKILL.md +75 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/add-project-local-conventions.md +83 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-agents.md +47 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-context-loading.md +84 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-hooks.md +57 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-skills-or-commands.md +77 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-spec-structure.md +83 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-task-lifecycle.md +88 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-workflow.md +65 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/overview.md +55 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/context-injection.md +68 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/generated-files.md +83 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/overview.md +51 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/spec-system.md +102 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/task-system.md +133 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/workflow.md +75 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/workspace-memory.md +71 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/platform-files/agents.md +69 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/platform-files/hooks-and-settings.md +59 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/platform-files/overview.md +45 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/platform-files/platform-map.md +45 -0
- package/dist/templates/common/bundled-skills/pactile-meta/references/platform-files/skills-and-commands.md +108 -0
- package/dist/templates/common/bundled-skills/pactile-micro-grill/SKILL.md +68 -0
- package/dist/templates/common/bundled-skills/pactile-skill-creator/SKILL.md +47 -0
- package/dist/templates/common/bundled-skills/pactile-skill-creator/references/authoring-rules.md +171 -0
- package/dist/templates/common/bundled-skills/pactile-skill-creator/references/general-authoring.md +189 -0
- package/dist/templates/common/bundled-skills/pactile-skill-creator/references/pactile-skill-locations.md +52 -0
- package/dist/templates/common/bundled-skills/pactile-skill-creator/references/review-checklist.md +52 -0
- package/dist/templates/common/bundled-skills/pactile-spec-bootstrap/SKILL.md +41 -0
- package/dist/templates/common/bundled-skills/pactile-spec-bootstrap/references/mcp-setup.md +90 -0
- package/dist/templates/common/bundled-skills/pactile-spec-bootstrap/references/repository-analysis.md +59 -0
- package/dist/templates/common/bundled-skills/pactile-spec-bootstrap/references/spec-task-planning.md +61 -0
- package/dist/templates/common/bundled-skills/pactile-spec-bootstrap/references/spec-writing.md +70 -0
- package/dist/templates/common/bundled-skills/smart-search-cli/SKILL.md +393 -0
- package/dist/templates/common/bundled-skills/smart-search-cli/agents/openai.yaml +3 -0
- package/dist/templates/common/bundled-skills/smart-search-cli/examples/batch-search.md +98 -0
- package/dist/templates/common/bundled-skills/smart-search-cli/examples/evidence-gathering.md +89 -0
- package/dist/templates/common/bundled-skills/smart-search-cli/references/cli-contract.md +323 -0
- package/dist/templates/common/commands/continue.md +62 -0
- package/dist/templates/common/commands/finish-work.md +82 -0
- package/dist/templates/common/commands/start.md +66 -0
- package/dist/templates/common/index.d.ts +68 -0
- package/dist/templates/common/index.d.ts.map +1 -0
- package/dist/templates/common/index.js +123 -0
- package/dist/templates/common/index.js.map +1 -0
- package/dist/templates/common/optional-skills/chrome-cdp/SKILL.md +179 -0
- package/dist/templates/common/optional-skills/chrome-cdp/examples/fetch-hook-api-capture.md +149 -0
- package/dist/templates/common/optional-skills/chrome-cdp/scripts/cdp.mjs +904 -0
- package/dist/templates/common/skills/before-dev.md +35 -0
- package/dist/templates/common/skills/brainstorm.md +204 -0
- package/dist/templates/common/skills/break-loop.md +125 -0
- package/dist/templates/common/skills/update-spec.md +364 -0
- package/dist/templates/cursor/agents/pactile-check.md +58 -0
- package/dist/templates/cursor/agents/pactile-implement.md +56 -0
- package/dist/templates/cursor/agents/pactile-research.md +51 -0
- package/dist/templates/cursor/commands/handoff.md +101 -0
- package/dist/templates/cursor/fixtures/expected-rules.d.ts +14 -0
- package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -0
- package/dist/templates/cursor/fixtures/expected-rules.js +20 -0
- package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -0
- package/dist/templates/cursor/hooks.json +52 -0
- package/dist/templates/cursor/index.d.ts +19 -0
- package/dist/templates/cursor/index.d.ts.map +1 -0
- package/dist/templates/cursor/index.js +19 -0
- package/dist/templates/cursor/index.js.map +1 -0
- package/dist/templates/cursor/rules/pactile-bootstrap.mdc +18 -0
- package/dist/templates/cursor/worktrees.json +11 -0
- package/dist/templates/extract.d.ts +38 -0
- package/dist/templates/extract.d.ts.map +1 -0
- package/dist/templates/extract.js +120 -0
- package/dist/templates/extract.js.map +1 -0
- package/dist/templates/markdown/agents.md +14 -0
- package/dist/templates/markdown/framework/artifact-locale-guide.md.txt +93 -0
- package/dist/templates/markdown/framework/cursor-context-injection-guide.md.txt +85 -0
- package/dist/templates/markdown/framework/cursor-native-modes-guide.md.txt +239 -0
- package/dist/templates/markdown/framework/cursor-semantic-compliance.md.txt +41 -0
- package/dist/templates/markdown/framework/cursor-subagent-policy.md.txt +90 -0
- package/dist/templates/markdown/framework/dogfood-only-surfaces.md.txt +28 -0
- package/dist/templates/markdown/framework/execution-strategy.md.txt +45 -0
- package/dist/templates/markdown/framework/index.md.txt +31 -0
- package/dist/templates/markdown/framework/injection-budget-guide.md.txt +111 -0
- package/dist/templates/markdown/framework/internal-skills-cursor-reachability.md.txt +45 -0
- package/dist/templates/markdown/framework/middleware-protocol.md.txt +238 -0
- package/dist/templates/markdown/framework/parallel-first-execution.md.txt +116 -0
- package/dist/templates/markdown/framework/prd-grill-frontier.md.txt +105 -0
- package/dist/templates/markdown/framework/release-boundary.md.txt +30 -0
- package/dist/templates/markdown/framework/retrieval-daily-guide.md.txt +199 -0
- package/dist/templates/markdown/framework/upgrade.md.txt +15 -0
- package/dist/templates/markdown/framework/verification-strength-guide.md.txt +185 -0
- package/dist/templates/markdown/gitignore.txt +15 -0
- package/dist/templates/markdown/index.d.ts +59 -0
- package/dist/templates/markdown/index.d.ts.map +1 -0
- package/dist/templates/markdown/index.js +144 -0
- package/dist/templates/markdown/index.js.map +1 -0
- package/dist/templates/markdown/prompts/run-semantic-slice-12.md.txt +25 -0
- package/dist/templates/markdown/spec/backend/database-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/backend/directory-structure.md.txt +54 -0
- package/dist/templates/markdown/spec/backend/error-handling.md.txt +51 -0
- package/dist/templates/markdown/spec/backend/index.md.txt +38 -0
- package/dist/templates/markdown/spec/backend/logging-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/backend/quality-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/frontend/component-guidelines.md.txt +59 -0
- package/dist/templates/markdown/spec/frontend/directory-structure.md.txt +54 -0
- package/dist/templates/markdown/spec/frontend/hook-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/frontend/index.md.txt +39 -0
- package/dist/templates/markdown/spec/frontend/quality-guidelines.md.txt +51 -0
- package/dist/templates/markdown/spec/frontend/state-management.md.txt +51 -0
- package/dist/templates/markdown/spec/frontend/type-safety.md.txt +51 -0
- package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +174 -0
- package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +162 -0
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +634 -0
- package/dist/templates/markdown/spec/guides/debug-loop-guide.md.txt +227 -0
- package/dist/templates/markdown/spec/guides/durable-learning-decision-guide.md.txt +76 -0
- package/dist/templates/markdown/spec/guides/e2e-walkthrough-guide.md.txt +93 -0
- package/dist/templates/markdown/spec/guides/index.md.txt +131 -0
- package/dist/templates/markdown/spec/guides/prototype-guide.md.txt +139 -0
- package/dist/templates/markdown/spec/guides/test-discipline-guide.md.txt +179 -0
- package/dist/templates/markdown/workspace-index.md +125 -0
- package/dist/templates/markdown/worktree.yaml.txt +58 -0
- package/dist/templates/pactile/CONTEXT.md +113 -0
- package/dist/templates/pactile/config/execution-strategy-rules.json +31 -0
- package/dist/templates/pactile/config.yaml +130 -0
- package/dist/templates/pactile/docs/adr/README.md +21 -0
- package/dist/templates/pactile/gitignore.txt +35 -0
- package/dist/templates/pactile/index.d.ts +134 -0
- package/dist/templates/pactile/index.d.ts.map +1 -0
- package/dist/templates/pactile/index.js +316 -0
- package/dist/templates/pactile/index.js.map +1 -0
- package/dist/templates/pactile/modules/approval-personal/contract.md +43 -0
- package/dist/templates/pactile/modules/candidate-pool/contract.md +37 -0
- package/dist/templates/pactile/modules/catalog.d.ts +32 -0
- package/dist/templates/pactile/modules/catalog.d.ts.map +1 -0
- package/dist/templates/pactile/modules/catalog.js +46 -0
- package/dist/templates/pactile/modules/catalog.js.map +1 -0
- package/dist/templates/pactile/modules/close-basic/contract.md +38 -0
- package/dist/templates/pactile/modules/context-progressive/contract.md +44 -0
- package/dist/templates/pactile/modules/debug-recovery/contract.md +44 -0
- package/dist/templates/pactile/modules/define-basic/contract.md +39 -0
- package/dist/templates/pactile/modules/define-extended/contract.md +45 -0
- package/dist/templates/pactile/modules/execute-agent/contract.md +38 -0
- package/dist/templates/pactile/modules/independent-check/contract.md +35 -0
- package/dist/templates/pactile/modules/index.json +133 -0
- package/dist/templates/pactile/modules/intake-basic/contract.md +34 -0
- package/dist/templates/pactile/modules/observability-local/contract.md +31 -0
- package/dist/templates/pactile/modules/parent-child/contract.md +45 -0
- package/dist/templates/pactile/modules/personal-memory/contract.md +56 -0
- package/dist/templates/pactile/modules/retention-storage/contract.md +43 -0
- package/dist/templates/pactile/modules/retrieval-extended/contract.md +77 -0
- package/dist/templates/pactile/modules/session-transfer/contract.md +41 -0
- package/dist/templates/pactile/modules/spec-learning/contract.md +50 -0
- package/dist/templates/pactile/modules/vcs-integration/contract.md +46 -0
- package/dist/templates/pactile/modules/verify-basic/contract.md +38 -0
- package/dist/templates/pactile/modules/worker-orchestration/contract.md +47 -0
- package/dist/templates/pactile/pool/README.md +149 -0
- package/dist/templates/pactile/pool/items/.gitkeep +0 -0
- package/dist/templates/pactile/pool/plan.md +50 -0
- package/dist/templates/pactile/scripts/__init__.py +5 -0
- package/dist/templates/pactile/scripts/add_session.py +547 -0
- package/dist/templates/pactile/scripts/build_context_pack.py +17 -0
- package/dist/templates/pactile/scripts/build_retrieval_pack.py +376 -0
- package/dist/templates/pactile/scripts/codegraph_session_smoke.py +76 -0
- package/dist/templates/pactile/scripts/common/__init__.py +103 -0
- package/dist/templates/pactile/scripts/common/active_task.py +576 -0
- package/dist/templates/pactile/scripts/common/adapter_middleware.py +1373 -0
- package/dist/templates/pactile/scripts/common/artifact_locale.py +303 -0
- package/dist/templates/pactile/scripts/common/artifact_search.py +587 -0
- package/dist/templates/pactile/scripts/common/cli_adapter.py +135 -0
- package/dist/templates/pactile/scripts/common/cli_environment.py +155 -0
- package/dist/templates/pactile/scripts/common/codebase_retrieval_router.py +1339 -0
- package/dist/templates/pactile/scripts/common/config.py +497 -0
- package/dist/templates/pactile/scripts/common/context_pack.py +371 -0
- package/dist/templates/pactile/scripts/common/cursor_retrieval_env.py +31 -0
- package/dist/templates/pactile/scripts/common/developer.py +190 -0
- package/dist/templates/pactile/scripts/common/execution_strategy.py +268 -0
- package/dist/templates/pactile/scripts/common/full_quality.py +237 -0
- package/dist/templates/pactile/scripts/common/git.py +37 -0
- package/dist/templates/pactile/scripts/common/git_context.py +198 -0
- package/dist/templates/pactile/scripts/common/injection_budget.py +317 -0
- package/dist/templates/pactile/scripts/common/io.py +37 -0
- package/dist/templates/pactile/scripts/common/kernel_command.py +369 -0
- package/dist/templates/pactile/scripts/common/lite_context.py +280 -0
- package/dist/templates/pactile/scripts/common/log.py +45 -0
- package/dist/templates/pactile/scripts/common/ondemand_topology.py +419 -0
- package/dist/templates/pactile/scripts/common/packages_context.py +239 -0
- package/dist/templates/pactile/scripts/common/pactile_config.py +131 -0
- package/dist/templates/pactile/scripts/common/parallel_declaration.py +369 -0
- package/dist/templates/pactile/scripts/common/parent_orchestration.py +1198 -0
- package/dist/templates/pactile/scripts/common/paths.py +756 -0
- package/dist/templates/pactile/scripts/common/pool_refs.py +119 -0
- package/dist/templates/pactile/scripts/common/pool_slice.py +269 -0
- package/dist/templates/pactile/scripts/common/pool_store.py +766 -0
- package/dist/templates/pactile/scripts/common/project_file_stats.py +91 -0
- package/dist/templates/pactile/scripts/common/retrieval_adapter_metadata.py +943 -0
- package/dist/templates/pactile/scripts/common/retrieval_agent_instructions.py +123 -0
- package/dist/templates/pactile/scripts/common/retrieval_evidence.py +915 -0
- package/dist/templates/pactile/scripts/common/retrieval_pack.py +422 -0
- package/dist/templates/pactile/scripts/common/retrieval_pack_context.py +122 -0
- package/dist/templates/pactile/scripts/common/retrieval_plan_gate.py +61 -0
- package/dist/templates/pactile/scripts/common/retrieval_result_ranking.py +104 -0
- package/dist/templates/pactile/scripts/common/retrieval_tool_classification.py +166 -0
- package/dist/templates/pactile/scripts/common/safe_commit.py +285 -0
- package/dist/templates/pactile/scripts/common/semantic_plan_gate.py +53 -0
- package/dist/templates/pactile/scripts/common/session_context.py +1213 -0
- package/dist/templates/pactile/scripts/common/session_memory.py +386 -0
- package/dist/templates/pactile/scripts/common/session_pack.py +886 -0
- package/dist/templates/pactile/scripts/common/smart_search_evidence.py +514 -0
- package/dist/templates/pactile/scripts/common/smart_search_resolve.py +150 -0
- package/dist/templates/pactile/scripts/common/subagent_dispatch.py +592 -0
- package/dist/templates/pactile/scripts/common/task_context.py +241 -0
- package/dist/templates/pactile/scripts/common/task_dashboard.py +286 -0
- package/dist/templates/pactile/scripts/common/task_dependencies.py +648 -0
- package/dist/templates/pactile/scripts/common/task_gates.py +2478 -0
- package/dist/templates/pactile/scripts/common/task_map.py +703 -0
- package/dist/templates/pactile/scripts/common/task_queue.py +188 -0
- package/dist/templates/pactile/scripts/common/task_store.py +2252 -0
- package/dist/templates/pactile/scripts/common/task_utils.py +274 -0
- package/dist/templates/pactile/scripts/common/tasks.py +231 -0
- package/dist/templates/pactile/scripts/common/test_adapter_middleware.py +199 -0
- package/dist/templates/pactile/scripts/common/test_depends_mode_block.py +509 -0
- package/dist/templates/pactile/scripts/common/test_full_quality.py +197 -0
- package/dist/templates/pactile/scripts/common/test_kernel_command.py +336 -0
- package/dist/templates/pactile/scripts/common/test_lite_path.py +190 -0
- package/dist/templates/pactile/scripts/common/test_observable_defaults.py +141 -0
- package/dist/templates/pactile/scripts/common/test_ondemand_topology.py +237 -0
- package/dist/templates/pactile/scripts/common/test_parallel_declaration.py +264 -0
- package/dist/templates/pactile/scripts/common/test_pool_slice.py +198 -0
- package/dist/templates/pactile/scripts/common/test_pool_store.py +452 -0
- package/dist/templates/pactile/scripts/common/test_retrieval_arbitration.py +331 -0
- package/dist/templates/pactile/scripts/common/test_task_dashboard.py +155 -0
- package/dist/templates/pactile/scripts/common/test_task_dependencies.py +329 -0
- package/dist/templates/pactile/scripts/common/test_task_store_kernel_patch.py +163 -0
- package/dist/templates/pactile/scripts/common/types.py +110 -0
- package/dist/templates/pactile/scripts/common/workflow_phase.py +194 -0
- package/dist/templates/pactile/scripts/compile_session_pack.py +15 -0
- package/dist/templates/pactile/scripts/cursor_retrieval_probe.py +64 -0
- package/dist/templates/pactile/scripts/cursor_retrieval_probe_prompt.md +33 -0
- package/dist/templates/pactile/scripts/generate_dispatch_prompt.py +182 -0
- package/dist/templates/pactile/scripts/get_context.py +19 -0
- package/dist/templates/pactile/scripts/get_developer.py +26 -0
- package/dist/templates/pactile/scripts/hooks/linear_sync.py +243 -0
- package/dist/templates/pactile/scripts/init_developer.py +51 -0
- package/dist/templates/pactile/scripts/injection_budget_probe.py +69 -0
- package/dist/templates/pactile/scripts/pool.py +237 -0
- package/dist/templates/pactile/scripts/rank_retrieval_candidates.py +98 -0
- package/dist/templates/pactile/scripts/retrieval_probe_matrix_template.json +43 -0
- package/dist/templates/pactile/scripts/route_codebase_retrieval.py +72 -0
- package/dist/templates/pactile/scripts/run_smart_search.py +12 -0
- package/dist/templates/pactile/scripts/score_evidence.py +131 -0
- package/dist/templates/pactile/scripts/search_artifacts.py +13 -0
- package/dist/templates/pactile/scripts/search_memory.py +12 -0
- package/dist/templates/pactile/scripts/spec_health_outcomes.py +160 -0
- package/dist/templates/pactile/scripts/task.py +1118 -0
- package/dist/templates/pactile/scripts/verify_evidence_probe.py +138 -0
- package/dist/templates/pactile/tasks/.gitkeep +0 -0
- package/dist/templates/pactile/tasks/locale/en/default-prd.md +27 -0
- package/dist/templates/pactile/tasks/locale/zh/default-prd.md +27 -0
- package/dist/templates/pactile/tasks/templates/release-execution/design.md +61 -0
- package/dist/templates/pactile/tasks/templates/release-execution/handoff-template.md +29 -0
- package/dist/templates/pactile/tasks/templates/release-execution/implement.md +29 -0
- package/dist/templates/pactile/tasks/templates/release-execution/prd.md +31 -0
- package/dist/templates/pactile/tasks/templates/release-readiness/design.md +50 -0
- package/dist/templates/pactile/tasks/templates/release-readiness/handoff-template.md +39 -0
- package/dist/templates/pactile/tasks/templates/release-readiness/implement.md +29 -0
- package/dist/templates/pactile/tasks/templates/release-readiness/prd.md +31 -0
- package/dist/templates/pactile/workflow.md +64 -0
- package/dist/templates/shared-hooks/event-bridge.py +121 -0
- package/dist/templates/shared-hooks/index.d.ts +50 -0
- package/dist/templates/shared-hooks/index.d.ts.map +1 -0
- package/dist/templates/shared-hooks/index.js +96 -0
- package/dist/templates/shared-hooks/index.js.map +1 -0
- package/dist/templates/shared-hooks/inject-retrieval-plan.py +217 -0
- package/dist/templates/shared-hooks/inject-shell-session-context.py +183 -0
- package/dist/templates/shared-hooks/inject-subagent-context.py +267 -0
- package/dist/templates/shared-hooks/inject-workflow-state.py +106 -0
- package/dist/templates/shared-hooks/rename-session-for-task.py +231 -0
- package/dist/templates/shared-hooks/research-end-retrieval-pack.py +345 -0
- package/dist/templates/shared-hooks/session-start.py +355 -0
- package/dist/templates/shared-hooks/spec-write-audit.py +173 -0
- package/dist/templates/template-utils.d.ts +28 -0
- package/dist/templates/template-utils.d.ts.map +1 -0
- package/dist/templates/template-utils.js +83 -0
- package/dist/templates/template-utils.js.map +1 -0
- package/dist/types/ai-tools.d.ts +102 -0
- package/dist/types/ai-tools.d.ts.map +1 -0
- package/dist/types/ai-tools.js +60 -0
- package/dist/types/ai-tools.js.map +1 -0
- package/dist/types/migration.d.ts +125 -0
- package/dist/types/migration.d.ts.map +1 -0
- package/dist/types/migration.js +8 -0
- package/dist/types/migration.js.map +1 -0
- package/dist/utils/agents-md.d.ts +27 -0
- package/dist/utils/agents-md.d.ts.map +1 -0
- package/dist/utils/agents-md.js +70 -0
- package/dist/utils/agents-md.js.map +1 -0
- package/dist/utils/codebase-retrieval-router.d.ts +31 -0
- package/dist/utils/codebase-retrieval-router.d.ts.map +1 -0
- package/dist/utils/codebase-retrieval-router.js +88 -0
- package/dist/utils/codebase-retrieval-router.js.map +1 -0
- package/dist/utils/compare-versions.d.ts +12 -0
- package/dist/utils/compare-versions.d.ts.map +1 -0
- package/dist/utils/compare-versions.js +86 -0
- package/dist/utils/compare-versions.js.map +1 -0
- package/dist/utils/cursor-retrieval-env.d.ts +17 -0
- package/dist/utils/cursor-retrieval-env.d.ts.map +1 -0
- package/dist/utils/cursor-retrieval-env.js +19 -0
- package/dist/utils/cursor-retrieval-env.js.map +1 -0
- package/dist/utils/cwd-guard.d.ts +38 -0
- package/dist/utils/cwd-guard.d.ts.map +1 -0
- package/dist/utils/cwd-guard.js +63 -0
- package/dist/utils/cwd-guard.js.map +1 -0
- package/dist/utils/file-writer.d.ts +36 -0
- package/dist/utils/file-writer.d.ts.map +1 -0
- package/dist/utils/file-writer.js +203 -0
- package/dist/utils/file-writer.js.map +1 -0
- package/dist/utils/manifest-prune.d.ts +59 -0
- package/dist/utils/manifest-prune.d.ts.map +1 -0
- package/dist/utils/manifest-prune.js +176 -0
- package/dist/utils/manifest-prune.js.map +1 -0
- package/dist/utils/mirror-check.d.ts +22 -0
- package/dist/utils/mirror-check.d.ts.map +1 -0
- package/dist/utils/mirror-check.js +90 -0
- package/dist/utils/mirror-check.js.map +1 -0
- package/dist/utils/normalize-text.d.ts +7 -0
- package/dist/utils/normalize-text.d.ts.map +1 -0
- package/dist/utils/normalize-text.js +16 -0
- package/dist/utils/normalize-text.js.map +1 -0
- package/dist/utils/p36-upgrade.d.ts +62 -0
- package/dist/utils/p36-upgrade.d.ts.map +1 -0
- package/dist/utils/p36-upgrade.js +165 -0
- package/dist/utils/p36-upgrade.js.map +1 -0
- package/dist/utils/posix.d.ts +13 -0
- package/dist/utils/posix.d.ts.map +1 -0
- package/dist/utils/posix.js +15 -0
- package/dist/utils/posix.js.map +1 -0
- package/dist/utils/post-update-smoke.d.ts +7 -0
- package/dist/utils/post-update-smoke.d.ts.map +1 -0
- package/dist/utils/post-update-smoke.js +40 -0
- package/dist/utils/post-update-smoke.js.map +1 -0
- package/dist/utils/project-capabilities.d.ts +82 -0
- package/dist/utils/project-capabilities.d.ts.map +1 -0
- package/dist/utils/project-capabilities.js +691 -0
- package/dist/utils/project-capabilities.js.map +1 -0
- package/dist/utils/project-detector.d.ts +46 -0
- package/dist/utils/project-detector.d.ts.map +1 -0
- package/dist/utils/project-detector.js +666 -0
- package/dist/utils/project-detector.js.map +1 -0
- package/dist/utils/proxy.d.ts +25 -0
- package/dist/utils/proxy.d.ts.map +1 -0
- package/dist/utils/proxy.js +60 -0
- package/dist/utils/proxy.js.map +1 -0
- package/dist/utils/readiness.d.ts +40 -0
- package/dist/utils/readiness.d.ts.map +1 -0
- package/dist/utils/readiness.js +491 -0
- package/dist/utils/readiness.js.map +1 -0
- package/dist/utils/retrieval-agent-instructions.d.ts +10 -0
- package/dist/utils/retrieval-agent-instructions.d.ts.map +1 -0
- package/dist/utils/retrieval-agent-instructions.js +97 -0
- package/dist/utils/retrieval-agent-instructions.js.map +1 -0
- package/dist/utils/retrieval-execution-telemetry.d.ts +53 -0
- package/dist/utils/retrieval-execution-telemetry.d.ts.map +1 -0
- package/dist/utils/retrieval-execution-telemetry.js +116 -0
- package/dist/utils/retrieval-execution-telemetry.js.map +1 -0
- package/dist/utils/retrieval-result-ranking.d.ts +34 -0
- package/dist/utils/retrieval-result-ranking.d.ts.map +1 -0
- package/dist/utils/retrieval-result-ranking.js +71 -0
- package/dist/utils/retrieval-result-ranking.js.map +1 -0
- package/dist/utils/retrieval-tool-classification.d.ts +18 -0
- package/dist/utils/retrieval-tool-classification.d.ts.map +1 -0
- package/dist/utils/retrieval-tool-classification.js +118 -0
- package/dist/utils/retrieval-tool-classification.js.map +1 -0
- package/dist/utils/semantic-plan-gate.d.ts +8 -0
- package/dist/utils/semantic-plan-gate.d.ts.map +1 -0
- package/dist/utils/semantic-plan-gate.js +24 -0
- package/dist/utils/semantic-plan-gate.js.map +1 -0
- package/dist/utils/task-json.d.ts +13 -0
- package/dist/utils/task-json.d.ts.map +1 -0
- package/dist/utils/task-json.js +12 -0
- package/dist/utils/task-json.js.map +1 -0
- package/dist/utils/template-fetcher.d.ts +150 -0
- package/dist/utils/template-fetcher.d.ts.map +1 -0
- package/dist/utils/template-fetcher.js +907 -0
- package/dist/utils/template-fetcher.js.map +1 -0
- package/dist/utils/template-hash.d.ts +123 -0
- package/dist/utils/template-hash.d.ts.map +1 -0
- package/dist/utils/template-hash.js +375 -0
- package/dist/utils/template-hash.js.map +1 -0
- package/dist/utils/uninstall-scrubbers.d.ts +30 -0
- package/dist/utils/uninstall-scrubbers.d.ts.map +1 -0
- package/dist/utils/uninstall-scrubbers.js +196 -0
- package/dist/utils/uninstall-scrubbers.js.map +1 -0
- package/dist/utils/update-rollout-report.d.ts +171 -0
- package/dist/utils/update-rollout-report.d.ts.map +1 -0
- package/dist/utils/update-rollout-report.js +126 -0
- package/dist/utils/update-rollout-report.js.map +1 -0
- package/dist/utils/validate-rules.d.ts +21 -0
- package/dist/utils/validate-rules.d.ts.map +1 -0
- package/dist/utils/validate-rules.js +88 -0
- package/dist/utils/validate-rules.js.map +1 -0
- package/dist/utils/workflow-dir.d.ts +15 -0
- package/dist/utils/workflow-dir.d.ts.map +1 -0
- package/dist/utils/workflow-dir.js +33 -0
- package/dist/utils/workflow-dir.js.map +1 -0
- package/dist/utils/workflow-resolver.d.ts +86 -0
- package/dist/utils/workflow-resolver.d.ts.map +1 -0
- package/dist/utils/workflow-resolver.js +265 -0
- package/dist/utils/workflow-resolver.js.map +1 -0
- package/package.json +117 -0
- package/scripts/postinstall.js +6 -0
|
@@ -0,0 +1,2459 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
import { DIR_NAMES, FILE_NAMES, PATHS, isUserMiddlewareOverlayPath, } from "../constants/paths.js";
|
|
7
|
+
import { VERSION, PACKAGE_NAME } from "../constants/version.js";
|
|
8
|
+
import { getMigrationsForVersion, getAllMigrations, getMigrationMetadata, getConfigSectionsAddedBetween, } from "../migrations/index.js";
|
|
9
|
+
import { isWorkflowInitialized, resolveWorkflowDirName, workflowPath, } from "../utils/workflow-dir.js";
|
|
10
|
+
import { loadHashes, saveHashes, updateHashes, isTemplateModified, removeHash, renameHash, computeHash, } from "../utils/template-hash.js";
|
|
11
|
+
import { compareVersions } from "../utils/compare-versions.js";
|
|
12
|
+
import { toPosix } from "../utils/posix.js";
|
|
13
|
+
import { setupProxy } from "../utils/proxy.js";
|
|
14
|
+
import { reportUpdateReadiness, snapshotReadinessForRollout, } from "../utils/readiness.js";
|
|
15
|
+
import { collectProjectCapabilityTemplates, loadProjectCapabilities, } from "../utils/project-capabilities.js";
|
|
16
|
+
import { applyArtifactMigration, applyKernelCreate, planArtifactMigration, planWaveC, scanContractMigration, writeWaveCConfirmed, } from "@blxzer/pactile-core/task";
|
|
17
|
+
import { emptyTaskJson } from "../utils/task-json.js";
|
|
18
|
+
import { applyOfficialRetire, composeP36Plan, officialWorkPending, planOfficialSurfaceA, printP36Vernacular, p36SummaryForRollout, waveCWorkPending, } from "../utils/p36-upgrade.js";
|
|
19
|
+
// Import templates for comparison
|
|
20
|
+
import { getAllScripts, getAllPoolSkeleton,
|
|
21
|
+
// Configuration
|
|
22
|
+
configYamlTemplate, gitignoreTemplate, workflowMdTemplate, executionStrategyRulesJson, } from "../templates/pactile/index.js";
|
|
23
|
+
import { collectUserModuleTemplates } from "../templates/extract.js";
|
|
24
|
+
import { frameworkDocs } from "../templates/markdown/index.js";
|
|
25
|
+
import { ALL_MANAGED_DIRS, getConfiguredPlatforms, isManagedPath, isManagedRootDir, } from "../configurators/index.js";
|
|
26
|
+
import { getWorkflowRootTemplateFiles } from "../configurators/workflow.js";
|
|
27
|
+
import { replacePythonCommandLiterals } from "../configurators/shared.js";
|
|
28
|
+
import { pruneOrphanManifestKeys } from "../utils/manifest-prune.js";
|
|
29
|
+
import { runPostUpdateSmoke } from "../utils/post-update-smoke.js";
|
|
30
|
+
import { cleanupRetiredAlternateClientResidue } from "../pactile/compat/retired-alternate-client.js";
|
|
31
|
+
import { buildFilePlanFromChanges, createBaseRolloutReport, emitRolloutReport, lifecycleResultToSummary, migrationResultToSummary, summarizeMigrationPlan, } from "../utils/update-rollout-report.js";
|
|
32
|
+
import { collectCanonicalGenerationFiles, discoverCanonicalGenerationPaths, installedPactilePlatforms, isCanonicalGenerationPath, materializeCanonicalGeneration, seedCanonicalBuildRoot, runLifecycleCommand, } from "../pactile/lifecycle/index.js";
|
|
33
|
+
import { InstallStateStore } from "../pactile/runtime/stores.js";
|
|
34
|
+
import { filterReadOnlyLegacyMigrationItems } from "../pactile/compat/legacy-migrations.js";
|
|
35
|
+
import { printLegacyCursorSkillResidueNotice, printRetiredAlternateClientNotice, } from "../pactile/compat/update-residue.js";
|
|
36
|
+
import { LEGACY_UPDATE_BLOCK_MESSAGE } from "../pactile/compat/cli-options.js";
|
|
37
|
+
// Paths that should never be touched (true user data)
|
|
38
|
+
// spec/ is user-customized content created during init; update should never modify it
|
|
39
|
+
const PROTECTED_PATHS = [
|
|
40
|
+
`${DIR_NAMES.WORKFLOW}/${DIR_NAMES.WORKSPACE}`, // workspace/
|
|
41
|
+
`${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}`, // tasks/
|
|
42
|
+
`${DIR_NAMES.WORKFLOW}/${DIR_NAMES.SPEC}`, // spec/
|
|
43
|
+
PATHS.MIDDLEWARE, // user overlay ā never write/delete/hash
|
|
44
|
+
`${DIR_NAMES.WORKFLOW}/.developer`,
|
|
45
|
+
`${DIR_NAMES.WORKFLOW}/.current-task`,
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Check if a path is blocked by PROTECTED_PATHS
|
|
49
|
+
*/
|
|
50
|
+
function isProtectedPath(filePath) {
|
|
51
|
+
return PROTECTED_PATHS.some((pp) => filePath === pp || filePath.startsWith(pp.endsWith("/") ? pp : pp + "/"));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Collect and classify safe-file-delete migrations
|
|
55
|
+
*
|
|
56
|
+
* safe-file-delete auto-executes (no --migrate needed) when:
|
|
57
|
+
* - File exists
|
|
58
|
+
* - Content hash matches allowed_hashes
|
|
59
|
+
* - Path is not protected or in update.skip
|
|
60
|
+
*/
|
|
61
|
+
function collectSafeFileDeletes(migrations, cwd, skipPaths,
|
|
62
|
+
/**
|
|
63
|
+
* Bypass `update.skip` for safe-file-delete. Enable this for breaking releases
|
|
64
|
+
* where honoring skip would leave the project half-migrated (old files at
|
|
65
|
+
* protected paths sitting next to the new architecture forever). The hash
|
|
66
|
+
* check in `allowed_hashes` is still the ultimate safety net ā user-modified
|
|
67
|
+
* files still stay put with a "skip-modified" warning.
|
|
68
|
+
*/
|
|
69
|
+
bypassUpdateSkip = false) {
|
|
70
|
+
const safeDeletes = migrations.filter((m) => m.type === "safe-file-delete");
|
|
71
|
+
const results = [];
|
|
72
|
+
for (const item of safeDeletes) {
|
|
73
|
+
const fullPath = path.join(cwd, item.from);
|
|
74
|
+
// Check: file exists?
|
|
75
|
+
if (!fs.existsSync(fullPath)) {
|
|
76
|
+
results.push({ item, action: "skip-missing" });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Check: protected path? (user data dirs ā always protected, never bypassed)
|
|
80
|
+
if (isProtectedPath(item.from)) {
|
|
81
|
+
results.push({ item, action: "skip-protected" });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Check: update.skip? (can be bypassed for breaking releases)
|
|
85
|
+
if (!bypassUpdateSkip &&
|
|
86
|
+
skipPaths.some((skip) => item.from === skip ||
|
|
87
|
+
item.from.startsWith(skip.endsWith("/") ? skip : skip + "/"))) {
|
|
88
|
+
results.push({ item, action: "skip-update-skip" });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// Check: hash matches allowed_hashes?
|
|
92
|
+
if (!item.allowed_hashes || item.allowed_hashes.length === 0) {
|
|
93
|
+
// No allowed hashes defined ā skip for safety
|
|
94
|
+
results.push({ item, action: "skip-modified" });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
99
|
+
const fileHash = computeHash(content);
|
|
100
|
+
if (item.allowed_hashes.includes(fileHash)) {
|
|
101
|
+
results.push({ item, action: "delete" });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
results.push({ item, action: "skip-modified" });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
results.push({ item, action: "skip-missing" });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Print safe-file-delete summary
|
|
115
|
+
*/
|
|
116
|
+
function printSafeFileDeleteSummary(classified) {
|
|
117
|
+
const toDelete = classified.filter((c) => c.action === "delete");
|
|
118
|
+
const modified = classified.filter((c) => c.action === "skip-modified");
|
|
119
|
+
const updateSkip = classified.filter((c) => c.action === "skip-update-skip");
|
|
120
|
+
if (toDelete.length === 0 &&
|
|
121
|
+
modified.length === 0 &&
|
|
122
|
+
updateSkip.length === 0) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
console.log(chalk.cyan(" Deprecated commands cleanup:"));
|
|
126
|
+
if (toDelete.length > 0) {
|
|
127
|
+
for (const c of toDelete) {
|
|
128
|
+
console.log(chalk.green(` ā ${c.item.from}${c.item.description ? ` (${c.item.description})` : ""}`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (modified.length > 0) {
|
|
132
|
+
for (const c of modified) {
|
|
133
|
+
console.log(chalk.yellow(` ? ${c.item.from} (modified, skipped)`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (updateSkip.length > 0) {
|
|
137
|
+
for (const c of updateSkip) {
|
|
138
|
+
console.log(chalk.gray(` ā ${c.item.from} (skipped, update.skip)`));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
console.log("");
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Execute safe-file-delete items (delete files + clean up empty dirs)
|
|
145
|
+
*/
|
|
146
|
+
function executeSafeFileDeletes(classified, cwd) {
|
|
147
|
+
const toDelete = classified.filter((c) => c.action === "delete");
|
|
148
|
+
let deleted = 0;
|
|
149
|
+
for (const c of toDelete) {
|
|
150
|
+
const fullPath = path.join(cwd, c.item.from);
|
|
151
|
+
try {
|
|
152
|
+
// Re-check after staging. Adapter reconciliation may have touched the
|
|
153
|
+
// same host path since classification; never delete bytes whose current
|
|
154
|
+
// hash no longer matches the manifest allow-list.
|
|
155
|
+
const current = fs.readFileSync(fullPath, "utf-8");
|
|
156
|
+
if (!c.item.allowed_hashes?.includes(computeHash(current)))
|
|
157
|
+
continue;
|
|
158
|
+
fs.unlinkSync(fullPath);
|
|
159
|
+
removeHash(cwd, c.item.from);
|
|
160
|
+
cleanupEmptyDirs(cwd, path.dirname(c.item.from));
|
|
161
|
+
deleted++;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// File may have been removed between classify and execute
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return deleted;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Load update.skip paths from the canonical config file.
|
|
171
|
+
*
|
|
172
|
+
* Parses simple YAML structure:
|
|
173
|
+
* update:
|
|
174
|
+
* skip:
|
|
175
|
+
* - path1
|
|
176
|
+
* - path2
|
|
177
|
+
*
|
|
178
|
+
* @internal Exported for testing only
|
|
179
|
+
*/
|
|
180
|
+
export function loadUpdateSkipPaths(cwd) {
|
|
181
|
+
const configPath = workflowPath(cwd, "config.yaml");
|
|
182
|
+
if (!configPath || !fs.existsSync(configPath))
|
|
183
|
+
return [];
|
|
184
|
+
try {
|
|
185
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
186
|
+
const lines = content.split("\n");
|
|
187
|
+
const paths = [];
|
|
188
|
+
let inUpdate = false;
|
|
189
|
+
let inSkip = false;
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
const trimmed = line.trimEnd();
|
|
192
|
+
// Check for "update:" section (no indentation or at root level)
|
|
193
|
+
if (/^update:\s*$/.test(trimmed)) {
|
|
194
|
+
inUpdate = true;
|
|
195
|
+
inSkip = false;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// Check for "skip:" under update (indented)
|
|
199
|
+
if (inUpdate && /^\s+skip:\s*$/.test(trimmed)) {
|
|
200
|
+
inSkip = true;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
// Collect list items under skip
|
|
204
|
+
if (inSkip) {
|
|
205
|
+
const match = trimmed.match(/^\s+-\s+(.+)$/);
|
|
206
|
+
if (match) {
|
|
207
|
+
paths.push(match[1].trim().replace(/^['"]|['"]$/g, ""));
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
// If line is non-empty and not a list item, we've left the skip section
|
|
211
|
+
if (trimmed !== "" && !trimmed.startsWith("#")) {
|
|
212
|
+
inSkip = false;
|
|
213
|
+
inUpdate = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// If we're in update but hit a non-indented line, we've left the update section
|
|
217
|
+
if (inUpdate &&
|
|
218
|
+
trimmed !== "" &&
|
|
219
|
+
!trimmed.startsWith(" ") &&
|
|
220
|
+
!trimmed.startsWith("#")) {
|
|
221
|
+
inUpdate = false;
|
|
222
|
+
inSkip = false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return paths;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Config exists but failed to parse ā warn user that skip rules won't apply
|
|
229
|
+
console.warn(`Warning: failed to parse ${configPath}, update.skip rules will not be applied`);
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function normalizeNewlines(text) {
|
|
234
|
+
return text.replace(/\r\n/g, "\n");
|
|
235
|
+
}
|
|
236
|
+
function parseSkipListItemPath(line) {
|
|
237
|
+
const match = line.trimEnd().match(/^\s+-\s+(.+)$/);
|
|
238
|
+
if (!match)
|
|
239
|
+
return null;
|
|
240
|
+
return match[1].trim().replace(/^['"]|['"]$/g, "");
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* File-level `update.skip` entries that no longer protect a fork: the file on
|
|
244
|
+
* disk already matches the official template (typical after a release that
|
|
245
|
+
* absorbs a local dogfood patch). Directory skips are never treated as stale.
|
|
246
|
+
*
|
|
247
|
+
* @internal Exported for testing only
|
|
248
|
+
*/
|
|
249
|
+
export function collectStaleUpdateSkipPaths(cwd, skipPaths, officialTemplates) {
|
|
250
|
+
const stale = [];
|
|
251
|
+
for (const skip of skipPaths) {
|
|
252
|
+
if (!skip ||
|
|
253
|
+
skip.endsWith("/") ||
|
|
254
|
+
skip === `${DIR_NAMES.WORKFLOW}/config.yaml`) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const official = officialTemplates.get(skip);
|
|
258
|
+
if (official === undefined)
|
|
259
|
+
continue;
|
|
260
|
+
const fullPath = path.join(cwd, skip);
|
|
261
|
+
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile())
|
|
262
|
+
continue;
|
|
263
|
+
const disk = fs.readFileSync(fullPath, "utf-8");
|
|
264
|
+
if (normalizeNewlines(disk) === normalizeNewlines(official)) {
|
|
265
|
+
stale.push(skip);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return stale;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Drop listed paths from canonical `config.yaml` `update.skip`. If the skip list
|
|
272
|
+
* becomes empty, remove `skip:` and a now-empty `update:` key. Does not touch
|
|
273
|
+
* surrounding comments.
|
|
274
|
+
*
|
|
275
|
+
* @internal Exported for testing only
|
|
276
|
+
*/
|
|
277
|
+
export function removeUpdateSkipPathsFromConfig(cwd, pathsToRemove) {
|
|
278
|
+
if (pathsToRemove.length === 0)
|
|
279
|
+
return [];
|
|
280
|
+
const removeSet = new Set(pathsToRemove);
|
|
281
|
+
const configPath = workflowPath(cwd, "config.yaml");
|
|
282
|
+
if (!configPath || !fs.existsSync(configPath))
|
|
283
|
+
return [];
|
|
284
|
+
const original = fs.readFileSync(configPath, "utf-8");
|
|
285
|
+
const nl = original.includes("\r\n") ? "\r\n" : "\n";
|
|
286
|
+
const lines = original.split(/\r?\n/);
|
|
287
|
+
const keep = lines.map(() => true);
|
|
288
|
+
let inUpdate = false;
|
|
289
|
+
let inSkip = false;
|
|
290
|
+
let updateLine = -1;
|
|
291
|
+
let skipLine = -1;
|
|
292
|
+
const remainingSkipItems = [];
|
|
293
|
+
const removed = [];
|
|
294
|
+
for (let i = 0; i < lines.length; i++) {
|
|
295
|
+
const trimmed = lines[i].trimEnd();
|
|
296
|
+
if (/^update:\s*$/.test(trimmed)) {
|
|
297
|
+
inUpdate = true;
|
|
298
|
+
inSkip = false;
|
|
299
|
+
updateLine = i;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (inUpdate && /^\s+skip:\s*$/.test(trimmed)) {
|
|
303
|
+
inSkip = true;
|
|
304
|
+
skipLine = i;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (inSkip) {
|
|
308
|
+
const itemPath = parseSkipListItemPath(lines[i]);
|
|
309
|
+
if (itemPath !== null) {
|
|
310
|
+
if (removeSet.has(itemPath)) {
|
|
311
|
+
keep[i] = false;
|
|
312
|
+
removed.push(itemPath);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
remainingSkipItems.push(itemPath);
|
|
316
|
+
}
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (trimmed !== "" && !trimmed.startsWith("#")) {
|
|
320
|
+
inSkip = false;
|
|
321
|
+
inUpdate = false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (inUpdate &&
|
|
325
|
+
trimmed !== "" &&
|
|
326
|
+
!trimmed.startsWith(" ") &&
|
|
327
|
+
!trimmed.startsWith("#")) {
|
|
328
|
+
inUpdate = false;
|
|
329
|
+
inSkip = false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (removed.length === 0)
|
|
333
|
+
return [];
|
|
334
|
+
if (remainingSkipItems.length === 0 && skipLine >= 0) {
|
|
335
|
+
keep[skipLine] = false;
|
|
336
|
+
}
|
|
337
|
+
if (updateLine >= 0 && remainingSkipItems.length === 0) {
|
|
338
|
+
let hasOtherUpdateKey = false;
|
|
339
|
+
for (let i = updateLine + 1; i < lines.length; i++) {
|
|
340
|
+
if (!keep[i])
|
|
341
|
+
continue;
|
|
342
|
+
const trimmed = lines[i].trimEnd();
|
|
343
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
344
|
+
continue;
|
|
345
|
+
if (!trimmed.startsWith(" ") && !trimmed.startsWith("\t"))
|
|
346
|
+
break;
|
|
347
|
+
if (/^\s+\S/.test(trimmed) && !/^\s+skip:\s*$/.test(trimmed)) {
|
|
348
|
+
hasOtherUpdateKey = true;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (!hasOtherUpdateKey) {
|
|
353
|
+
keep[updateLine] = false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
let newContent = lines.filter((_, i) => keep[i]).join(nl);
|
|
357
|
+
if (original.endsWith("\n") && !newContent.endsWith("\n")) {
|
|
358
|
+
newContent += original.includes("\r\n") ? "\r\n" : "\n";
|
|
359
|
+
}
|
|
360
|
+
if (newContent !== original) {
|
|
361
|
+
fs.writeFileSync(configPath, newContent);
|
|
362
|
+
}
|
|
363
|
+
return removed;
|
|
364
|
+
}
|
|
365
|
+
function logStaleUpdateSkip(paths, dryRun) {
|
|
366
|
+
if (paths.length === 0)
|
|
367
|
+
return;
|
|
368
|
+
const prefix = dryRun
|
|
369
|
+
? "Would remove stale update.skip"
|
|
370
|
+
: "Removed stale update.skip";
|
|
371
|
+
console.log(chalk.cyan(` ${prefix} (file already matches official template):`));
|
|
372
|
+
for (const skipPath of paths) {
|
|
373
|
+
console.log(chalk.cyan(` - ${skipPath}`));
|
|
374
|
+
}
|
|
375
|
+
console.log("");
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Extract a "section" from a config.yaml-style template by sectionHeading.
|
|
379
|
+
*
|
|
380
|
+
* A section is delimited by `#---...---` separator lines (the same pattern
|
|
381
|
+
* used in the bundled `config.yaml` template). The first line inside the
|
|
382
|
+
* separator block whose `# ` content matches `sectionHeading` identifies the
|
|
383
|
+
* section; the section spans from that opening separator block through the
|
|
384
|
+
* line preceding the next `#---` separator block (or EOF).
|
|
385
|
+
*
|
|
386
|
+
* Returns the extracted text including its leading separator block, or `null`
|
|
387
|
+
* when no matching section is found.
|
|
388
|
+
*
|
|
389
|
+
* @internal Exported for testing only.
|
|
390
|
+
*/
|
|
391
|
+
export function extractConfigSection(template, sectionHeading) {
|
|
392
|
+
const lines = template.split("\n");
|
|
393
|
+
const isSeparator = (line) => /^#-{3,}\s*$/.test(line.trimEnd());
|
|
394
|
+
for (let i = 0; i < lines.length; i++) {
|
|
395
|
+
if (!isSeparator(lines[i]))
|
|
396
|
+
continue;
|
|
397
|
+
// Look ahead for `# <heading>` then another separator that closes the
|
|
398
|
+
// heading block.
|
|
399
|
+
const headingLine = lines[i + 1];
|
|
400
|
+
const closingSeparator = lines[i + 2];
|
|
401
|
+
if (headingLine === undefined || closingSeparator === undefined)
|
|
402
|
+
continue;
|
|
403
|
+
if (!headingLine.startsWith("# "))
|
|
404
|
+
continue;
|
|
405
|
+
if (!isSeparator(closingSeparator))
|
|
406
|
+
continue;
|
|
407
|
+
if (headingLine.slice(2).trim() !== sectionHeading)
|
|
408
|
+
continue;
|
|
409
|
+
// Section starts at i; find the next separator block to bound it.
|
|
410
|
+
let end = lines.length;
|
|
411
|
+
for (let j = i + 3; j < lines.length; j++) {
|
|
412
|
+
if (isSeparator(lines[j])) {
|
|
413
|
+
end = j;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return lines.slice(i, end).join("\n").replace(/\n+$/, "");
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Apply additive config.yaml sections introduced between two versions.
|
|
423
|
+
*
|
|
424
|
+
* Walks the supplied entries, dedupes by `file+sentinel`, and for each unique
|
|
425
|
+
* entry: if the user file exists and lacks the sentinel, extracts the named
|
|
426
|
+
* section from `templateContent` and appends it. Idempotent ā re-running the
|
|
427
|
+
* step on a file that already contains the sentinel is a no-op.
|
|
428
|
+
*
|
|
429
|
+
* @internal Exported for testing only.
|
|
430
|
+
*/
|
|
431
|
+
export function applyConfigSectionsAdded(entries, cwd, bundledTemplates) {
|
|
432
|
+
const seen = new Set();
|
|
433
|
+
let appended = 0;
|
|
434
|
+
for (const entry of entries) {
|
|
435
|
+
const dedupeKey = `${entry.file}::${entry.sentinel}`;
|
|
436
|
+
if (seen.has(dedupeKey))
|
|
437
|
+
continue;
|
|
438
|
+
seen.add(dedupeKey);
|
|
439
|
+
const targetPath = path.join(cwd, entry.file);
|
|
440
|
+
if (!fs.existsSync(targetPath))
|
|
441
|
+
continue;
|
|
442
|
+
let userContent;
|
|
443
|
+
try {
|
|
444
|
+
userContent = fs.readFileSync(targetPath, "utf-8");
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (userContent.includes(entry.sentinel))
|
|
450
|
+
continue;
|
|
451
|
+
const template = bundledTemplates.get(entry.file);
|
|
452
|
+
if (!template)
|
|
453
|
+
continue;
|
|
454
|
+
const section = extractConfigSection(template, entry.sectionHeading);
|
|
455
|
+
if (!section)
|
|
456
|
+
continue;
|
|
457
|
+
const separator = userContent.endsWith("\n") ? "\n" : "\n\n";
|
|
458
|
+
const newContent = userContent + separator + section + "\n";
|
|
459
|
+
try {
|
|
460
|
+
fs.writeFileSync(targetPath, newContent);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
console.log(chalk.green(` + Added config section "${entry.sectionHeading}" to ${entry.file}`));
|
|
466
|
+
appended++;
|
|
467
|
+
}
|
|
468
|
+
return { appended };
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Collect all template files that should be managed by update.
|
|
472
|
+
* Only collects templates for platforms that are already configured (have directories).
|
|
473
|
+
* Exported so tests can assert the hash set includes canonical modules and
|
|
474
|
+
* never user middleware.
|
|
475
|
+
*/
|
|
476
|
+
export function collectTemplateFiles(cwd,
|
|
477
|
+
/**
|
|
478
|
+
* Bypass `update.skip` when collecting templates. Enable this for breaking
|
|
479
|
+
* releases so new files (e.g. `continue.md` added in 0.5.0) and template
|
|
480
|
+
* updates can land even under skip-protected paths. Without this, users with
|
|
481
|
+
* `.cursor/commands/` in their skip list would silently miss new commands.
|
|
482
|
+
* Existing user customizations are still guarded at WRITE time via the
|
|
483
|
+
* "Modified by you" conflict prompt ā they can skip per-file there.
|
|
484
|
+
*/
|
|
485
|
+
bypassUpdateSkip = false) {
|
|
486
|
+
const files = new Map();
|
|
487
|
+
// Python scripts (single source of truth: getAllScripts())
|
|
488
|
+
for (const [scriptPath, content] of getAllScripts()) {
|
|
489
|
+
files.set(`${PATHS.SCRIPTS}/${scriptPath}`, content);
|
|
490
|
+
}
|
|
491
|
+
// Review-pool skeleton (mechanism only ā never overwrite user items/)
|
|
492
|
+
for (const [poolPath, content] of getAllPoolSkeleton()) {
|
|
493
|
+
files.set(`${PATHS.POOL}/${poolPath}`, content);
|
|
494
|
+
}
|
|
495
|
+
// P29 short contracts (index.json + <id>/contract.md). Not scripts.
|
|
496
|
+
for (const [modulePath, content] of collectUserModuleTemplates()) {
|
|
497
|
+
files.set(`${PATHS.MODULES}/${modulePath}`, content);
|
|
498
|
+
}
|
|
499
|
+
// Configuration
|
|
500
|
+
files.set(`${DIR_NAMES.WORKFLOW}/config.yaml`, configYamlTemplate);
|
|
501
|
+
files.set(`${DIR_NAMES.WORKFLOW}/config/execution-strategy-rules.json`, executionStrategyRulesJson);
|
|
502
|
+
files.set(`${DIR_NAMES.WORKFLOW}/.gitignore`, gitignoreTemplate);
|
|
503
|
+
// Retired alternate-client files are handled only by the explicit
|
|
504
|
+
// compatibility residue cleaner and are never refreshed or injected.
|
|
505
|
+
// workflow.md is included here because it is runtime-parsed by
|
|
506
|
+
// get_context.py and shared hooks. Keep it on the normal template update
|
|
507
|
+
// path: if the installed file still matches the tracked hash, update the
|
|
508
|
+
// whole file. If the user edited it, the standard modified-file prompt /
|
|
509
|
+
// --force behavior applies. Partial tag-block merging is unsafe because
|
|
510
|
+
// platform routing markers outside [workflow-state:*] blocks are also
|
|
511
|
+
// script-consumed.
|
|
512
|
+
files.set(`${DIR_NAMES.WORKFLOW}/workflow.md`, workflowMdTemplate);
|
|
513
|
+
// Framework docs are framework-owned and refreshed by update.
|
|
514
|
+
// New files flow through the standard new/auto-update/hash-conflict
|
|
515
|
+
// analysis; canonical spec content stays fully protected.
|
|
516
|
+
for (const doc of frameworkDocs) {
|
|
517
|
+
files.set(`${PATHS.FRAMEWORK}/${doc.name}`, doc.content);
|
|
518
|
+
}
|
|
519
|
+
// Project domain glossary stub + ADR rules (user-maintained; conflict
|
|
520
|
+
// handling works the same as workflow.md ā modified files prompt first).
|
|
521
|
+
for (const [relativePath, content] of getWorkflowRootTemplateFiles()) {
|
|
522
|
+
files.set(relativePath, content);
|
|
523
|
+
}
|
|
524
|
+
// workspace/index.md stays excluded ā it's runtime-appended by add_session.py
|
|
525
|
+
// (journal index) and has no script-parsed structure.
|
|
526
|
+
for (const [filePath, content] of collectProjectCapabilityTemplates(cwd, [])) {
|
|
527
|
+
files.set(filePath, content);
|
|
528
|
+
}
|
|
529
|
+
// Apply update.skip from config.yaml (unless bypassed for breaking release)
|
|
530
|
+
if (!bypassUpdateSkip) {
|
|
531
|
+
const skipPaths = loadUpdateSkipPaths(cwd);
|
|
532
|
+
if (skipPaths.length > 0) {
|
|
533
|
+
for (const [filePath] of [...files]) {
|
|
534
|
+
if (skipPaths.some((skip) => filePath === skip ||
|
|
535
|
+
filePath.startsWith(skip.endsWith("/") ? skip : skip + "/"))) {
|
|
536
|
+
files.delete(filePath);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Apply python3āpython replacement for Windows consistency with init-time writes
|
|
542
|
+
for (const [filePath, content] of files) {
|
|
543
|
+
files.set(filePath, replacePythonCommandLiterals(content));
|
|
544
|
+
}
|
|
545
|
+
// User overlay is never a template ā strip even if a future collector
|
|
546
|
+
// accidentally adds it. Retired alternate-client cleanup is a separate allow-list
|
|
547
|
+
// and must not target this directory.
|
|
548
|
+
for (const [filePath] of [...files]) {
|
|
549
|
+
if (isUserMiddlewareOverlayPath(filePath)) {
|
|
550
|
+
files.delete(filePath);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return files;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Analyze changes between current files and templates
|
|
557
|
+
*
|
|
558
|
+
* Uses hash tracking to distinguish between:
|
|
559
|
+
* - User didn't modify + template same = skip (unchangedFiles)
|
|
560
|
+
* - User didn't modify + template updated = auto-update (autoUpdateFiles)
|
|
561
|
+
* - User modified = needs confirmation (changedFiles)
|
|
562
|
+
*/
|
|
563
|
+
function analyzeChanges(cwd, hashes, templates) {
|
|
564
|
+
const result = {
|
|
565
|
+
newFiles: [],
|
|
566
|
+
unchangedFiles: [],
|
|
567
|
+
autoUpdateFiles: [],
|
|
568
|
+
changedFiles: [],
|
|
569
|
+
userDeletedFiles: [],
|
|
570
|
+
protectedPaths: PROTECTED_PATHS,
|
|
571
|
+
};
|
|
572
|
+
for (const [relativePath, newContent] of templates) {
|
|
573
|
+
const fullPath = path.join(cwd, relativePath);
|
|
574
|
+
const exists = fs.existsSync(fullPath);
|
|
575
|
+
const change = {
|
|
576
|
+
path: fullPath,
|
|
577
|
+
relativePath,
|
|
578
|
+
newContent,
|
|
579
|
+
status: "new",
|
|
580
|
+
};
|
|
581
|
+
if (!exists) {
|
|
582
|
+
const storedHash = hashes[relativePath];
|
|
583
|
+
if (storedHash) {
|
|
584
|
+
// Previously installed but user deleted ā respect deletion
|
|
585
|
+
result.userDeletedFiles.push(change);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
change.status = "new";
|
|
589
|
+
result.newFiles.push(change);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
const existingContent = fs.readFileSync(fullPath, "utf-8");
|
|
594
|
+
if (existingContent === newContent) {
|
|
595
|
+
// Content same as template - already up to date
|
|
596
|
+
change.status = "unchanged";
|
|
597
|
+
result.unchangedFiles.push(change);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
// Content differs - check if user modified or template updated
|
|
601
|
+
const storedHash = hashes[relativePath];
|
|
602
|
+
const currentHash = computeHash(existingContent);
|
|
603
|
+
if (storedHash && storedHash === currentHash) {
|
|
604
|
+
// Either the tracked hash matches, or this is a known pristine template
|
|
605
|
+
// from before the path was hash-tracked. Safe to auto-update.
|
|
606
|
+
change.status = "changed";
|
|
607
|
+
result.autoUpdateFiles.push(change);
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
// Hash differs (or no stored hash) - user modified the file
|
|
611
|
+
// Needs confirmation
|
|
612
|
+
change.status = "changed";
|
|
613
|
+
result.changedFiles.push(change);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Unchanged templates with no stored hash (canary hand-copy, first hash
|
|
622
|
+
* tracking, or a newly shipped canonical module path). Record the
|
|
623
|
+
* hash so the next real template edit auto-updates instead of prompting
|
|
624
|
+
* "Modified by you" or listing the path as new.
|
|
625
|
+
*/
|
|
626
|
+
export function collectMissingTemplateHashes(changes, hashes) {
|
|
627
|
+
const files = new Map();
|
|
628
|
+
for (const file of changes.unchangedFiles) {
|
|
629
|
+
if (isUserMiddlewareOverlayPath(file.relativePath)) {
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (!hashes[file.relativePath]) {
|
|
633
|
+
files.set(file.relativePath, file.newContent);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return files;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Print change summary
|
|
640
|
+
*/
|
|
641
|
+
function printChangeSummary(changes) {
|
|
642
|
+
console.log("\nScanning for changes...\n");
|
|
643
|
+
if (changes.newFiles.length > 0) {
|
|
644
|
+
console.log(chalk.green(" New files (will add):"));
|
|
645
|
+
for (const file of changes.newFiles) {
|
|
646
|
+
console.log(chalk.green(` + ${file.relativePath}`));
|
|
647
|
+
}
|
|
648
|
+
console.log("");
|
|
649
|
+
}
|
|
650
|
+
if (changes.autoUpdateFiles.length > 0) {
|
|
651
|
+
console.log(chalk.cyan(" Template updated (will auto-update):"));
|
|
652
|
+
for (const file of changes.autoUpdateFiles) {
|
|
653
|
+
console.log(chalk.cyan(` ā ${file.relativePath}`));
|
|
654
|
+
}
|
|
655
|
+
console.log("");
|
|
656
|
+
}
|
|
657
|
+
if (changes.unchangedFiles.length > 0) {
|
|
658
|
+
console.log(chalk.gray(" Unchanged files (will skip):"));
|
|
659
|
+
for (const file of changes.unchangedFiles.slice(0, 5)) {
|
|
660
|
+
console.log(chalk.gray(` ā ${file.relativePath}`));
|
|
661
|
+
}
|
|
662
|
+
if (changes.unchangedFiles.length > 5) {
|
|
663
|
+
console.log(chalk.gray(` ... and ${changes.unchangedFiles.length - 5} more`));
|
|
664
|
+
}
|
|
665
|
+
console.log("");
|
|
666
|
+
}
|
|
667
|
+
if (changes.changedFiles.length > 0) {
|
|
668
|
+
console.log(chalk.yellow(" Modified by you (need your decision):"));
|
|
669
|
+
for (const file of changes.changedFiles) {
|
|
670
|
+
console.log(chalk.yellow(` ? ${file.relativePath}`));
|
|
671
|
+
}
|
|
672
|
+
console.log("");
|
|
673
|
+
}
|
|
674
|
+
if (changes.userDeletedFiles.length > 0) {
|
|
675
|
+
console.log(chalk.gray(" Deleted by you (preserved):"));
|
|
676
|
+
for (const file of changes.userDeletedFiles) {
|
|
677
|
+
console.log(chalk.gray(` \u2715 ${file.relativePath}`));
|
|
678
|
+
}
|
|
679
|
+
console.log("");
|
|
680
|
+
}
|
|
681
|
+
// Only show protected paths that actually exist
|
|
682
|
+
const existingProtectedPaths = changes.protectedPaths.filter((p) => {
|
|
683
|
+
const fullPath = path.join(process.cwd(), p);
|
|
684
|
+
return fs.existsSync(fullPath);
|
|
685
|
+
});
|
|
686
|
+
if (existingProtectedPaths.length > 0) {
|
|
687
|
+
console.log(chalk.gray(" User data (preserved):"));
|
|
688
|
+
for (const protectedPath of existingProtectedPaths) {
|
|
689
|
+
console.log(chalk.gray(` ā ${protectedPath}/`));
|
|
690
|
+
}
|
|
691
|
+
console.log("");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Prompt user for conflict resolution
|
|
696
|
+
*/
|
|
697
|
+
async function promptConflictResolution(file, options, applyToAll) {
|
|
698
|
+
// If we have a batch action, use it
|
|
699
|
+
if (applyToAll.action) {
|
|
700
|
+
return applyToAll.action;
|
|
701
|
+
}
|
|
702
|
+
// Check command-line options
|
|
703
|
+
if (options.force) {
|
|
704
|
+
return "overwrite";
|
|
705
|
+
}
|
|
706
|
+
if (options.skipAll) {
|
|
707
|
+
return "skip";
|
|
708
|
+
}
|
|
709
|
+
if (options.createNew) {
|
|
710
|
+
return "create-new";
|
|
711
|
+
}
|
|
712
|
+
// Interactive prompt
|
|
713
|
+
const { action } = await inquirer.prompt([
|
|
714
|
+
{
|
|
715
|
+
type: "list",
|
|
716
|
+
name: "action",
|
|
717
|
+
message: `${file.relativePath} has changes.`,
|
|
718
|
+
choices: [
|
|
719
|
+
{
|
|
720
|
+
name: "[1] Overwrite - Replace with new version",
|
|
721
|
+
value: "overwrite",
|
|
722
|
+
},
|
|
723
|
+
{ name: "[2] Skip - Keep your current version", value: "skip" },
|
|
724
|
+
{
|
|
725
|
+
name: "[3] Create copy - Save new version as .new",
|
|
726
|
+
value: "create-new",
|
|
727
|
+
},
|
|
728
|
+
{ name: "[a] Apply Overwrite to all", value: "overwrite-all" },
|
|
729
|
+
{ name: "[s] Apply Skip to all", value: "skip-all" },
|
|
730
|
+
{ name: "[n] Apply Create copy to all", value: "create-new-all" },
|
|
731
|
+
],
|
|
732
|
+
default: "skip",
|
|
733
|
+
},
|
|
734
|
+
]);
|
|
735
|
+
if (action === "overwrite-all") {
|
|
736
|
+
applyToAll.action = "overwrite";
|
|
737
|
+
return "overwrite";
|
|
738
|
+
}
|
|
739
|
+
if (action === "skip-all") {
|
|
740
|
+
applyToAll.action = "skip";
|
|
741
|
+
return "skip";
|
|
742
|
+
}
|
|
743
|
+
if (action === "create-new-all") {
|
|
744
|
+
applyToAll.action = "create-new";
|
|
745
|
+
return "create-new";
|
|
746
|
+
}
|
|
747
|
+
return action;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Create a timestamped backup directory path
|
|
751
|
+
*/
|
|
752
|
+
function createBackupDirPath(cwd) {
|
|
753
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
754
|
+
const workflowRoot = workflowPath(cwd, `.backup-${timestamp}`) ??
|
|
755
|
+
path.join(cwd, DIR_NAMES.WORKFLOW, `.backup-${timestamp}`);
|
|
756
|
+
return workflowRoot;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Backup a single file to the backup directory
|
|
760
|
+
*/
|
|
761
|
+
function backupFile(cwd, backupDir, relativePath) {
|
|
762
|
+
const srcPath = path.join(cwd, relativePath);
|
|
763
|
+
if (!fs.existsSync(srcPath))
|
|
764
|
+
return;
|
|
765
|
+
const backupPath = path.join(backupDir, relativePath);
|
|
766
|
+
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
767
|
+
fs.copyFileSync(srcPath, backupPath);
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Directories to backup as complete snapshot (derived from platform registry)
|
|
771
|
+
*/
|
|
772
|
+
const BACKUP_DIRS = ALL_MANAGED_DIRS;
|
|
773
|
+
/** Root-level managed files to include in update backups. */
|
|
774
|
+
const BACKUP_FILES = [FILE_NAMES.AGENTS];
|
|
775
|
+
/**
|
|
776
|
+
* Patterns to exclude from backup (user data that shouldn't be backed up)
|
|
777
|
+
*/
|
|
778
|
+
const BACKUP_EXCLUDE_PATTERNS = [
|
|
779
|
+
".backup-", // Previous backups
|
|
780
|
+
"/node_modules", // Installed dependencies; restore via package manager
|
|
781
|
+
"/workspace/", // Developer workspace (user data)
|
|
782
|
+
"/tasks/", // Task data (user data)
|
|
783
|
+
"/spec/", // Spec files (user-customized content)
|
|
784
|
+
"/middleware/", // User middleware overlay (never managed)
|
|
785
|
+
"/backlog/", // Backlog data (user data)
|
|
786
|
+
"/agent-traces/", // Agent traces (user data, legacy name)
|
|
787
|
+
// Platform-native worktree dirs ā these are full sub-repos the CLI
|
|
788
|
+
// spawns for parallel sessions. Backing them up on every update would
|
|
789
|
+
// snapshot the entire nested working tree. Confirmed convention:
|
|
790
|
+
// Cursor CLI: .cursor/worktrees/
|
|
791
|
+
// Matches any platform using the same convention (future-proof).
|
|
792
|
+
"/worktrees/",
|
|
793
|
+
"/worktree/",
|
|
794
|
+
];
|
|
795
|
+
/**
|
|
796
|
+
* Check if a path should be excluded from backup
|
|
797
|
+
* @internal Exported for testing only
|
|
798
|
+
*/
|
|
799
|
+
export function shouldExcludeFromBackup(relativePath) {
|
|
800
|
+
// Normalize Windows backslashes to forward slashes so patterns like
|
|
801
|
+
// "/worktrees/" / "/tasks/" match regardless of host OS. Without this,
|
|
802
|
+
// Windows `path.relative` returns `.cursor\worktrees\...` and none of
|
|
803
|
+
// the slash-prefixed exclude patterns trigger ā which causes
|
|
804
|
+
// `collectAllFiles` to descend into platform worktrees (full nested
|
|
805
|
+
// project copies) and explode the scan. Same normalization pattern
|
|
806
|
+
// used by `isManagedPath` in configurators/index.ts.
|
|
807
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
808
|
+
for (const pattern of BACKUP_EXCLUDE_PATTERNS) {
|
|
809
|
+
if (normalized.includes(pattern)) {
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Create complete snapshot backup of all managed directories
|
|
817
|
+
* Backs up all managed platform/workflow directories entirely
|
|
818
|
+
* (excluding user data like workspace/, tasks/, backlog/)
|
|
819
|
+
*/
|
|
820
|
+
function createFullBackup(cwd) {
|
|
821
|
+
const backupDir = createBackupDirPath(cwd);
|
|
822
|
+
let hasFiles = false;
|
|
823
|
+
for (const dir of BACKUP_DIRS) {
|
|
824
|
+
const dirPath = path.join(cwd, dir);
|
|
825
|
+
if (!fs.existsSync(dirPath))
|
|
826
|
+
continue;
|
|
827
|
+
const files = collectAllFiles(dirPath, cwd);
|
|
828
|
+
for (const fullPath of files) {
|
|
829
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
830
|
+
// Skip excluded paths
|
|
831
|
+
if (shouldExcludeFromBackup(relativePath))
|
|
832
|
+
continue;
|
|
833
|
+
// Create backup
|
|
834
|
+
if (!hasFiles) {
|
|
835
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
836
|
+
hasFiles = true;
|
|
837
|
+
}
|
|
838
|
+
backupFile(cwd, backupDir, relativePath);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
for (const relativePath of BACKUP_FILES) {
|
|
842
|
+
const fullPath = path.join(cwd, relativePath);
|
|
843
|
+
if (!fs.existsSync(fullPath))
|
|
844
|
+
continue;
|
|
845
|
+
if (shouldExcludeFromBackup(relativePath))
|
|
846
|
+
continue;
|
|
847
|
+
if (!hasFiles) {
|
|
848
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
849
|
+
hasFiles = true;
|
|
850
|
+
}
|
|
851
|
+
backupFile(cwd, backupDir, relativePath);
|
|
852
|
+
}
|
|
853
|
+
return hasFiles ? backupDir : null;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Update version file
|
|
857
|
+
*/
|
|
858
|
+
function updateVersionFile(cwd) {
|
|
859
|
+
const dirName = resolveWorkflowDirName(cwd) ?? DIR_NAMES.WORKFLOW;
|
|
860
|
+
const versionPath = path.join(cwd, dirName, ".version");
|
|
861
|
+
fs.writeFileSync(versionPath, VERSION);
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Get current installed version
|
|
865
|
+
*/
|
|
866
|
+
function getInstalledVersion(cwd) {
|
|
867
|
+
const versionPath = workflowPath(cwd, ".version");
|
|
868
|
+
if (versionPath && fs.existsSync(versionPath)) {
|
|
869
|
+
return fs.readFileSync(versionPath, "utf-8").trim();
|
|
870
|
+
}
|
|
871
|
+
return "unknown";
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Fetch latest version from npm registry
|
|
875
|
+
*/
|
|
876
|
+
async function getLatestNpmVersion() {
|
|
877
|
+
try {
|
|
878
|
+
const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`);
|
|
879
|
+
if (!response.ok) {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
const data = (await response.json());
|
|
883
|
+
return data.version ?? null;
|
|
884
|
+
}
|
|
885
|
+
catch {
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Recursively collect all files in a directory
|
|
891
|
+
*/
|
|
892
|
+
function collectAllFiles(dirPath, cwd = process.cwd()) {
|
|
893
|
+
if (!fs.existsSync(dirPath))
|
|
894
|
+
return [];
|
|
895
|
+
const files = [];
|
|
896
|
+
const stack = [dirPath];
|
|
897
|
+
while (stack.length > 0) {
|
|
898
|
+
const currentDir = stack.pop();
|
|
899
|
+
if (!currentDir)
|
|
900
|
+
continue;
|
|
901
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
902
|
+
for (const entry of entries) {
|
|
903
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
904
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
905
|
+
// Never follow symlinks / Windows directory junctions ā a junction
|
|
906
|
+
// pointing at an ancestor would loop the scan forever. Node's
|
|
907
|
+
// `isSymbolicLink()` returns true for NTFS junctions since v12.
|
|
908
|
+
if (entry.isSymbolicLink())
|
|
909
|
+
continue;
|
|
910
|
+
if (entry.isDirectory()) {
|
|
911
|
+
if (!shouldExcludeFromBackup(relativePath)) {
|
|
912
|
+
stack.push(fullPath);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
else if (entry.isFile()) {
|
|
916
|
+
files.push(fullPath);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return files;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Check if a directory only contains unmodified template files
|
|
924
|
+
* Returns true if safe to delete:
|
|
925
|
+
* - All files are tracked and unmodified, OR
|
|
926
|
+
* - All files match current template content (even if not tracked)
|
|
927
|
+
*/
|
|
928
|
+
function isDirectorySafeToReplace(cwd, dirRelativePath, hashes, templates) {
|
|
929
|
+
const dirFullPath = path.join(cwd, dirRelativePath);
|
|
930
|
+
if (!fs.existsSync(dirFullPath))
|
|
931
|
+
return true;
|
|
932
|
+
const files = collectAllFiles(dirFullPath, cwd);
|
|
933
|
+
if (files.length === 0)
|
|
934
|
+
return true; // Empty directory is safe
|
|
935
|
+
for (const fullPath of files) {
|
|
936
|
+
// POSIX-normalize: hashes/templates keys are persisted as POSIX, but
|
|
937
|
+
// `path.relative` returns OS-native separators (backslash on Windows).
|
|
938
|
+
const relativePath = toPosix(path.relative(cwd, fullPath));
|
|
939
|
+
const storedHash = hashes[relativePath];
|
|
940
|
+
const templateContent = templates.get(relativePath);
|
|
941
|
+
// Check if file matches template content (handles untracked files)
|
|
942
|
+
if (templateContent) {
|
|
943
|
+
const currentContent = fs.readFileSync(fullPath, "utf-8");
|
|
944
|
+
if (currentContent === templateContent) {
|
|
945
|
+
// File matches template - safe
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
// Check if file is tracked and unmodified
|
|
950
|
+
if (storedHash && !isTemplateModified(cwd, relativePath, hashes)) {
|
|
951
|
+
// Tracked and unmodified - safe
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
// File is either user-created or user-modified - not safe
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Recursively delete a directory
|
|
961
|
+
*/
|
|
962
|
+
function removeDirectoryRecursive(dirPath) {
|
|
963
|
+
if (!fs.existsSync(dirPath))
|
|
964
|
+
return;
|
|
965
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Check if a file is safe to overwrite (matches template content)
|
|
969
|
+
*/
|
|
970
|
+
function isFileSafeToReplace(cwd, relativePath, templates) {
|
|
971
|
+
const fullPath = path.join(cwd, relativePath);
|
|
972
|
+
if (!fs.existsSync(fullPath))
|
|
973
|
+
return true;
|
|
974
|
+
const templateContent = templates.get(relativePath);
|
|
975
|
+
if (!templateContent)
|
|
976
|
+
return false; // Not a template file
|
|
977
|
+
const currentContent = fs.readFileSync(fullPath, "utf-8");
|
|
978
|
+
return currentContent === templateContent;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Classify migrations based on file state and user modifications
|
|
982
|
+
*/
|
|
983
|
+
function classifyMigrations(migrations, cwd, hashes, templates) {
|
|
984
|
+
const result = {
|
|
985
|
+
auto: [],
|
|
986
|
+
confirm: [],
|
|
987
|
+
conflict: [],
|
|
988
|
+
skip: [],
|
|
989
|
+
};
|
|
990
|
+
for (const item of migrations) {
|
|
991
|
+
// safe-file-delete handled separately (not via --migrate)
|
|
992
|
+
if (item.type === "safe-file-delete")
|
|
993
|
+
continue;
|
|
994
|
+
// Enforce PROTECTED_PATHS ā never migrate FROM protected paths (prevents moving/deleting user data)
|
|
995
|
+
if (isProtectedPath(item.from)) {
|
|
996
|
+
result.skip.push(item);
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
// For non-rename types, also block writing TO protected paths
|
|
1000
|
+
// Historical rename operations may target paths now protected as user data.
|
|
1001
|
+
if (item.to &&
|
|
1002
|
+
isProtectedPath(item.to) &&
|
|
1003
|
+
item.type !== "rename" &&
|
|
1004
|
+
item.type !== "rename-dir") {
|
|
1005
|
+
result.skip.push(item);
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
const oldPath = path.join(cwd, item.from);
|
|
1009
|
+
const oldExists = fs.existsSync(oldPath);
|
|
1010
|
+
if (!oldExists) {
|
|
1011
|
+
// Old file doesn't exist, nothing to migrate
|
|
1012
|
+
result.skip.push(item);
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (item.type === "rename" && item.to) {
|
|
1016
|
+
const newPath = path.join(cwd, item.to);
|
|
1017
|
+
const newExists = fs.existsSync(newPath);
|
|
1018
|
+
if (newExists) {
|
|
1019
|
+
// Both exist - check if new file matches template (safe to overwrite)
|
|
1020
|
+
if (isFileSafeToReplace(cwd, item.to, templates)) {
|
|
1021
|
+
// New file is just template content - safe to delete and rename
|
|
1022
|
+
result.auto.push(item);
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
// New file has user content - conflict
|
|
1026
|
+
result.conflict.push(item);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
else if (isTemplateModified(cwd, item.from, hashes)) {
|
|
1030
|
+
// User has modified the file - needs confirmation
|
|
1031
|
+
result.confirm.push(item);
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
// Unmodified template - safe to auto-migrate
|
|
1035
|
+
result.auto.push(item);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
else if (item.type === "rename-dir" && item.to) {
|
|
1039
|
+
const newPath = path.join(cwd, item.to);
|
|
1040
|
+
const newExists = fs.existsSync(newPath);
|
|
1041
|
+
if (newExists) {
|
|
1042
|
+
// Target exists - check if it only contains unmodified template files
|
|
1043
|
+
if (isDirectorySafeToReplace(cwd, item.to, hashes, templates)) {
|
|
1044
|
+
// Safe to delete target and rename source
|
|
1045
|
+
result.auto.push(item);
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
// Target has user modifications - conflict
|
|
1049
|
+
result.conflict.push(item);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
// Directory rename - always auto (includes user files)
|
|
1054
|
+
result.auto.push(item);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
else if (item.type === "delete") {
|
|
1058
|
+
if (isTemplateModified(cwd, item.from, hashes)) {
|
|
1059
|
+
// User has modified - needs confirmation before delete
|
|
1060
|
+
result.confirm.push(item);
|
|
1061
|
+
}
|
|
1062
|
+
else {
|
|
1063
|
+
// Unmodified - safe to auto-delete
|
|
1064
|
+
result.auto.push(item);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return result;
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Print migration summary
|
|
1072
|
+
*/
|
|
1073
|
+
function printMigrationSummary(classified) {
|
|
1074
|
+
const total = classified.auto.length +
|
|
1075
|
+
classified.confirm.length +
|
|
1076
|
+
classified.conflict.length +
|
|
1077
|
+
classified.skip.length;
|
|
1078
|
+
if (total === 0) {
|
|
1079
|
+
console.log(chalk.gray(" No migrations to apply.\n"));
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (classified.auto.length > 0) {
|
|
1083
|
+
console.log(chalk.green(" ā Auto-migrate (unmodified):"));
|
|
1084
|
+
for (const item of classified.auto) {
|
|
1085
|
+
if (item.type === "rename") {
|
|
1086
|
+
console.log(chalk.green(` ${item.from} ā ${item.to}`));
|
|
1087
|
+
}
|
|
1088
|
+
else if (item.type === "rename-dir") {
|
|
1089
|
+
console.log(chalk.green(` [dir] ${item.from}/ ā ${item.to}/`));
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
console.log(chalk.green(` ā ${item.from}`));
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
console.log("");
|
|
1096
|
+
}
|
|
1097
|
+
if (classified.confirm.length > 0) {
|
|
1098
|
+
console.log(chalk.yellow(" ā Requires confirmation (modified by user):"));
|
|
1099
|
+
for (const item of classified.confirm) {
|
|
1100
|
+
if (item.type === "rename") {
|
|
1101
|
+
console.log(chalk.yellow(` ${item.from} ā ${item.to}`));
|
|
1102
|
+
}
|
|
1103
|
+
else {
|
|
1104
|
+
console.log(chalk.yellow(` ā ${item.from}`));
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
console.log("");
|
|
1108
|
+
}
|
|
1109
|
+
if (classified.conflict.length > 0) {
|
|
1110
|
+
console.log(chalk.red(" ā Conflict (both old and new exist):"));
|
|
1111
|
+
for (const item of classified.conflict) {
|
|
1112
|
+
if (item.type === "rename-dir") {
|
|
1113
|
+
console.log(chalk.red(` [dir] ${item.from}/ ā ${item.to}/`));
|
|
1114
|
+
}
|
|
1115
|
+
else {
|
|
1116
|
+
console.log(chalk.red(` ${item.from} ā ${item.to}`));
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
console.log(chalk.gray(" ā Resolve manually: merge or delete one, then re-run update"));
|
|
1120
|
+
console.log("");
|
|
1121
|
+
}
|
|
1122
|
+
if (classified.skip.length > 0) {
|
|
1123
|
+
console.log(chalk.gray(" ā Skipping (old file not found):"));
|
|
1124
|
+
for (const item of classified.skip.slice(0, 3)) {
|
|
1125
|
+
console.log(chalk.gray(` ${item.from}`));
|
|
1126
|
+
}
|
|
1127
|
+
if (classified.skip.length > 3) {
|
|
1128
|
+
console.log(chalk.gray(` ... and ${classified.skip.length - 3} more`));
|
|
1129
|
+
}
|
|
1130
|
+
console.log("");
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Prompt user for migration action on a single item.
|
|
1135
|
+
*
|
|
1136
|
+
* Design notes:
|
|
1137
|
+
* - Default is `backup-rename`: safest ā preserves user's content as a .backup
|
|
1138
|
+
* alongside the rename, so Enter-to-continue never destroys work or leaves
|
|
1139
|
+
* stale paths behind.
|
|
1140
|
+
* - "Skip" leaves a stale old path that won't be cleaned by later updates ā
|
|
1141
|
+
* warn explicitly so users understand the consequence.
|
|
1142
|
+
* - Show manifest description + why-flagged so users can make an informed
|
|
1143
|
+
* choice without needing to dig through the diff.
|
|
1144
|
+
*/
|
|
1145
|
+
async function promptMigrationAction(item) {
|
|
1146
|
+
const headline = item.type === "rename"
|
|
1147
|
+
? `${chalk.cyan(item.from)} ā ${chalk.green(item.to)}`
|
|
1148
|
+
: `${chalk.red("Delete")} ${chalk.cyan(item.from)}`;
|
|
1149
|
+
const description = item.description ?? "No description provided in manifest.";
|
|
1150
|
+
// Actions with inline guidance so users see the trade-off per choice.
|
|
1151
|
+
const renameLabel = item.type === "rename"
|
|
1152
|
+
? "[r] Rename anyway ā use if the file is unchanged, or any edits are fine to move as-is"
|
|
1153
|
+
: "[d] Delete anyway ā use if you don't need this file (already migrated to replacement)";
|
|
1154
|
+
const backupLabel = item.type === "rename"
|
|
1155
|
+
? "[b] Backup original, then proceed ā SAFEST: writes <new-path>.backup with your current content, then renames"
|
|
1156
|
+
: "[b] Backup original, then proceed ā SAFEST: writes <path>.backup with your current content, then deletes";
|
|
1157
|
+
const skipLabel = item.type === "rename"
|
|
1158
|
+
? "[s] Skip ā leaves the old path in place (you'll see it flagged on future updates until cleaned up manually)"
|
|
1159
|
+
: "[s] Skip ā keeps the deprecated file (you'll see it flagged on future updates until cleaned up manually)";
|
|
1160
|
+
// Prefer the per-migration `reason` (version-specific context authored in the
|
|
1161
|
+
// manifest) over a generic fallback. Hardcoding version-specific hints here
|
|
1162
|
+
// rots fast ā every release gets a new set of edge cases.
|
|
1163
|
+
const whyFlagged = item.reason
|
|
1164
|
+
? chalk.gray(item.reason
|
|
1165
|
+
.split("\n")
|
|
1166
|
+
.map((line) => ` ${line}`)
|
|
1167
|
+
.join("\n"))
|
|
1168
|
+
: chalk.gray(` Why prompted: file content doesn't match the Pactile template hash\n` +
|
|
1169
|
+
` for this path ā usually local customization. If unsure, pick [b].`);
|
|
1170
|
+
const message = [
|
|
1171
|
+
headline,
|
|
1172
|
+
"",
|
|
1173
|
+
chalk.bold(" What:") + " " + description,
|
|
1174
|
+
whyFlagged,
|
|
1175
|
+
"",
|
|
1176
|
+
chalk.bold(" Choose:"),
|
|
1177
|
+
].join("\n");
|
|
1178
|
+
const { choice } = await inquirer.prompt([
|
|
1179
|
+
{
|
|
1180
|
+
type: "list",
|
|
1181
|
+
name: "choice",
|
|
1182
|
+
message,
|
|
1183
|
+
choices: [
|
|
1184
|
+
{ name: backupLabel, value: "backup-rename" },
|
|
1185
|
+
{ name: renameLabel, value: "rename" },
|
|
1186
|
+
{ name: skipLabel, value: "skip" },
|
|
1187
|
+
],
|
|
1188
|
+
default: "backup-rename",
|
|
1189
|
+
},
|
|
1190
|
+
]);
|
|
1191
|
+
return choice;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Clean up empty directories after file migration
|
|
1195
|
+
* Recursively remove empty parent directories up to the managed root.
|
|
1196
|
+
*/
|
|
1197
|
+
/** @internal Exported for testing only */
|
|
1198
|
+
export function cleanupEmptyDirs(cwd, dirPath) {
|
|
1199
|
+
const fullPath = path.join(cwd, dirPath);
|
|
1200
|
+
// Safety: don't delete outside of managed directories
|
|
1201
|
+
if (!isManagedPath(dirPath)) {
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
// Safety: never delete managed root directories themselves.
|
|
1205
|
+
if (isManagedRootDir(dirPath)) {
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
// Check if directory exists and is empty
|
|
1209
|
+
if (!fs.existsSync(fullPath))
|
|
1210
|
+
return;
|
|
1211
|
+
try {
|
|
1212
|
+
const stat = fs.statSync(fullPath);
|
|
1213
|
+
if (!stat.isDirectory())
|
|
1214
|
+
return;
|
|
1215
|
+
const contents = fs.readdirSync(fullPath);
|
|
1216
|
+
if (contents.length === 0) {
|
|
1217
|
+
fs.rmdirSync(fullPath);
|
|
1218
|
+
// Recursively check parent (but stop at root directories)
|
|
1219
|
+
const parent = path.dirname(dirPath);
|
|
1220
|
+
if (parent !== "." && parent !== dirPath && !isManagedRootDir(parent)) {
|
|
1221
|
+
cleanupEmptyDirs(cwd, parent);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
catch {
|
|
1226
|
+
// Ignore errors (permission issues, etc.)
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Sort migrations for safe execution order
|
|
1231
|
+
* - rename-dir with deeper paths first (to handle nested directories)
|
|
1232
|
+
* - rename-dir before rename/delete
|
|
1233
|
+
*/
|
|
1234
|
+
/** @internal Exported for testing only */
|
|
1235
|
+
export function sortMigrationsForExecution(migrations) {
|
|
1236
|
+
return [...migrations].sort((a, b) => {
|
|
1237
|
+
// rename-dir should be sorted by path depth (deeper first)
|
|
1238
|
+
if (a.type === "rename-dir" && b.type === "rename-dir") {
|
|
1239
|
+
const aDepth = a.from.split("/").length;
|
|
1240
|
+
const bDepth = b.from.split("/").length;
|
|
1241
|
+
return bDepth - aDepth; // Deeper paths first
|
|
1242
|
+
}
|
|
1243
|
+
// rename-dir before rename/delete (directories first)
|
|
1244
|
+
if (a.type === "rename-dir" && b.type !== "rename-dir")
|
|
1245
|
+
return -1;
|
|
1246
|
+
if (a.type !== "rename-dir" && b.type === "rename-dir")
|
|
1247
|
+
return 1;
|
|
1248
|
+
return 0;
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Execute classified migrations
|
|
1253
|
+
*
|
|
1254
|
+
* @param options.force - Force migrate modified files without asking
|
|
1255
|
+
* @param options.skipAll - Skip all modified files without asking
|
|
1256
|
+
* If neither is set, prompts interactively for modified files
|
|
1257
|
+
*/
|
|
1258
|
+
async function executeMigrations(classified, cwd, options) {
|
|
1259
|
+
const result = {
|
|
1260
|
+
renamed: 0,
|
|
1261
|
+
deleted: 0,
|
|
1262
|
+
skipped: 0,
|
|
1263
|
+
conflicts: classified.conflict.length,
|
|
1264
|
+
};
|
|
1265
|
+
// Sort migrations for safe execution order
|
|
1266
|
+
const sortedAuto = sortMigrationsForExecution(classified.auto);
|
|
1267
|
+
// 1. Execute auto migrations (unmodified files and directories)
|
|
1268
|
+
for (const item of sortedAuto) {
|
|
1269
|
+
if (item.type === "rename" && item.to) {
|
|
1270
|
+
const oldPath = path.join(cwd, item.from);
|
|
1271
|
+
const newPath = path.join(cwd, item.to);
|
|
1272
|
+
// Ensure target directory exists
|
|
1273
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
1274
|
+
fs.renameSync(oldPath, newPath);
|
|
1275
|
+
// Update hash tracking
|
|
1276
|
+
renameHash(cwd, item.from, item.to);
|
|
1277
|
+
// Make executable if it's a script
|
|
1278
|
+
if (item.to.endsWith(".sh") || item.to.endsWith(".py")) {
|
|
1279
|
+
fs.chmodSync(newPath, "755");
|
|
1280
|
+
}
|
|
1281
|
+
// Clean up empty source directory
|
|
1282
|
+
cleanupEmptyDirs(cwd, path.dirname(item.from));
|
|
1283
|
+
result.renamed++;
|
|
1284
|
+
}
|
|
1285
|
+
else if (item.type === "rename-dir" && item.to) {
|
|
1286
|
+
const oldPath = path.join(cwd, item.from);
|
|
1287
|
+
const newPath = path.join(cwd, item.to);
|
|
1288
|
+
// If target exists (safe to replace, already checked in classification)
|
|
1289
|
+
// delete it first before renaming
|
|
1290
|
+
if (fs.existsSync(newPath)) {
|
|
1291
|
+
removeDirectoryRecursive(newPath);
|
|
1292
|
+
}
|
|
1293
|
+
// Ensure parent directory exists
|
|
1294
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
1295
|
+
// Rename the entire directory (includes all user files)
|
|
1296
|
+
fs.renameSync(oldPath, newPath);
|
|
1297
|
+
// Batch update hash tracking for all files in the directory
|
|
1298
|
+
const hashes = loadHashes(cwd);
|
|
1299
|
+
const oldPrefix = item.from.endsWith("/") ? item.from : item.from + "/";
|
|
1300
|
+
const newPrefix = item.to.endsWith("/") ? item.to : item.to + "/";
|
|
1301
|
+
const updatedHashes = {};
|
|
1302
|
+
for (const [hashPath, hashValue] of Object.entries(hashes)) {
|
|
1303
|
+
if (hashPath.startsWith(oldPrefix)) {
|
|
1304
|
+
// Rename path: old prefix -> new prefix
|
|
1305
|
+
const newHashPath = newPrefix + hashPath.slice(oldPrefix.length);
|
|
1306
|
+
updatedHashes[newHashPath] = hashValue;
|
|
1307
|
+
}
|
|
1308
|
+
else if (hashPath.startsWith(newPrefix)) {
|
|
1309
|
+
// Skip old hashes from deleted target directory
|
|
1310
|
+
// (they will be replaced by renamed source files)
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
else {
|
|
1314
|
+
// Keep unchanged
|
|
1315
|
+
updatedHashes[hashPath] = hashValue;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
saveHashes(cwd, updatedHashes);
|
|
1319
|
+
result.renamed++;
|
|
1320
|
+
}
|
|
1321
|
+
else if (item.type === "delete") {
|
|
1322
|
+
const filePath = path.join(cwd, item.from);
|
|
1323
|
+
fs.unlinkSync(filePath);
|
|
1324
|
+
// Remove from hash tracking
|
|
1325
|
+
removeHash(cwd, item.from);
|
|
1326
|
+
// Clean up empty directory
|
|
1327
|
+
cleanupEmptyDirs(cwd, path.dirname(item.from));
|
|
1328
|
+
result.deleted++;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
// 2. Handle confirm items (modified files)
|
|
1332
|
+
// Note: All files are already backed up by createMigrationBackup before execution
|
|
1333
|
+
for (const item of classified.confirm) {
|
|
1334
|
+
let action;
|
|
1335
|
+
if (options.force) {
|
|
1336
|
+
// Force mode: proceed (already backed up)
|
|
1337
|
+
action = "rename";
|
|
1338
|
+
}
|
|
1339
|
+
else if (options.skipAll) {
|
|
1340
|
+
// Skip mode: skip all modified files
|
|
1341
|
+
action = "skip";
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
// Default: interactive prompt
|
|
1345
|
+
action = await promptMigrationAction(item);
|
|
1346
|
+
}
|
|
1347
|
+
if (action === "skip") {
|
|
1348
|
+
result.skipped++;
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
// For `backup-rename`, leave an inline .backup copy of the user's modified
|
|
1352
|
+
// original next to the new location (for rename) or in place (for delete).
|
|
1353
|
+
// This is in addition to the full canonical project snapshot backup.
|
|
1354
|
+
// the inline copy is more discoverable when the user wants to diff or merge
|
|
1355
|
+
// their customizations against the new template.
|
|
1356
|
+
if (item.type === "rename" && item.to) {
|
|
1357
|
+
const oldPath = path.join(cwd, item.from);
|
|
1358
|
+
const newPath = path.join(cwd, item.to);
|
|
1359
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
1360
|
+
if (action === "backup-rename") {
|
|
1361
|
+
// Copy original alongside the new path before the rename overwrites nothing
|
|
1362
|
+
// (target dir is guaranteed fresh since `conflict` is handled elsewhere).
|
|
1363
|
+
fs.copyFileSync(oldPath, newPath + ".backup");
|
|
1364
|
+
}
|
|
1365
|
+
fs.renameSync(oldPath, newPath);
|
|
1366
|
+
renameHash(cwd, item.from, item.to);
|
|
1367
|
+
if (item.to.endsWith(".sh") || item.to.endsWith(".py")) {
|
|
1368
|
+
fs.chmodSync(newPath, "755");
|
|
1369
|
+
}
|
|
1370
|
+
// Clean up empty source directory
|
|
1371
|
+
cleanupEmptyDirs(cwd, path.dirname(item.from));
|
|
1372
|
+
result.renamed++;
|
|
1373
|
+
}
|
|
1374
|
+
else if (item.type === "delete") {
|
|
1375
|
+
const filePath = path.join(cwd, item.from);
|
|
1376
|
+
if (action === "backup-rename") {
|
|
1377
|
+
// Keep a .backup copy in place before deletion so the user can recover
|
|
1378
|
+
// inline without digging through the full snapshot backup.
|
|
1379
|
+
fs.copyFileSync(filePath, filePath + ".backup");
|
|
1380
|
+
}
|
|
1381
|
+
fs.unlinkSync(filePath);
|
|
1382
|
+
removeHash(cwd, item.from);
|
|
1383
|
+
// Clean up empty directory
|
|
1384
|
+
cleanupEmptyDirs(cwd, path.dirname(item.from));
|
|
1385
|
+
result.deleted++;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
// 3. Skip count already tracked (old files not found)
|
|
1389
|
+
result.skipped += classified.skip.length;
|
|
1390
|
+
return result;
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Print migration result summary
|
|
1394
|
+
*/
|
|
1395
|
+
function printMigrationResult(result) {
|
|
1396
|
+
const parts = [];
|
|
1397
|
+
if (result.renamed > 0) {
|
|
1398
|
+
parts.push(`${result.renamed} renamed`);
|
|
1399
|
+
}
|
|
1400
|
+
if (result.deleted > 0) {
|
|
1401
|
+
parts.push(`${result.deleted} deleted`);
|
|
1402
|
+
}
|
|
1403
|
+
if (result.skipped > 0) {
|
|
1404
|
+
parts.push(`${result.skipped} skipped`);
|
|
1405
|
+
}
|
|
1406
|
+
if (result.conflicts > 0) {
|
|
1407
|
+
parts.push(`${result.conflicts} conflict${result.conflicts > 1 ? "s" : ""}`);
|
|
1408
|
+
}
|
|
1409
|
+
if (parts.length > 0) {
|
|
1410
|
+
console.log(chalk.cyan(`Migration complete: ${parts.join(", ")}`));
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
function rolloutOptionsFromUpdate(options) {
|
|
1414
|
+
return {
|
|
1415
|
+
dryRun: Boolean(options.dryRun),
|
|
1416
|
+
force: Boolean(options.force),
|
|
1417
|
+
skipAll: Boolean(options.skipAll),
|
|
1418
|
+
createNew: Boolean(options.createNew),
|
|
1419
|
+
migrate: Boolean(options.migrate),
|
|
1420
|
+
allowDowngrade: Boolean(options.allowDowngrade),
|
|
1421
|
+
skipReadiness: Boolean(options.skipReadiness),
|
|
1422
|
+
writeArtifacts: Boolean(options.writeArtifacts),
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
function upgradeDirectionFromCompare(cliVsProject, projectVersion) {
|
|
1426
|
+
if (projectVersion === "unknown")
|
|
1427
|
+
return "unknown";
|
|
1428
|
+
if (cliVsProject > 0)
|
|
1429
|
+
return "upgrade";
|
|
1430
|
+
if (cliVsProject < 0)
|
|
1431
|
+
return "downgrade";
|
|
1432
|
+
return "same";
|
|
1433
|
+
}
|
|
1434
|
+
function releaseBlockersFromReadiness(readiness, cliBehindNpm, cliVersion, latestNpmVersion) {
|
|
1435
|
+
const blockers = [];
|
|
1436
|
+
if (cliBehindNpm && latestNpmVersion) {
|
|
1437
|
+
blockers.push({
|
|
1438
|
+
code: "cli_behind_npm",
|
|
1439
|
+
message: `CLI ${cliVersion} is behind npm ${latestNpmVersion}`,
|
|
1440
|
+
recovery: ["pactile upgrade"],
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
if (!readiness.skipped && !readiness.smartSearch.ok) {
|
|
1444
|
+
blockers.push({
|
|
1445
|
+
code: "smart_search_readiness",
|
|
1446
|
+
message: "Smart Search readiness unverified (does not block Proceed?)",
|
|
1447
|
+
recovery: [
|
|
1448
|
+
"smart-search doctor --format json",
|
|
1449
|
+
"Treat Smart Search as unverified; official files can still update after Proceed?",
|
|
1450
|
+
],
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
for (const cap of readiness.capabilities) {
|
|
1454
|
+
if (!cap.ok) {
|
|
1455
|
+
blockers.push({
|
|
1456
|
+
code: `capability_${cap.id}`,
|
|
1457
|
+
message: `Capability ${cap.id} readiness unverified (does not block Proceed?)`,
|
|
1458
|
+
recovery: [
|
|
1459
|
+
"Treat this capability as unverified; official files can still update after Proceed?",
|
|
1460
|
+
],
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return blockers;
|
|
1465
|
+
}
|
|
1466
|
+
function finishRollout(options, report) {
|
|
1467
|
+
options.lastReport = report;
|
|
1468
|
+
emitRolloutReport(report, options.json);
|
|
1469
|
+
}
|
|
1470
|
+
function candidatePath(projectRoot, canonicalBuildRoot, relativePath) {
|
|
1471
|
+
const root = isCanonicalGenerationPath(relativePath)
|
|
1472
|
+
? canonicalBuildRoot
|
|
1473
|
+
: projectRoot;
|
|
1474
|
+
return path.join(root, ...relativePath.replace(/\\/g, "/").split("/"));
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Apply template writes that are outside the canonical generation only after
|
|
1478
|
+
* lifecycle commit. Canonical paths are staged in the OS temporary root; live
|
|
1479
|
+
* paths are kept as bytes in memory until ProjectionStore/lifecycle succeeds.
|
|
1480
|
+
*/
|
|
1481
|
+
function applyDeferredLiveWrites(cwd, writes) {
|
|
1482
|
+
for (const [relativePath, write] of writes) {
|
|
1483
|
+
const target = path.join(cwd, ...relativePath.replace(/\\/g, "/").split("/"));
|
|
1484
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
1485
|
+
fs.writeFileSync(target, write.content);
|
|
1486
|
+
if (write.executable)
|
|
1487
|
+
fs.chmodSync(target, 0o755);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
function migrationRoot(item) {
|
|
1491
|
+
const paths = [item.from, item.to].filter((value) => typeof value === "string" && value.length > 0);
|
|
1492
|
+
const canonical = paths.map(isCanonicalGenerationPath);
|
|
1493
|
+
if (canonical.some(Boolean) && !canonical.every(Boolean))
|
|
1494
|
+
throw new Error("canonical-migration-cross-boundary");
|
|
1495
|
+
return canonical.length > 0 && canonical.every(Boolean)
|
|
1496
|
+
? "canonical"
|
|
1497
|
+
: "live";
|
|
1498
|
+
}
|
|
1499
|
+
function partitionMigrations(classified) {
|
|
1500
|
+
const empty = () => ({
|
|
1501
|
+
auto: [],
|
|
1502
|
+
confirm: [],
|
|
1503
|
+
conflict: [],
|
|
1504
|
+
skip: [],
|
|
1505
|
+
});
|
|
1506
|
+
const result = { canonical: empty(), live: empty() };
|
|
1507
|
+
for (const key of ["auto", "confirm", "conflict", "skip"]) {
|
|
1508
|
+
for (const item of classified[key])
|
|
1509
|
+
result[migrationRoot(item)][key].push(item);
|
|
1510
|
+
}
|
|
1511
|
+
return result;
|
|
1512
|
+
}
|
|
1513
|
+
function mergeMigrationResults(left, right) {
|
|
1514
|
+
return {
|
|
1515
|
+
renamed: left.renamed + right.renamed,
|
|
1516
|
+
deleted: left.deleted + right.deleted,
|
|
1517
|
+
skipped: left.skipped + right.skipped,
|
|
1518
|
+
conflicts: left.conflicts + right.conflicts,
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
function reportUpdateLifecycle(result) {
|
|
1522
|
+
if (result.status === "review" || result.status === "interrupted") {
|
|
1523
|
+
throw new Error(`Pactile lifecycle update stopped: ${result.reason}`);
|
|
1524
|
+
}
|
|
1525
|
+
if (result.status === "degraded") {
|
|
1526
|
+
const failures = result.adapters
|
|
1527
|
+
.filter((adapter) => adapter.status !== "succeeded")
|
|
1528
|
+
.map((adapter) => `${adapter.adapterId}: ${adapter.reason ?? "pending"}${adapter.retryable ? " (retryable)" : ""}`);
|
|
1529
|
+
console.warn(chalk.yellow(`Canonical update committed; Adapter reconciliation is degraded: ${failures.join(", ")}`));
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
console.log(chalk.green("ā Canonical generation and Adapter projections reconciled"));
|
|
1533
|
+
}
|
|
1534
|
+
async function runUpdateLifecycle(cwd, operation, candidateFiles) {
|
|
1535
|
+
const installed = new InstallStateStore(cwd).read();
|
|
1536
|
+
if (!installed)
|
|
1537
|
+
throw new Error("Missing .pactile install state. Re-run `pactile init` to recover before update.");
|
|
1538
|
+
const platforms = installedPactilePlatforms(installed.state);
|
|
1539
|
+
const files = candidateFiles ??
|
|
1540
|
+
collectCanonicalGenerationFiles(cwd, discoverCanonicalGenerationPaths(cwd), platforms, VERSION);
|
|
1541
|
+
const result = await runLifecycleCommand({
|
|
1542
|
+
projectRoot: cwd,
|
|
1543
|
+
operation,
|
|
1544
|
+
runtimeVersion: VERSION,
|
|
1545
|
+
files,
|
|
1546
|
+
platforms,
|
|
1547
|
+
materializeCanonical: candidateFiles
|
|
1548
|
+
? (context) => materializeCanonicalGeneration(cwd, context)
|
|
1549
|
+
: undefined,
|
|
1550
|
+
});
|
|
1551
|
+
reportUpdateLifecycle(result);
|
|
1552
|
+
return result;
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Main update command
|
|
1556
|
+
*/
|
|
1557
|
+
export async function update(options) {
|
|
1558
|
+
const cwd = process.cwd();
|
|
1559
|
+
const rolloutOpts = rolloutOptionsFromUpdate(options);
|
|
1560
|
+
let readinessSnapshot = null;
|
|
1561
|
+
let projectVersion = "unknown";
|
|
1562
|
+
let cliVersion = VERSION;
|
|
1563
|
+
let latestNpmVersion = null;
|
|
1564
|
+
let cliVsProject = 0;
|
|
1565
|
+
let cliVsNpm = 0;
|
|
1566
|
+
const emitEarly = (outcome, extra) => {
|
|
1567
|
+
const readiness = readinessSnapshot ??
|
|
1568
|
+
snapshotReadinessForRollout({
|
|
1569
|
+
cwd,
|
|
1570
|
+
selected: loadProjectCapabilities(cwd),
|
|
1571
|
+
skipReadiness: options.skipReadiness,
|
|
1572
|
+
});
|
|
1573
|
+
const report = createBaseRolloutReport({
|
|
1574
|
+
mode: options.dryRun ? "dry-run" : "apply",
|
|
1575
|
+
outcome,
|
|
1576
|
+
projectPath: cwd,
|
|
1577
|
+
projectVersionBefore: projectVersion,
|
|
1578
|
+
projectVersionAfter: extra?.projectVersionAfter ?? null,
|
|
1579
|
+
latestNpmVersion,
|
|
1580
|
+
cliBehindNpm: cliVsNpm < 0 && latestNpmVersion !== null,
|
|
1581
|
+
options: rolloutOpts,
|
|
1582
|
+
readiness,
|
|
1583
|
+
upgradeDirection: upgradeDirectionFromCompare(cliVsProject, projectVersion),
|
|
1584
|
+
files: extra?.files ??
|
|
1585
|
+
buildFilePlanFromChanges({
|
|
1586
|
+
newFiles: [],
|
|
1587
|
+
unchangedFiles: [],
|
|
1588
|
+
autoUpdateFiles: [],
|
|
1589
|
+
changedFiles: [],
|
|
1590
|
+
userDeletedFiles: [],
|
|
1591
|
+
}),
|
|
1592
|
+
conflictsPending: extra?.conflictsPending ?? [],
|
|
1593
|
+
migrations: extra?.migrations ?? summarizeMigrationPlan(null, 0),
|
|
1594
|
+
breakingMigrationGateRequired: extra?.breakingMigrationGateRequired ?? false,
|
|
1595
|
+
apply: extra?.apply,
|
|
1596
|
+
postUpdateSmoke: extra?.postUpdateSmoke,
|
|
1597
|
+
releaseBlockers: releaseBlockersFromReadiness(readiness, cliVsNpm < 0 && latestNpmVersion !== null, cliVersion, latestNpmVersion),
|
|
1598
|
+
});
|
|
1599
|
+
finishRollout(options, report);
|
|
1600
|
+
};
|
|
1601
|
+
// Resolve canonical state first; legacy-only projects receive import guidance.
|
|
1602
|
+
if (!isWorkflowInitialized(cwd)) {
|
|
1603
|
+
console.log(chalk.red("Error: Pactile not initialized in this directory."));
|
|
1604
|
+
console.log(chalk.gray("Run 'pactile init' first."));
|
|
1605
|
+
emitEarly("blocked_not_initialized");
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
if (resolveWorkflowDirName(cwd) !== DIR_NAMES.WORKFLOW) {
|
|
1609
|
+
console.log(chalk.red(LEGACY_UPDATE_BLOCK_MESSAGE));
|
|
1610
|
+
emitEarly("blocked_not_initialized");
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
console.log(chalk.cyan("\nPactile Update"));
|
|
1614
|
+
console.log(chalk.cyan("āāāāāāāāāāāāāā\n"));
|
|
1615
|
+
// Set up proxy before any network calls (npm version check)
|
|
1616
|
+
setupProxy();
|
|
1617
|
+
readinessSnapshot = snapshotReadinessForRollout({
|
|
1618
|
+
cwd,
|
|
1619
|
+
selected: loadProjectCapabilities(cwd),
|
|
1620
|
+
skipReadiness: options.skipReadiness,
|
|
1621
|
+
});
|
|
1622
|
+
reportUpdateReadiness(readinessSnapshot);
|
|
1623
|
+
// Get versions
|
|
1624
|
+
projectVersion = getInstalledVersion(cwd);
|
|
1625
|
+
cliVersion = VERSION;
|
|
1626
|
+
latestNpmVersion = await getLatestNpmVersion();
|
|
1627
|
+
// Version comparison
|
|
1628
|
+
cliVsProject = compareVersions(cliVersion, projectVersion);
|
|
1629
|
+
cliVsNpm = latestNpmVersion
|
|
1630
|
+
? compareVersions(cliVersion, latestNpmVersion)
|
|
1631
|
+
: 0;
|
|
1632
|
+
// Display versions with context
|
|
1633
|
+
console.log(`Project version: ${chalk.white(projectVersion)}`);
|
|
1634
|
+
console.log(`CLI version: ${chalk.white(cliVersion)}`);
|
|
1635
|
+
if (latestNpmVersion) {
|
|
1636
|
+
console.log(`Latest on npm: ${chalk.white(latestNpmVersion)}`);
|
|
1637
|
+
}
|
|
1638
|
+
else {
|
|
1639
|
+
console.log(chalk.gray("Latest on npm: (unable to fetch)"));
|
|
1640
|
+
}
|
|
1641
|
+
console.log("");
|
|
1642
|
+
// Check if CLI is outdated compared to npm
|
|
1643
|
+
if (cliVsNpm < 0 && latestNpmVersion) {
|
|
1644
|
+
console.log(chalk.yellow(`ā ļø Your CLI (${cliVersion}) is behind npm (${latestNpmVersion}).`));
|
|
1645
|
+
console.log(chalk.yellow(` Run: pactile upgrade\n`));
|
|
1646
|
+
}
|
|
1647
|
+
// Check for downgrade situation
|
|
1648
|
+
if (cliVsProject < 0) {
|
|
1649
|
+
console.log(chalk.red(`ā Cannot update: CLI version (${cliVersion}) < project version (${projectVersion})`));
|
|
1650
|
+
console.log(chalk.red(` This would DOWNGRADE your project!\n`));
|
|
1651
|
+
if (!options.allowDowngrade) {
|
|
1652
|
+
console.log(chalk.gray("Solutions:"));
|
|
1653
|
+
console.log(chalk.gray(` 1. Update your CLI: pactile upgrade`));
|
|
1654
|
+
console.log(chalk.gray(` 2. Force downgrade: pactile update --allow-downgrade\n`));
|
|
1655
|
+
emitEarly("blocked_downgrade");
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
console.log(chalk.yellow("ā ļø --allow-downgrade flag set. Proceeding with downgrade...\n"));
|
|
1659
|
+
}
|
|
1660
|
+
// Migration metadata is displayed at the end to prevent scrolling off screen
|
|
1661
|
+
// Load template hashes for modification detection
|
|
1662
|
+
let hashes = loadHashes(cwd);
|
|
1663
|
+
const isFirstHashTracking = Object.keys(hashes).length === 0;
|
|
1664
|
+
// Handle unknown version - skip regular migrations but safe-file-delete still runs
|
|
1665
|
+
const isUnknownVersion = projectVersion === "unknown";
|
|
1666
|
+
if (isUnknownVersion) {
|
|
1667
|
+
console.log(chalk.yellow("ā ļø No version file found. Skipping migrations ā run pactile init to fix."));
|
|
1668
|
+
console.log(chalk.gray(" Template updates will still be applied."));
|
|
1669
|
+
console.log(chalk.gray(" Safe file cleanup will still run (hash-verified).\n"));
|
|
1670
|
+
}
|
|
1671
|
+
// Self-heal poisoned manifests: prune entries that no current platform
|
|
1672
|
+
// configurator owns. This silently removes user-owned paths that early
|
|
1673
|
+
// Older init versions over-hashed some user-owned session directories.
|
|
1674
|
+
let prunedManifest = false;
|
|
1675
|
+
{
|
|
1676
|
+
const configuredPlatforms = new Set(getConfiguredPlatforms(cwd));
|
|
1677
|
+
const prune = pruneOrphanManifestKeys(cwd, [...configuredPlatforms], hashes, { persist: false });
|
|
1678
|
+
if (prune.pruned.length > 0) {
|
|
1679
|
+
console.log(chalk.gray(` Pruned ${prune.pruned.length} orphan manifest entries from .template-hashes.json`));
|
|
1680
|
+
hashes = prune.hashes;
|
|
1681
|
+
prunedManifest = true;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
// For breaking releases with recommendMigrate + --migrate, bypass update.skip
|
|
1685
|
+
// across the board (safe-file-delete, new file writes, template updates).
|
|
1686
|
+
// Why: honoring skip here leaves users forever half-migrated ā old deprecated
|
|
1687
|
+
// files persist under skip-protected paths, new commands like `continue.md`
|
|
1688
|
+
// never land, and every future update re-flags the same mess. Rename
|
|
1689
|
+
// migrations already ignore update.skip; this makes the rest consistent
|
|
1690
|
+
// during a breaking upgrade. User customizations are still guarded by the
|
|
1691
|
+
// per-file conflict prompt ("Modified by you") at write time.
|
|
1692
|
+
const breakingBypass = options.migrate === true &&
|
|
1693
|
+
cliVsProject > 0 &&
|
|
1694
|
+
projectVersion !== "unknown" &&
|
|
1695
|
+
(() => {
|
|
1696
|
+
const md = getMigrationMetadata(projectVersion, cliVersion);
|
|
1697
|
+
return md.breaking && md.recommendMigrate;
|
|
1698
|
+
})();
|
|
1699
|
+
// Collect templates (used for both migration classification and change analysis).
|
|
1700
|
+
// Official set ignores update.skip so we can detect skip entries that already
|
|
1701
|
+
// match the shipped template (dogfood pin absorbed by this release).
|
|
1702
|
+
const officialTemplates = collectTemplateFiles(cwd, true);
|
|
1703
|
+
const templates = collectTemplateFiles(cwd, breakingBypass);
|
|
1704
|
+
printLegacyCursorSkillResidueNotice(cwd);
|
|
1705
|
+
// Load update.skip paths (used for both safe-file-delete and template collection)
|
|
1706
|
+
const skipPaths = loadUpdateSkipPaths(cwd);
|
|
1707
|
+
const staleSkipPaths = collectStaleUpdateSkipPaths(cwd, skipPaths, officialTemplates);
|
|
1708
|
+
// Collect safe-file-delete items from ALL manifests (hash match is the safety net)
|
|
1709
|
+
// This runs regardless of version ā unknown version still gets safe cleanup
|
|
1710
|
+
const allMigrations = getAllMigrations();
|
|
1711
|
+
const safeFileDeletes = collectSafeFileDeletes(allMigrations, cwd, skipPaths, breakingBypass);
|
|
1712
|
+
const hasSafeDeletes = safeFileDeletes.filter((c) => c.action === "delete").length > 0;
|
|
1713
|
+
// Check for pending regular migrations (skip if unknown version)
|
|
1714
|
+
let pendingMigrations = isUnknownVersion
|
|
1715
|
+
? []
|
|
1716
|
+
: filterReadOnlyLegacyMigrationItems(getMigrationsForVersion(projectVersion, cliVersion));
|
|
1717
|
+
// Also check for "orphaned" migrations - where source still exists but version says we shouldn't migrate
|
|
1718
|
+
// This handles cases where version was updated but migrations weren't applied
|
|
1719
|
+
const orphanedMigrations = allMigrations.filter((item) => {
|
|
1720
|
+
// Only check rename and rename-dir migrations
|
|
1721
|
+
if (item.type !== "rename" && item.type !== "rename-dir")
|
|
1722
|
+
return false;
|
|
1723
|
+
if (!item.from || !item.to)
|
|
1724
|
+
return false;
|
|
1725
|
+
const oldPath = path.join(cwd, item.from);
|
|
1726
|
+
const newPath = path.join(cwd, item.to);
|
|
1727
|
+
// Orphaned if: source exists AND target doesn't exist
|
|
1728
|
+
// AND this migration isn't already in pendingMigrations
|
|
1729
|
+
const sourceExists = fs.existsSync(oldPath);
|
|
1730
|
+
const targetExists = fs.existsSync(newPath);
|
|
1731
|
+
const alreadyPending = pendingMigrations.some((m) => m.from === item.from && m.to === item.to);
|
|
1732
|
+
return sourceExists && !targetExists && !alreadyPending;
|
|
1733
|
+
});
|
|
1734
|
+
// Add orphaned migrations to pending (they need to be applied)
|
|
1735
|
+
if (orphanedMigrations.length > 0) {
|
|
1736
|
+
console.log(chalk.yellow("ā ļø Detected incomplete migrations from previous updates:"));
|
|
1737
|
+
for (const item of orphanedMigrations) {
|
|
1738
|
+
console.log(chalk.yellow(` ${item.from} ā ${item.to}`));
|
|
1739
|
+
}
|
|
1740
|
+
console.log("");
|
|
1741
|
+
pendingMigrations = filterReadOnlyLegacyMigrationItems([
|
|
1742
|
+
...pendingMigrations,
|
|
1743
|
+
...orphanedMigrations,
|
|
1744
|
+
]);
|
|
1745
|
+
}
|
|
1746
|
+
const hasMigrations = pendingMigrations.length > 0;
|
|
1747
|
+
// Classify migrations (stored for later backup creation)
|
|
1748
|
+
let classifiedMigrations = null;
|
|
1749
|
+
if (hasMigrations) {
|
|
1750
|
+
console.log(chalk.cyan("Analyzing migrations...\n"));
|
|
1751
|
+
classifiedMigrations = classifyMigrations(pendingMigrations, cwd, hashes, templates);
|
|
1752
|
+
printMigrationSummary(classifiedMigrations);
|
|
1753
|
+
// Hard-stop: pending rename/delete work from a breaking release requires --migrate.
|
|
1754
|
+
// Why: without --migrate, those entries are skipped and update()'s later path silently
|
|
1755
|
+
// bumps the version stamp, leaving old paths orphaned next to new templates. Force
|
|
1756
|
+
// explicit opt-in so the user can't half-migrate by accident.
|
|
1757
|
+
const pendingMigrationCount = classifiedMigrations.auto.length +
|
|
1758
|
+
classifiedMigrations.confirm.length +
|
|
1759
|
+
classifiedMigrations.conflict.length;
|
|
1760
|
+
if (pendingMigrationCount > 0 &&
|
|
1761
|
+
!options.migrate &&
|
|
1762
|
+
!options.dryRun &&
|
|
1763
|
+
cliVsProject > 0 &&
|
|
1764
|
+
projectVersion !== "unknown") {
|
|
1765
|
+
const gateMetadata = getMigrationMetadata(projectVersion, cliVersion);
|
|
1766
|
+
if (gateMetadata.breaking && gateMetadata.recommendMigrate) {
|
|
1767
|
+
console.log(chalk.bgRed.white.bold(" ā MIGRATION REQUIRED ") +
|
|
1768
|
+
chalk.red(` Breaking changes between ${projectVersion} ā ${cliVersion} require --migrate.`));
|
|
1769
|
+
console.log("");
|
|
1770
|
+
console.log(chalk.yellow(` Run: pactile update --migrate`));
|
|
1771
|
+
console.log("");
|
|
1772
|
+
console.log(chalk.gray(" Without --migrate, renamed/relocated files from breaking releases aren't moved,\n" +
|
|
1773
|
+
" leaving your project with stale paths alongside new templates.\n" +
|
|
1774
|
+
" Use --dry-run to preview what --migrate will do."));
|
|
1775
|
+
const safeDeleteCandidateCount = safeFileDeletes.filter((c) => c.action === "delete").length;
|
|
1776
|
+
emitEarly("blocked_migration_required", {
|
|
1777
|
+
breakingMigrationGateRequired: true,
|
|
1778
|
+
migrations: summarizeMigrationPlan(classifiedMigrations, safeDeleteCandidateCount),
|
|
1779
|
+
});
|
|
1780
|
+
process.exit(1);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
// Soft hint: non-breaking migrations or projects that chose not to set recommendMigrate
|
|
1784
|
+
if (!options.migrate) {
|
|
1785
|
+
const autoCount = classifiedMigrations.auto.length;
|
|
1786
|
+
const confirmCount = classifiedMigrations.confirm.length;
|
|
1787
|
+
if (autoCount > 0 || confirmCount > 0) {
|
|
1788
|
+
console.log(chalk.gray(`Tip: Use --migrate to apply migrations (prompts for modified files).`));
|
|
1789
|
+
if (confirmCount > 0) {
|
|
1790
|
+
console.log(chalk.gray(` Use --migrate -f to force all, or --migrate -s to skip modified.\n`));
|
|
1791
|
+
}
|
|
1792
|
+
else {
|
|
1793
|
+
console.log("");
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
// Print safe-file-delete summary (always shown, runs without --migrate)
|
|
1799
|
+
if (safeFileDeletes.length > 0) {
|
|
1800
|
+
printSafeFileDeleteSummary(safeFileDeletes);
|
|
1801
|
+
}
|
|
1802
|
+
// Preview retired alternate-client cleanup (hash-safe; always considered).
|
|
1803
|
+
const alternateClientResiduePreview = cleanupRetiredAlternateClientResidue(cwd, {
|
|
1804
|
+
dryRun: true,
|
|
1805
|
+
});
|
|
1806
|
+
if (alternateClientResiduePreview.deleted.length > 0) {
|
|
1807
|
+
console.log(chalk.cyan("\nRetired alternate-client cleanup (hash-safe):"));
|
|
1808
|
+
for (const rel of alternateClientResiduePreview.deleted) {
|
|
1809
|
+
console.log(chalk.gray(` would delete: ${rel}`));
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
if (alternateClientResiduePreview.preservedModified.length > 0) {
|
|
1813
|
+
console.log(chalk.yellow(` preserve (user-modified): ${alternateClientResiduePreview.preservedModified.join(", ")}`));
|
|
1814
|
+
}
|
|
1815
|
+
// Analyze changes (pass hashes for modification detection)
|
|
1816
|
+
const changes = analyzeChanges(cwd, hashes, templates);
|
|
1817
|
+
const missingTemplateHashes = collectMissingTemplateHashes(changes, hashes);
|
|
1818
|
+
const safeDeleteCandidateCount = safeFileDeletes.filter((c) => c.action === "delete").length;
|
|
1819
|
+
const migrationPlanSummary = summarizeMigrationPlan(classifiedMigrations, safeDeleteCandidateCount);
|
|
1820
|
+
const safeDeletePaths = safeFileDeletes
|
|
1821
|
+
.filter((c) => c.action === "delete")
|
|
1822
|
+
.map((c) => c.item.from);
|
|
1823
|
+
const conflictsPending = changes.changedFiles.map((f) => f.relativePath);
|
|
1824
|
+
const buildRolloutFilePlan = () => buildFilePlanFromChanges({
|
|
1825
|
+
newFiles: changes.newFiles,
|
|
1826
|
+
unchangedFiles: changes.unchangedFiles,
|
|
1827
|
+
autoUpdateFiles: changes.autoUpdateFiles,
|
|
1828
|
+
changedFiles: changes.changedFiles,
|
|
1829
|
+
userDeletedFiles: changes.userDeletedFiles,
|
|
1830
|
+
safeDeletePaths,
|
|
1831
|
+
});
|
|
1832
|
+
const p36State = {};
|
|
1833
|
+
let lifecycleResult = null;
|
|
1834
|
+
const emitRollout = (outcome, extra) => {
|
|
1835
|
+
const readiness = readinessSnapshot ??
|
|
1836
|
+
snapshotReadinessForRollout({
|
|
1837
|
+
cwd,
|
|
1838
|
+
selected: loadProjectCapabilities(cwd),
|
|
1839
|
+
skipReadiness: options.skipReadiness,
|
|
1840
|
+
});
|
|
1841
|
+
const report = createBaseRolloutReport({
|
|
1842
|
+
mode: options.dryRun ? "dry-run" : "apply",
|
|
1843
|
+
outcome,
|
|
1844
|
+
projectPath: cwd,
|
|
1845
|
+
projectVersionBefore: projectVersion,
|
|
1846
|
+
projectVersionAfter: extra?.projectVersionAfter ?? null,
|
|
1847
|
+
latestNpmVersion,
|
|
1848
|
+
cliBehindNpm: cliVsNpm < 0 && latestNpmVersion !== null,
|
|
1849
|
+
options: rolloutOpts,
|
|
1850
|
+
readiness,
|
|
1851
|
+
upgradeDirection: upgradeDirectionFromCompare(cliVsProject, projectVersion),
|
|
1852
|
+
files: extra?.files ?? buildRolloutFilePlan(),
|
|
1853
|
+
conflictsPending,
|
|
1854
|
+
migrations: migrationPlanSummary,
|
|
1855
|
+
breakingMigrationGateRequired: extra?.breakingMigrationGateRequired ?? false,
|
|
1856
|
+
backupPath: extra?.backupPath ?? null,
|
|
1857
|
+
apply: extra?.apply,
|
|
1858
|
+
postUpdateSmoke: extra?.postUpdateSmoke,
|
|
1859
|
+
lifecycle: extra?.lifecycle ?? lifecycleResultToSummary(lifecycleResult),
|
|
1860
|
+
releaseBlockers: releaseBlockersFromReadiness(readiness, cliVsNpm < 0 && latestNpmVersion !== null, cliVersion, latestNpmVersion),
|
|
1861
|
+
p36: extra?.p36 ?? p36State.report,
|
|
1862
|
+
});
|
|
1863
|
+
finishRollout(options, report);
|
|
1864
|
+
};
|
|
1865
|
+
// Print summary
|
|
1866
|
+
printChangeSummary(changes);
|
|
1867
|
+
if (options.dryRun) {
|
|
1868
|
+
logStaleUpdateSkip(staleSkipPaths, true);
|
|
1869
|
+
}
|
|
1870
|
+
const officialPlan = planOfficialSurfaceA({
|
|
1871
|
+
cwd,
|
|
1872
|
+
hashes,
|
|
1873
|
+
refresh: changes.autoUpdateFiles.map((file) => file.relativePath),
|
|
1874
|
+
preserved: changes.changedFiles.map((file) => file.relativePath),
|
|
1875
|
+
added: changes.newFiles.map((file) => file.relativePath),
|
|
1876
|
+
});
|
|
1877
|
+
const artifactPlan = planArtifactMigration({ root: cwd });
|
|
1878
|
+
const waveCPlan = planWaveC({
|
|
1879
|
+
root: cwd,
|
|
1880
|
+
report: scanContractMigration({ root: cwd }),
|
|
1881
|
+
});
|
|
1882
|
+
const p36Plan = composeP36Plan({
|
|
1883
|
+
official: officialPlan,
|
|
1884
|
+
artifacts: artifactPlan,
|
|
1885
|
+
writeArtifacts: Boolean(options.writeArtifacts),
|
|
1886
|
+
waveC: waveCPlan,
|
|
1887
|
+
});
|
|
1888
|
+
p36State.report = p36SummaryForRollout(p36Plan);
|
|
1889
|
+
printP36Vernacular(p36Plan);
|
|
1890
|
+
// First-time hash tracking hint
|
|
1891
|
+
if (isFirstHashTracking && changes.changedFiles.length > 0) {
|
|
1892
|
+
console.log(chalk.cyan("ā¹ļø First update with hash tracking enabled."));
|
|
1893
|
+
console.log(chalk.gray(" Changed files shown above may not be actual user modifications."));
|
|
1894
|
+
console.log(chalk.gray(" After this update, hash tracking will accurately detect changes.\n"));
|
|
1895
|
+
}
|
|
1896
|
+
// Check if there's anything to do
|
|
1897
|
+
const isUpgrade = cliVsProject > 0;
|
|
1898
|
+
const isDowngrade = cliVsProject < 0;
|
|
1899
|
+
const isSameVersion = cliVsProject === 0;
|
|
1900
|
+
// Check if we have pending migrations that need to be applied
|
|
1901
|
+
const hasPendingMigrations = options.migrate &&
|
|
1902
|
+
classifiedMigrations &&
|
|
1903
|
+
(classifiedMigrations.auto.length > 0 ||
|
|
1904
|
+
classifiedMigrations.confirm.length > 0);
|
|
1905
|
+
const hasOfficialP36 = officialWorkPending(officialPlan);
|
|
1906
|
+
const hasMaintainerArtifactWrites = Boolean(options.writeArtifacts) && artifactPlan.writable.length > 0;
|
|
1907
|
+
const hasWaveCPending = waveCWorkPending(p36Plan);
|
|
1908
|
+
if (changes.newFiles.length === 0 &&
|
|
1909
|
+
changes.autoUpdateFiles.length === 0 &&
|
|
1910
|
+
changes.changedFiles.length === 0 &&
|
|
1911
|
+
!hasPendingMigrations &&
|
|
1912
|
+
!hasSafeDeletes &&
|
|
1913
|
+
!hasOfficialP36 &&
|
|
1914
|
+
!hasMaintainerArtifactWrites &&
|
|
1915
|
+
!hasWaveCPending) {
|
|
1916
|
+
if (!options.dryRun) {
|
|
1917
|
+
const canonicalBuildRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pactile-update-"));
|
|
1918
|
+
try {
|
|
1919
|
+
seedCanonicalBuildRoot(cwd, canonicalBuildRoot);
|
|
1920
|
+
if (prunedManifest)
|
|
1921
|
+
saveHashes(canonicalBuildRoot, hashes);
|
|
1922
|
+
if (missingTemplateHashes.size > 0) {
|
|
1923
|
+
updateHashes(canonicalBuildRoot, missingTemplateHashes);
|
|
1924
|
+
}
|
|
1925
|
+
if (!isSameVersion)
|
|
1926
|
+
updateVersionFile(canonicalBuildRoot);
|
|
1927
|
+
const activePlatforms = installedPactilePlatforms(new InstallStateStore(cwd).read()?.state ?? null);
|
|
1928
|
+
const candidate = collectCanonicalGenerationFiles(canonicalBuildRoot, discoverCanonicalGenerationPaths(canonicalBuildRoot), activePlatforms, VERSION);
|
|
1929
|
+
lifecycleResult = await runUpdateLifecycle(cwd, isSameVersion ? "reconcile" : "update", candidate);
|
|
1930
|
+
}
|
|
1931
|
+
finally {
|
|
1932
|
+
fs.rmSync(canonicalBuildRoot, { recursive: true, force: true });
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
if (isSameVersion) {
|
|
1936
|
+
console.log(chalk.green("ā Already up to date!"));
|
|
1937
|
+
}
|
|
1938
|
+
else {
|
|
1939
|
+
if (isUpgrade) {
|
|
1940
|
+
console.log(chalk.green(`ā No file changes needed for ${projectVersion} ā ${cliVersion}`));
|
|
1941
|
+
}
|
|
1942
|
+
else if (isDowngrade) {
|
|
1943
|
+
console.log(chalk.green(`ā No file changes needed for ${projectVersion} ā ${cliVersion} (downgrade)`));
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
const afterVersion = getInstalledVersion(cwd);
|
|
1947
|
+
emitRollout(lifecycleResult?.status === "degraded"
|
|
1948
|
+
? "applied_degraded"
|
|
1949
|
+
: "no_changes", {
|
|
1950
|
+
projectVersionAfter: afterVersion,
|
|
1951
|
+
files: buildRolloutFilePlan(),
|
|
1952
|
+
});
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
// Show what this operation will do
|
|
1956
|
+
if (isUpgrade) {
|
|
1957
|
+
console.log(chalk.green(`This will UPGRADE: ${projectVersion} ā ${cliVersion}\n`));
|
|
1958
|
+
}
|
|
1959
|
+
else if (isDowngrade) {
|
|
1960
|
+
console.log(chalk.red(`ā ļø This will DOWNGRADE: ${projectVersion} ā ${cliVersion}\n`));
|
|
1961
|
+
}
|
|
1962
|
+
// Show breaking change warning before confirm
|
|
1963
|
+
if (cliVsProject > 0 && projectVersion !== "unknown") {
|
|
1964
|
+
const preConfirmMetadata = getMigrationMetadata(projectVersion, cliVersion);
|
|
1965
|
+
if (preConfirmMetadata.breaking) {
|
|
1966
|
+
console.log(chalk.cyan("ā".repeat(60)));
|
|
1967
|
+
console.log(chalk.bgRed.white.bold(" ā ļø BREAKING CHANGES ") +
|
|
1968
|
+
chalk.red.bold(" Review the changes above carefully!"));
|
|
1969
|
+
if (preConfirmMetadata.changelog.length > 0) {
|
|
1970
|
+
console.log("");
|
|
1971
|
+
console.log(chalk.white(preConfirmMetadata.changelog[0]));
|
|
1972
|
+
}
|
|
1973
|
+
if (preConfirmMetadata.recommendMigrate && !options.migrate) {
|
|
1974
|
+
console.log("");
|
|
1975
|
+
console.log(chalk.bgGreen.black.bold(" š” RECOMMENDED ") +
|
|
1976
|
+
chalk.green.bold(" Run with --migrate to complete the migration"));
|
|
1977
|
+
}
|
|
1978
|
+
// Notice when update.skip is bypassed so user isn't surprised when
|
|
1979
|
+
// skipPaths-protected files get cleaned up during this breaking upgrade.
|
|
1980
|
+
if (breakingBypass && skipPaths.length > 0) {
|
|
1981
|
+
const willBypass = safeFileDeletes.filter((c) => c.action === "delete" &&
|
|
1982
|
+
skipPaths.some((skip) => c.item.from === skip ||
|
|
1983
|
+
c.item.from.startsWith(skip.endsWith("/") ? skip : skip + "/")));
|
|
1984
|
+
if (willBypass.length > 0) {
|
|
1985
|
+
console.log("");
|
|
1986
|
+
console.log(chalk.bgYellow.black.bold(" ā update.skip BYPASSED ") +
|
|
1987
|
+
chalk.yellow.bold(` Breaking release ā ${willBypass.length.toString()} file(s) under your update.skip paths will be cleaned up.`));
|
|
1988
|
+
console.log(chalk.gray(" Hash-verified: only files matching known Pactile templates are deleted. Your local customizations (hash mismatch) are still preserved."));
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
console.log(chalk.cyan("ā".repeat(60)));
|
|
1992
|
+
console.log("");
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
// Dry run mode
|
|
1996
|
+
if (options.dryRun) {
|
|
1997
|
+
console.log(chalk.gray("[Dry run] No changes made."));
|
|
1998
|
+
emitRollout("would_apply", { files: buildRolloutFilePlan() });
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
// File-conflict flags (--force / --skip-all / --create-new) consent to apply
|
|
2002
|
+
// official A writes. They are not Wave C stop-read confirm. Only interactive
|
|
2003
|
+
// Proceed? yes writes the canonical Wave C decision artifact.
|
|
2004
|
+
let waveCInteractivelyConfirmed = false;
|
|
2005
|
+
// Batch-resolution flags are explicit consent for non-interactive runs.
|
|
2006
|
+
// Prompting here breaks CI and `node ... update --force --migrate` smoke tests.
|
|
2007
|
+
if (!options.force && !options.skipAll && !options.createNew) {
|
|
2008
|
+
// Non-interactive (no TTY on stdin) and no explicit consent flag: hard-fail
|
|
2009
|
+
// instead of hanging on inquirer.prompt, which waits forever for stdin that
|
|
2010
|
+
// never arrives (observed when update is run in a backgrounded shell, CI
|
|
2011
|
+
// pipeline, or any redirected-stdin context). Guide the operator toward an
|
|
2012
|
+
// explicit consent flag or a dry-run first.
|
|
2013
|
+
if (!process.stdin.isTTY) {
|
|
2014
|
+
throw new Error("Non-interactive `pactile update` requires an explicit consent flag. " +
|
|
2015
|
+
"Re-run with --force (apply all), --skip-all (preserve modified), --create-new (.new copies), or --dry-run (preview only).");
|
|
2016
|
+
}
|
|
2017
|
+
const { proceed } = await inquirer.prompt([
|
|
2018
|
+
{
|
|
2019
|
+
type: "confirm",
|
|
2020
|
+
name: "proceed",
|
|
2021
|
+
message: "Proceed?",
|
|
2022
|
+
default: true,
|
|
2023
|
+
},
|
|
2024
|
+
]);
|
|
2025
|
+
if (!proceed) {
|
|
2026
|
+
console.log(chalk.yellow("Update cancelled."));
|
|
2027
|
+
emitRollout("cancelled", { files: buildRolloutFilePlan() });
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
waveCInteractivelyConfirmed = true;
|
|
2031
|
+
}
|
|
2032
|
+
// Create complete backup of all managed platform/workflow directories
|
|
2033
|
+
const canonicalBuildRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pactile-update-"));
|
|
2034
|
+
seedCanonicalBuildRoot(cwd, canonicalBuildRoot);
|
|
2035
|
+
if (prunedManifest)
|
|
2036
|
+
saveHashes(canonicalBuildRoot, hashes);
|
|
2037
|
+
const backupDir = createFullBackup(cwd);
|
|
2038
|
+
let migrationApplyResult = null;
|
|
2039
|
+
let officialRetired = 0;
|
|
2040
|
+
let safeDeleted = 0;
|
|
2041
|
+
let liveMigrationApplyResult = null;
|
|
2042
|
+
let deferredLiveSafeDeletes = [];
|
|
2043
|
+
let deferredLiveMigrations = null;
|
|
2044
|
+
let added = 0;
|
|
2045
|
+
let autoUpdated = 0;
|
|
2046
|
+
let updated = 0;
|
|
2047
|
+
let skipped = 0;
|
|
2048
|
+
let createdNew = 0;
|
|
2049
|
+
let configSectionsAppended = 0;
|
|
2050
|
+
let deferredLiveConfigSections = [];
|
|
2051
|
+
let artifactApply = null;
|
|
2052
|
+
const overwrittenPaths = [];
|
|
2053
|
+
const skippedConflictPaths = [];
|
|
2054
|
+
const createdNewPaths = [];
|
|
2055
|
+
const deferredLiveWrites = new Map();
|
|
2056
|
+
const stageTemplateWrite = (relativePath, content) => {
|
|
2057
|
+
const executable = relativePath.endsWith(".sh") || relativePath.endsWith(".py");
|
|
2058
|
+
if (!isCanonicalGenerationPath(relativePath)) {
|
|
2059
|
+
deferredLiveWrites.set(relativePath, { content, executable });
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
const target = candidatePath(cwd, canonicalBuildRoot, relativePath);
|
|
2063
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
2064
|
+
fs.writeFileSync(target, content);
|
|
2065
|
+
if (executable)
|
|
2066
|
+
fs.chmodSync(target, 0o755);
|
|
2067
|
+
};
|
|
2068
|
+
if (backupDir) {
|
|
2069
|
+
console.log(chalk.gray(`\nBackup created: ${path.relative(cwd, backupDir)}/`));
|
|
2070
|
+
}
|
|
2071
|
+
try {
|
|
2072
|
+
// Build all canonical changes in the isolated generation root first.
|
|
2073
|
+
// Legacy host mutations are intentionally deferred until lifecycle has
|
|
2074
|
+
// durably activated that generation below.
|
|
2075
|
+
if (options.migrate && classifiedMigrations) {
|
|
2076
|
+
const migrations = partitionMigrations(classifiedMigrations);
|
|
2077
|
+
const canonicalResult = await executeMigrations(migrations.canonical, canonicalBuildRoot, { force: options.force, skipAll: options.skipAll });
|
|
2078
|
+
migrationApplyResult = canonicalResult;
|
|
2079
|
+
deferredLiveMigrations = migrations.live;
|
|
2080
|
+
}
|
|
2081
|
+
// Canonical safe deletes are staged with the candidate. Host deletes are
|
|
2082
|
+
// held until after the canonical lifecycle commit.
|
|
2083
|
+
if (hasSafeDeletes) {
|
|
2084
|
+
const canonicalSafeDeletes = safeFileDeletes.filter((item) => isCanonicalGenerationPath(item.item.from));
|
|
2085
|
+
deferredLiveSafeDeletes = safeFileDeletes.filter((item) => !isCanonicalGenerationPath(item.item.from));
|
|
2086
|
+
safeDeleted = executeSafeFileDeletes(canonicalSafeDeletes, canonicalBuildRoot);
|
|
2087
|
+
if (safeDeleted > 0) {
|
|
2088
|
+
console.log(chalk.cyan(`\nCleaned up ${safeDeleted} deprecated command file(s)`));
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
// Add new files
|
|
2092
|
+
if (changes.newFiles.length > 0) {
|
|
2093
|
+
console.log(chalk.blue("\nAdding new files..."));
|
|
2094
|
+
for (const file of changes.newFiles) {
|
|
2095
|
+
stageTemplateWrite(file.relativePath, file.newContent);
|
|
2096
|
+
console.log(chalk.green(` + ${file.relativePath}`));
|
|
2097
|
+
added++;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
// Auto-update files (template updated, user didn't modify)
|
|
2101
|
+
if (changes.autoUpdateFiles.length > 0) {
|
|
2102
|
+
console.log(chalk.blue("\nAuto-updating template files..."));
|
|
2103
|
+
for (const file of changes.autoUpdateFiles) {
|
|
2104
|
+
stageTemplateWrite(file.relativePath, file.newContent);
|
|
2105
|
+
console.log(chalk.cyan(` ā ${file.relativePath}`));
|
|
2106
|
+
autoUpdated++;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
// Handle changed files
|
|
2110
|
+
if (changes.changedFiles.length > 0) {
|
|
2111
|
+
console.log(chalk.blue("\n--- Resolving conflicts ---\n"));
|
|
2112
|
+
const applyToAll = { action: null };
|
|
2113
|
+
for (const file of changes.changedFiles) {
|
|
2114
|
+
const action = await promptConflictResolution(file, options, applyToAll);
|
|
2115
|
+
if (action === "overwrite") {
|
|
2116
|
+
stageTemplateWrite(file.relativePath, file.newContent);
|
|
2117
|
+
console.log(chalk.yellow(` ā Overwritten: ${file.relativePath}`));
|
|
2118
|
+
updated++;
|
|
2119
|
+
overwrittenPaths.push(file.relativePath);
|
|
2120
|
+
}
|
|
2121
|
+
else if (action === "create-new") {
|
|
2122
|
+
stageTemplateWrite(`${file.relativePath}.new`, file.newContent);
|
|
2123
|
+
console.log(chalk.blue(` ā Created: ${file.relativePath}.new`));
|
|
2124
|
+
createdNew++;
|
|
2125
|
+
createdNewPaths.push(`${file.relativePath}.new`);
|
|
2126
|
+
}
|
|
2127
|
+
else {
|
|
2128
|
+
console.log(chalk.gray(` ā Skipped: ${file.relativePath}`));
|
|
2129
|
+
skipped++;
|
|
2130
|
+
skippedConflictPaths.push(file.relativePath);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
// Append additive config.yaml sections introduced between versions.
|
|
2135
|
+
// Sentinel-gated, so users keep their customizations and re-running update
|
|
2136
|
+
// on already-migrated files is a no-op. Skipped on unknown / downgrade.
|
|
2137
|
+
if (cliVsProject > 0 && projectVersion !== "unknown") {
|
|
2138
|
+
const sectionEntries = getConfigSectionsAddedBetween(projectVersion, cliVersion);
|
|
2139
|
+
if (sectionEntries.length > 0) {
|
|
2140
|
+
const canonicalSections = sectionEntries.filter((entry) => isCanonicalGenerationPath(entry.file));
|
|
2141
|
+
const liveSections = sectionEntries.filter((entry) => !isCanonicalGenerationPath(entry.file));
|
|
2142
|
+
configSectionsAppended = applyConfigSectionsAdded(canonicalSections, canonicalBuildRoot, templates).appended;
|
|
2143
|
+
deferredLiveConfigSections = liveSections;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
// Update version file
|
|
2147
|
+
updateVersionFile(canonicalBuildRoot);
|
|
2148
|
+
// Update template hashes for new, auto-updated, and overwritten files
|
|
2149
|
+
const filesToHash = new Map(missingTemplateHashes);
|
|
2150
|
+
for (const file of changes.newFiles) {
|
|
2151
|
+
filesToHash.set(file.relativePath, file.newContent);
|
|
2152
|
+
}
|
|
2153
|
+
// Auto-updated files always get new hash
|
|
2154
|
+
for (const file of changes.autoUpdateFiles) {
|
|
2155
|
+
filesToHash.set(file.relativePath, file.newContent);
|
|
2156
|
+
}
|
|
2157
|
+
// Only hash overwritten files (not skipped or .new copies)
|
|
2158
|
+
for (const file of changes.changedFiles) {
|
|
2159
|
+
const staged = isCanonicalGenerationPath(file.relativePath)
|
|
2160
|
+
? (() => {
|
|
2161
|
+
const fullPath = candidatePath(cwd, canonicalBuildRoot, file.relativePath);
|
|
2162
|
+
return fs.existsSync(fullPath)
|
|
2163
|
+
? fs.readFileSync(fullPath, "utf-8")
|
|
2164
|
+
: null;
|
|
2165
|
+
})()
|
|
2166
|
+
: deferredLiveWrites.get(file.relativePath)?.content ?? null;
|
|
2167
|
+
if (staged === file.newContent)
|
|
2168
|
+
filesToHash.set(file.relativePath, file.newContent);
|
|
2169
|
+
}
|
|
2170
|
+
if (filesToHash.size > 0) {
|
|
2171
|
+
updateHashes(canonicalBuildRoot, filesToHash);
|
|
2172
|
+
}
|
|
2173
|
+
const activePlatforms = installedPactilePlatforms(new InstallStateStore(cwd).read()?.state ?? null);
|
|
2174
|
+
const canonicalCandidate = collectCanonicalGenerationFiles(canonicalBuildRoot, discoverCanonicalGenerationPaths(canonicalBuildRoot), activePlatforms, VERSION);
|
|
2175
|
+
lifecycleResult = await runUpdateLifecycle(cwd, "update", canonicalCandidate);
|
|
2176
|
+
}
|
|
2177
|
+
finally {
|
|
2178
|
+
fs.rmSync(canonicalBuildRoot, { recursive: true, force: true });
|
|
2179
|
+
}
|
|
2180
|
+
// The canonical lifecycle (including Adapter ProjectionStore writes) is now
|
|
2181
|
+
// committed. Only at this point may legacy host cleanup, live migrations,
|
|
2182
|
+
// noncanonical template bytes, and optional artifact projections touch cwd.
|
|
2183
|
+
officialRetired = applyOfficialRetire(cwd, officialPlan);
|
|
2184
|
+
if (officialRetired > 0) {
|
|
2185
|
+
console.log(chalk.cyan(`\nStopped ${officialRetired} extra official always-on rule(s)`));
|
|
2186
|
+
}
|
|
2187
|
+
if (deferredLiveMigrations) {
|
|
2188
|
+
liveMigrationApplyResult = await executeMigrations(deferredLiveMigrations, cwd, { force: options.force, skipAll: options.skipAll });
|
|
2189
|
+
// Hardcoded: Rename traces-*.md to journal-*.md in workspace directories.
|
|
2190
|
+
// Trace file names are variable and therefore cannot be represented by the
|
|
2191
|
+
// fixed-path migration manifest. This remains a post-commit user-state
|
|
2192
|
+
// migration and never runs before the canonical generation is active.
|
|
2193
|
+
const workspaceDir = path.join(cwd, PATHS.WORKSPACE);
|
|
2194
|
+
if (fs.existsSync(workspaceDir)) {
|
|
2195
|
+
let journalRenamed = 0;
|
|
2196
|
+
const devDirs = fs.readdirSync(workspaceDir);
|
|
2197
|
+
for (const dev of devDirs) {
|
|
2198
|
+
const devPath = path.join(workspaceDir, dev);
|
|
2199
|
+
if (!fs.statSync(devPath).isDirectory())
|
|
2200
|
+
continue;
|
|
2201
|
+
const files = fs.readdirSync(devPath);
|
|
2202
|
+
for (const file of files) {
|
|
2203
|
+
if (!file.startsWith("traces-") || !file.endsWith(".md"))
|
|
2204
|
+
continue;
|
|
2205
|
+
const oldPath = path.join(devPath, file);
|
|
2206
|
+
const newPath = path.join(devPath, file.replace("traces-", "journal-"));
|
|
2207
|
+
fs.renameSync(oldPath, newPath);
|
|
2208
|
+
journalRenamed++;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
if (journalRenamed > 0) {
|
|
2212
|
+
console.log(chalk.cyan(`Renamed ${journalRenamed} traces file(s) to journal`));
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
if (deferredLiveSafeDeletes.length > 0) {
|
|
2217
|
+
const deleted = executeSafeFileDeletes(deferredLiveSafeDeletes, cwd);
|
|
2218
|
+
safeDeleted += deleted;
|
|
2219
|
+
}
|
|
2220
|
+
const alternateClientCleanup = cleanupRetiredAlternateClientResidue(cwd);
|
|
2221
|
+
if (alternateClientCleanup.deleted.length > 0) {
|
|
2222
|
+
safeDeleted += alternateClientCleanup.deleted.length;
|
|
2223
|
+
console.log(chalk.cyan(`\nCleaned up ${alternateClientCleanup.deleted.length} retired alternate-client file(s)`));
|
|
2224
|
+
}
|
|
2225
|
+
if (alternateClientCleanup.preservedModified.length > 0) {
|
|
2226
|
+
console.log(chalk.yellow(`Preserved user-modified alternate-client residue: ${alternateClientCleanup.preservedModified.join(", ")}`));
|
|
2227
|
+
}
|
|
2228
|
+
applyDeferredLiveWrites(cwd, deferredLiveWrites);
|
|
2229
|
+
if (deferredLiveConfigSections.length > 0) {
|
|
2230
|
+
configSectionsAppended += applyConfigSectionsAdded(deferredLiveConfigSections, cwd, templates).appended;
|
|
2231
|
+
}
|
|
2232
|
+
if (options.writeArtifacts && artifactPlan.writable.length > 0) {
|
|
2233
|
+
artifactApply = applyArtifactMigration({
|
|
2234
|
+
root: cwd,
|
|
2235
|
+
plan: artifactPlan,
|
|
2236
|
+
});
|
|
2237
|
+
if (artifactApply.ok) {
|
|
2238
|
+
console.log(chalk.cyan(`\nWrote ${artifactApply.written} artifact projection(s); business text kept.`));
|
|
2239
|
+
}
|
|
2240
|
+
else {
|
|
2241
|
+
console.log(chalk.yellow("äŗ§ē©åå
„失蓄ļ¼å·²åå°å读ć锹ē®ä»åÆēØļ¼åÆåč· updateć"));
|
|
2242
|
+
if (artifactApply.error)
|
|
2243
|
+
console.log(chalk.gray(` ${artifactApply.error}`));
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
if (migrationApplyResult || liveMigrationApplyResult) {
|
|
2247
|
+
migrationApplyResult = mergeMigrationResults(migrationApplyResult ?? {
|
|
2248
|
+
renamed: 0,
|
|
2249
|
+
deleted: 0,
|
|
2250
|
+
skipped: 0,
|
|
2251
|
+
conflicts: 0,
|
|
2252
|
+
}, liveMigrationApplyResult ?? {
|
|
2253
|
+
renamed: 0,
|
|
2254
|
+
deleted: 0,
|
|
2255
|
+
skipped: 0,
|
|
2256
|
+
conflicts: 0,
|
|
2257
|
+
});
|
|
2258
|
+
printMigrationResult(migrationApplyResult);
|
|
2259
|
+
}
|
|
2260
|
+
// Print summary
|
|
2261
|
+
console.log(chalk.cyan("\n--- Summary ---\n"));
|
|
2262
|
+
if (added > 0) {
|
|
2263
|
+
console.log(` Added: ${added} file(s)`);
|
|
2264
|
+
}
|
|
2265
|
+
if (autoUpdated > 0) {
|
|
2266
|
+
console.log(` Auto-updated: ${autoUpdated} file(s)`);
|
|
2267
|
+
}
|
|
2268
|
+
if (updated > 0) {
|
|
2269
|
+
console.log(` Updated: ${updated} file(s)`);
|
|
2270
|
+
}
|
|
2271
|
+
if (skipped > 0) {
|
|
2272
|
+
console.log(` Skipped: ${skipped} file(s)`);
|
|
2273
|
+
}
|
|
2274
|
+
if (createdNew > 0) {
|
|
2275
|
+
console.log(` Created .new copies: ${createdNew} file(s)`);
|
|
2276
|
+
}
|
|
2277
|
+
if (safeDeleted > 0) {
|
|
2278
|
+
console.log(` Cleaned up: ${safeDeleted} deprecated file(s)`);
|
|
2279
|
+
}
|
|
2280
|
+
if (configSectionsAppended > 0) {
|
|
2281
|
+
console.log(` Config sections added: ${configSectionsAppended}`);
|
|
2282
|
+
}
|
|
2283
|
+
if (officialRetired > 0) {
|
|
2284
|
+
console.log(` Official always-on stopped: ${officialRetired}`);
|
|
2285
|
+
}
|
|
2286
|
+
if (artifactApply?.ok && artifactApply.written > 0) {
|
|
2287
|
+
console.log(` Artifact projections written: ${artifactApply.written}`);
|
|
2288
|
+
}
|
|
2289
|
+
if (artifactApply && !artifactApply.ok) {
|
|
2290
|
+
console.log(" Artifact projections: rolled back to dual-read");
|
|
2291
|
+
}
|
|
2292
|
+
if (hasWaveCPending && waveCInteractivelyConfirmed) {
|
|
2293
|
+
try {
|
|
2294
|
+
writeWaveCConfirmed(cwd);
|
|
2295
|
+
console.log(" 已甮认å读ę§å½¢ē¶ļ¼leftover kind/mode/classification äøåå½åÆäøēēøļ¼");
|
|
2296
|
+
}
|
|
2297
|
+
catch (err) {
|
|
2298
|
+
console.log(chalk.yellow("å读甮认ęŖåäøć锹ē®ä»åÆēØļ¼ę§åꮵä»å读ļ¼åÆåč· updateć"));
|
|
2299
|
+
if (err instanceof Error && err.message) {
|
|
2300
|
+
console.log(chalk.gray(` ${err.message}`));
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
else if (hasWaveCPending) {
|
|
2305
|
+
console.log(chalk.yellow(" ę§åꮵä»å读ćå读éč¦åč·äøę¬”äŗ¤äŗå¼ `pactile update` 并甮认 Proceed?ļ¼--force / --skip-all / --create-new äøē®å读甮认ļ¼ć"));
|
|
2306
|
+
}
|
|
2307
|
+
if (backupDir) {
|
|
2308
|
+
console.log(` Backup: ${path.relative(cwd, backupDir)}/`);
|
|
2309
|
+
}
|
|
2310
|
+
const skipAfterApply = loadUpdateSkipPaths(cwd);
|
|
2311
|
+
const staleSkipAfterApply = collectStaleUpdateSkipPaths(cwd, skipAfterApply, officialTemplates);
|
|
2312
|
+
const prunedSkip = removeUpdateSkipPathsFromConfig(cwd, staleSkipAfterApply);
|
|
2313
|
+
if (prunedSkip.length > 0) {
|
|
2314
|
+
logStaleUpdateSkip(prunedSkip, false);
|
|
2315
|
+
}
|
|
2316
|
+
const actionWord = isDowngrade ? "Downgrade" : "Update";
|
|
2317
|
+
console.log(chalk.green(`\nā
${actionWord} complete! (${projectVersion} ā ${cliVersion})`));
|
|
2318
|
+
printRetiredAlternateClientNotice(cwd);
|
|
2319
|
+
if (createdNew > 0) {
|
|
2320
|
+
console.log(chalk.gray("\nTip: Review .new files and merge changes manually if needed."));
|
|
2321
|
+
}
|
|
2322
|
+
// Create migration task if there are breaking changes with migration guides
|
|
2323
|
+
if (cliVsProject > 0 && projectVersion !== "unknown") {
|
|
2324
|
+
const metadata = getMigrationMetadata(projectVersion, cliVersion);
|
|
2325
|
+
if (metadata.breaking && metadata.migrationGuides.length > 0) {
|
|
2326
|
+
// Create task directory
|
|
2327
|
+
const today = new Date();
|
|
2328
|
+
const monthDay = `${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
|
|
2329
|
+
const taskSlug = `migrate-to-${cliVersion}`;
|
|
2330
|
+
const taskDirName = `${monthDay}-${taskSlug}`;
|
|
2331
|
+
const tasksDir = path.join(cwd, DIR_NAMES.WORKFLOW, DIR_NAMES.TASKS);
|
|
2332
|
+
const taskDir = path.join(tasksDir, taskDirName);
|
|
2333
|
+
// Check if task already exists
|
|
2334
|
+
if (!fs.existsSync(taskDir)) {
|
|
2335
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
2336
|
+
// Get current developer for assignee.
|
|
2337
|
+
// `.developer` is a key=value file (written by init_developer.py):
|
|
2338
|
+
// name=<developer-name>
|
|
2339
|
+
// initialized_at=<iso8601>
|
|
2340
|
+
// Reading it raw and .trim()-ing embeds the entire file contents
|
|
2341
|
+
// (including the `name=` prefix and the `initialized_at` line) into
|
|
2342
|
+
// the assignee field, producing bogus assignees like
|
|
2343
|
+
// "name=suyuan\ninitialized_at=2026-04-07T23:41:21.978312" that
|
|
2344
|
+
// later break session-start task rendering.
|
|
2345
|
+
const developerFile = path.join(cwd, DIR_NAMES.WORKFLOW, ".developer");
|
|
2346
|
+
let currentDeveloper = "unknown";
|
|
2347
|
+
if (fs.existsSync(developerFile)) {
|
|
2348
|
+
const raw = fs.readFileSync(developerFile, "utf-8");
|
|
2349
|
+
const nameMatch = raw.match(/^\s*name\s*=\s*(.+?)\s*$/m);
|
|
2350
|
+
if (nameMatch) {
|
|
2351
|
+
currentDeveloper = nameMatch[1];
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
// Build task.json ā canonical 24-field shape via shared factory.
|
|
2355
|
+
const taskTitle = `Migrate to v${cliVersion}`;
|
|
2356
|
+
const todayStr = today.toISOString().split("T")[0];
|
|
2357
|
+
const taskJson = emptyTaskJson({
|
|
2358
|
+
id: taskSlug,
|
|
2359
|
+
name: taskSlug,
|
|
2360
|
+
title: taskTitle,
|
|
2361
|
+
description: `Breaking change migration from v${projectVersion} to v${cliVersion}`,
|
|
2362
|
+
status: "planning",
|
|
2363
|
+
scope: "migration",
|
|
2364
|
+
priority: "P1",
|
|
2365
|
+
creator: "pactile-update",
|
|
2366
|
+
assignee: currentDeveloper,
|
|
2367
|
+
createdAt: todayStr,
|
|
2368
|
+
});
|
|
2369
|
+
applyKernelCreate({
|
|
2370
|
+
taskDir,
|
|
2371
|
+
actor: "pactile update",
|
|
2372
|
+
idempotencyKey: `update:${taskSlug}`,
|
|
2373
|
+
record: taskJson,
|
|
2374
|
+
evidence: "pactile update migration skeleton",
|
|
2375
|
+
});
|
|
2376
|
+
// Build PRD content
|
|
2377
|
+
let prdContent = `# Migration Task: Upgrade to v${cliVersion}\n\n`;
|
|
2378
|
+
prdContent += `**Created**: ${todayStr}\n`;
|
|
2379
|
+
prdContent += `**From Version**: ${projectVersion}\n`;
|
|
2380
|
+
prdContent += `**To Version**: ${cliVersion}\n`;
|
|
2381
|
+
prdContent += `**Assignee**: ${currentDeveloper}\n\n`;
|
|
2382
|
+
prdContent += `## Status\n\n- [ ] Review migration guide\n- [ ] Update custom files\n- [ ] Run \`pactile update --migrate\`\n- [ ] Test workflows\n\n`;
|
|
2383
|
+
for (const { version, guide, aiInstructions, } of metadata.migrationGuides) {
|
|
2384
|
+
prdContent += `---\n\n## v${version} Migration Guide\n\n`;
|
|
2385
|
+
prdContent += guide;
|
|
2386
|
+
prdContent += "\n\n";
|
|
2387
|
+
if (aiInstructions) {
|
|
2388
|
+
prdContent += `### AI Assistant Instructions\n\n`;
|
|
2389
|
+
prdContent += `When helping with this migration:\n\n`;
|
|
2390
|
+
prdContent += aiInstructions;
|
|
2391
|
+
prdContent += "\n\n";
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
// Write PRD
|
|
2395
|
+
const prdPath = path.join(taskDir, "prd.md");
|
|
2396
|
+
fs.writeFileSync(prdPath, prdContent);
|
|
2397
|
+
console.log("");
|
|
2398
|
+
console.log(chalk.bgCyan.black.bold(" š MIGRATION TASK CREATED "));
|
|
2399
|
+
console.log(chalk.cyan(`A task has been created to help you complete the migration:`));
|
|
2400
|
+
console.log(chalk.white(` ${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}/${taskDirName}/`));
|
|
2401
|
+
console.log("");
|
|
2402
|
+
console.log(chalk.gray("Use AI to help: Ask Claude/Cursor to read the task and fix your custom files."));
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
// Display breaking change warnings at the very end (so they don't scroll off screen)
|
|
2407
|
+
if (cliVsProject > 0 && projectVersion !== "unknown") {
|
|
2408
|
+
const finalMetadata = getMigrationMetadata(projectVersion, cliVersion);
|
|
2409
|
+
if (finalMetadata.breaking || finalMetadata.changelog.length > 0) {
|
|
2410
|
+
console.log("");
|
|
2411
|
+
console.log(chalk.cyan("ā".repeat(60)));
|
|
2412
|
+
if (finalMetadata.breaking) {
|
|
2413
|
+
console.log(chalk.bgRed.white.bold(" ā ļø BREAKING CHANGES ") +
|
|
2414
|
+
chalk.red.bold(" This update contains breaking changes!"));
|
|
2415
|
+
console.log("");
|
|
2416
|
+
}
|
|
2417
|
+
if (finalMetadata.changelog.length > 0) {
|
|
2418
|
+
console.log(chalk.cyan.bold("š What's Changed:"));
|
|
2419
|
+
for (const entry of finalMetadata.changelog) {
|
|
2420
|
+
console.log(chalk.white(` ${entry}`));
|
|
2421
|
+
}
|
|
2422
|
+
console.log("");
|
|
2423
|
+
}
|
|
2424
|
+
if (finalMetadata.recommendMigrate && !options.migrate) {
|
|
2425
|
+
console.log(chalk.bgGreen.black.bold(" š” RECOMMENDED ") +
|
|
2426
|
+
chalk.green.bold(" Run with --migrate to complete the migration"));
|
|
2427
|
+
console.log(chalk.gray(" This will remove legacy files and apply all changes."));
|
|
2428
|
+
console.log("");
|
|
2429
|
+
}
|
|
2430
|
+
console.log(chalk.cyan("ā".repeat(60)));
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
const appliedFilePlan = buildRolloutFilePlan();
|
|
2434
|
+
appliedFilePlan.added = changes.newFiles.map((f) => f.relativePath);
|
|
2435
|
+
appliedFilePlan.autoUpdated = changes.autoUpdateFiles.map((f) => f.relativePath);
|
|
2436
|
+
appliedFilePlan.overwritten = overwrittenPaths;
|
|
2437
|
+
appliedFilePlan.skipped = skippedConflictPaths;
|
|
2438
|
+
appliedFilePlan.createdNew = createdNewPaths;
|
|
2439
|
+
if (safeDeleted > 0) {
|
|
2440
|
+
appliedFilePlan.safeDeleted = safeFileDeletes
|
|
2441
|
+
.filter((c) => c.action === "delete")
|
|
2442
|
+
.map((c) => c.item.from);
|
|
2443
|
+
}
|
|
2444
|
+
const postSmoke = options.skipPostUpdateSmoke ? [] : runPostUpdateSmoke(cwd);
|
|
2445
|
+
const relBackup = backupDir ? path.relative(cwd, backupDir) : null;
|
|
2446
|
+
emitRollout(lifecycleResult?.status === "degraded" ? "applied_degraded" : "applied", {
|
|
2447
|
+
projectVersionAfter: getInstalledVersion(cwd),
|
|
2448
|
+
files: appliedFilePlan,
|
|
2449
|
+
backupPath: relBackup,
|
|
2450
|
+
apply: {
|
|
2451
|
+
backupPath: relBackup,
|
|
2452
|
+
migrations: migrationResultToSummary(migrationApplyResult),
|
|
2453
|
+
safeDeleted,
|
|
2454
|
+
configSectionsAppended,
|
|
2455
|
+
},
|
|
2456
|
+
postUpdateSmoke: postSmoke,
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
//# sourceMappingURL=update.js.map
|