light-plan 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +3181 -2
- package/assets/README.md +111 -0
- package/assets/agents/lpm-developer.md +468 -0
- package/assets/agents/lpm-planner.md +532 -0
- package/assets/harnesses/claude.yml +43 -0
- package/assets/harnesses/copilot.yml +54 -0
- package/assets/harnesses/reasonix.yml +49 -0
- package/assets/hcm/light-plan.yml +45 -0
- package/assets/skills/lpm-board-health.md +243 -0
- package/assets/skills/lpm-board-setup.md +354 -0
- package/assets/skills/lpm-delivery.md +344 -0
- package/assets/skills/lpm-planning.md +411 -0
- package/assets/skills/lpm-templates.md +144 -0
- package/assets/skills/lpm.md +267 -0
- package/dist/cli/agent-view.d.ts +27 -0
- package/dist/cli/agent-view.js +247 -0
- package/dist/cli/agent-view.js.map +1 -0
- package/dist/cli/commands/agent/assets.d.ts +64 -0
- package/dist/cli/commands/agent/assets.js +106 -0
- package/dist/cli/commands/agent/assets.js.map +1 -0
- package/dist/cli/commands/agent/glob.d.ts +25 -0
- package/dist/cli/commands/agent/glob.js +82 -0
- package/dist/cli/commands/agent/glob.js.map +1 -0
- package/dist/cli/commands/agent/index.d.ts +11 -0
- package/dist/cli/commands/agent/index.js +284 -0
- package/dist/cli/commands/agent/index.js.map +1 -0
- package/dist/cli/commands/agent/install.d.ts +59 -0
- package/dist/cli/commands/agent/install.js +131 -0
- package/dist/cli/commands/agent/install.js.map +1 -0
- package/dist/cli/commands/agent/mapping.d.ts +175 -0
- package/dist/cli/commands/agent/mapping.js +256 -0
- package/dist/cli/commands/agent/mapping.js.map +1 -0
- package/dist/cli/commands/check.d.ts +2 -0
- package/dist/cli/commands/check.js +81 -0
- package/dist/cli/commands/check.js.map +1 -0
- package/dist/cli/commands/comment.d.ts +2 -0
- package/dist/cli/commands/comment.js +104 -0
- package/dist/cli/commands/comment.js.map +1 -0
- package/dist/cli/commands/convert.d.ts +2 -0
- package/dist/cli/commands/convert.js +115 -0
- package/dist/cli/commands/convert.js.map +1 -0
- package/dist/cli/commands/copy.d.ts +2 -0
- package/dist/cli/commands/copy.js +69 -0
- package/dist/cli/commands/copy.js.map +1 -0
- package/dist/cli/commands/export.d.ts +11 -0
- package/dist/cli/commands/export.js +226 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/flag.d.ts +2 -0
- package/dist/cli/commands/flag.js +172 -0
- package/dist/cli/commands/flag.js.map +1 -0
- package/dist/cli/commands/git.d.ts +11 -0
- package/dist/cli/commands/git.js +357 -0
- package/dist/cli/commands/git.js.map +1 -0
- package/dist/cli/commands/hcm/bundle.d.ts +60 -0
- package/dist/cli/commands/hcm/bundle.js +166 -0
- package/dist/cli/commands/hcm/bundle.js.map +1 -0
- package/dist/cli/commands/hcm/index.d.ts +11 -0
- package/dist/cli/commands/hcm/index.js +162 -0
- package/dist/cli/commands/hcm/index.js.map +1 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.js +81 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/insert.d.ts +2 -0
- package/dist/cli/commands/insert.js +70 -0
- package/dist/cli/commands/insert.js.map +1 -0
- package/dist/cli/commands/instructions.d.ts +7 -0
- package/dist/cli/commands/instructions.js +256 -0
- package/dist/cli/commands/instructions.js.map +1 -0
- package/dist/cli/commands/link.d.ts +2 -0
- package/dist/cli/commands/link.js +121 -0
- package/dist/cli/commands/link.js.map +1 -0
- package/dist/cli/commands/mcp/config.d.ts +71 -0
- package/dist/cli/commands/mcp/config.js +119 -0
- package/dist/cli/commands/mcp/config.js.map +1 -0
- package/dist/cli/commands/mcp/index.d.ts +9 -0
- package/dist/cli/commands/mcp/index.js +92 -0
- package/dist/cli/commands/mcp/index.js.map +1 -0
- package/dist/cli/commands/mcp/serve.d.ts +1 -0
- package/dist/cli/commands/mcp/serve.js +76 -0
- package/dist/cli/commands/mcp/serve.js.map +1 -0
- package/dist/cli/commands/mcp/setup.d.ts +1 -0
- package/dist/cli/commands/mcp/setup.js +83 -0
- package/dist/cli/commands/mcp/setup.js.map +1 -0
- package/dist/cli/commands/me.d.ts +2 -0
- package/dist/cli/commands/me.js +85 -0
- package/dist/cli/commands/me.js.map +1 -0
- package/dist/cli/commands/move.d.ts +2 -0
- package/dist/cli/commands/move.js +81 -0
- package/dist/cli/commands/move.js.map +1 -0
- package/dist/cli/commands/new.d.ts +2 -0
- package/dist/cli/commands/new.js +222 -0
- package/dist/cli/commands/new.js.map +1 -0
- package/dist/cli/commands/open.d.ts +2 -0
- package/dist/cli/commands/open.js +43 -0
- package/dist/cli/commands/open.js.map +1 -0
- package/dist/cli/commands/period.d.ts +10 -0
- package/dist/cli/commands/period.js +165 -0
- package/dist/cli/commands/period.js.map +1 -0
- package/dist/cli/commands/profile.d.ts +2 -0
- package/dist/cli/commands/profile.js +133 -0
- package/dist/cli/commands/profile.js.map +1 -0
- package/dist/cli/commands/queue.d.ts +2 -0
- package/dist/cli/commands/queue.js +582 -0
- package/dist/cli/commands/queue.js.map +1 -0
- package/dist/cli/commands/remote.d.ts +10 -0
- package/dist/cli/commands/remote.js +3142 -0
- package/dist/cli/commands/remote.js.map +1 -0
- package/dist/cli/commands/rm.d.ts +2 -0
- package/dist/cli/commands/rm.js +63 -0
- package/dist/cli/commands/rm.js.map +1 -0
- package/dist/cli/commands/set.d.ts +2 -0
- package/dist/cli/commands/set.js +139 -0
- package/dist/cli/commands/set.js.map +1 -0
- package/dist/cli/commands/split.d.ts +2 -0
- package/dist/cli/commands/split.js +90 -0
- package/dist/cli/commands/split.js.map +1 -0
- package/dist/cli/commands/task.d.ts +2 -0
- package/dist/cli/commands/task.js +298 -0
- package/dist/cli/commands/task.js.map +1 -0
- package/dist/cli/commands/team.d.ts +2 -0
- package/dist/cli/commands/team.js +114 -0
- package/dist/cli/commands/team.js.map +1 -0
- package/dist/cli/commands/template.d.ts +2 -0
- package/dist/cli/commands/template.js +338 -0
- package/dist/cli/commands/template.js.map +1 -0
- package/dist/cli/commands/ui.d.ts +2 -0
- package/dist/cli/commands/ui.js +98 -0
- package/dist/cli/commands/ui.js.map +1 -0
- package/dist/cli/commands/upstream.d.ts +2 -0
- package/dist/cli/commands/upstream.js +218 -0
- package/dist/cli/commands/upstream.js.map +1 -0
- package/dist/cli/context.d.ts +24 -0
- package/dist/cli/context.js +52 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +208 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/live.d.ts +71 -0
- package/dist/cli/live.js +182 -0
- package/dist/cli/live.js.map +1 -0
- package/dist/cli/plan.d.ts +28 -0
- package/dist/cli/plan.js +76 -0
- package/dist/cli/plan.js.map +1 -0
- package/dist/cli/prompt.d.ts +60 -0
- package/dist/cli/prompt.js +185 -0
- package/dist/cli/prompt.js.map +1 -0
- package/dist/cli/ui.d.ts +65 -0
- package/dist/cli/ui.js +79 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/core/board/dependency-rollup.d.ts +54 -0
- package/dist/core/board/dependency-rollup.js +72 -0
- package/dist/core/board/dependency-rollup.js.map +1 -0
- package/dist/core/board/flag-rollup.d.ts +33 -0
- package/dist/core/board/flag-rollup.js +56 -0
- package/dist/core/board/flag-rollup.js.map +1 -0
- package/dist/core/board/index.d.ts +21 -0
- package/dist/core/board/index.js +22 -0
- package/dist/core/board/index.js.map +1 -0
- package/dist/core/board/load/cache.d.ts +52 -0
- package/dist/core/board/load/cache.js +105 -0
- package/dist/core/board/load/cache.js.map +1 -0
- package/dist/core/board/load/fields.d.ts +33 -0
- package/dist/core/board/load/fields.js +154 -0
- package/dist/core/board/load/fields.js.map +1 -0
- package/dist/core/board/load/scan.d.ts +39 -0
- package/dist/core/board/load/scan.js +88 -0
- package/dist/core/board/load/scan.js.map +1 -0
- package/dist/core/board/load/tree.d.ts +10 -0
- package/dist/core/board/load/tree.js +114 -0
- package/dist/core/board/load/tree.js.map +1 -0
- package/dist/core/board/load.d.ts +69 -0
- package/dist/core/board/load.js +204 -0
- package/dist/core/board/load.js.map +1 -0
- package/dist/core/board/query.d.ts +126 -0
- package/dist/core/board/query.js +291 -0
- package/dist/core/board/query.js.map +1 -0
- package/dist/core/board/registry.d.ts +51 -0
- package/dist/core/board/registry.js +98 -0
- package/dist/core/board/registry.js.map +1 -0
- package/dist/core/board/remote-scopes.d.ts +71 -0
- package/dist/core/board/remote-scopes.js +103 -0
- package/dist/core/board/remote-scopes.js.map +1 -0
- package/dist/core/board/rollup.d.ts +32 -0
- package/dist/core/board/rollup.js +67 -0
- package/dist/core/board/rollup.js.map +1 -0
- package/dist/core/board/scope.d.ts +81 -0
- package/dist/core/board/scope.js +134 -0
- package/dist/core/board/scope.js.map +1 -0
- package/dist/core/board/simulate.d.ts +138 -0
- package/dist/core/board/simulate.js +141 -0
- package/dist/core/board/simulate.js.map +1 -0
- package/dist/core/board/tasks/index.d.ts +25 -0
- package/dist/core/board/tasks/index.js +26 -0
- package/dist/core/board/tasks/index.js.map +1 -0
- package/dist/core/board/tasks/ranking.d.ts +155 -0
- package/dist/core/board/tasks/ranking.js +374 -0
- package/dist/core/board/tasks/ranking.js.map +1 -0
- package/dist/core/board/tasks/roster.d.ts +42 -0
- package/dist/core/board/tasks/roster.js +82 -0
- package/dist/core/board/tasks/roster.js.map +1 -0
- package/dist/core/board/tasks.d.ts +1 -0
- package/dist/core/board/tasks.js +2 -0
- package/dist/core/board/tasks.js.map +1 -0
- package/dist/core/config/index.d.ts +9 -0
- package/dist/core/config/index.js +10 -0
- package/dist/core/config/index.js.map +1 -0
- package/dist/core/config/lookup.d.ts +114 -0
- package/dist/core/config/lookup.js +221 -0
- package/dist/core/config/lookup.js.map +1 -0
- package/dist/core/config/remote-blocks.d.ts +17 -0
- package/dist/core/config/remote-blocks.js +47 -0
- package/dist/core/config/remote-blocks.js.map +1 -0
- package/dist/core/config/schema.d.ts +10 -0
- package/dist/core/config/schema.js +451 -0
- package/dist/core/config/schema.js.map +1 -0
- package/dist/core/errors.d.ts +23 -0
- package/dist/core/errors.js +31 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/gitsync/hosts.d.ts +40 -0
- package/dist/core/gitsync/hosts.js +115 -0
- package/dist/core/gitsync/hosts.js.map +1 -0
- package/dist/core/gitsync/index.d.ts +25 -0
- package/dist/core/gitsync/index.js +25 -0
- package/dist/core/gitsync/index.js.map +1 -0
- package/dist/core/gitsync/integrate.d.ts +72 -0
- package/dist/core/gitsync/integrate.js +115 -0
- package/dist/core/gitsync/integrate.js.map +1 -0
- package/dist/core/gitsync/repo.d.ts +61 -0
- package/dist/core/gitsync/repo.js +213 -0
- package/dist/core/gitsync/repo.js.map +1 -0
- package/dist/core/gitsync/run.d.ts +58 -0
- package/dist/core/gitsync/run.js +102 -0
- package/dist/core/gitsync/run.js.map +1 -0
- package/dist/core/gitsync/status.d.ts +59 -0
- package/dist/core/gitsync/status.js +81 -0
- package/dist/core/gitsync/status.js.map +1 -0
- package/dist/core/gitsync/sync.d.ts +111 -0
- package/dist/core/gitsync/sync.js +275 -0
- package/dist/core/gitsync/sync.js.map +1 -0
- package/dist/core/index.d.ts +31 -0
- package/dist/core/index.js +32 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/instructions/analyze.d.ts +70 -0
- package/dist/core/instructions/analyze.js +275 -0
- package/dist/core/instructions/analyze.js.map +1 -0
- package/dist/core/instructions/builtin.d.ts +21 -0
- package/dist/core/instructions/builtin.js +133 -0
- package/dist/core/instructions/builtin.js.map +1 -0
- package/dist/core/instructions/context.d.ts +155 -0
- package/dist/core/instructions/context.js +209 -0
- package/dist/core/instructions/context.js.map +1 -0
- package/dist/core/instructions/index.d.ts +29 -0
- package/dist/core/instructions/index.js +30 -0
- package/dist/core/instructions/index.js.map +1 -0
- package/dist/core/instructions/instructions.d.ts +90 -0
- package/dist/core/instructions/instructions.js +179 -0
- package/dist/core/instructions/instructions.js.map +1 -0
- package/dist/core/instructions/template.d.ts +108 -0
- package/dist/core/instructions/template.js +252 -0
- package/dist/core/instructions/template.js.map +1 -0
- package/dist/core/model/attributes.d.ts +16 -0
- package/dist/core/model/attributes.js +119 -0
- package/dist/core/model/attributes.js.map +1 -0
- package/dist/core/model/index.d.ts +10 -0
- package/dist/core/model/index.js +11 -0
- package/dist/core/model/index.js.map +1 -0
- package/dist/core/model/links.d.ts +34 -0
- package/dist/core/model/links.js +114 -0
- package/dist/core/model/links.js.map +1 -0
- package/dist/core/model/profile.d.ts +31 -0
- package/dist/core/model/profile.js +8 -0
- package/dist/core/model/profile.js.map +1 -0
- package/dist/core/model/types.d.ts +441 -0
- package/dist/core/model/types.js +167 -0
- package/dist/core/model/types.js.map +1 -0
- package/dist/core/model/zod.d.ts +6 -0
- package/dist/core/model/zod.js +11 -0
- package/dist/core/model/zod.js.map +1 -0
- package/dist/core/operations/board-index.d.ts +45 -0
- package/dist/core/operations/board-index.js +147 -0
- package/dist/core/operations/board-index.js.map +1 -0
- package/dist/core/operations/claim.d.ts +69 -0
- package/dist/core/operations/claim.js +115 -0
- package/dist/core/operations/claim.js.map +1 -0
- package/dist/core/operations/comment.d.ts +30 -0
- package/dist/core/operations/comment.js +46 -0
- package/dist/core/operations/comment.js.map +1 -0
- package/dist/core/operations/create.d.ts +82 -0
- package/dist/core/operations/create.js +306 -0
- package/dist/core/operations/create.js.map +1 -0
- package/dist/core/operations/flag.d.ts +58 -0
- package/dist/core/operations/flag.js +95 -0
- package/dist/core/operations/flag.js.map +1 -0
- package/dist/core/operations/git-sync.d.ts +148 -0
- package/dist/core/operations/git-sync.js +341 -0
- package/dist/core/operations/git-sync.js.map +1 -0
- package/dist/core/operations/index.d.ts +44 -0
- package/dist/core/operations/index.js +45 -0
- package/dist/core/operations/index.js.map +1 -0
- package/dist/core/operations/init.d.ts +25 -0
- package/dist/core/operations/init.js +122 -0
- package/dist/core/operations/init.js.map +1 -0
- package/dist/core/operations/link.d.ts +40 -0
- package/dist/core/operations/link.js +131 -0
- package/dist/core/operations/link.js.map +1 -0
- package/dist/core/operations/move.d.ts +43 -0
- package/dist/core/operations/move.js +103 -0
- package/dist/core/operations/move.js.map +1 -0
- package/dist/core/operations/profile.d.ts +23 -0
- package/dist/core/operations/profile.js +26 -0
- package/dist/core/operations/profile.js.map +1 -0
- package/dist/core/operations/remotes-off.d.ts +12 -0
- package/dist/core/operations/remotes-off.js +67 -0
- package/dist/core/operations/remotes-off.js.map +1 -0
- package/dist/core/operations/remove.d.ts +18 -0
- package/dist/core/operations/remove.js +114 -0
- package/dist/core/operations/remove.js.map +1 -0
- package/dist/core/operations/retype.d.ts +25 -0
- package/dist/core/operations/retype.js +59 -0
- package/dist/core/operations/retype.js.map +1 -0
- package/dist/core/operations/rollup.d.ts +74 -0
- package/dist/core/operations/rollup.js +121 -0
- package/dist/core/operations/rollup.js.map +1 -0
- package/dist/core/operations/shared.d.ts +133 -0
- package/dist/core/operations/shared.js +383 -0
- package/dist/core/operations/shared.js.map +1 -0
- package/dist/core/operations/update.d.ts +43 -0
- package/dist/core/operations/update.js +161 -0
- package/dist/core/operations/update.js.map +1 -0
- package/dist/core/operations/user.d.ts +27 -0
- package/dist/core/operations/user.js +68 -0
- package/dist/core/operations/user.js.map +1 -0
- package/dist/core/profile/current.d.ts +35 -0
- package/dist/core/profile/current.js +31 -0
- package/dist/core/profile/current.js.map +1 -0
- package/dist/core/profile/index.d.ts +14 -0
- package/dist/core/profile/index.js +15 -0
- package/dist/core/profile/index.js.map +1 -0
- package/dist/core/profile/schema.d.ts +9 -0
- package/dist/core/profile/schema.js +100 -0
- package/dist/core/profile/schema.js.map +1 -0
- package/dist/core/storage/activity.d.ts +78 -0
- package/dist/core/storage/activity.js +131 -0
- package/dist/core/storage/activity.js.map +1 -0
- package/dist/core/storage/atomic.d.ts +34 -0
- package/dist/core/storage/atomic.js +117 -0
- package/dist/core/storage/atomic.js.map +1 -0
- package/dist/core/storage/comments.d.ts +39 -0
- package/dist/core/storage/comments.js +88 -0
- package/dist/core/storage/comments.js.map +1 -0
- package/dist/core/storage/document.d.ts +16 -0
- package/dist/core/storage/document.js +98 -0
- package/dist/core/storage/document.js.map +1 -0
- package/dist/core/storage/frontmatter.d.ts +16 -0
- package/dist/core/storage/frontmatter.js +33 -0
- package/dist/core/storage/frontmatter.js.map +1 -0
- package/dist/core/storage/git.d.ts +8 -0
- package/dist/core/storage/git.js +48 -0
- package/dist/core/storage/git.js.map +1 -0
- package/dist/core/storage/index.d.ts +27 -0
- package/dist/core/storage/index.js +28 -0
- package/dist/core/storage/index.js.map +1 -0
- package/dist/core/storage/local.d.ts +57 -0
- package/dist/core/storage/local.js +133 -0
- package/dist/core/storage/local.js.map +1 -0
- package/dist/core/storage/lock.d.ts +85 -0
- package/dist/core/storage/lock.js +364 -0
- package/dist/core/storage/lock.js.map +1 -0
- package/dist/core/storage/paths.d.ts +125 -0
- package/dist/core/storage/paths.js +202 -0
- package/dist/core/storage/paths.js.map +1 -0
- package/dist/core/storage/state.d.ts +32 -0
- package/dist/core/storage/state.js +71 -0
- package/dist/core/storage/state.js.map +1 -0
- package/dist/core/storage/templates.d.ts +29 -0
- package/dist/core/storage/templates.js +62 -0
- package/dist/core/storage/templates.js.map +1 -0
- package/dist/core/storage/views.d.ts +18 -0
- package/dist/core/storage/views.js +62 -0
- package/dist/core/storage/views.js.map +1 -0
- package/dist/core/validation/check.d.ts +4 -0
- package/dist/core/validation/check.js +22 -0
- package/dist/core/validation/check.js.map +1 -0
- package/dist/core/validation/checks/collection.d.ts +4 -0
- package/dist/core/validation/checks/collection.js +152 -0
- package/dist/core/validation/checks/collection.js.map +1 -0
- package/dist/core/validation/checks/counters.d.ts +3 -0
- package/dist/core/validation/checks/counters.js +23 -0
- package/dist/core/validation/checks/counters.js.map +1 -0
- package/dist/core/validation/checks/dependencies.d.ts +3 -0
- package/dist/core/validation/checks/dependencies.js +58 -0
- package/dist/core/validation/checks/dependencies.js.map +1 -0
- package/dist/core/validation/checks/gitignore.d.ts +10 -0
- package/dist/core/validation/checks/gitignore.js +22 -0
- package/dist/core/validation/checks/gitignore.js.map +1 -0
- package/dist/core/validation/checks/index-file.d.ts +8 -0
- package/dist/core/validation/checks/index-file.js +26 -0
- package/dist/core/validation/checks/index-file.js.map +1 -0
- package/dist/core/validation/checks/index.d.ts +20 -0
- package/dist/core/validation/checks/index.js +21 -0
- package/dist/core/validation/checks/index.js.map +1 -0
- package/dist/core/validation/checks/issues.d.ts +3 -0
- package/dist/core/validation/checks/issues.js +152 -0
- package/dist/core/validation/checks/issues.js.map +1 -0
- package/dist/core/validation/checks/periods.d.ts +3 -0
- package/dist/core/validation/checks/periods.js +89 -0
- package/dist/core/validation/checks/periods.js.map +1 -0
- package/dist/core/validation/checks/remotes.d.ts +20 -0
- package/dist/core/validation/checks/remotes.js +42 -0
- package/dist/core/validation/checks/remotes.js.map +1 -0
- package/dist/core/validation/checks/resources.d.ts +3 -0
- package/dist/core/validation/checks/resources.js +77 -0
- package/dist/core/validation/checks/resources.js.map +1 -0
- package/dist/core/validation/checks/rollup.d.ts +27 -0
- package/dist/core/validation/checks/rollup.js +53 -0
- package/dist/core/validation/checks/rollup.js.map +1 -0
- package/dist/core/validation/checks/squads.d.ts +3 -0
- package/dist/core/validation/checks/squads.js +42 -0
- package/dist/core/validation/checks/squads.js.map +1 -0
- package/dist/core/validation/checks/templates.d.ts +14 -0
- package/dist/core/validation/checks/templates.js +119 -0
- package/dist/core/validation/checks/templates.js.map +1 -0
- package/dist/core/validation/fix.d.ts +7 -0
- package/dist/core/validation/fix.js +273 -0
- package/dist/core/validation/fix.js.map +1 -0
- package/dist/core/validation/index.d.ts +7 -0
- package/dist/core/validation/index.js +8 -0
- package/dist/core/validation/index.js.map +1 -0
- package/dist/core/validation/shared.d.ts +47 -0
- package/dist/core/validation/shared.js +66 -0
- package/dist/core/validation/shared.js.map +1 -0
- package/dist/mcp/context.d.ts +70 -0
- package/dist/mcp/context.js +110 -0
- package/dist/mcp/context.js.map +1 -0
- package/dist/mcp/index.d.ts +49 -0
- package/dist/mcp/index.js +90 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/reply.d.ts +28 -0
- package/dist/mcp/reply.js +34 -0
- package/dist/mcp/reply.js.map +1 -0
- package/dist/mcp/tools/plan/create.d.ts +3 -0
- package/dist/mcp/tools/plan/create.js +128 -0
- package/dist/mcp/tools/plan/create.js.map +1 -0
- package/dist/mcp/tools/plan/delete.d.ts +3 -0
- package/dist/mcp/tools/plan/delete.js +35 -0
- package/dist/mcp/tools/plan/delete.js.map +1 -0
- package/dist/mcp/tools/plan/graph.d.ts +3 -0
- package/dist/mcp/tools/plan/graph.js +53 -0
- package/dist/mcp/tools/plan/graph.js.map +1 -0
- package/dist/mcp/tools/plan/index.d.ts +15 -0
- package/dist/mcp/tools/plan/index.js +16 -0
- package/dist/mcp/tools/plan/index.js.map +1 -0
- package/dist/mcp/tools/plan/link.d.ts +3 -0
- package/dist/mcp/tools/plan/link.js +46 -0
- package/dist/mcp/tools/plan/link.js.map +1 -0
- package/dist/mcp/tools/plan/registrar.d.ts +10 -0
- package/dist/mcp/tools/plan/registrar.js +24 -0
- package/dist/mcp/tools/plan/registrar.js.map +1 -0
- package/dist/mcp/tools/plan/reshape.d.ts +3 -0
- package/dist/mcp/tools/plan/reshape.js +95 -0
- package/dist/mcp/tools/plan/reshape.js.map +1 -0
- package/dist/mcp/tools/plan/timeline.d.ts +3 -0
- package/dist/mcp/tools/plan/timeline.js +68 -0
- package/dist/mcp/tools/plan/timeline.js.map +1 -0
- package/dist/mcp/tools/plan/upstream.d.ts +14 -0
- package/dist/mcp/tools/plan/upstream.js +69 -0
- package/dist/mcp/tools/plan/upstream.js.map +1 -0
- package/dist/mcp/tools/plan.d.ts +1 -0
- package/dist/mcp/tools/plan.js +2 -0
- package/dist/mcp/tools/plan.js.map +1 -0
- package/dist/mcp/tools/read.d.ts +3 -0
- package/dist/mcp/tools/read.js +339 -0
- package/dist/mcp/tools/read.js.map +1 -0
- package/dist/mcp/tools/remote.d.ts +3 -0
- package/dist/mcp/tools/remote.js +254 -0
- package/dist/mcp/tools/remote.js.map +1 -0
- package/dist/mcp/tools/templates.d.ts +12 -0
- package/dist/mcp/tools/templates.js +165 -0
- package/dist/mcp/tools/templates.js.map +1 -0
- package/dist/mcp/tools/work.d.ts +16 -0
- package/dist/mcp/tools/work.js +359 -0
- package/dist/mcp/tools/work.js.map +1 -0
- package/dist/remote/accounts.d.ts +158 -0
- package/dist/remote/accounts.js +207 -0
- package/dist/remote/accounts.js.map +1 -0
- package/dist/remote/adopt.d.ts +139 -0
- package/dist/remote/adopt.js +181 -0
- package/dist/remote/adopt.js.map +1 -0
- package/dist/remote/anchor.d.ts +35 -0
- package/dist/remote/anchor.js +53 -0
- package/dist/remote/anchor.js.map +1 -0
- package/dist/remote/attributes.d.ts +103 -0
- package/dist/remote/attributes.js +245 -0
- package/dist/remote/attributes.js.map +1 -0
- package/dist/remote/audit.d.ts +112 -0
- package/dist/remote/audit.js +215 -0
- package/dist/remote/audit.js.map +1 -0
- package/dist/remote/capabilities.d.ts +267 -0
- package/dist/remote/capabilities.js +225 -0
- package/dist/remote/capabilities.js.map +1 -0
- package/dist/remote/check.d.ts +49 -0
- package/dist/remote/check.js +356 -0
- package/dist/remote/check.js.map +1 -0
- package/dist/remote/comments.d.ts +86 -0
- package/dist/remote/comments.js +87 -0
- package/dist/remote/comments.js.map +1 -0
- package/dist/remote/config-file.d.ts +237 -0
- package/dist/remote/config-file.js +640 -0
- package/dist/remote/config-file.js.map +1 -0
- package/dist/remote/conflicts.d.ts +110 -0
- package/dist/remote/conflicts.js +188 -0
- package/dist/remote/conflicts.js.map +1 -0
- package/dist/remote/connection-catalogue.d.ts +65 -0
- package/dist/remote/connection-catalogue.js +66 -0
- package/dist/remote/connection-catalogue.js.map +1 -0
- package/dist/remote/connection.d.ts +84 -0
- package/dist/remote/connection.js +229 -0
- package/dist/remote/connection.js.map +1 -0
- package/dist/remote/coverage.d.ts +65 -0
- package/dist/remote/coverage.js +244 -0
- package/dist/remote/coverage.js.map +1 -0
- package/dist/remote/credentials.d.ts +95 -0
- package/dist/remote/credentials.js +210 -0
- package/dist/remote/credentials.js.map +1 -0
- package/dist/remote/execute.d.ts +178 -0
- package/dist/remote/execute.js +1245 -0
- package/dist/remote/execute.js.map +1 -0
- package/dist/remote/fingerprint.d.ts +172 -0
- package/dist/remote/fingerprint.js +337 -0
- package/dist/remote/fingerprint.js.map +1 -0
- package/dist/remote/fixtures.d.ts +176 -0
- package/dist/remote/fixtures.js +432 -0
- package/dist/remote/fixtures.js.map +1 -0
- package/dist/remote/guard.d.ts +90 -0
- package/dist/remote/guard.js +83 -0
- package/dist/remote/guard.js.map +1 -0
- package/dist/remote/hierarchy.d.ts +141 -0
- package/dist/remote/hierarchy.js +129 -0
- package/dist/remote/hierarchy.js.map +1 -0
- package/dist/remote/index.d.ts +145 -0
- package/dist/remote/index.js +146 -0
- package/dist/remote/index.js.map +1 -0
- package/dist/remote/inspect.d.ts +103 -0
- package/dist/remote/inspect.js +152 -0
- package/dist/remote/inspect.js.map +1 -0
- package/dist/remote/labels.d.ts +75 -0
- package/dist/remote/labels.js +137 -0
- package/dist/remote/labels.js.map +1 -0
- package/dist/remote/ladder.d.ts +145 -0
- package/dist/remote/ladder.js +387 -0
- package/dist/remote/ladder.js.map +1 -0
- package/dist/remote/ledger.d.ts +97 -0
- package/dist/remote/ledger.js +148 -0
- package/dist/remote/ledger.js.map +1 -0
- package/dist/remote/lifecycle.d.ts +175 -0
- package/dist/remote/lifecycle.js +274 -0
- package/dist/remote/lifecycle.js.map +1 -0
- package/dist/remote/links.d.ts +411 -0
- package/dist/remote/links.js +748 -0
- package/dist/remote/links.js.map +1 -0
- package/dist/remote/managed-block.d.ts +182 -0
- package/dist/remote/managed-block.js +406 -0
- package/dist/remote/managed-block.js.map +1 -0
- package/dist/remote/managed-comment.d.ts +104 -0
- package/dist/remote/managed-comment.js +92 -0
- package/dist/remote/managed-comment.js.map +1 -0
- package/dist/remote/mapping.d.ts +282 -0
- package/dist/remote/mapping.js +336 -0
- package/dist/remote/mapping.js.map +1 -0
- package/dist/remote/merge.d.ts +172 -0
- package/dist/remote/merge.js +221 -0
- package/dist/remote/merge.js.map +1 -0
- package/dist/remote/periods.d.ts +209 -0
- package/dist/remote/periods.js +224 -0
- package/dist/remote/periods.js.map +1 -0
- package/dist/remote/plan.d.ts +671 -0
- package/dist/remote/plan.js +1210 -0
- package/dist/remote/plan.js.map +1 -0
- package/dist/remote/policy.d.ts +67 -0
- package/dist/remote/policy.js +67 -0
- package/dist/remote/policy.js.map +1 -0
- package/dist/remote/preflight.d.ts +112 -0
- package/dist/remote/preflight.js +342 -0
- package/dist/remote/preflight.js.map +1 -0
- package/dist/remote/prerequisites.d.ts +113 -0
- package/dist/remote/prerequisites.js +161 -0
- package/dist/remote/prerequisites.js.map +1 -0
- package/dist/remote/provider.d.ts +802 -0
- package/dist/remote/provider.js +27 -0
- package/dist/remote/provider.js.map +1 -0
- package/dist/remote/providers/github/config.d.ts +84 -0
- package/dist/remote/providers/github/config.js +161 -0
- package/dist/remote/providers/github/config.js.map +1 -0
- package/dist/remote/providers/github/connector.d.ts +33 -0
- package/dist/remote/providers/github/connector.js +774 -0
- package/dist/remote/providers/github/connector.js.map +1 -0
- package/dist/remote/providers/github/edges.d.ts +29 -0
- package/dist/remote/providers/github/edges.js +56 -0
- package/dist/remote/providers/github/edges.js.map +1 -0
- package/dist/remote/providers/github/hierarchy.d.ts +21 -0
- package/dist/remote/providers/github/hierarchy.js +46 -0
- package/dist/remote/providers/github/hierarchy.js.map +1 -0
- package/dist/remote/providers/github/index.d.ts +26 -0
- package/dist/remote/providers/github/index.js +69 -0
- package/dist/remote/providers/github/index.js.map +1 -0
- package/dist/remote/providers/github/project-iteration.d.ts +71 -0
- package/dist/remote/providers/github/project-iteration.js +203 -0
- package/dist/remote/providers/github/project-iteration.js.map +1 -0
- package/dist/remote/providers/github/project-provision.d.ts +130 -0
- package/dist/remote/providers/github/project-provision.js +247 -0
- package/dist/remote/providers/github/project-provision.js.map +1 -0
- package/dist/remote/providers/github/project-status.d.ts +115 -0
- package/dist/remote/providers/github/project-status.js +317 -0
- package/dist/remote/providers/github/project-status.js.map +1 -0
- package/dist/remote/providers/github/projects.d.ts +181 -0
- package/dist/remote/providers/github/projects.js +481 -0
- package/dist/remote/providers/github/projects.js.map +1 -0
- package/dist/remote/providers/github/translator.d.ts +103 -0
- package/dist/remote/providers/github/translator.js +400 -0
- package/dist/remote/providers/github/translator.js.map +1 -0
- package/dist/remote/providers/jira/config.d.ts +96 -0
- package/dist/remote/providers/jira/config.js +200 -0
- package/dist/remote/providers/jira/config.js.map +1 -0
- package/dist/remote/providers/jira/connector.d.ts +42 -0
- package/dist/remote/providers/jira/connector.js +1688 -0
- package/dist/remote/providers/jira/connector.js.map +1 -0
- package/dist/remote/providers/jira/fields.d.ts +292 -0
- package/dist/remote/providers/jira/fields.js +463 -0
- package/dist/remote/providers/jira/fields.js.map +1 -0
- package/dist/remote/providers/jira/hierarchy.d.ts +28 -0
- package/dist/remote/providers/jira/hierarchy.js +60 -0
- package/dist/remote/providers/jira/hierarchy.js.map +1 -0
- package/dist/remote/providers/jira/index.d.ts +29 -0
- package/dist/remote/providers/jira/index.js +100 -0
- package/dist/remote/providers/jira/index.js.map +1 -0
- package/dist/remote/providers/jira/links.d.ts +41 -0
- package/dist/remote/providers/jira/links.js +102 -0
- package/dist/remote/providers/jira/links.js.map +1 -0
- package/dist/remote/providers/jira/sprints.d.ts +80 -0
- package/dist/remote/providers/jira/sprints.js +107 -0
- package/dist/remote/providers/jira/sprints.js.map +1 -0
- package/dist/remote/providers/jira/translator.d.ts +61 -0
- package/dist/remote/providers/jira/translator.js +382 -0
- package/dist/remote/providers/jira/translator.js.map +1 -0
- package/dist/remote/providers/jira/types.d.ts +112 -0
- package/dist/remote/providers/jira/types.js +160 -0
- package/dist/remote/providers/jira/types.js.map +1 -0
- package/dist/remote/providers/jira/vocabulary.d.ts +55 -0
- package/dist/remote/providers/jira/vocabulary.js +91 -0
- package/dist/remote/providers/jira/vocabulary.js.map +1 -0
- package/dist/remote/providers/jsonfile/config.d.ts +61 -0
- package/dist/remote/providers/jsonfile/config.js +83 -0
- package/dist/remote/providers/jsonfile/config.js.map +1 -0
- package/dist/remote/providers/jsonfile/connector.d.ts +34 -0
- package/dist/remote/providers/jsonfile/connector.js +251 -0
- package/dist/remote/providers/jsonfile/connector.js.map +1 -0
- package/dist/remote/providers/jsonfile/index.d.ts +38 -0
- package/dist/remote/providers/jsonfile/index.js +90 -0
- package/dist/remote/providers/jsonfile/index.js.map +1 -0
- package/dist/remote/providers/jsonfile/store.d.ts +59 -0
- package/dist/remote/providers/jsonfile/store.js +48 -0
- package/dist/remote/providers/jsonfile/store.js.map +1 -0
- package/dist/remote/providers/jsonfile/translator.d.ts +54 -0
- package/dist/remote/providers/jsonfile/translator.js +199 -0
- package/dist/remote/providers/jsonfile/translator.js.map +1 -0
- package/dist/remote/providers/linear/config.d.ts +83 -0
- package/dist/remote/providers/linear/config.js +140 -0
- package/dist/remote/providers/linear/config.js.map +1 -0
- package/dist/remote/providers/linear/connector.d.ts +36 -0
- package/dist/remote/providers/linear/connector.js +438 -0
- package/dist/remote/providers/linear/connector.js.map +1 -0
- package/dist/remote/providers/linear/estimate.d.ts +63 -0
- package/dist/remote/providers/linear/estimate.js +112 -0
- package/dist/remote/providers/linear/estimate.js.map +1 -0
- package/dist/remote/providers/linear/index.d.ts +31 -0
- package/dist/remote/providers/linear/index.js +75 -0
- package/dist/remote/providers/linear/index.js.map +1 -0
- package/dist/remote/providers/linear/labels.d.ts +51 -0
- package/dist/remote/providers/linear/labels.js +129 -0
- package/dist/remote/providers/linear/labels.js.map +1 -0
- package/dist/remote/providers/linear/states.d.ts +97 -0
- package/dist/remote/providers/linear/states.js +124 -0
- package/dist/remote/providers/linear/states.js.map +1 -0
- package/dist/remote/providers/linear/translator.d.ts +48 -0
- package/dist/remote/providers/linear/translator.js +343 -0
- package/dist/remote/providers/linear/translator.js.map +1 -0
- package/dist/remote/providers/linear/vocabulary.d.ts +27 -0
- package/dist/remote/providers/linear/vocabulary.js +42 -0
- package/dist/remote/providers/linear/vocabulary.js.map +1 -0
- package/dist/remote/provision.d.ts +45 -0
- package/dist/remote/provision.js +81 -0
- package/dist/remote/provision.js.map +1 -0
- package/dist/remote/pull.d.ts +52 -0
- package/dist/remote/pull.js +116 -0
- package/dist/remote/pull.js.map +1 -0
- package/dist/remote/readiness.d.ts +67 -0
- package/dist/remote/readiness.js +336 -0
- package/dist/remote/readiness.js.map +1 -0
- package/dist/remote/rebase.d.ts +157 -0
- package/dist/remote/rebase.js +255 -0
- package/dist/remote/rebase.js.map +1 -0
- package/dist/remote/reconcile.d.ts +115 -0
- package/dist/remote/reconcile.js +140 -0
- package/dist/remote/reconcile.js.map +1 -0
- package/dist/remote/redact.d.ts +70 -0
- package/dist/remote/redact.js +132 -0
- package/dist/remote/redact.js.map +1 -0
- package/dist/remote/registry.d.ts +32 -0
- package/dist/remote/registry.js +54 -0
- package/dist/remote/registry.js.map +1 -0
- package/dist/remote/remotes.d.ts +94 -0
- package/dist/remote/remotes.js +182 -0
- package/dist/remote/remotes.js.map +1 -0
- package/dist/remote/render.d.ts +147 -0
- package/dist/remote/render.js +683 -0
- package/dist/remote/render.js.map +1 -0
- package/dist/remote/report.d.ts +63 -0
- package/dist/remote/report.js +345 -0
- package/dist/remote/report.js.map +1 -0
- package/dist/remote/resolutions.d.ts +115 -0
- package/dist/remote/resolutions.js +217 -0
- package/dist/remote/resolutions.js.map +1 -0
- package/dist/remote/scaffold.d.ts +134 -0
- package/dist/remote/scaffold.js +370 -0
- package/dist/remote/scaffold.js.map +1 -0
- package/dist/remote/scope.d.ts +57 -0
- package/dist/remote/scope.js +91 -0
- package/dist/remote/scope.js.map +1 -0
- package/dist/remote/selection.d.ts +38 -0
- package/dist/remote/selection.js +50 -0
- package/dist/remote/selection.js.map +1 -0
- package/dist/remote/shape.d.ts +141 -0
- package/dist/remote/shape.js +220 -0
- package/dist/remote/shape.js.map +1 -0
- package/dist/remote/status.d.ts +104 -0
- package/dist/remote/status.js +238 -0
- package/dist/remote/status.js.map +1 -0
- package/dist/remote/sync.d.ts +182 -0
- package/dist/remote/sync.js +523 -0
- package/dist/remote/sync.js.map +1 -0
- package/dist/remote/transport/budget.d.ts +48 -0
- package/dist/remote/transport/budget.js +68 -0
- package/dist/remote/transport/budget.js.map +1 -0
- package/dist/remote/transport/connector.d.ts +115 -0
- package/dist/remote/transport/connector.js +21 -0
- package/dist/remote/transport/connector.js.map +1 -0
- package/dist/remote/transport/error.d.ts +88 -0
- package/dist/remote/transport/error.js +196 -0
- package/dist/remote/transport/error.js.map +1 -0
- package/dist/remote/transport/gh.d.ts +20 -0
- package/dist/remote/transport/gh.js +41 -0
- package/dist/remote/transport/gh.js.map +1 -0
- package/dist/remote/transport/graphql.d.ts +25 -0
- package/dist/remote/transport/graphql.js +81 -0
- package/dist/remote/transport/graphql.js.map +1 -0
- package/dist/remote/transport/http.d.ts +47 -0
- package/dist/remote/transport/http.js +195 -0
- package/dist/remote/transport/http.js.map +1 -0
- package/dist/remote/transport/index.d.ts +18 -0
- package/dist/remote/transport/index.js +19 -0
- package/dist/remote/transport/index.js.map +1 -0
- package/dist/remote/transport/process.d.ts +39 -0
- package/dist/remote/transport/process.js +121 -0
- package/dist/remote/transport/process.js.map +1 -0
- package/dist/remote/transport/rest.d.ts +22 -0
- package/dist/remote/transport/rest.js +57 -0
- package/dist/remote/transport/rest.js.map +1 -0
- package/dist/remote/transport/retry.d.ts +91 -0
- package/dist/remote/transport/retry.js +138 -0
- package/dist/remote/transport/retry.js.map +1 -0
- package/dist/remote/transport-error.d.ts +19 -0
- package/dist/remote/transport-error.js +22 -0
- package/dist/remote/transport-error.js.map +1 -0
- package/dist/remote/vocabulary.d.ts +126 -0
- package/dist/remote/vocabulary.js +57 -0
- package/dist/remote/vocabulary.js.map +1 -0
- package/dist/runner/config.d.ts +7 -0
- package/dist/runner/config.js +61 -0
- package/dist/runner/config.js.map +1 -0
- package/dist/runner/git.d.ts +19 -0
- package/dist/runner/git.js +26 -0
- package/dist/runner/git.js.map +1 -0
- package/dist/runner/index.d.ts +20 -0
- package/dist/runner/index.js +21 -0
- package/dist/runner/index.js.map +1 -0
- package/dist/runner/loop.d.ts +46 -0
- package/dist/runner/loop.js +243 -0
- package/dist/runner/loop.js.map +1 -0
- package/dist/runner/pi.d.ts +39 -0
- package/dist/runner/pi.js +276 -0
- package/dist/runner/pi.js.map +1 -0
- package/dist/runner/shell.d.ts +44 -0
- package/dist/runner/shell.js +174 -0
- package/dist/runner/shell.js.map +1 -0
- package/dist/runner/stats.d.ts +19 -0
- package/dist/runner/stats.js +58 -0
- package/dist/runner/stats.js.map +1 -0
- package/dist/runner/types.d.ts +218 -0
- package/dist/runner/types.js +37 -0
- package/dist/runner/types.js.map +1 -0
- package/dist/server/git.d.ts +8 -0
- package/dist/server/git.js +50 -0
- package/dist/server/git.js.map +1 -0
- package/dist/server/http/respond.d.ts +13 -0
- package/dist/server/http/respond.js +59 -0
- package/dist/server/http/respond.js.map +1 -0
- package/dist/server/http/router.d.ts +27 -0
- package/dist/server/http/router.js +48 -0
- package/dist/server/http/router.js.map +1 -0
- package/dist/server/http/static.d.ts +14 -0
- package/dist/server/http/static.js +55 -0
- package/dist/server/http/static.js.map +1 -0
- package/dist/server/index.d.ts +58 -0
- package/dist/server/index.js +124 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/routes/board.d.ts +8 -0
- package/dist/server/routes/board.js +19 -0
- package/dist/server/routes/board.js.map +1 -0
- package/dist/server/routes/comments.d.ts +13 -0
- package/dist/server/routes/comments.js +36 -0
- package/dist/server/routes/comments.js.map +1 -0
- package/dist/server/routes/flags.d.ts +16 -0
- package/dist/server/routes/flags.js +37 -0
- package/dist/server/routes/flags.js.map +1 -0
- package/dist/server/routes/git.d.ts +13 -0
- package/dist/server/routes/git.js +84 -0
- package/dist/server/routes/git.js.map +1 -0
- package/dist/server/routes/remotes.d.ts +36 -0
- package/dist/server/routes/remotes.js +749 -0
- package/dist/server/routes/remotes.js.map +1 -0
- package/dist/server/routes/views.d.ts +10 -0
- package/dist/server/routes/views.js +63 -0
- package/dist/server/routes/views.js.map +1 -0
- package/dist/server/views/schema.d.ts +2 -0
- package/dist/server/views/schema.js +121 -0
- package/dist/server/views/schema.js.map +1 -0
- package/dist/server/views/store.d.ts +17 -0
- package/dist/server/views/store.js +67 -0
- package/dist/server/views/store.js.map +1 -0
- package/dist/shared/adf.d.ts +83 -0
- package/dist/shared/adf.js +795 -0
- package/dist/shared/adf.js.map +1 -0
- package/dist/shared/blocking.d.ts +130 -0
- package/dist/shared/blocking.js +179 -0
- package/dist/shared/blocking.js.map +1 -0
- package/dist/shared/changes.d.ts +100 -0
- package/dist/shared/changes.js +112 -0
- package/dist/shared/changes.js.map +1 -0
- package/dist/shared/cohesion.d.ts +75 -0
- package/dist/shared/cohesion.js +132 -0
- package/dist/shared/cohesion.js.map +1 -0
- package/dist/shared/dependency-rollup.d.ts +91 -0
- package/dist/shared/dependency-rollup.js +132 -0
- package/dist/shared/dependency-rollup.js.map +1 -0
- package/dist/shared/errors.d.ts +23 -0
- package/dist/shared/errors.js +24 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/flag-rollup.d.ts +88 -0
- package/dist/shared/flag-rollup.js +119 -0
- package/dist/shared/flag-rollup.js.map +1 -0
- package/dist/shared/git-sync.d.ts +90 -0
- package/dist/shared/git-sync.js +9 -0
- package/dist/shared/git-sync.js.map +1 -0
- package/dist/shared/index.d.ts +40 -0
- package/dist/shared/index.js +41 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/model.d.ts +218 -0
- package/dist/shared/model.js +95 -0
- package/dist/shared/model.js.map +1 -0
- package/dist/shared/period-query.d.ts +21 -0
- package/dist/shared/period-query.js +26 -0
- package/dist/shared/period-query.js.map +1 -0
- package/dist/shared/period-stance.d.ts +45 -0
- package/dist/shared/period-stance.js +50 -0
- package/dist/shared/period-stance.js.map +1 -0
- package/dist/shared/plans/breakdown.d.ts +66 -0
- package/dist/shared/plans/breakdown.js +186 -0
- package/dist/shared/plans/breakdown.js.map +1 -0
- package/dist/shared/plans/index.d.ts +21 -0
- package/dist/shared/plans/index.js +22 -0
- package/dist/shared/plans/index.js.map +1 -0
- package/dist/shared/plans/instantiate.d.ts +61 -0
- package/dist/shared/plans/instantiate.js +184 -0
- package/dist/shared/plans/instantiate.js.map +1 -0
- package/dist/shared/plans/reading.d.ts +79 -0
- package/dist/shared/plans/reading.js +149 -0
- package/dist/shared/plans/reading.js.map +1 -0
- package/dist/shared/plans/reparent.d.ts +54 -0
- package/dist/shared/plans/reparent.js +217 -0
- package/dist/shared/plans/reparent.js.map +1 -0
- package/dist/shared/plans/timeline.d.ts +93 -0
- package/dist/shared/plans/timeline.js +224 -0
- package/dist/shared/plans/timeline.js.map +1 -0
- package/dist/shared/plans/upstream.d.ts +67 -0
- package/dist/shared/plans/upstream.js +73 -0
- package/dist/shared/plans/upstream.js.map +1 -0
- package/dist/shared/remote-api.d.ts +413 -0
- package/dist/shared/remote-api.js +11 -0
- package/dist/shared/remote-api.js.map +1 -0
- package/dist/shared/remote-coverage.d.ts +120 -0
- package/dist/shared/remote-coverage.js +98 -0
- package/dist/shared/remote-coverage.js.map +1 -0
- package/dist/shared/remote-readiness.d.ts +162 -0
- package/dist/shared/remote-readiness.js +49 -0
- package/dist/shared/remote-readiness.js.map +1 -0
- package/dist/shared/remote-status.d.ts +268 -0
- package/dist/shared/remote-status.js +79 -0
- package/dist/shared/remote-status.js.map +1 -0
- package/dist/shared/rollup.d.ts +90 -0
- package/dist/shared/rollup.js +119 -0
- package/dist/shared/rollup.js.map +1 -0
- package/dist/shared/static.d.ts +65 -0
- package/dist/shared/static.js +107 -0
- package/dist/shared/static.js.map +1 -0
- package/dist/shared/template-params.d.ts +80 -0
- package/dist/shared/template-params.js +141 -0
- package/dist/shared/template-params.js.map +1 -0
- package/dist/shared/view.d.ts +105 -0
- package/dist/shared/view.js +44 -0
- package/dist/shared/view.js.map +1 -0
- package/dist/shared/work-unit.d.ts +34 -0
- package/dist/shared/work-unit.js +47 -0
- package/dist/shared/work-unit.js.map +1 -0
- package/dist/sync/apply.d.ts +21 -0
- package/dist/sync/apply.js +149 -0
- package/dist/sync/apply.js.map +1 -0
- package/dist/sync/dto.d.ts +9 -0
- package/dist/sync/dto.js +162 -0
- package/dist/sync/dto.js.map +1 -0
- package/dist/sync/index.d.ts +24 -0
- package/dist/sync/index.js +25 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/patch.d.ts +13 -0
- package/dist/sync/patch.js +212 -0
- package/dist/sync/patch.js.map +1 -0
- package/dist/sync/session.d.ts +31 -0
- package/dist/sync/session.js +55 -0
- package/dist/sync/session.js.map +1 -0
- package/dist/sync/static.d.ts +11 -0
- package/dist/sync/static.js +33 -0
- package/dist/sync/static.js.map +1 -0
- package/package.json +95 -4
- package/templates/blank.yml +71 -0
- package/templates/context/bug.md +107 -0
- package/templates/context/default.md +119 -0
- package/templates/context/epic.md +81 -0
- package/templates/context/feature.md +91 -0
- package/templates/context/research.md +87 -0
- package/templates/context/review.md +81 -0
- package/templates/context/story.md +94 -0
- package/templates/context/sub_task.md +108 -0
- package/templates/context/task.md +86 -0
- package/templates/context/test.md +73 -0
- package/templates/context/user_story.md +147 -0
- package/templates/kanban.yml +226 -0
- package/templates/scrum.yml +567 -0
- package/web/dist/assets/index-D0j8OnHq.js +35 -0
- package/web/dist/assets/index-DFL7v8aP.css +1 -0
- package/web/dist/index.html +13 -0
- package/web/dist-viewer/assets/index-Bcns6OuC.css +1 -0
- package/web/dist-viewer/assets/index-vVXzeiFo.js +7 -0
- package/web/dist-viewer/index.html +14 -0
|
@@ -0,0 +1,1688 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Jira Cloud connector — a live connection to one Cloud site, driven by the
|
|
3
|
+
* executor (LP-490).
|
|
4
|
+
*
|
|
5
|
+
* Built from a validated `connection` block (`site`, `project`, `board`,
|
|
6
|
+
* `email`, `token`, `tls_verify`). The HTTP and Basic Auth header construction
|
|
7
|
+
* are owned by the wrapped client — `jira.js`, an optional *peer* dependency
|
|
8
|
+
* (never installed with light-plan, because Jira sync is experimental) loaded through a computed specifier so the engine still
|
|
9
|
+
* compiles and runs on a machine that never installed it (exactly the pattern
|
|
10
|
+
* `runner/pi.ts` uses for the pi SDK). The client lives entirely inside this
|
|
11
|
+
* file, behind the provider `Connector` contract (LP-289), so the retry/budget
|
|
12
|
+
* decorators and the conformance suite see an ordinary connector and nothing
|
|
13
|
+
* here leaks into the planners or core.
|
|
14
|
+
*
|
|
15
|
+
* TLS verification is **on by default and stays on**. A corporate MITM proxy
|
|
16
|
+
* with a custom root CA is handled outside this file — `NODE_EXTRA_CA_CERTS`
|
|
17
|
+
* or Node's `--use-system-ca` inject the root into the OS trust store that
|
|
18
|
+
* Node's built-in `fetch` already verifies against. `tls_verify: false` is the
|
|
19
|
+
* explicit, loud fallback: it warns on every build and is never the default.
|
|
20
|
+
*
|
|
21
|
+
* Three failure modes produce three different messages (LP-294's shapes):
|
|
22
|
+
* a site that cannot be reached (DNS/TLS/non-Jira), an account email the site
|
|
23
|
+
* rejects, and an API token the site rejects. The email-vs-token distinction
|
|
24
|
+
* is a documented heuristic on Atlassian's 401 body — both are a 401, and the
|
|
25
|
+
* message names the most likely fix for each.
|
|
26
|
+
*
|
|
27
|
+
* Server/Data Center is out of scope, consistent with the epic's Cloud-first
|
|
28
|
+
* decision: a URL that answers but has no Cloud v3 API is reported as "not
|
|
29
|
+
* supported" rather than as a confusing 401 or 404.
|
|
30
|
+
*/
|
|
31
|
+
import { BoardError } from '../../../core/errors.js';
|
|
32
|
+
import { markdownToAdf } from '../../../shared/adf.js';
|
|
33
|
+
import { normalizeAccountMapping } from '../../accounts.js';
|
|
34
|
+
import { normalizePeriodMapping } from '../../periods.js';
|
|
35
|
+
import { parseFields } from './fields.js';
|
|
36
|
+
import { isClosedSprint, parseSprints, sprintByName, sprintFieldIdOf, } from './sprints.js';
|
|
37
|
+
import { hierarchyDepthOf, isSubtaskType } from './types.js';
|
|
38
|
+
/** The non-literal specifier, so `tsc` does not require jira.js at build time. */
|
|
39
|
+
const JIRA_PACKAGE = 'jira.js';
|
|
40
|
+
/** Read a Jira issue's `fields.updated` (the revision marker), or null. */
|
|
41
|
+
function updatedOf(record) {
|
|
42
|
+
const fields = record['fields'];
|
|
43
|
+
if (fields && typeof fields === 'object') {
|
|
44
|
+
const updated = fields['updated'];
|
|
45
|
+
if (typeof updated === 'string')
|
|
46
|
+
return updated;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/** The messages a 401 body may carry, as plain strings. */
|
|
51
|
+
/**
|
|
52
|
+
* A board period's dates are **inclusive calendar days** — "Sprint 1 runs from
|
|
53
|
+
* the 31st through the 31st" is a one-day sprint, and a board may well have
|
|
54
|
+
* one. A Jira sprint is a pair of *instants* and it requires the start to be
|
|
55
|
+
* strictly before the end, so sending the bare dates makes an inclusive
|
|
56
|
+
* one-day sprint into `start === end`, which Jira refuses outright
|
|
57
|
+
* (`startDate: The start date of a sprint must be before the end date`).
|
|
58
|
+
*
|
|
59
|
+
* So the start is the beginning of its day and the end is the *end* of its
|
|
60
|
+
* day. That is not a workaround for the refusal: it is what the board already
|
|
61
|
+
* meant. Mapping an inclusive end date onto midnight would silently shorten
|
|
62
|
+
* every sprint by a day, which is worse than the error.
|
|
63
|
+
*
|
|
64
|
+
* A value that already carries a time is passed through untouched — a caller
|
|
65
|
+
* that has been explicit is not second-guessed.
|
|
66
|
+
*/
|
|
67
|
+
function sprintStart(date) {
|
|
68
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(date.trim()) ? `${date.trim()}T00:00:00.000Z` : date;
|
|
69
|
+
}
|
|
70
|
+
function sprintEnd(date) {
|
|
71
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(date.trim()) ? `${date.trim()}T23:59:59.000Z` : date;
|
|
72
|
+
}
|
|
73
|
+
/** A Jira issue key — `PAY-100`, `SCRUM-17` — as opposed to a numeric id. */
|
|
74
|
+
const ISSUE_KEY_RE = /^[A-Za-z][A-Za-z0-9_]*-\d+$/;
|
|
75
|
+
/**
|
|
76
|
+
* How to name a parent to Jira: `{ id }` for the numeric id, `{ key }` for the
|
|
77
|
+
* human-facing key.
|
|
78
|
+
*
|
|
79
|
+
* Chosen by the shape of the value, because both reach this code. The executor
|
|
80
|
+
* resolves a parent through the link store, which records Jira's **id**
|
|
81
|
+
* (`10016`) — and sending that under `key` is rejected with "Please select
|
|
82
|
+
* valid parent issue", a message that reads like a hierarchy problem and is
|
|
83
|
+
* not one. A caller that adopted a twin by key (`lpm remote link`) has the key.
|
|
84
|
+
* Neither should have to know which field the other uses.
|
|
85
|
+
*/
|
|
86
|
+
function parentRef(parent) {
|
|
87
|
+
return ISSUE_KEY_RE.test(parent) ? { key: parent } : { id: parent };
|
|
88
|
+
}
|
|
89
|
+
function errorMessagesOf(body) {
|
|
90
|
+
if (body === null || typeof body !== 'object')
|
|
91
|
+
return [];
|
|
92
|
+
const record = body;
|
|
93
|
+
const out = [];
|
|
94
|
+
const messages = record['errorMessages'];
|
|
95
|
+
if (Array.isArray(messages)) {
|
|
96
|
+
out.push(...messages.filter((entry) => typeof entry === 'string'));
|
|
97
|
+
}
|
|
98
|
+
// The field-keyed map — `{ errors: { originBoardId: "The board does not
|
|
99
|
+
// support sprints" } }`. Jira uses it wherever the complaint is about one
|
|
100
|
+
// field, which is most of the Agile API, and it sits beside an *empty*
|
|
101
|
+
// `errorMessages`. Reading only the array threw the explanation away: a
|
|
102
|
+
// sprint that could not be created reported no reason at all, under a hint
|
|
103
|
+
// about issue types that had nothing to do with it. Jira's own words are the
|
|
104
|
+
// most useful thing in the error, so they are never dropped.
|
|
105
|
+
const errors = record['errors'];
|
|
106
|
+
if (errors !== null && typeof errors === 'object' && !Array.isArray(errors)) {
|
|
107
|
+
for (const [field, message] of Object.entries(errors)) {
|
|
108
|
+
if (typeof message === 'string' && message !== '')
|
|
109
|
+
out.push(`${field}: ${message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (out.length === 0) {
|
|
113
|
+
const message = record['message'];
|
|
114
|
+
if (typeof message === 'string')
|
|
115
|
+
out.push(message);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* True when the 401 body suggests the *email* is the problem rather than the
|
|
121
|
+
* token: Atlassian's "Basic authentication with passwords is deprecated"
|
|
122
|
+
* message (and its relatives) fire when the credentials are read as a password
|
|
123
|
+
* pair, which is what a wrong account email looks like. A plain "invalid
|
|
124
|
+
* credentials" blames the token. This is a documented heuristic — a 401 cannot
|
|
125
|
+
* name the failing half, and the message names the most likely fix.
|
|
126
|
+
*/
|
|
127
|
+
function bodyBlamesEmail(body) {
|
|
128
|
+
return errorMessagesOf(body).some((message) => /password|username|email/i.test(message));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* True when Jira refused a write because the issue is a sub-task and the sprint
|
|
132
|
+
* is not its to hold.
|
|
133
|
+
*
|
|
134
|
+
* **Jira owns a sub-task's sprint**: it is always the parent's, it cannot be
|
|
135
|
+
* set, and it cannot be cleared either. That is not a permission or a bad
|
|
136
|
+
* value, it is a field that does not belong to this issue type — the same kind
|
|
137
|
+
* of rule as "an Epic cannot sit under an Epic", discovered the same way, by
|
|
138
|
+
* being refused.
|
|
139
|
+
*
|
|
140
|
+
* It matters in the *clearing* direction most, which is the one nobody
|
|
141
|
+
* predicts: light-plan sees a sprint on a twin whose board document has no
|
|
142
|
+
* period, plans an update to take it off, and Jira refuses. Nothing about that
|
|
143
|
+
* resolves by itself, so every push failed on the same thirteen sub-tasks for
|
|
144
|
+
* ever.
|
|
145
|
+
*/
|
|
146
|
+
function isSubtaskSprintRefusal(error) {
|
|
147
|
+
const err = error;
|
|
148
|
+
if (err?.status !== 400)
|
|
149
|
+
return false;
|
|
150
|
+
return errorMessagesOf(err.body).some((message) => /subtask.*cannot be associated to a sprint|cannot be associated to a sprint/i.test(message));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* True when Jira refused a write because it was given an issue type for a
|
|
154
|
+
* sub-task without the parent that must accompany it.
|
|
155
|
+
*
|
|
156
|
+
* The executor hands the translator the **whole document** on an update, not
|
|
157
|
+
* just the diff, because a label-carrying provider derives its type and state
|
|
158
|
+
* from it — so every Jira edit restates `issuetype` even when nothing about
|
|
159
|
+
* the type changed. Jira tolerates that on a standard issue and refuses it on
|
|
160
|
+
* a sub-task, where an `issuetype` in the body must be accompanied by a
|
|
161
|
+
* `parent`. The result was a permanent failure on every sub-task with any
|
|
162
|
+
* other field to write.
|
|
163
|
+
*/
|
|
164
|
+
function isSubtaskTypeRefusal(error) {
|
|
165
|
+
const err = error;
|
|
166
|
+
if (err?.status !== 400)
|
|
167
|
+
return false;
|
|
168
|
+
return errorMessagesOf(err.body).some((message) => /sub-?task but parent issue key or id not specified/i.test(message));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Map a thrown jira.js error to a `BoardError` whose message and hints name the
|
|
172
|
+
* fix — the one place the three failure modes (site, email, token) become three
|
|
173
|
+
* different messages.
|
|
174
|
+
*/
|
|
175
|
+
function describeFailure(error, conn, purpose) {
|
|
176
|
+
const err = error;
|
|
177
|
+
// No HTTP status but a `code` / `transient`: the request never produced a
|
|
178
|
+
// response — DNS, TLS, a reset socket. That is the "wrong site" case.
|
|
179
|
+
if (err.status === undefined && err.code !== undefined) {
|
|
180
|
+
const tls = /CERT|TLS|SELF_SIGNED|UNABLE_TO_VERIFY/i.test(err.code);
|
|
181
|
+
const hints = [
|
|
182
|
+
`check connection.site — it must be your Cloud site (https://<org>.atlassian.net or a Cloud custom domain)`,
|
|
183
|
+
];
|
|
184
|
+
if (tls) {
|
|
185
|
+
hints.push('a corporate proxy is intercepting TLS — inject its root CA with NODE_EXTRA_CA_CERTS=<file> or node --use-system-ca');
|
|
186
|
+
hints.push('do not set tls_verify: false except as a last resort');
|
|
187
|
+
}
|
|
188
|
+
return new BoardError(`cannot reach the Jira site at ${conn.site} while ${purpose}`, hints);
|
|
189
|
+
}
|
|
190
|
+
if (err.status === 401) {
|
|
191
|
+
if (bodyBlamesEmail(err.body)) {
|
|
192
|
+
return new BoardError(`the Jira account email was rejected at ${conn.site}`, [
|
|
193
|
+
`check connection.email matches the account that owns the API token`,
|
|
194
|
+
'the value must be an API token, not the account password',
|
|
195
|
+
`resolve it with \${JIRA_EMAIL} or: lpm remote login`,
|
|
196
|
+
]);
|
|
197
|
+
}
|
|
198
|
+
return new BoardError(`the Jira API token was rejected at ${conn.site}`, [
|
|
199
|
+
'revoke and regenerate it at https://id.atlassian.com/manage-profile/security/api-tokens',
|
|
200
|
+
'store the new value with: lpm remote login <name> (or ${JIRA_API_TOKEN})',
|
|
201
|
+
]);
|
|
202
|
+
}
|
|
203
|
+
if (err.status === 403) {
|
|
204
|
+
return new BoardError(`Jira refused the request (permission denied) while ${purpose}`, [
|
|
205
|
+
'the credential is valid but lacks the required project permission',
|
|
206
|
+
'check the account has Browse/Create/Edit issues on the project',
|
|
207
|
+
]);
|
|
208
|
+
}
|
|
209
|
+
if (err.status === 400) {
|
|
210
|
+
const messages = errorMessagesOf(err.body);
|
|
211
|
+
const detail = messages.length > 0 ? `: ${messages.join('; ')}` : '';
|
|
212
|
+
// The hints follow the *purpose*. A 400 while creating a sprint has
|
|
213
|
+
// nothing to do with issue types or parents, and saying so sent somebody
|
|
214
|
+
// looking at the wrong half of their config for an afternoon.
|
|
215
|
+
if (/sprint/i.test(purpose)) {
|
|
216
|
+
// When Jira explained itself, its words are the diagnosis: a guess
|
|
217
|
+
// printed beside them is how somebody ends up debugging the wrong thing.
|
|
218
|
+
// (A sprint name over 30 characters and a board that cannot hold created
|
|
219
|
+
// sprints are both real, and they are not the same problem.) The
|
|
220
|
+
// candidates are offered only when it said nothing at all.
|
|
221
|
+
return new BoardError(`Jira refused the request while ${purpose}${detail}`, messages.length > 0
|
|
222
|
+
? ['that is Jira’s own message; the sprint was not created and the push carried on without it']
|
|
223
|
+
: [
|
|
224
|
+
'sprints can only be *created* on a company-managed Scrum board — a team-managed ("next-gen") project reports its board as type `simple`',
|
|
225
|
+
'check connection.board names a Scrum board and that the account has Manage Sprints on it',
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
return new BoardError(`Jira refused the request while ${purpose}${detail}`, [
|
|
229
|
+
'a type that cannot sit under its parent, or a field Jira rejects, is refused rather than coerced',
|
|
230
|
+
'check the issue type and its parent against the project\u2019s actual type scheme',
|
|
231
|
+
]);
|
|
232
|
+
}
|
|
233
|
+
if (err.status === 404) {
|
|
234
|
+
const notCloud = !conn.site.includes('.atlassian.net');
|
|
235
|
+
return new BoardError(`Jira answered 404 while ${purpose}`, [
|
|
236
|
+
...(notCloud
|
|
237
|
+
? [
|
|
238
|
+
`the site ${conn.site} is not a *.atlassian.net Cloud site — if it is a Jira Server/Data Center instance, it is not supported (Cloud only)`,
|
|
239
|
+
]
|
|
240
|
+
: ['the resource may have been deleted, or the account cannot see it']),
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
if (err.status === 429) {
|
|
244
|
+
return new BoardError(`Jira rate-limited the request while ${purpose}`, [
|
|
245
|
+
'slow the sync down and retry — the remote said so',
|
|
246
|
+
]);
|
|
247
|
+
}
|
|
248
|
+
if (err.status !== undefined && err.status >= 500) {
|
|
249
|
+
return new BoardError(`Jira failed (HTTP ${err.status}) while ${purpose}`, [
|
|
250
|
+
'retrying may succeed',
|
|
251
|
+
]);
|
|
252
|
+
}
|
|
253
|
+
// A `BoardError` is already the legible, structured message (a workflow
|
|
254
|
+
// refusal, a missing transition field) — pass it through, never rewrap it.
|
|
255
|
+
if (error instanceof BoardError)
|
|
256
|
+
return error;
|
|
257
|
+
return new BoardError(`Jira rejected the request while ${purpose}`, [
|
|
258
|
+
err.message ?? String(error),
|
|
259
|
+
]);
|
|
260
|
+
}
|
|
261
|
+
/** A Jira issue record → `ConnectorResult`, reading the post-write state. */
|
|
262
|
+
/** The workflow status name on a Jira issue record, or `''` when it carries none. */
|
|
263
|
+
function statusNameOf(record) {
|
|
264
|
+
const fields = (record['fields'] ?? {});
|
|
265
|
+
const status = (fields['status'] ?? {});
|
|
266
|
+
return typeof status.name === 'string' ? status.name : '';
|
|
267
|
+
}
|
|
268
|
+
function resultOf(record, site) {
|
|
269
|
+
const id = typeof record['id'] === 'string' ? record['id'] : String(record['id'] ?? '');
|
|
270
|
+
const key = typeof record['key'] === 'string' ? record['key'] : '';
|
|
271
|
+
const rev = updatedOf(record) ?? '';
|
|
272
|
+
// The link records the *browse* URL a person opens, not the REST self link:
|
|
273
|
+
// `https://<org>.atlassian.net/browse/PAY-418`.
|
|
274
|
+
const browseUrl = key ? `${site}/browse/${key}` : '';
|
|
275
|
+
return {
|
|
276
|
+
remoteId: id,
|
|
277
|
+
remoteKey: key || id,
|
|
278
|
+
remoteUrl: browseUrl,
|
|
279
|
+
remoteRev: rev,
|
|
280
|
+
record,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/** A Jira comment response → `ConnectorResult` (comment id rides along). */
|
|
284
|
+
function commentResult(comment, issueRemoteId, issueKey) {
|
|
285
|
+
const id = typeof comment['id'] === 'string' ? comment['id'] : String(comment['id'] ?? '');
|
|
286
|
+
return {
|
|
287
|
+
remoteId: issueRemoteId,
|
|
288
|
+
remoteKey: issueKey,
|
|
289
|
+
remoteUrl: '',
|
|
290
|
+
remoteRev: '',
|
|
291
|
+
commentId: id,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/** The `issuelinks` entries a Jira record carries, or an empty list. */
|
|
295
|
+
function issueLinksOf(record) {
|
|
296
|
+
const fields = record['fields'];
|
|
297
|
+
if (!fields || typeof fields !== 'object')
|
|
298
|
+
return [];
|
|
299
|
+
const links = fields['issuelinks'];
|
|
300
|
+
if (!Array.isArray(links))
|
|
301
|
+
return [];
|
|
302
|
+
return links.filter((link) => link !== null && typeof link === 'object');
|
|
303
|
+
}
|
|
304
|
+
/** The name of an issue link's type (`Blocks`, `Relates`), or ''. */
|
|
305
|
+
function linkTypeNameOf(link) {
|
|
306
|
+
const type = link['type'];
|
|
307
|
+
if (!type || typeof type !== 'object')
|
|
308
|
+
return '';
|
|
309
|
+
const name = type['name'];
|
|
310
|
+
return typeof name === 'string' ? name : '';
|
|
311
|
+
}
|
|
312
|
+
/** The remote id of a link endpoint (`outwardIssue` / `inwardIssue`), or ''. */
|
|
313
|
+
function endpointIdOf(endpoint) {
|
|
314
|
+
if (!endpoint || typeof endpoint !== 'object')
|
|
315
|
+
return '';
|
|
316
|
+
const id = endpoint['id'];
|
|
317
|
+
if (typeof id === 'string')
|
|
318
|
+
return id;
|
|
319
|
+
if (typeof id === 'number')
|
|
320
|
+
return String(id);
|
|
321
|
+
return '';
|
|
322
|
+
}
|
|
323
|
+
/** Case-insensitive, whitespace-tolerant status name equality. */
|
|
324
|
+
function nameMatches(a, b) {
|
|
325
|
+
return typeof a === 'string' && a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
326
|
+
}
|
|
327
|
+
/** The transition reaching `target` in one hop, when the list has one. */
|
|
328
|
+
function singleHopTo(list, target) {
|
|
329
|
+
return list.find((transition) => nameMatches(transition.to?.name, target));
|
|
330
|
+
}
|
|
331
|
+
/** The statuses one transition away, for the "reachable" report. */
|
|
332
|
+
function singleHopReachable(list) {
|
|
333
|
+
const names = list
|
|
334
|
+
.map((transition) => transition.to?.name)
|
|
335
|
+
.filter((name) => typeof name === 'string' && name.length > 0);
|
|
336
|
+
return [...new Set(names)].sort();
|
|
337
|
+
}
|
|
338
|
+
/** The fields a transition's screen marks required, with their display names. */
|
|
339
|
+
function requiredFieldsOf(transition) {
|
|
340
|
+
const fields = transition.fields ?? {};
|
|
341
|
+
const required = [];
|
|
342
|
+
for (const [key, meta] of Object.entries(fields)) {
|
|
343
|
+
if (meta && typeof meta === 'object' && meta['required'] === true) {
|
|
344
|
+
const name = meta['name'];
|
|
345
|
+
required.push({ key, name: typeof name === 'string' ? name : undefined });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return required;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* The `fields` body for a transition, answered from `transition_fields` in
|
|
352
|
+
* config. Returns the fields to send plus the required fields config did not
|
|
353
|
+
* answer — those are the ones a transition must refuse on rather than guess.
|
|
354
|
+
*/
|
|
355
|
+
function fieldsFor(transition, configured) {
|
|
356
|
+
const missing = [];
|
|
357
|
+
const fields = {};
|
|
358
|
+
for (const field of requiredFieldsOf(transition)) {
|
|
359
|
+
if (Object.prototype.hasOwnProperty.call(configured, field.key)) {
|
|
360
|
+
fields[field.key] = configured[field.key];
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
missing.push(field);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { fields: Object.keys(fields).length > 0 ? fields : undefined, missing };
|
|
367
|
+
}
|
|
368
|
+
/** Build the workflow's status graph from a `readWorkflows` response. */
|
|
369
|
+
function workflowGraphOf(response) {
|
|
370
|
+
const nameOfRef = new Map();
|
|
371
|
+
for (const status of response.statuses ?? []) {
|
|
372
|
+
if (typeof status.statusReference === 'string' && typeof status.name === 'string') {
|
|
373
|
+
nameOfRef.set(status.statusReference, status.name);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const adjacency = new Map();
|
|
377
|
+
const display = new Map();
|
|
378
|
+
const addEdge = (from, to) => {
|
|
379
|
+
const fromKey = from.trim().toLowerCase();
|
|
380
|
+
const toKey = to.trim().toLowerCase();
|
|
381
|
+
if (fromKey === '' || toKey === '' || fromKey === toKey)
|
|
382
|
+
return;
|
|
383
|
+
display.set(fromKey, from);
|
|
384
|
+
display.set(toKey, to);
|
|
385
|
+
let set = adjacency.get(fromKey);
|
|
386
|
+
if (!set) {
|
|
387
|
+
set = new Set();
|
|
388
|
+
adjacency.set(fromKey, set);
|
|
389
|
+
}
|
|
390
|
+
set.add(toKey);
|
|
391
|
+
};
|
|
392
|
+
for (const workflow of response.workflows ?? []) {
|
|
393
|
+
for (const transition of workflow.transitions ?? []) {
|
|
394
|
+
const toName = typeof transition.toStatusReference === 'string'
|
|
395
|
+
? nameOfRef.get(transition.toStatusReference)
|
|
396
|
+
: undefined;
|
|
397
|
+
if (!toName)
|
|
398
|
+
continue;
|
|
399
|
+
for (const link of transition.links ?? []) {
|
|
400
|
+
const fromName = typeof link.fromStatusReference === 'string'
|
|
401
|
+
? nameOfRef.get(link.fromStatusReference)
|
|
402
|
+
: undefined;
|
|
403
|
+
if (fromName)
|
|
404
|
+
addEdge(fromName, toName);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return { adjacency, display };
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* A breadth-first path of status *names* from `fromName` to `toName` — the
|
|
412
|
+
* intermediate statuses plus the target, never the starting status. `null`
|
|
413
|
+
* when the workflow offers no path.
|
|
414
|
+
*/
|
|
415
|
+
function findPath(graph, fromName, toName) {
|
|
416
|
+
const start = fromName.trim().toLowerCase();
|
|
417
|
+
const goal = toName.trim().toLowerCase();
|
|
418
|
+
if (start === goal)
|
|
419
|
+
return [toName];
|
|
420
|
+
const seen = new Set([start]);
|
|
421
|
+
const queue = [{ key: start, path: [] }];
|
|
422
|
+
while (queue.length > 0) {
|
|
423
|
+
const { key, path } = queue.shift();
|
|
424
|
+
for (const next of graph.adjacency.get(key) ?? []) {
|
|
425
|
+
if (seen.has(next))
|
|
426
|
+
continue;
|
|
427
|
+
seen.add(next);
|
|
428
|
+
const nextPath = [...path, next];
|
|
429
|
+
if (next === goal)
|
|
430
|
+
return nextPath.map((k) => graph.display.get(k) ?? k);
|
|
431
|
+
queue.push({ key: next, path: nextPath });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
/** Every status reachable from `fromName` by any number of transitions. */
|
|
437
|
+
function reachableNames(graph, fromName) {
|
|
438
|
+
const start = fromName.trim().toLowerCase();
|
|
439
|
+
const seen = new Set();
|
|
440
|
+
const stack = [start];
|
|
441
|
+
while (stack.length > 0) {
|
|
442
|
+
const key = stack.pop();
|
|
443
|
+
if (seen.has(key))
|
|
444
|
+
continue;
|
|
445
|
+
seen.add(key);
|
|
446
|
+
for (const next of graph.adjacency.get(key) ?? []) {
|
|
447
|
+
if (!seen.has(next))
|
|
448
|
+
stack.push(next);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
seen.delete(start);
|
|
452
|
+
return [...seen].map((key) => graph.display.get(key) ?? key).sort();
|
|
453
|
+
}
|
|
454
|
+
function isPermissionDenied(error) {
|
|
455
|
+
return error?.status === 403;
|
|
456
|
+
}
|
|
457
|
+
/** A transition screen's required field cannot be answered from config. */
|
|
458
|
+
function missingFieldsError(remoteId, target, missing) {
|
|
459
|
+
const list = missing
|
|
460
|
+
.map((field) => (field.name ? `${field.key} (${field.name})` : field.key))
|
|
461
|
+
.join(', ');
|
|
462
|
+
return new BoardError(`Cannot move ${remoteId} to "${target}": the transition requires ${missing.length === 1 ? 'field' : 'fields'} ${list}`, [
|
|
463
|
+
...missing.map((field) => `set mapping.transition_fields.${field.key} to the value the workflow expects`),
|
|
464
|
+
'a transition screen field must come from config — it is never guessed',
|
|
465
|
+
]);
|
|
466
|
+
}
|
|
467
|
+
/** The target is reachable only through several transitions, and that is off. */
|
|
468
|
+
function multiHopRefusedError(remoteId, target, pathNames) {
|
|
469
|
+
const hops = Math.max(0, pathNames.length - 1);
|
|
470
|
+
return new BoardError(`Moving ${remoteId} to "${target}" needs ${hops} transition${hops === 1 ? '' : 's'}`, [
|
|
471
|
+
`path: ${pathNames.join(' → ')}`,
|
|
472
|
+
"multi-hop transitions fire automations, notify people and stamp resolutions — the decision is the operator's, not the sync's",
|
|
473
|
+
'set mapping.transitions.multi_hop: true to execute this path automatically',
|
|
474
|
+
]);
|
|
475
|
+
}
|
|
476
|
+
/** The target has no path from where the issue stands. */
|
|
477
|
+
function unreachableError(remoteId, target, reachable) {
|
|
478
|
+
return new BoardError(`Cannot move ${remoteId} to "${target}"`, [
|
|
479
|
+
...(reachable.length > 0
|
|
480
|
+
? [`reachable statuses: ${[...new Set(reachable)].join(', ')}`]
|
|
481
|
+
: ["no statuses are reachable from the issue's current status"]),
|
|
482
|
+
'a Jira status change is a workflow transition, and the target may be unreachable from where the issue stands',
|
|
483
|
+
]);
|
|
484
|
+
}
|
|
485
|
+
/** The credential can transition but not read the workflow to find a path. */
|
|
486
|
+
function workflowPermissionError(remoteId, target, reachable) {
|
|
487
|
+
return new BoardError(`Cannot move ${remoteId} to "${target}": the credential cannot read the workflow to compute a path`, [
|
|
488
|
+
...(reachable.length > 0
|
|
489
|
+
? [`reachable in one hop: ${reachable.join(', ')}`]
|
|
490
|
+
: ["no transitions are reachable from the issue's current status"]),
|
|
491
|
+
'reading the workflow graph needs "View workflow" or "Administer projects" project permission',
|
|
492
|
+
'grant the token that permission, or move the issue one status at a time',
|
|
493
|
+
]);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Build a Jira connector from a validated connection block (and mapping, whose
|
|
497
|
+
* `transitions` / `transition_fields` shape how a status change is applied).
|
|
498
|
+
*
|
|
499
|
+
* `site` and `project` are required (the schema enforces them); `email` and
|
|
500
|
+
* `token` are the *resolved* secrets (the resolver filled them from the
|
|
501
|
+
* credential chain before this is called); `tls_verify` defaults to on.
|
|
502
|
+
*/
|
|
503
|
+
export function jiraConnector(connection, mapping = {}) {
|
|
504
|
+
const raw = connection;
|
|
505
|
+
const mappingConfig = (mapping ?? {});
|
|
506
|
+
const multiHop = mappingConfig.transitions?.multi_hop === true;
|
|
507
|
+
const transitionFields = mappingConfig.transition_fields ?? {};
|
|
508
|
+
// Which resource attribute carries the Jira account (LP-329). `email` is the
|
|
509
|
+
// one value that needs a search: Jira addresses an assignee by account id,
|
|
510
|
+
// so an email must be resolved through the user search before it can be
|
|
511
|
+
// written. Any other attribute (a `jira_account_id` attribute) already holds
|
|
512
|
+
// the account id and is written directly, with no search.
|
|
513
|
+
const accountsVia = normalizeAccountMapping(mapping['accounts'])?.via;
|
|
514
|
+
const conn = {
|
|
515
|
+
site: (raw.site ?? '').trim().replace(/\/+$/, ''),
|
|
516
|
+
project: raw.project,
|
|
517
|
+
board: raw.board,
|
|
518
|
+
email: typeof raw.email === 'string' ? raw.email : '',
|
|
519
|
+
token: typeof raw.token === 'string' ? raw.token : '',
|
|
520
|
+
tlsVerify: raw.tls_verify !== false,
|
|
521
|
+
};
|
|
522
|
+
// TLS: verification stays on unless the operator explicitly disabled it, and
|
|
523
|
+
// the disable is loud — every build of the connector, i.e. every run.
|
|
524
|
+
if (!conn.tlsVerify) {
|
|
525
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
526
|
+
process.emitWarning(`Jira connection to ${conn.site}: TLS verification is DISABLED (connection.tls_verify: false). ` +
|
|
527
|
+
'Traffic is not protected against interception. Prefer NODE_EXTRA_CA_CERTS=<file> or node --use-system-ca to trust your proxy root CA instead.');
|
|
528
|
+
}
|
|
529
|
+
// The client is loaded lazily, on first use: a board that declares a Jira
|
|
530
|
+
// remote but never syncs it pays nothing, and a missing jira.js surfaces as
|
|
531
|
+
// the install-command message only when something actually talks to Jira.
|
|
532
|
+
// The Cloud (v3) and Agile (software) surfaces share one build, so both are
|
|
533
|
+
// memoized together behind one import (LP-328 needs the Agile sprint API).
|
|
534
|
+
let clientsPromise;
|
|
535
|
+
function clients() {
|
|
536
|
+
clientsPromise ??= buildClients();
|
|
537
|
+
return clientsPromise;
|
|
538
|
+
}
|
|
539
|
+
async function client() {
|
|
540
|
+
return (await clients()).cloud;
|
|
541
|
+
}
|
|
542
|
+
async function agile() {
|
|
543
|
+
return (await clients()).agile;
|
|
544
|
+
}
|
|
545
|
+
async function buildClients() {
|
|
546
|
+
let mod;
|
|
547
|
+
try {
|
|
548
|
+
mod = await import(JIRA_PACKAGE);
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
throw new BoardError('The Jira client (jira.js) is not available', [
|
|
552
|
+
'Jira sync is experimental, so its client is not installed with light-plan.',
|
|
553
|
+
'Install it beside light-plan: npm install -g jira.js (a global install),',
|
|
554
|
+
'npm install jira.js (a project), or npx -p light-plan -p jira.js lpm … (npx).',
|
|
555
|
+
'It requires Node 22 or newer.',
|
|
556
|
+
error.message,
|
|
557
|
+
]);
|
|
558
|
+
}
|
|
559
|
+
const { createCloudClient, createAgileClient } = mod;
|
|
560
|
+
const config = {
|
|
561
|
+
host: conn.site,
|
|
562
|
+
auth: { type: 'basic', email: conn.email, apiToken: conn.token },
|
|
563
|
+
};
|
|
564
|
+
return { cloud: createCloudClient(config), agile: createAgileClient(config) };
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Every status name this project's issue types can be in, fetched once and
|
|
568
|
+
* cached. Jira answers per issue type; the union is what the project has.
|
|
569
|
+
*/
|
|
570
|
+
let projectStatusesPromise;
|
|
571
|
+
function projectStatuses() {
|
|
572
|
+
projectStatusesPromise ??= fetchProjectStatuses();
|
|
573
|
+
return projectStatusesPromise;
|
|
574
|
+
}
|
|
575
|
+
async function fetchProjectStatuses() {
|
|
576
|
+
const c = await client();
|
|
577
|
+
let raw;
|
|
578
|
+
try {
|
|
579
|
+
raw = await c.projects.getAllStatuses({ projectIdOrKey: conn.project });
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
throw describeFailure(error, conn, `reading the statuses of ${conn.project}`);
|
|
583
|
+
}
|
|
584
|
+
if (!Array.isArray(raw))
|
|
585
|
+
return [];
|
|
586
|
+
const names = new Set();
|
|
587
|
+
for (const entry of raw) {
|
|
588
|
+
if (entry === null || typeof entry !== 'object')
|
|
589
|
+
continue;
|
|
590
|
+
const statuses = entry['statuses'];
|
|
591
|
+
if (!Array.isArray(statuses))
|
|
592
|
+
continue;
|
|
593
|
+
for (const status of statuses) {
|
|
594
|
+
if (status === null || typeof status !== 'object')
|
|
595
|
+
continue;
|
|
596
|
+
const name = status['name'];
|
|
597
|
+
if (typeof name === 'string' && name !== '')
|
|
598
|
+
names.add(name);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return [...names];
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* The project's issue types, fetched once per connector and cached — the
|
|
605
|
+
* *actual* scheme the type mapping is validated against (LP-324). `probe`
|
|
606
|
+
* and `create` share the one fetch; a board that never probes and never
|
|
607
|
+
* creates a sub-task pays nothing.
|
|
608
|
+
*/
|
|
609
|
+
let typeSchemePromise;
|
|
610
|
+
function typeScheme() {
|
|
611
|
+
typeSchemePromise ??= fetchTypeScheme();
|
|
612
|
+
return typeSchemePromise;
|
|
613
|
+
}
|
|
614
|
+
async function fetchTypeScheme() {
|
|
615
|
+
const c = await client();
|
|
616
|
+
try {
|
|
617
|
+
const project = await c.projects.getProject({
|
|
618
|
+
projectIdOrKey: conn.project,
|
|
619
|
+
expand: 'issueTypes',
|
|
620
|
+
});
|
|
621
|
+
const raw = (project['issueTypes'] ?? []);
|
|
622
|
+
if (!Array.isArray(raw))
|
|
623
|
+
return [];
|
|
624
|
+
const types = [];
|
|
625
|
+
for (const entry of raw) {
|
|
626
|
+
if (entry === null || typeof entry !== 'object')
|
|
627
|
+
continue;
|
|
628
|
+
const record = entry;
|
|
629
|
+
const name = typeof record['name'] === 'string' ? record['name'] : '';
|
|
630
|
+
if (name === '')
|
|
631
|
+
continue;
|
|
632
|
+
types.push({
|
|
633
|
+
name,
|
|
634
|
+
...(typeof record['subtask'] === 'boolean' ? { subtask: record['subtask'] } : {}),
|
|
635
|
+
...(typeof record['hierarchyLevel'] === 'number'
|
|
636
|
+
? { hierarchyLevel: record['hierarchyLevel'] }
|
|
637
|
+
: {}),
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
return types;
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
throw describeFailure(error, conn, `reading the issue types of ${conn.project}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* True when `typeName` names a sub-task type. Best-effort: when the scheme
|
|
648
|
+
* cannot be read, this returns false and Jira's own refusal is surfaced — a
|
|
649
|
+
* top-level sub-task is refused by Jira whether or not we saw it coming.
|
|
650
|
+
*/
|
|
651
|
+
async function isSubtask(typeName) {
|
|
652
|
+
try {
|
|
653
|
+
const scheme = await typeScheme();
|
|
654
|
+
return scheme.some((type) => type.name === typeName && isSubtaskType(type));
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* The instance's custom fields, fetched once per connector and cached — the
|
|
662
|
+
* list `mapping.attributes` names resolve against (LP-327). System fields
|
|
663
|
+
* are dropped: the mapping carries only custom fields, so the cache stays
|
|
664
|
+
* small and `validateFieldMapping`'s "it has:" list names only the fields a
|
|
665
|
+
* mapping may point at.
|
|
666
|
+
*/
|
|
667
|
+
let fieldsPromise;
|
|
668
|
+
function fields() {
|
|
669
|
+
fieldsPromise ??= fetchFields();
|
|
670
|
+
return fieldsPromise;
|
|
671
|
+
}
|
|
672
|
+
async function fetchFields() {
|
|
673
|
+
const c = await client();
|
|
674
|
+
try {
|
|
675
|
+
const raw = await c.issueFields.getFields();
|
|
676
|
+
return parseFields(raw).filter((field) => field.custom);
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
throw describeFailure(error, conn, 'listing custom fields');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
// -- sprints (LP-328) ------------------------------------------------------
|
|
683
|
+
// Jira's only native period container is a sprint, so a period mapping on
|
|
684
|
+
// this connector *is* sprint mapping — the schema already stamps
|
|
685
|
+
// `carrier: 'sprint'`, and reading `normalizePeriodMapping` here keeps the
|
|
686
|
+
// connector correct even when a test builds it with the bare
|
|
687
|
+
// `{ container }` spelling.
|
|
688
|
+
const usesSprints = normalizePeriodMapping(mapping['periods']) !== undefined;
|
|
689
|
+
/** The sprint custom field's per-instance id, discovered once and cached. */
|
|
690
|
+
let sprintFieldIdPromise;
|
|
691
|
+
function sprintFieldId() {
|
|
692
|
+
sprintFieldIdPromise ??= fetchSprintFieldId();
|
|
693
|
+
return sprintFieldIdPromise;
|
|
694
|
+
}
|
|
695
|
+
async function fetchSprintFieldId() {
|
|
696
|
+
try {
|
|
697
|
+
return sprintFieldIdOf(await fields());
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
throw describeFailure(error, conn, 'finding the sprint field');
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/** The board's sprints (every state), fetched once per connector and cached. */
|
|
704
|
+
let sprintsPromise;
|
|
705
|
+
function sprints() {
|
|
706
|
+
sprintsPromise ??= fetchSprints();
|
|
707
|
+
return sprintsPromise;
|
|
708
|
+
}
|
|
709
|
+
async function fetchSprints() {
|
|
710
|
+
if (conn.board === undefined)
|
|
711
|
+
return [];
|
|
712
|
+
const a = await agile();
|
|
713
|
+
try {
|
|
714
|
+
const out = [];
|
|
715
|
+
let startAt = 0;
|
|
716
|
+
for (;;) {
|
|
717
|
+
const page = await a.board.getAllSprints({
|
|
718
|
+
boardId: Number(conn.board),
|
|
719
|
+
startAt,
|
|
720
|
+
maxResults: 50,
|
|
721
|
+
});
|
|
722
|
+
out.push(...parseSprints(page));
|
|
723
|
+
const values = Array.isArray(page.values) ? page.values : [];
|
|
724
|
+
if (page.isLast === true || values.length === 0)
|
|
725
|
+
break;
|
|
726
|
+
startAt += values.length;
|
|
727
|
+
}
|
|
728
|
+
return out;
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
throw describeFailure(error, conn, `listing sprints on board ${conn.board}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Which Agile board this project's sprints live on, asked of Jira rather than
|
|
736
|
+
* of the person.
|
|
737
|
+
*
|
|
738
|
+
* `connection.board` is the one connection value a reader cannot reasonably
|
|
739
|
+
* be expected to know: it is not the project key, it does not appear in the
|
|
740
|
+
* project settings, and the usual way to find it is to open a board and read
|
|
741
|
+
* a number out of the URL. The credential that can file an issue can also
|
|
742
|
+
* list the project's boards, so setup asks Jira and writes the answer down.
|
|
743
|
+
*
|
|
744
|
+
* Returns nothing at all when the board id is already set, or when the
|
|
745
|
+
* mapping carries no periods — there is nothing to discover in either case.
|
|
746
|
+
* A failure is swallowed into an empty list: discovery is an offer, and the
|
|
747
|
+
* push preflight is what actually holds the line.
|
|
748
|
+
*/
|
|
749
|
+
async function discoverConnection() {
|
|
750
|
+
if (conn.board !== undefined && conn.board !== '')
|
|
751
|
+
return [];
|
|
752
|
+
if (mapping['periods'] === undefined)
|
|
753
|
+
return [];
|
|
754
|
+
try {
|
|
755
|
+
const a = await agile();
|
|
756
|
+
const page = await a.board.getAllBoards({ projectKeyOrId: conn.project, maxResults: 50 });
|
|
757
|
+
const values = Array.isArray(page.values) ? page.values : [];
|
|
758
|
+
return values.flatMap((entry) => {
|
|
759
|
+
const id = entry['id'];
|
|
760
|
+
if (typeof id !== 'number' && typeof id !== 'string')
|
|
761
|
+
return [];
|
|
762
|
+
const name = typeof entry['name'] === 'string' ? entry['name'] : `board ${String(id)}`;
|
|
763
|
+
const kind = typeof entry['type'] === 'string' ? ` (${entry['type']})` : '';
|
|
764
|
+
return [{ key: 'board', value: String(id), label: `${name}${kind}` }];
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
return [];
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* The sprint field value a create/update body should carry: `{ id }` to move
|
|
773
|
+
* the issue into the sprint, `{ null }` to clear it, or `{}` when the
|
|
774
|
+
* request carries no period.
|
|
775
|
+
*
|
|
776
|
+
* **A sprint that does not exist yet is not an error.** The board's timeline
|
|
777
|
+
* and the board's work are pushed independently — file the stories today,
|
|
778
|
+
* file the sprints when you are ready — so an issue scheduled into a sprint
|
|
779
|
+
* Jira has not got is filed *unscheduled*, and `unwritten` says so. The
|
|
780
|
+
* executor records the period as unset, and the push that files the sprint
|
|
781
|
+
* writes the assignment. Refusing here would make the whole timeline a
|
|
782
|
+
* precondition of filing a single story.
|
|
783
|
+
*
|
|
784
|
+
* A **closed** sprint is different and still refuses: the work cannot go
|
|
785
|
+
* there at all, and pretending otherwise would lose the scheduling silently.
|
|
786
|
+
* Unless the issue is *already* in it, which is a no-op rather than a move.
|
|
787
|
+
*/
|
|
788
|
+
async function sprintFieldFor(request, remoteId) {
|
|
789
|
+
if (!usesSprints)
|
|
790
|
+
return { fields: {} };
|
|
791
|
+
if (request.period === undefined)
|
|
792
|
+
return { fields: {} };
|
|
793
|
+
const fieldId = await sprintFieldId();
|
|
794
|
+
if (request.period === null) {
|
|
795
|
+
// Clearing a sprint: without a field there is nothing to clear, and the
|
|
796
|
+
// issue is already out of every sprint.
|
|
797
|
+
return { fields: fieldId === undefined ? {} : { [fieldId]: null } };
|
|
798
|
+
}
|
|
799
|
+
// No board and no sprint field are both "this instance cannot hold the
|
|
800
|
+
// schedule", which is the same situation as a sprint that does not exist:
|
|
801
|
+
// file the work, say what was dropped, and let a later push carry it.
|
|
802
|
+
if (conn.board === undefined || fieldId === undefined) {
|
|
803
|
+
return { fields: {}, unwritten: 'period' };
|
|
804
|
+
}
|
|
805
|
+
const sprint = sprintByName(await sprints(), request.period.name);
|
|
806
|
+
if (sprint === undefined) {
|
|
807
|
+
return { fields: {}, unwritten: 'period' };
|
|
808
|
+
}
|
|
809
|
+
if (isClosedSprint(sprint)) {
|
|
810
|
+
// Already sitting in this closed sprint? Then nothing moves, and a title
|
|
811
|
+
// or body edit must not be refused for it. Only a genuine move into a
|
|
812
|
+
// closed sprint is refused.
|
|
813
|
+
if (remoteId !== undefined && (await currentSprintNameOf(remoteId, fieldId)) === sprint.name) {
|
|
814
|
+
return { fields: {} };
|
|
815
|
+
}
|
|
816
|
+
throw new BoardError(`Cannot move work into sprint "${sprint.name}" — it is closed`, [
|
|
817
|
+
'Closed sprints are read-only; move the issue into an active or future sprint, or reopen the sprint in Jira.',
|
|
818
|
+
]);
|
|
819
|
+
}
|
|
820
|
+
const numeric = Number(sprint.id);
|
|
821
|
+
return { fields: { [fieldId]: Number.isNaN(numeric) ? sprint.id : numeric } };
|
|
822
|
+
}
|
|
823
|
+
/** Read a record's sprint field value into a `{ name, state, starts, ends }` observation. */
|
|
824
|
+
function sprintObservationOf(record, fieldId) {
|
|
825
|
+
const fields = record['fields'];
|
|
826
|
+
if (!fields || typeof fields !== 'object')
|
|
827
|
+
return undefined;
|
|
828
|
+
const value = fields[fieldId];
|
|
829
|
+
if (value === null || value === undefined || typeof value !== 'object')
|
|
830
|
+
return undefined;
|
|
831
|
+
// Parallel sprints arrive as an array; take the first open one, else the
|
|
832
|
+
// first — the sync refuses ambiguity rather than inventing a winner.
|
|
833
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
834
|
+
for (const entry of entries) {
|
|
835
|
+
if (entry === null || typeof entry !== 'object')
|
|
836
|
+
continue;
|
|
837
|
+
const sprint = entry;
|
|
838
|
+
if (typeof sprint['name'] !== 'string')
|
|
839
|
+
continue;
|
|
840
|
+
const observation = { name: sprint['name'] };
|
|
841
|
+
if (typeof sprint['state'] === 'string')
|
|
842
|
+
observation.state = sprint['state'];
|
|
843
|
+
if (typeof sprint['startDate'] === 'string')
|
|
844
|
+
observation.starts = sprint['startDate'];
|
|
845
|
+
if (typeof sprint['endDate'] === 'string')
|
|
846
|
+
observation.ends = sprint['endDate'];
|
|
847
|
+
return observation;
|
|
848
|
+
}
|
|
849
|
+
return undefined;
|
|
850
|
+
}
|
|
851
|
+
/** The name of the sprint an issue currently sits in, or `undefined`. */
|
|
852
|
+
async function currentSprintNameOf(remoteId, fieldId) {
|
|
853
|
+
const c = await client();
|
|
854
|
+
try {
|
|
855
|
+
const record = await c.issues.getIssue({ issueIdOrKey: remoteId });
|
|
856
|
+
return sprintObservationOf(record, fieldId)?.['name'];
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
return undefined;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Add `sprint` (name + dates) to a record so the pull resolves the period.
|
|
864
|
+
* Best-effort: no sprint field, no board, or no sprint on the issue simply
|
|
865
|
+
* stays unenriched — the carrier has nothing to recover, which is the honest
|
|
866
|
+
* answer rather than a failure.
|
|
867
|
+
*/
|
|
868
|
+
async function enrichSprint(record) {
|
|
869
|
+
if (record === null)
|
|
870
|
+
return null;
|
|
871
|
+
if (!usesSprints || conn.board === undefined)
|
|
872
|
+
return record;
|
|
873
|
+
try {
|
|
874
|
+
const fieldId = await sprintFieldId();
|
|
875
|
+
if (fieldId === undefined)
|
|
876
|
+
return record;
|
|
877
|
+
const observation = sprintObservationOf(record, fieldId);
|
|
878
|
+
if (observation === undefined)
|
|
879
|
+
return record;
|
|
880
|
+
return { ...record, sprint: observation };
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
// Enrichment never fails a read: a missing board or a permission loss on
|
|
884
|
+
// the field list is not a missing issue.
|
|
885
|
+
return record;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* The create-screen field ids, keyed by issue type name, fetched once per
|
|
890
|
+
* connector (LP-327). This is the metadata that answers "is this field
|
|
891
|
+
* writable on a create of this type" before any write; the create screen
|
|
892
|
+
* stands in for the edit screen too, since no issue exists to query
|
|
893
|
+
* `editmeta` against and Jira's default screen scheme serves one screen for
|
|
894
|
+
* both operations.
|
|
895
|
+
*/
|
|
896
|
+
let createMetaPromise;
|
|
897
|
+
function createMeta() {
|
|
898
|
+
createMetaPromise ??= fetchCreateMeta();
|
|
899
|
+
return createMetaPromise;
|
|
900
|
+
}
|
|
901
|
+
async function fetchCreateMeta() {
|
|
902
|
+
const c = await client();
|
|
903
|
+
try {
|
|
904
|
+
const page = await c.issues.getCreateIssueMetaIssueTypes({ projectIdOrKey: conn.project });
|
|
905
|
+
const rawTypes = (page['issueTypes'] ?? []);
|
|
906
|
+
const map = new Map();
|
|
907
|
+
if (!Array.isArray(rawTypes))
|
|
908
|
+
return map;
|
|
909
|
+
for (const entry of rawTypes) {
|
|
910
|
+
if (entry === null || typeof entry !== 'object')
|
|
911
|
+
continue;
|
|
912
|
+
const record = entry;
|
|
913
|
+
const id = record['id'];
|
|
914
|
+
const name = record['name'];
|
|
915
|
+
if (typeof id !== 'string' || typeof name !== 'string')
|
|
916
|
+
continue;
|
|
917
|
+
const fieldsPage = await c.issues.getCreateIssueMetaIssueTypeId({
|
|
918
|
+
projectIdOrKey: conn.project,
|
|
919
|
+
issueTypeId: id,
|
|
920
|
+
});
|
|
921
|
+
const rawFields = (fieldsPage['fields'] ?? []);
|
|
922
|
+
const set = new Set();
|
|
923
|
+
if (Array.isArray(rawFields)) {
|
|
924
|
+
for (const rawField of rawFields) {
|
|
925
|
+
if (rawField === null || typeof rawField !== 'object')
|
|
926
|
+
continue;
|
|
927
|
+
const fieldRecord = rawField;
|
|
928
|
+
const fieldId = fieldRecord['fieldId'];
|
|
929
|
+
const key = fieldRecord['key'];
|
|
930
|
+
if (typeof fieldId === 'string')
|
|
931
|
+
set.add(fieldId);
|
|
932
|
+
if (typeof key === 'string')
|
|
933
|
+
set.add(key);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
map.set(name, set);
|
|
937
|
+
}
|
|
938
|
+
return map;
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
941
|
+
throw describeFailure(error, conn, 'reading the create-screen metadata');
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/** Create a custom field (needs the Administer Jira global permission). */
|
|
945
|
+
async function createField(name, typeKey, signal) {
|
|
946
|
+
const c = await client();
|
|
947
|
+
try {
|
|
948
|
+
const created = await c.issueFields.createCustomField({ name, type: typeKey });
|
|
949
|
+
void signal;
|
|
950
|
+
const id = typeof created['id'] === 'string' ? created['id'] : '';
|
|
951
|
+
if (id === '') {
|
|
952
|
+
throw new BoardError(`Jira created the field "${name}" but returned no id`, [
|
|
953
|
+
'The field exists but its id could not be read; re-run discovery to pick it up.',
|
|
954
|
+
]);
|
|
955
|
+
}
|
|
956
|
+
return { id };
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
throw describeFailure(error, conn, `creating the custom field "${name}"`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/** Add a field to the default screen (needs the Administer Jira permission). */
|
|
963
|
+
async function addFieldToScreen(fieldId, signal) {
|
|
964
|
+
const c = await client();
|
|
965
|
+
try {
|
|
966
|
+
await c.screens.addFieldToDefaultScreen({ fieldId });
|
|
967
|
+
void signal;
|
|
968
|
+
}
|
|
969
|
+
catch (error) {
|
|
970
|
+
throw describeFailure(error, conn, `adding ${fieldId} to the screen`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
/** A Jira issue read back as its own record shape, or null when gone/unseen. */
|
|
974
|
+
async function get(remoteId) {
|
|
975
|
+
const c = await client();
|
|
976
|
+
try {
|
|
977
|
+
const issue = await c.issues.getIssue({ issueIdOrKey: remoteId });
|
|
978
|
+
return await enrichSprint(issue);
|
|
979
|
+
}
|
|
980
|
+
catch (error) {
|
|
981
|
+
const err = error;
|
|
982
|
+
if (err.status === 404)
|
|
983
|
+
return null;
|
|
984
|
+
throw describeFailure(error, conn, `fetching issue ${remoteId}`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
/** The fields of a request, in the shape `createIssue` / `editIssue` expect. */
|
|
988
|
+
function fieldsOf(request) {
|
|
989
|
+
const fields = {
|
|
990
|
+
project: { key: conn.project },
|
|
991
|
+
};
|
|
992
|
+
if (request.type !== undefined)
|
|
993
|
+
fields['issuetype'] = { name: request.type };
|
|
994
|
+
if (request.title !== undefined)
|
|
995
|
+
fields['summary'] = request.title;
|
|
996
|
+
// Jira Cloud's description is ADF, and this is the wire — the last place
|
|
997
|
+
// the body is anybody's but Jira's. A string arrives from the translator
|
|
998
|
+
// (prose) and from the executor (prose plus the managed block, composed in
|
|
999
|
+
// markdown); an ADF document arrives from a caller that has already done
|
|
1000
|
+
// the conversion, and is passed through.
|
|
1001
|
+
if (request.body !== undefined) {
|
|
1002
|
+
fields['description'] =
|
|
1003
|
+
typeof request.body === 'string' ? markdownToAdf(request.body) : request.body;
|
|
1004
|
+
}
|
|
1005
|
+
if (request.labels !== undefined)
|
|
1006
|
+
fields['labels'] = request.labels;
|
|
1007
|
+
if (request.parent !== undefined && request.parent !== '') {
|
|
1008
|
+
fields['parent'] = parentRef(request.parent);
|
|
1009
|
+
}
|
|
1010
|
+
return fields;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* The `assignee` field value a create/update should carry (LP-329). Jira
|
|
1014
|
+
* addresses an assignee by account id, never by email or name, so:
|
|
1015
|
+
*
|
|
1016
|
+
* - `request.assignee === null` (or an empty string, defensively) → the
|
|
1017
|
+
* issue goes unassigned;
|
|
1018
|
+
* - a non-empty value under `accounts.via: email` → resolved to an account
|
|
1019
|
+
* id through Jira's user search;
|
|
1020
|
+
* - a non-empty value under any other `via` (a `jira_account_id`
|
|
1021
|
+
* attribute) → the account id itself, written directly with no search.
|
|
1022
|
+
*
|
|
1023
|
+
* `undefined` means the op does not carry an assignee, so the remote side is
|
|
1024
|
+
* left untouched.
|
|
1025
|
+
*/
|
|
1026
|
+
async function assigneeFieldFor(request) {
|
|
1027
|
+
if (request.assignee === undefined)
|
|
1028
|
+
return {};
|
|
1029
|
+
if (request.assignee === null || request.assignee === '') {
|
|
1030
|
+
return { assignee: null };
|
|
1031
|
+
}
|
|
1032
|
+
return { assignee: { id: await resolveAccount(request.assignee) } };
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Turn a `mapping.accounts.via` value into the Jira account id it names.
|
|
1036
|
+
*
|
|
1037
|
+
* Only `via: email` searches: the value is an email address and Jira assigns
|
|
1038
|
+
* by account id, so the user search must find the id. An empty result is the
|
|
1039
|
+
* documented signature of a privacy-restricted (GDPR-mode) instance, where
|
|
1040
|
+
* email lookups are disabled — that is refused with the `jira_account_id`
|
|
1041
|
+
* alternative rather than reported as "user not found". Any other `via` is
|
|
1042
|
+
* already the account id and is returned as-is, no search made.
|
|
1043
|
+
*/
|
|
1044
|
+
async function resolveAccount(value) {
|
|
1045
|
+
if (accountsVia !== 'email')
|
|
1046
|
+
return value;
|
|
1047
|
+
const c = await client();
|
|
1048
|
+
let users;
|
|
1049
|
+
try {
|
|
1050
|
+
users = (await c.userSearch.findUsers({ query: value }));
|
|
1051
|
+
}
|
|
1052
|
+
catch (error) {
|
|
1053
|
+
throw describeFailure(error, conn, `resolving the Jira account for "${value}"`);
|
|
1054
|
+
}
|
|
1055
|
+
const needle = value.trim().toLowerCase();
|
|
1056
|
+
for (const user of users) {
|
|
1057
|
+
const email = user['emailAddress'];
|
|
1058
|
+
if (typeof email === 'string' && email.trim().toLowerCase() === needle) {
|
|
1059
|
+
const id = user['accountId'];
|
|
1060
|
+
if (typeof id === 'string' && id !== '')
|
|
1061
|
+
return id;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
throw new BoardError(`Cannot resolve "${value}" to a Jira account by email search`, [
|
|
1065
|
+
'the search returned no matching account, which is what a privacy-restricted (GDPR-mode) instance does for email lookups',
|
|
1066
|
+
'set mapping.accounts.via: jira_account_id and put each person\u2019s Jira account id in that attribute',
|
|
1067
|
+
'the connector then assigns by account id directly, with no search',
|
|
1068
|
+
]);
|
|
1069
|
+
}
|
|
1070
|
+
/** Move the issue to `target` status, resolving transitions (single- or multi-hop). */
|
|
1071
|
+
async function transitionTo(c, remoteId, target, current) {
|
|
1072
|
+
// Already there is not a move. Jira workflows rarely define a transition
|
|
1073
|
+
// whose `to` is the status you are in, so asking for one falls through to
|
|
1074
|
+
// the multi-hop path and is refused — which would make every issue whose
|
|
1075
|
+
// mapped status *is* the workflow's first status fail on create.
|
|
1076
|
+
if (current !== undefined && current.trim().toLowerCase() === target.trim().toLowerCase()) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const transitions = await c.issues.getTransitions({
|
|
1080
|
+
issueIdOrKey: remoteId,
|
|
1081
|
+
expand: 'transitions.fields',
|
|
1082
|
+
});
|
|
1083
|
+
const list = transitions.transitions ?? [];
|
|
1084
|
+
// One hop: the transition whose `to` status is the target.
|
|
1085
|
+
const single = singleHopTo(list, target);
|
|
1086
|
+
if (single) {
|
|
1087
|
+
const { fields, missing } = fieldsFor(single, transitionFields);
|
|
1088
|
+
if (missing.length > 0)
|
|
1089
|
+
throw missingFieldsError(remoteId, target, missing);
|
|
1090
|
+
await c.issues.doTransition({
|
|
1091
|
+
issueIdOrKey: remoteId,
|
|
1092
|
+
transition: { id: single.id },
|
|
1093
|
+
...(fields ? { fields } : {}),
|
|
1094
|
+
});
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
// No single hop. A path is computed from the workflow, which needs a read
|
|
1098
|
+
// the transitioning permission does not imply — the two failures are
|
|
1099
|
+
// different, and the messages say so.
|
|
1100
|
+
const issue = await c.issues.getIssue({ issueIdOrKey: remoteId });
|
|
1101
|
+
const issueFields = (issue['fields'] ?? {});
|
|
1102
|
+
const status = (issueFields['status'] ?? {});
|
|
1103
|
+
const project = (issueFields['project'] ?? {});
|
|
1104
|
+
const issuetype = (issueFields['issuetype'] ?? {});
|
|
1105
|
+
const currentStatusName = typeof status.name === 'string' ? status.name : '';
|
|
1106
|
+
const projectId = project.id === undefined ? '' : String(project.id);
|
|
1107
|
+
const issueTypeId = issuetype.id === undefined ? '' : String(issuetype.id);
|
|
1108
|
+
if (projectId === '' || issueTypeId === '') {
|
|
1109
|
+
throw new BoardError(`Cannot compute a multi-hop path for ${remoteId} to "${target}"`, [
|
|
1110
|
+
'the issue record did not carry a project id or issue type id',
|
|
1111
|
+
]);
|
|
1112
|
+
}
|
|
1113
|
+
let graph;
|
|
1114
|
+
try {
|
|
1115
|
+
const response = await c.workflows.readWorkflows({
|
|
1116
|
+
projectAndIssueTypes: [{ projectId, issueTypeId }],
|
|
1117
|
+
});
|
|
1118
|
+
graph = workflowGraphOf(response);
|
|
1119
|
+
}
|
|
1120
|
+
catch (error) {
|
|
1121
|
+
if (isPermissionDenied(error)) {
|
|
1122
|
+
throw workflowPermissionError(remoteId, target, singleHopReachable(list));
|
|
1123
|
+
}
|
|
1124
|
+
throw error;
|
|
1125
|
+
}
|
|
1126
|
+
const path = findPath(graph, currentStatusName, target);
|
|
1127
|
+
if (path === null) {
|
|
1128
|
+
const reachable = reachableNames(graph, currentStatusName);
|
|
1129
|
+
throw unreachableError(remoteId, target, reachable.length > 0 ? reachable : singleHopReachable(list));
|
|
1130
|
+
}
|
|
1131
|
+
if (!multiHop) {
|
|
1132
|
+
throw multiHopRefusedError(remoteId, target, [currentStatusName, ...path]);
|
|
1133
|
+
}
|
|
1134
|
+
// Execute the path one status at a time. The first hop is reachable from
|
|
1135
|
+
// the transitions already fetched (`list`); each later hop re-reads the
|
|
1136
|
+
// transitions now available from the intermediate status the issue just
|
|
1137
|
+
// moved to — the graph says which statuses lead where, but transition ids
|
|
1138
|
+
// and required fields are per-status facts.
|
|
1139
|
+
let available = list;
|
|
1140
|
+
for (let i = 0; i < path.length; i++) {
|
|
1141
|
+
const next = path[i];
|
|
1142
|
+
const step = singleHopTo(available, next);
|
|
1143
|
+
if (!step) {
|
|
1144
|
+
throw new BoardError(`Cannot move ${remoteId} to "${target}"`, [
|
|
1145
|
+
`the workflow path says "${next}" follows, but no transition to it is available now`,
|
|
1146
|
+
'the issue may have moved, or a condition on the transition no longer holds',
|
|
1147
|
+
]);
|
|
1148
|
+
}
|
|
1149
|
+
const { fields, missing } = fieldsFor(step, transitionFields);
|
|
1150
|
+
if (missing.length > 0)
|
|
1151
|
+
throw missingFieldsError(remoteId, next, missing);
|
|
1152
|
+
await c.issues.doTransition({
|
|
1153
|
+
issueIdOrKey: remoteId,
|
|
1154
|
+
transition: { id: step.id },
|
|
1155
|
+
...(fields ? { fields } : {}),
|
|
1156
|
+
});
|
|
1157
|
+
if (i + 1 < path.length) {
|
|
1158
|
+
const refreshed = await c.issues.getTransitions({
|
|
1159
|
+
issueIdOrKey: remoteId,
|
|
1160
|
+
expand: 'transitions.fields',
|
|
1161
|
+
});
|
|
1162
|
+
available = refreshed.transitions ?? [];
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return {
|
|
1167
|
+
name: 'jira',
|
|
1168
|
+
async create(request, signal) {
|
|
1169
|
+
const c = await client();
|
|
1170
|
+
try {
|
|
1171
|
+
// A sub-task must have a parent — Jira refuses a top-level sub-task, and
|
|
1172
|
+
// this local refusal names the rule before the round-trip (LP-324).
|
|
1173
|
+
if ((request.parent === undefined || request.parent === '') &&
|
|
1174
|
+
typeof request.type === 'string' &&
|
|
1175
|
+
(await isSubtask(request.type))) {
|
|
1176
|
+
throw new BoardError(`Cannot file "${request.type}" without a parent`, [
|
|
1177
|
+
`${request.type} is a sub-task issue type — Jira requires a parent issue for it`,
|
|
1178
|
+
'place the issue under its parent before pushing, or map it to a standard type',
|
|
1179
|
+
]);
|
|
1180
|
+
}
|
|
1181
|
+
const sprintField = await sprintFieldFor(request);
|
|
1182
|
+
const created = await c.issues.createIssue({
|
|
1183
|
+
fields: { ...fieldsOf(request), ...(await assigneeFieldFor(request)), ...sprintField.fields },
|
|
1184
|
+
});
|
|
1185
|
+
// **A created issue is in the workflow's first status, never the one
|
|
1186
|
+
// the board asked for**, because Jira has no way to set a status on
|
|
1187
|
+
// `POST /issue` — status moves only through a transition. So a create
|
|
1188
|
+
// has to do what `update` does, or a board of finished work arrives as
|
|
1189
|
+
// a project of "To Do" and *stays* that way: the base is recomputed
|
|
1190
|
+
// from the echo, Jira's first status usually maps back to more than one
|
|
1191
|
+
// board status (`backlog` and `ready` both claim "To Do"), the
|
|
1192
|
+
// ambiguous read leaves the local value standing, and both sides then
|
|
1193
|
+
// agree on a status the remote does not hold. No later push disagrees,
|
|
1194
|
+
// because there is nothing left to disagree with.
|
|
1195
|
+
// The post-write record: the create response carries id/key/self only,
|
|
1196
|
+
// so read the issue back to record the base from what Jira stored.
|
|
1197
|
+
let record = await c.issues.getIssue({ issueIdOrKey: created.id });
|
|
1198
|
+
if (request.state !== undefined && request.state !== '') {
|
|
1199
|
+
const before = statusNameOf(record);
|
|
1200
|
+
if (before !== request.state) {
|
|
1201
|
+
await transitionTo(c, created.id, request.state, before);
|
|
1202
|
+
// Read again: the base must record the status the issue ended in.
|
|
1203
|
+
record = await c.issues.getIssue({ issueIdOrKey: created.id });
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
void signal;
|
|
1207
|
+
return {
|
|
1208
|
+
...resultOf(record, conn.site),
|
|
1209
|
+
...(sprintField.unwritten !== undefined ? { unwritten: [sprintField.unwritten] } : {}),
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
catch (error) {
|
|
1213
|
+
throw describeFailure(error, conn, 'creating an issue');
|
|
1214
|
+
}
|
|
1215
|
+
},
|
|
1216
|
+
async update(remoteId, request, signal) {
|
|
1217
|
+
const c = await client();
|
|
1218
|
+
try {
|
|
1219
|
+
const sprintField = await sprintFieldFor(request, remoteId);
|
|
1220
|
+
const fields = { ...fieldsOf(request), ...(await assigneeFieldFor(request)), ...sprintField.fields };
|
|
1221
|
+
delete fields['project'];
|
|
1222
|
+
// Whether the sprint was actually written — `unwritten` has to report
|
|
1223
|
+
// the sprint Jira would not take, and that is only known after trying.
|
|
1224
|
+
let unwritten = sprintField.unwritten;
|
|
1225
|
+
if (Object.keys(fields).length > 0) {
|
|
1226
|
+
// **Jira validates one field at a time**, so recovering from a
|
|
1227
|
+
// refusal is a loop rather than a retry. Two of its rules are about
|
|
1228
|
+
// a field that was never this issue's to carry, and both surface
|
|
1229
|
+
// only once the earlier one is gone: drop the sprint and the next
|
|
1230
|
+
// attempt complains about the issue type. A single retry fixed the
|
|
1231
|
+
// first and failed on the second, which is how thirteen sub-tasks
|
|
1232
|
+
// went on failing after the sprint half was fixed.
|
|
1233
|
+
//
|
|
1234
|
+
// Anything not recognised is rethrown untouched, and the loop can
|
|
1235
|
+
// only ever shrink the request, so it terminates.
|
|
1236
|
+
const sprintKeys = Object.keys(sprintField.fields);
|
|
1237
|
+
let attempt = { ...fields };
|
|
1238
|
+
for (;;) {
|
|
1239
|
+
try {
|
|
1240
|
+
await c.issues.editIssue({ issueIdOrKey: remoteId, fields: attempt });
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
catch (error) {
|
|
1244
|
+
const next = { ...attempt };
|
|
1245
|
+
// A sub-task's sprint is its parent's: it can be neither set nor
|
|
1246
|
+
// cleared, so the field is reported unwritten rather than retried
|
|
1247
|
+
// for ever.
|
|
1248
|
+
if (isSubtaskSprintRefusal(error) && sprintKeys.some((key) => key in next)) {
|
|
1249
|
+
for (const key of sprintKeys)
|
|
1250
|
+
delete next[key];
|
|
1251
|
+
unwritten = 'period';
|
|
1252
|
+
attempt = next;
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
// An `issuetype` restated on a sub-task: the executor hands the
|
|
1256
|
+
// translator the whole document, so every edit carries the type
|
|
1257
|
+
// even when nothing about it changed, and Jira wants a `parent`
|
|
1258
|
+
// beside it. Dropping it is only safe when the type is not
|
|
1259
|
+
// actually changing, so that is **checked** — a real retype fails
|
|
1260
|
+
// loudly with the reason instead of reporting a success that did
|
|
1261
|
+
// not happen.
|
|
1262
|
+
if (isSubtaskTypeRefusal(error) && next['issuetype'] !== undefined) {
|
|
1263
|
+
const current = await c.issues.getIssue({
|
|
1264
|
+
issueIdOrKey: remoteId,
|
|
1265
|
+
fields: ['issuetype'],
|
|
1266
|
+
});
|
|
1267
|
+
const currentType = (current['fields'] ?? {})['issuetype'];
|
|
1268
|
+
const currentName = currentType && typeof currentType === 'object'
|
|
1269
|
+
? currentType.name
|
|
1270
|
+
: undefined;
|
|
1271
|
+
if (currentName !== request.type) {
|
|
1272
|
+
throw new BoardError(`Cannot retype ${remoteId} to "${String(request.type)}"`, [
|
|
1273
|
+
'Jira needs the parent issue in the same request when an issue type changes on a sub-task',
|
|
1274
|
+
'move the document out of its parent first, or change the type in Jira',
|
|
1275
|
+
]);
|
|
1276
|
+
}
|
|
1277
|
+
delete next['issuetype'];
|
|
1278
|
+
attempt = next;
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
throw error;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
// Everything the refusals removed is gone; nothing left to send is a
|
|
1285
|
+
// finished edit, not a failure.
|
|
1286
|
+
}
|
|
1287
|
+
if (request.state !== undefined && request.state !== '') {
|
|
1288
|
+
await transitionTo(c, remoteId, request.state);
|
|
1289
|
+
}
|
|
1290
|
+
const record = await c.issues.getIssue({ issueIdOrKey: remoteId });
|
|
1291
|
+
void signal;
|
|
1292
|
+
return {
|
|
1293
|
+
...resultOf(record, conn.site),
|
|
1294
|
+
...(unwritten !== undefined ? { unwritten: [unwritten] } : {}),
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
catch (error) {
|
|
1298
|
+
throw describeFailure(error, conn, `updating issue ${remoteId}`);
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
async delete(remoteId, signal) {
|
|
1302
|
+
const c = await client();
|
|
1303
|
+
try {
|
|
1304
|
+
await c.issues.deleteIssue({ issueIdOrKey: remoteId });
|
|
1305
|
+
void signal;
|
|
1306
|
+
// Jira deletes hard; there is no post-delete record to record.
|
|
1307
|
+
return { remoteId, remoteKey: remoteId, remoteUrl: '', remoteRev: '' };
|
|
1308
|
+
}
|
|
1309
|
+
catch (error) {
|
|
1310
|
+
throw describeFailure(error, conn, `deleting issue ${remoteId}`);
|
|
1311
|
+
}
|
|
1312
|
+
},
|
|
1313
|
+
get,
|
|
1314
|
+
async resolve(key) {
|
|
1315
|
+
// `PAY-123` (or a bare numeric id) → the live issue. Jira keys survive
|
|
1316
|
+
// lookups by id too, but the key is what a person pastes.
|
|
1317
|
+
const record = await get(key);
|
|
1318
|
+
if (record === null)
|
|
1319
|
+
return null;
|
|
1320
|
+
return resultOf(record, conn.site);
|
|
1321
|
+
},
|
|
1322
|
+
describe(record) {
|
|
1323
|
+
const result = resultOf(record, conn.site);
|
|
1324
|
+
return { remoteKey: result.remoteKey, remoteUrl: result.remoteUrl };
|
|
1325
|
+
},
|
|
1326
|
+
async list(opts) {
|
|
1327
|
+
const c = await client();
|
|
1328
|
+
const jql = `project = ${conn.project}` +
|
|
1329
|
+
(opts?.cursor ? ` AND updated >= ${JSON.stringify(opts.cursor)}` : '') +
|
|
1330
|
+
' ORDER BY updated ASC';
|
|
1331
|
+
try {
|
|
1332
|
+
// The sprint field rides the search explicitly when sprint mapping is
|
|
1333
|
+
// on and a board is configured: `*all` does not reliably include it,
|
|
1334
|
+
// exactly as with `issuelinks` (LP-325). Without a board there is
|
|
1335
|
+
// nothing to enrich, and the field is left out.
|
|
1336
|
+
let searchFields = ['*all', 'issuelinks'];
|
|
1337
|
+
if (usesSprints && conn.board !== undefined) {
|
|
1338
|
+
const fieldId = await sprintFieldId();
|
|
1339
|
+
if (fieldId !== undefined)
|
|
1340
|
+
searchFields = ['*all', 'issuelinks', fieldId];
|
|
1341
|
+
}
|
|
1342
|
+
const records = [];
|
|
1343
|
+
let pages = 0;
|
|
1344
|
+
let nextPageToken;
|
|
1345
|
+
do {
|
|
1346
|
+
const page = await c.issueSearch.searchAndReconsileIssuesUsingJqlPost({
|
|
1347
|
+
jql,
|
|
1348
|
+
maxResults: 50,
|
|
1349
|
+
// `*all` does not reliably include `issuelinks` on Cloud — the
|
|
1350
|
+
// field is named explicitly so the pull can read native links back
|
|
1351
|
+
// (LP-325).
|
|
1352
|
+
fields: searchFields,
|
|
1353
|
+
...(nextPageToken ? { nextPageToken } : {}),
|
|
1354
|
+
});
|
|
1355
|
+
records.push(...(page.issues ?? []));
|
|
1356
|
+
pages += 1;
|
|
1357
|
+
opts?.onPage?.({ page: pages, records: records.length });
|
|
1358
|
+
nextPageToken = page.nextPageToken ?? undefined;
|
|
1359
|
+
} while (nextPageToken);
|
|
1360
|
+
let cursor = null;
|
|
1361
|
+
for (const record of records) {
|
|
1362
|
+
const updated = updatedOf(record);
|
|
1363
|
+
if (updated !== null && (cursor === null || updated > cursor))
|
|
1364
|
+
cursor = updated;
|
|
1365
|
+
}
|
|
1366
|
+
const enriched = [];
|
|
1367
|
+
for (const record of records) {
|
|
1368
|
+
enriched.push((await enrichSprint(record)) ?? record);
|
|
1369
|
+
}
|
|
1370
|
+
return { records: enriched, cursor };
|
|
1371
|
+
}
|
|
1372
|
+
catch (error) {
|
|
1373
|
+
throw describeFailure(error, conn, 'listing issues');
|
|
1374
|
+
}
|
|
1375
|
+
},
|
|
1376
|
+
async link(dependentRemoteId, dependencyRemoteId, signal, kind) {
|
|
1377
|
+
const c = await client();
|
|
1378
|
+
try {
|
|
1379
|
+
if (kind === 'relates') {
|
|
1380
|
+
// `relates_to` → the native `Relates` link. Symmetric, so either
|
|
1381
|
+
// orientation holds the same meaning; the local issue is the outward
|
|
1382
|
+
// ("relates to") side for consistency with how it is read back.
|
|
1383
|
+
await c.issueLinks.linkIssues({
|
|
1384
|
+
outwardIssue: { id: dependentRemoteId },
|
|
1385
|
+
inwardIssue: { id: dependencyRemoteId },
|
|
1386
|
+
type: { name: 'Relates' },
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
// `depends_on` → the dependent waits on the dependency, so the
|
|
1391
|
+
// dependency **blocks** the dependent.
|
|
1392
|
+
//
|
|
1393
|
+
// The field names invite the opposite arrangement, and it is wrong.
|
|
1394
|
+
// Measured against Jira Cloud: posting `{outwardIssue: A, inwardIssue: B}`
|
|
1395
|
+
// with type `Blocks` produces "**B** blocks A" — the *inward* issue
|
|
1396
|
+
// is the one that does the blocking. So the dependency goes in
|
|
1397
|
+
// `inwardIssue` and the dependent in `outwardIssue`.
|
|
1398
|
+
//
|
|
1399
|
+
// Reversed, this is silent: both sides link, the push reports
|
|
1400
|
+
// success, and every dependency on the board reaches Jira pointing
|
|
1401
|
+
// backwards — a plan that reads as its own mirror image to anybody
|
|
1402
|
+
// working in Jira.
|
|
1403
|
+
await c.issueLinks.linkIssues({
|
|
1404
|
+
outwardIssue: { id: dependentRemoteId },
|
|
1405
|
+
inwardIssue: { id: dependencyRemoteId },
|
|
1406
|
+
type: { name: 'Blocks' },
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
void signal;
|
|
1410
|
+
return { remoteId: dependentRemoteId, remoteKey: dependentRemoteId, remoteUrl: '', remoteRev: '' };
|
|
1411
|
+
}
|
|
1412
|
+
catch (error) {
|
|
1413
|
+
throw describeFailure(error, conn, `linking ${dependentRemoteId} to ${dependencyRemoteId}`);
|
|
1414
|
+
}
|
|
1415
|
+
},
|
|
1416
|
+
async unlink(dependentRemoteId, dependencyRemoteId, signal, kind) {
|
|
1417
|
+
// Jira links are removed by link id, which needs a read first: the
|
|
1418
|
+
// connector reads the issue's `issuelinks`, finds the link between the
|
|
1419
|
+
// two ids, and deletes it by id. A link already gone is a no-op —
|
|
1420
|
+
// unlinking is idempotent (LP-325).
|
|
1421
|
+
const c = await client();
|
|
1422
|
+
try {
|
|
1423
|
+
const typeName = kind === 'relates' ? 'Relates' : 'Blocks';
|
|
1424
|
+
const record = await c.issues.getIssue({ issueIdOrKey: dependentRemoteId, fields: ['issuelinks'] });
|
|
1425
|
+
// An `issuelinks` entry names only the *other* end — never both — so
|
|
1426
|
+
// matching on a pair matched nothing, and every unlink quietly did
|
|
1427
|
+
// nothing while reporting success. Read from the dependent's record,
|
|
1428
|
+
// the dependency is the issue named on the entry; for `Blocks` it is
|
|
1429
|
+
// specifically the `inwardIssue` ("this issue is blocked by that
|
|
1430
|
+
// one"), which is also what tells a `depends_on` edge apart from its
|
|
1431
|
+
// own reverse.
|
|
1432
|
+
const match = issueLinksOf(record).find((link) => {
|
|
1433
|
+
if (linkTypeNameOf(link) !== typeName)
|
|
1434
|
+
return false;
|
|
1435
|
+
if (kind === 'relates') {
|
|
1436
|
+
const other = link['inwardIssue'] !== undefined ? link['inwardIssue'] : link['outwardIssue'];
|
|
1437
|
+
return endpointIdOf(other) === dependencyRemoteId;
|
|
1438
|
+
}
|
|
1439
|
+
return endpointIdOf(link['inwardIssue']) === dependencyRemoteId;
|
|
1440
|
+
});
|
|
1441
|
+
if (match === undefined) {
|
|
1442
|
+
// Already gone — nothing to delete.
|
|
1443
|
+
return { remoteId: dependentRemoteId, remoteKey: dependentRemoteId, remoteUrl: '', remoteRev: '' };
|
|
1444
|
+
}
|
|
1445
|
+
const raw = match['id'];
|
|
1446
|
+
const linkId = typeof raw === 'string' ? raw : typeof raw === 'number' ? String(raw) : '';
|
|
1447
|
+
if (linkId === '') {
|
|
1448
|
+
throw new BoardError(`Cannot unlink ${dependentRemoteId} from ${dependencyRemoteId}`, [
|
|
1449
|
+
'the issue link carried no id to delete by',
|
|
1450
|
+
]);
|
|
1451
|
+
}
|
|
1452
|
+
await c.issueLinks.deleteIssueLink({ linkId });
|
|
1453
|
+
void signal;
|
|
1454
|
+
return { remoteId: dependentRemoteId, remoteKey: dependentRemoteId, remoteUrl: '', remoteRev: '' };
|
|
1455
|
+
}
|
|
1456
|
+
catch (error) {
|
|
1457
|
+
throw describeFailure(error, conn, `unlinking ${dependentRemoteId} from ${dependencyRemoteId}`);
|
|
1458
|
+
}
|
|
1459
|
+
},
|
|
1460
|
+
async comment(remoteId, body, signal) {
|
|
1461
|
+
const c = await client();
|
|
1462
|
+
try {
|
|
1463
|
+
const comment = await c.issueComments.addComment({ issueIdOrKey: remoteId, body });
|
|
1464
|
+
void signal;
|
|
1465
|
+
return commentResult(comment, remoteId, remoteId);
|
|
1466
|
+
}
|
|
1467
|
+
catch (error) {
|
|
1468
|
+
throw describeFailure(error, conn, `commenting on ${remoteId}`);
|
|
1469
|
+
}
|
|
1470
|
+
},
|
|
1471
|
+
async editComment(remoteId, commentId, body, signal) {
|
|
1472
|
+
const c = await client();
|
|
1473
|
+
try {
|
|
1474
|
+
const comment = await c.issueComments.updateComment({ issueIdOrKey: remoteId, id: commentId, body });
|
|
1475
|
+
void signal;
|
|
1476
|
+
return commentResult(comment, remoteId, remoteId);
|
|
1477
|
+
}
|
|
1478
|
+
catch (error) {
|
|
1479
|
+
throw describeFailure(error, conn, `editing a comment on ${remoteId}`);
|
|
1480
|
+
}
|
|
1481
|
+
},
|
|
1482
|
+
async deleteComment(remoteId, commentId, signal) {
|
|
1483
|
+
const c = await client();
|
|
1484
|
+
try {
|
|
1485
|
+
await c.issueComments.deleteComment({ issueIdOrKey: remoteId, id: commentId });
|
|
1486
|
+
void signal;
|
|
1487
|
+
return commentResult({ id: commentId }, remoteId, remoteId);
|
|
1488
|
+
}
|
|
1489
|
+
catch (error) {
|
|
1490
|
+
throw describeFailure(error, conn, `deleting a comment on ${remoteId}`);
|
|
1491
|
+
}
|
|
1492
|
+
},
|
|
1493
|
+
/**
|
|
1494
|
+
* Answer a capability probe by name (LP-274). `hierarchy` reports the
|
|
1495
|
+
* project's native parent levels, detected from its issue types rather than
|
|
1496
|
+
* assumed (LP-324). `custom_fields` reports the value shapes Jira custom
|
|
1497
|
+
* fields can hold, in the provider's own schema vocabulary (LP-327) — the
|
|
1498
|
+
* name→id resolution itself is `fields()` / `preflightFields`'s job.
|
|
1499
|
+
*/
|
|
1500
|
+
async probe(probeName) {
|
|
1501
|
+
if (probeName === 'hierarchy') {
|
|
1502
|
+
return hierarchyDepthOf(await typeScheme());
|
|
1503
|
+
}
|
|
1504
|
+
if (probeName === 'custom_fields') {
|
|
1505
|
+
return { valueTypes: ['string', 'number', 'date', 'datetime', 'option', 'array'] };
|
|
1506
|
+
}
|
|
1507
|
+
throw new BoardError(`The Jira connector does not answer probe "${probeName}"`, [
|
|
1508
|
+
'Only the "hierarchy" and "custom_fields" probes are implemented.',
|
|
1509
|
+
]);
|
|
1510
|
+
},
|
|
1511
|
+
/** The project's issue types, for the preflight to validate the mapping against. */
|
|
1512
|
+
async issueTypes() {
|
|
1513
|
+
return typeScheme();
|
|
1514
|
+
},
|
|
1515
|
+
/**
|
|
1516
|
+
* The project's own words: its issue types and every status any of its issue
|
|
1517
|
+
* types can reach (LP-537). `getAllStatuses` returns statuses *per issue
|
|
1518
|
+
* type*, so the set is the union — a status that only the Bug workflow has
|
|
1519
|
+
* is still a status this project has, and a mapping naming it is correct.
|
|
1520
|
+
*
|
|
1521
|
+
* Reading statuses needs only Browse Projects, unlike the workflow graph the
|
|
1522
|
+
* multi-hop transition path reads, so `lpm remote setup` works for the
|
|
1523
|
+
* ordinary syncing account.
|
|
1524
|
+
*/
|
|
1525
|
+
async vocabulary() {
|
|
1526
|
+
const types = (await typeScheme()).map((type) => type.name);
|
|
1527
|
+
return { types, statuses: await projectStatuses() };
|
|
1528
|
+
},
|
|
1529
|
+
/** The instance's custom fields, for discovery and preflight (LP-327). */
|
|
1530
|
+
fields,
|
|
1531
|
+
/** The create-screen field ids keyed by issue type, for the screen check (LP-327). */
|
|
1532
|
+
createMeta,
|
|
1533
|
+
/** Create a custom field, for `provision` (LP-327). */
|
|
1534
|
+
createField,
|
|
1535
|
+
/** Add a field to the default screen, for `provision` (LP-327). */
|
|
1536
|
+
addFieldToScreen,
|
|
1537
|
+
/**
|
|
1538
|
+
* The people this project can assign work to.
|
|
1539
|
+
*
|
|
1540
|
+
* `findAssignableUsers` rather than a plain user search: an account that
|
|
1541
|
+
* exists on the instance but is not assignable *here* is a failed write
|
|
1542
|
+
* either way, so listing it would be an answer that reads as reassuring
|
|
1543
|
+
* and is not. Paged, because a large site returns 50 at a time, and
|
|
1544
|
+
* `active` users only — a deactivated account is not somewhere work can
|
|
1545
|
+
* go. An instance that refuses the call (it needs Browse Users) reports
|
|
1546
|
+
* the refusal rather than an empty list, which would read as "nobody".
|
|
1547
|
+
*/
|
|
1548
|
+
async listUsers(signal) {
|
|
1549
|
+
void signal;
|
|
1550
|
+
const c = await client();
|
|
1551
|
+
const out = [];
|
|
1552
|
+
const seen = new Set();
|
|
1553
|
+
let startAt = 0;
|
|
1554
|
+
try {
|
|
1555
|
+
for (;;) {
|
|
1556
|
+
const page = await c.userSearch.findAssignableUsers({
|
|
1557
|
+
project: conn.project,
|
|
1558
|
+
maxResults: 50,
|
|
1559
|
+
startAt,
|
|
1560
|
+
});
|
|
1561
|
+
const users = Array.isArray(page) ? page : [];
|
|
1562
|
+
for (const user of users) {
|
|
1563
|
+
const id = user['accountId'];
|
|
1564
|
+
if (typeof id !== 'string' || id === '' || seen.has(id))
|
|
1565
|
+
continue;
|
|
1566
|
+
if (user['active'] === false)
|
|
1567
|
+
continue;
|
|
1568
|
+
seen.add(id);
|
|
1569
|
+
const email = user['emailAddress'];
|
|
1570
|
+
out.push({
|
|
1571
|
+
id,
|
|
1572
|
+
name: typeof user['displayName'] === 'string' ? user['displayName'] : id,
|
|
1573
|
+
...(typeof email === 'string' && email !== '' ? { email } : {}),
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
if (users.length < 50)
|
|
1577
|
+
break;
|
|
1578
|
+
startAt += users.length;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
catch (error) {
|
|
1582
|
+
throw describeFailure(error, conn, `listing the users assignable in ${conn.project}`);
|
|
1583
|
+
}
|
|
1584
|
+
return out;
|
|
1585
|
+
},
|
|
1586
|
+
/** Every sprint on the configured Agile board, for `provision` (LP-328). */
|
|
1587
|
+
async listSprints(signal) {
|
|
1588
|
+
void signal;
|
|
1589
|
+
return sprints();
|
|
1590
|
+
},
|
|
1591
|
+
/** Create a future sprint on the configured Agile board, for `provision` (LP-328). */
|
|
1592
|
+
async createSprint(name, starts, ends, signal) {
|
|
1593
|
+
if (conn.board === undefined) {
|
|
1594
|
+
throw new BoardError(`Cannot create sprint "${name}": no Agile board id`, [
|
|
1595
|
+
'Sprints belong to an Agile board, not a project — set connection.board to the board id first.',
|
|
1596
|
+
]);
|
|
1597
|
+
}
|
|
1598
|
+
const a = await agile();
|
|
1599
|
+
try {
|
|
1600
|
+
const created = await a.sprint.createSprint({
|
|
1601
|
+
name,
|
|
1602
|
+
originBoardId: Number(conn.board),
|
|
1603
|
+
...(starts !== undefined ? { startDate: sprintStart(starts) } : {}),
|
|
1604
|
+
...(ends !== undefined ? { endDate: sprintEnd(ends) } : {}),
|
|
1605
|
+
});
|
|
1606
|
+
void signal;
|
|
1607
|
+
const id = created['id'];
|
|
1608
|
+
if (typeof id !== 'string' && typeof id !== 'number') {
|
|
1609
|
+
throw new BoardError(`Jira created the sprint "${name}" but returned no id`, [
|
|
1610
|
+
'The sprint exists but its id could not be read; re-run provision to pick it up.',
|
|
1611
|
+
]);
|
|
1612
|
+
}
|
|
1613
|
+
// **The sprint list is cached, so the cache has just gone stale.**
|
|
1614
|
+
// A push creates its prerequisites *before* it writes any issue,
|
|
1615
|
+
// precisely so the issues can be filed into them — but the lookup that
|
|
1616
|
+
// resolves a period to a sprint reads this cache, which was filled
|
|
1617
|
+
// while the sprint did not exist yet. Without this line the sprint is
|
|
1618
|
+
// created and then not found, every issue in it is filed unscheduled
|
|
1619
|
+
// with `unwritten: 'period'`, and the schedule only lands on the
|
|
1620
|
+
// *next* push. Self-healing hid it: the board was always one push
|
|
1621
|
+
// behind on its own timeline.
|
|
1622
|
+
sprintsPromise = undefined;
|
|
1623
|
+
return { id: String(id) };
|
|
1624
|
+
}
|
|
1625
|
+
catch (error) {
|
|
1626
|
+
throw describeFailure(error, conn, `creating the sprint "${name}"`);
|
|
1627
|
+
}
|
|
1628
|
+
},
|
|
1629
|
+
discoverConnection,
|
|
1630
|
+
/**
|
|
1631
|
+
* Move an existing twin under a new parent, or out from under one.
|
|
1632
|
+
*
|
|
1633
|
+
* Without this the executor could file a parent on *create* and never
|
|
1634
|
+
* change it afterwards: a `reparent` op was reported "skipped — connector
|
|
1635
|
+
* cannot move an existing issue to a native parent", so a board whose
|
|
1636
|
+
* carrier changed (or whose document was reparented locally) kept the
|
|
1637
|
+
* shape it was first filed with, for ever.
|
|
1638
|
+
*
|
|
1639
|
+
* `null` clears the parent, which is how a document that moved to a level
|
|
1640
|
+
* the block carries gets its native edge taken away rather than left stale.
|
|
1641
|
+
*/
|
|
1642
|
+
async reparent(remoteId, parentRemoteId, signal) {
|
|
1643
|
+
const c = await client();
|
|
1644
|
+
try {
|
|
1645
|
+
await c.issues.editIssue({
|
|
1646
|
+
issueIdOrKey: remoteId,
|
|
1647
|
+
fields: { parent: parentRemoteId === null ? null : parentRef(parentRemoteId) },
|
|
1648
|
+
});
|
|
1649
|
+
const record = await c.issues.getIssue({ issueIdOrKey: remoteId });
|
|
1650
|
+
void signal;
|
|
1651
|
+
return resultOf(record, conn.site);
|
|
1652
|
+
}
|
|
1653
|
+
catch (error) {
|
|
1654
|
+
throw describeFailure(error, conn, `re-parenting issue ${remoteId}`);
|
|
1655
|
+
}
|
|
1656
|
+
},
|
|
1657
|
+
async reachable() {
|
|
1658
|
+
const c = await client();
|
|
1659
|
+
try {
|
|
1660
|
+
const project = await c.projects.getProject({ projectIdOrKey: conn.project });
|
|
1661
|
+
return {
|
|
1662
|
+
reachable: true,
|
|
1663
|
+
evidence: `GET /rest/api/3/project/${conn.project} → ${typeof project['key'] === 'string' ? `project ${project['key']} reachable` : '200'}`,
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
catch (error) {
|
|
1667
|
+
const err = error;
|
|
1668
|
+
if (err.status === 404) {
|
|
1669
|
+
return {
|
|
1670
|
+
reachable: false,
|
|
1671
|
+
evidence: `GET /rest/api/3/project/${conn.project} → 404`,
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
if (err.status === undefined && err.code !== undefined) {
|
|
1675
|
+
return {
|
|
1676
|
+
reachable: false,
|
|
1677
|
+
evidence: `could not reach ${conn.site}: ${err.code ?? 'network error'}`,
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
return {
|
|
1681
|
+
reachable: false,
|
|
1682
|
+
evidence: `GET /rest/api/3/project/${conn.project} → ${err.status ?? 'error'}`,
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
},
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
//# sourceMappingURL=connector.js.map
|