@plainconceptsplatform/agent-harness 3.4.1 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +455 -454
  3. package/cli/commands/join.js +259 -259
  4. package/cli/commands/migrate.js +310 -310
  5. package/cli/commands/shared.js +27 -27
  6. package/cli/commands/single.js +79 -79
  7. package/cli/commands/wizard.js +134 -134
  8. package/cli/fragments/ops-backlog/az.md +28 -28
  9. package/cli/fragments/ops-backlog/gh.md +28 -28
  10. package/cli/fragments/ops-backlog/jira.md +28 -28
  11. package/cli/fragments/ops-review/az.md +62 -62
  12. package/cli/fragments/ops-review/gh.md +51 -51
  13. package/cli/fragments/ops-review/gl.md +55 -55
  14. package/cli/fragments/ops-ship/gh.md +67 -67
  15. package/cli/fragments/ops-ship/gl.md +84 -84
  16. package/cli/index.js +116 -116
  17. package/cli/presets/clean.json +21 -21
  18. package/cli/presets/models.json +68 -68
  19. package/cli/presets/optimization.json +32 -32
  20. package/cli/presets/platforms.json +76 -76
  21. package/cli/presets/source.json +23 -23
  22. package/cli/steps/clean/index.js +120 -120
  23. package/cli/steps/copy/agents.js +120 -120
  24. package/cli/steps/copy/fullstack-engineer.js +89 -89
  25. package/cli/steps/copy/opencode-json.js +324 -324
  26. package/cli/steps/copy/skills.js +316 -316
  27. package/cli/steps/models/format.js +88 -88
  28. package/cli/steps/models/index.js +64 -64
  29. package/cli/steps/openspec/index.js +136 -136
  30. package/cli/steps/optimization/codegraph.js +141 -141
  31. package/cli/steps/optimization/detect.js +66 -66
  32. package/cli/steps/optimization/humanizer.js +17 -17
  33. package/cli/steps/optimization/index.js +158 -158
  34. package/cli/steps/optimization/memory.js +91 -91
  35. package/cli/steps/optimization/patch-guardrails.js +111 -111
  36. package/cli/steps/optimization/simple-english.js +17 -17
  37. package/cli/steps/optimization/skills-lock.js +30 -30
  38. package/cli/steps/platform/index.js +109 -109
  39. package/cli/steps/source/index.js +123 -123
  40. package/cli/utils/agent-color.js +111 -111
  41. package/cli/utils/copy.js +134 -134
  42. package/cli/utils/exec-spinner.js +47 -47
  43. package/cli/utils/exec.js +134 -134
  44. package/cli/utils/legacy-check.js +30 -30
  45. package/cli/utils/models-cache.js +58 -58
  46. package/cli/utils/models-pricing.js +42 -42
  47. package/cli/utils/paths.js +81 -81
  48. package/cli/utils/process.js +3 -3
  49. package/cli/utils/terminal.js +6 -6
  50. package/cli/utils/update-manifest.js +75 -75
  51. package/harness/.opencode/_gitignore +10 -10
  52. package/harness/.opencode/commands/ops-backlog.md +10 -10
  53. package/harness/.opencode/commands/ops-review.md +10 -10
  54. package/harness/.opencode/commands/ops-ship.md +9 -9
  55. package/harness/.opencode/commands/plan-archive.md +7 -7
  56. package/harness/.opencode/commands/plan-explore.md +11 -11
  57. package/harness/.opencode/commands/plan-propose.md +11 -11
  58. package/harness/.opencode/commands/plan-quick.md +7 -7
  59. package/harness/.opencode/commands/plan-story.md +11 -11
  60. package/harness/.opencode/commands/repo-audit.md +7 -7
  61. package/harness/.opencode/commands/repo-help.md +5 -5
  62. package/harness/.opencode/package.json +5 -5
  63. package/harness/.opencode/plugins/pc-commit-coauthor.js +41 -0
  64. package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
  65. package/harness/.opencode/plugins/pc-subagent-tiers.js +367 -367
  66. package/harness/.opencode/plugins/pc-system-reminders.js +386 -386
  67. package/harness/.opencode/scripts/check-diff.mjs +252 -244
  68. package/harness/.opencode/scripts/code-rules.mjs +288 -288
  69. package/harness/ARCHITECTURE.md +13 -13
  70. package/harness/DESIGN.md +13 -13
  71. package/harness/opencode.jsonc +36 -36
  72. package/harness/openspec/config.yaml +20 -20
  73. package/harness/skills-lock.json +17 -17
  74. package/package.json +14 -12
  75. package/skills/agent-harness-cli/SKILL.md +86 -86
  76. package/skills/pc-guardrails-generic/SKILL.md +53 -53
  77. package/skills/pc-guardrails-project/SKILL.md +8 -8
  78. package/skills/pc-make-architecture/structure-template.md +64 -64
  79. package/skills/pc-make-engineer/SKILL.md +59 -59
  80. package/skills/pc-make-engineer/template.md +36 -36
  81. package/skills/pc-make-guardrails/category-reference.md +112 -112
  82. package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
  83. package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
  84. package/skills/pc-ops-ship/SKILL.md +22 -22
  85. package/skills/pc-plan-apply/SKILL.md +88 -86
  86. package/skills/pc-plan-apply/simple-mode.md +21 -21
  87. package/skills/pc-plan-explore/SKILL.md +21 -21
  88. package/skills/pc-plan-goal/SKILL.md +91 -91
  89. package/skills/pc-plan-goal/failure-policy.md +30 -30
  90. package/skills/pc-plan-goal/output-mode.md +10 -10
  91. package/skills/pc-plan-propose/SKILL.md +125 -125
  92. package/skills/pc-plan-propose/task-annotation.md +39 -39
  93. package/skills/pc-plan-quick/SKILL.md +46 -46
  94. package/skills/pc-repo-audit/SKILL.md +44 -44
  95. package/skills/pc-repo-help/SKILL.md +87 -87
  96. package/skills/pc-repo-verify/SKILL.md +150 -150
@@ -1,111 +1,111 @@
1
- import fse from 'fs-extra'
2
- import path from 'path'
3
- import { fileURLToPath } from 'url'
4
- import { info, success } from '../../utils/exec.js'
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
- const GUARDRAILS_FRAGMENT_DIR = path.resolve(__dirname, '../../fragments/guardrails')
8
-
9
- const MARKER_SECTIONS = {
10
- rtk: 'RTK',
11
- codegraph: 'CODEGRAPH',
12
- memory: 'MEMORY',
13
- simpleEnglish: 'SIMPLE-ENGLISH',
14
- humanizer: 'HUMANIZER',
15
- }
16
-
17
- const OPTIONAL_TOOL_GUIDANCE = {
18
- codegraph: 'Use `codegraph_explore` to refine relevant symbols and file-disjointness for the current wave.',
19
- memory: 'Agentmemory carries context across sessions and agents: before exploring an unfamiliar area, call `memory_smart_search` with the key concepts of the task (components, error text) and reuse what it returns; when the work completes, call `memory_save` for durable non-obvious learnings (architecture constraints, root causes and fixes, decisions and why). Never store secrets.',
20
- }
21
-
22
- function replaceMarkerSection(content, startMarker, endMarker, replacement) {
23
- if (!content.includes(startMarker) || !content.includes(endMarker)) return content
24
- const escape = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
25
- const pattern = new RegExp(`${escape(startMarker)}[\\s\\S]*?${escape(endMarker)}`)
26
- // Replacer function so $&, $' and $` in injected content stay literal.
27
- return content.replace(pattern, () => `${startMarker}\n${replacement}\n${endMarker}`)
28
- }
29
-
30
- async function patchOptionalToolGuidance(destSkillsDir, selections) {
31
- const files = []
32
-
33
- async function collect(dir) {
34
- for (const entry of await fse.readdir(dir, { withFileTypes: true })) {
35
- const entryPath = path.join(dir, entry.name)
36
- if (entry.isDirectory()) await collect(entryPath)
37
- else if (entry.name.endsWith('.md')) files.push(entryPath)
38
- }
39
- }
40
-
41
- if (!await fse.pathExists(destSkillsDir)) return 0
42
- await collect(destSkillsDir)
43
-
44
- let patched = 0
45
- for (const filePath of files) {
46
- let content = await fse.readFile(filePath, 'utf-8')
47
- const original = content
48
- for (const [key, guidance] of Object.entries(OPTIONAL_TOOL_GUIDANCE)) {
49
- const suffix = key.toUpperCase()
50
- content = replaceMarkerSection(
51
- content,
52
- `<!-- PC-OPTIMIZATION-${suffix}-START -->`,
53
- `<!-- PC-OPTIMIZATION-${suffix}-END -->`,
54
- selections[key] ? guidance : '',
55
- )
56
- }
57
- if (content !== original) {
58
- await fse.writeFile(filePath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
59
- patched++
60
- }
61
- }
62
- return patched
63
- }
64
-
65
- export async function patchGuardrails(selections = {}, { cwd = process.cwd() } = {}) {
66
- const destSkillsDir = path.join(cwd, '.agents', 'skills')
67
- const guardrailsPath = path.join(destSkillsDir, 'pc-guardrails-generic', 'SKILL.md')
68
- let patchedCount = 0
69
-
70
- if (await fse.pathExists(guardrailsPath)) {
71
- let content = await fse.readFile(guardrailsPath, 'utf-8')
72
- content = replaceMarkerSection(
73
- content,
74
- '<!-- PC-GUARDRAILS-CAVEMAN-START -->',
75
- '<!-- PC-GUARDRAILS-CAVEMAN-END -->',
76
- '',
77
- ).replace(
78
- '<!-- PC-GUARDRAILS-CAVEMAN-START -->\n\n<!-- PC-GUARDRAILS-CAVEMAN-END -->',
79
- '<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-START -->\n<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-END -->',
80
- )
81
- // A third replace used to rewrite the engineer workflow's step 1 into "the
82
- // plugin has already loaded the skills". That was never true: the plugin
83
- // appends a reminder and never calls the skill tool. The shipped skill now
84
- // carries the honest wording, and update refreshes this file, so the
85
- // migration is gone rather than perpetuated.
86
- for (const [key, markerSuffix] of Object.entries(MARKER_SECTIONS)) {
87
- let sectionContent = ''
88
- if (selections[key]) {
89
- const presetName = key === 'simpleEnglish' ? 'simple-english' : key
90
- const presetPath = path.join(GUARDRAILS_FRAGMENT_DIR, `${presetName}.md`)
91
- if (await fse.pathExists(presetPath)) {
92
- sectionContent = (await fse.readFile(presetPath, 'utf-8')).trim()
93
- }
94
- }
95
- content = replaceMarkerSection(
96
- content,
97
- `<!-- PC-GUARDRAILS-${markerSuffix}-START -->`,
98
- `<!-- PC-GUARDRAILS-${markerSuffix}-END -->`,
99
- sectionContent,
100
- )
101
- if (selections[key]) patchedCount++
102
- }
103
- await fse.writeFile(guardrailsPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
104
- } else {
105
- info('pc-guardrails-generic not found, skipping guardrails patching')
106
- }
107
-
108
- const optionalPatched = await patchOptionalToolGuidance(destSkillsDir, selections)
109
- success(`Guardrails patched: ${patchedCount} sections injected; ${optionalPatched} optional tool file(s) updated`)
110
- return { patched: true, count: patchedCount, optionalPatched }
111
- }
1
+ import fse from 'fs-extra'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { info, success } from '../../utils/exec.js'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const GUARDRAILS_FRAGMENT_DIR = path.resolve(__dirname, '../../fragments/guardrails')
8
+
9
+ const MARKER_SECTIONS = {
10
+ rtk: 'RTK',
11
+ codegraph: 'CODEGRAPH',
12
+ memory: 'MEMORY',
13
+ simpleEnglish: 'SIMPLE-ENGLISH',
14
+ humanizer: 'HUMANIZER',
15
+ }
16
+
17
+ const OPTIONAL_TOOL_GUIDANCE = {
18
+ codegraph: 'Use `codegraph_explore` to refine relevant symbols and file-disjointness for the current wave.',
19
+ memory: 'Agentmemory carries context across sessions and agents: before exploring an unfamiliar area, call `memory_smart_search` with the key concepts of the task (components, error text) and reuse what it returns; when the work completes, call `memory_save` for durable non-obvious learnings (architecture constraints, root causes and fixes, decisions and why). Never store secrets.',
20
+ }
21
+
22
+ function replaceMarkerSection(content, startMarker, endMarker, replacement) {
23
+ if (!content.includes(startMarker) || !content.includes(endMarker)) return content
24
+ const escape = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
25
+ const pattern = new RegExp(`${escape(startMarker)}[\\s\\S]*?${escape(endMarker)}`)
26
+ // Replacer function so $&, $' and $` in injected content stay literal.
27
+ return content.replace(pattern, () => `${startMarker}\n${replacement}\n${endMarker}`)
28
+ }
29
+
30
+ async function patchOptionalToolGuidance(destSkillsDir, selections) {
31
+ const files = []
32
+
33
+ async function collect(dir) {
34
+ for (const entry of await fse.readdir(dir, { withFileTypes: true })) {
35
+ const entryPath = path.join(dir, entry.name)
36
+ if (entry.isDirectory()) await collect(entryPath)
37
+ else if (entry.name.endsWith('.md')) files.push(entryPath)
38
+ }
39
+ }
40
+
41
+ if (!await fse.pathExists(destSkillsDir)) return 0
42
+ await collect(destSkillsDir)
43
+
44
+ let patched = 0
45
+ for (const filePath of files) {
46
+ let content = await fse.readFile(filePath, 'utf-8')
47
+ const original = content
48
+ for (const [key, guidance] of Object.entries(OPTIONAL_TOOL_GUIDANCE)) {
49
+ const suffix = key.toUpperCase()
50
+ content = replaceMarkerSection(
51
+ content,
52
+ `<!-- PC-OPTIMIZATION-${suffix}-START -->`,
53
+ `<!-- PC-OPTIMIZATION-${suffix}-END -->`,
54
+ selections[key] ? guidance : '',
55
+ )
56
+ }
57
+ if (content !== original) {
58
+ await fse.writeFile(filePath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
59
+ patched++
60
+ }
61
+ }
62
+ return patched
63
+ }
64
+
65
+ export async function patchGuardrails(selections = {}, { cwd = process.cwd() } = {}) {
66
+ const destSkillsDir = path.join(cwd, '.agents', 'skills')
67
+ const guardrailsPath = path.join(destSkillsDir, 'pc-guardrails-generic', 'SKILL.md')
68
+ let patchedCount = 0
69
+
70
+ if (await fse.pathExists(guardrailsPath)) {
71
+ let content = await fse.readFile(guardrailsPath, 'utf-8')
72
+ content = replaceMarkerSection(
73
+ content,
74
+ '<!-- PC-GUARDRAILS-CAVEMAN-START -->',
75
+ '<!-- PC-GUARDRAILS-CAVEMAN-END -->',
76
+ '',
77
+ ).replace(
78
+ '<!-- PC-GUARDRAILS-CAVEMAN-START -->\n\n<!-- PC-GUARDRAILS-CAVEMAN-END -->',
79
+ '<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-START -->\n<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-END -->',
80
+ )
81
+ // A third replace used to rewrite the engineer workflow's step 1 into "the
82
+ // plugin has already loaded the skills". That was never true: the plugin
83
+ // appends a reminder and never calls the skill tool. The shipped skill now
84
+ // carries the honest wording, and update refreshes this file, so the
85
+ // migration is gone rather than perpetuated.
86
+ for (const [key, markerSuffix] of Object.entries(MARKER_SECTIONS)) {
87
+ let sectionContent = ''
88
+ if (selections[key]) {
89
+ const presetName = key === 'simpleEnglish' ? 'simple-english' : key
90
+ const presetPath = path.join(GUARDRAILS_FRAGMENT_DIR, `${presetName}.md`)
91
+ if (await fse.pathExists(presetPath)) {
92
+ sectionContent = (await fse.readFile(presetPath, 'utf-8')).trim()
93
+ }
94
+ }
95
+ content = replaceMarkerSection(
96
+ content,
97
+ `<!-- PC-GUARDRAILS-${markerSuffix}-START -->`,
98
+ `<!-- PC-GUARDRAILS-${markerSuffix}-END -->`,
99
+ sectionContent,
100
+ )
101
+ if (selections[key]) patchedCount++
102
+ }
103
+ await fse.writeFile(guardrailsPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
104
+ } else {
105
+ info('pc-guardrails-generic not found, skipping guardrails patching')
106
+ }
107
+
108
+ const optionalPatched = await patchOptionalToolGuidance(destSkillsDir, selections)
109
+ success(`Guardrails patched: ${patchedCount} sections injected; ${optionalPatched} optional tool file(s) updated`)
110
+ return { patched: true, count: patchedCount, optionalPatched }
111
+ }
@@ -1,17 +1,17 @@
1
- import { addSkillToLock } from './skills-lock.js'
2
- import { header, success, info } from '../../utils/exec.js'
3
-
4
- const SKILL_ENTRY = {
5
- source: 'AminBlg/SimpleEnglish',
6
- sourceType: 'github',
7
- skillPath: 'skills/simple-english/SKILL.md',
8
- }
9
-
10
- export async function installSimpleEnglish(options = {}) {
11
- if (!options.skipHeader) header('Installing Simple English')
12
-
13
- info('Adding Simple English to skills-lock.json for batch install')
14
- await addSkillToLock('simple-english', SKILL_ENTRY)
15
- success('Simple English queued for installation')
16
- return { optedIn: true, installed: true }
17
- }
1
+ import { addSkillToLock } from './skills-lock.js'
2
+ import { header, success, info } from '../../utils/exec.js'
3
+
4
+ const SKILL_ENTRY = {
5
+ source: 'AminBlg/SimpleEnglish',
6
+ sourceType: 'github',
7
+ skillPath: 'skills/simple-english/SKILL.md',
8
+ }
9
+
10
+ export async function installSimpleEnglish(options = {}) {
11
+ if (!options.skipHeader) header('Installing Simple English')
12
+
13
+ info('Adding Simple English to skills-lock.json for batch install')
14
+ await addSkillToLock('simple-english', SKILL_ENTRY)
15
+ success('Simple English queued for installation')
16
+ return { optedIn: true, installed: true }
17
+ }
@@ -1,30 +1,30 @@
1
- import fse from 'fs-extra'
2
- import path from 'path'
3
- import { info, warn } from '../../utils/exec.js'
4
-
5
- export async function addSkillToLock(skillName, entry) {
6
- if (!entry) {
7
- warn(`No entry provided for skills-lock.json: ${skillName}`)
8
- return false
9
- }
10
-
11
- const destLock = path.join(process.cwd(), 'skills-lock.json')
12
-
13
- let lock = { version: 1, skills: {} }
14
- if (await fse.pathExists(destLock)) {
15
- try {
16
- lock = await fse.readJson(destLock)
17
- if (!lock.skills) lock.skills = {}
18
- } catch {
19
- warn('Could not parse existing skills-lock.json, creating fresh')
20
- }
21
- }
22
-
23
- if (!lock.skills[skillName]) {
24
- lock.skills[skillName] = entry
25
- await fse.writeJson(destLock, lock, { spaces: 2 })
26
- info(`Added ${skillName} to skills-lock.json`)
27
- }
28
-
29
- return true
30
- }
1
+ import fse from 'fs-extra'
2
+ import path from 'path'
3
+ import { info, warn } from '../../utils/exec.js'
4
+
5
+ export async function addSkillToLock(skillName, entry) {
6
+ if (!entry) {
7
+ warn(`No entry provided for skills-lock.json: ${skillName}`)
8
+ return false
9
+ }
10
+
11
+ const destLock = path.join(process.cwd(), 'skills-lock.json')
12
+
13
+ let lock = { version: 1, skills: {} }
14
+ if (await fse.pathExists(destLock)) {
15
+ try {
16
+ lock = await fse.readJson(destLock)
17
+ if (!lock.skills) lock.skills = {}
18
+ } catch {
19
+ warn('Could not parse existing skills-lock.json, creating fresh')
20
+ }
21
+ }
22
+
23
+ if (!lock.skills[skillName]) {
24
+ lock.skills[skillName] = entry
25
+ await fse.writeJson(destLock, lock, { spaces: 2 })
26
+ info(`Added ${skillName} to skills-lock.json`)
27
+ }
28
+
29
+ return true
30
+ }
@@ -1,109 +1,109 @@
1
- import { select } from '@inquirer/prompts'
2
- import { execa } from 'execa'
3
- import fse from 'fs-extra'
4
- import path from 'path'
5
- import { fileURLToPath } from 'url'
6
- import { code, commandExists, header, info, success, warn } from '../../utils/exec.js'
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
- const PLATFORMS_PRESET_PATH = path.resolve(__dirname, '../../presets/platforms.json')
10
- const platformsPreset = await fse.readJson(PLATFORMS_PRESET_PATH)
11
-
12
-
13
- export async function checkPlatform(platform) {
14
- const preset = platformsPreset.find(p => p.value === platform) || platformsPreset[0]
15
- if (!preset.cli) {
16
- header(`Step 4, Checking ${preset.name} CLI`)
17
- info('No platform integration selected, skipping CLI checks.')
18
- success(`Platform: ${preset.name}`)
19
- return
20
- }
21
- await checkPlatformCli(preset)
22
- }
23
-
24
- export async function choosePlatform() {
25
- header('Step 3, Version control platform')
26
-
27
- const backlogChoices = platformsPreset.filter(p => !p.repoOnly)
28
- const backlogPlatform = await select({
29
- message: 'Where is your backlog (work items / issues)?',
30
- choices: backlogChoices.map(p => ({ name: p.name, value: p.value })),
31
- })
32
-
33
- const backlogPreset = platformsPreset.find(p => p.value === backlogPlatform)
34
- success(`Backlog platform: ${backlogPreset?.name || backlogPlatform}`)
35
-
36
- let repoPlatform = backlogPlatform
37
- if (backlogPlatform !== 'none') {
38
- // Exclude backlog-only platforms (e.g. Jira) from repo choices
39
- const repoChoices = platformsPreset.filter(p => !p.backlogOnly)
40
- repoPlatform = await select({
41
- message: 'Where is your code repository (PRs / code reviews)?',
42
- choices: repoChoices.map(p => ({ name: p.name, value: p.value })),
43
- default: backlogPreset.backlogOnly ? 'github' : backlogPlatform,
44
- })
45
- const repoPreset = platformsPreset.find(p => p.value === repoPlatform)
46
- success(`Repo platform: ${repoPreset?.name || repoPlatform}`)
47
- }
48
-
49
- // Check CLIs for both platforms (deduped)
50
- const platformsToCheck = [...new Set([backlogPlatform, repoPlatform])].filter(p => p !== 'none')
51
- for (const p of platformsToCheck) {
52
- const preset = platformsPreset.find(x => x.value === p)
53
- if (preset?.cli) await checkPlatformCli(preset)
54
- }
55
-
56
- return { backlogPlatform, repoPlatform }
57
- }
58
-
59
- async function checkPlatformCli(platformPreset) {
60
- const { cli } = platformPreset
61
- header(`Step 4, Checking ${platformPreset.name} CLI`)
62
-
63
- const hasCli = await commandExists(cli.command)
64
- if (!hasCli) {
65
- warn(`${cli.displayName} not found.`)
66
- info(`Install from: ${cli.installUrl}`)
67
- if (cli.authCheck?.commands?.length) {
68
- console.log()
69
- info('After installing, authenticate with:')
70
- code(cli.authCheck.commands)
71
- }
72
- return
73
- }
74
- success(`${cli.displayName} available`)
75
-
76
- if (cli.authCheck) await runAuthCheck(cli)
77
- if (cli.extensionCheck) await runExtensionCheck(cli)
78
- }
79
-
80
- async function runAuthCheck(cli) {
81
- try {
82
- const result = await execa(cli.command, cli.authCheck.args, { reject: false })
83
- if (result.exitCode === 0) {
84
- success(`${cli.displayName} authenticated`)
85
- } else {
86
- warn(cli.authCheck.notAuthenticatedMessage)
87
- code(cli.authCheck.commands)
88
- }
89
- } catch {
90
- warn(`Could not check ${cli.command} auth status.`)
91
- }
92
- }
93
-
94
- async function runExtensionCheck(cli) {
95
- try {
96
- const result = await execa(cli.command, cli.extensionCheck.args, { reject: false })
97
- const hasExtension = result.stdout && result.stdout.includes(cli.extensionCheck.expectedOutput)
98
-
99
- if (hasExtension) {
100
- success(`${cli.extensionCheck.expectedOutput} extension installed`)
101
- } else {
102
- warn(cli.extensionCheck.missingMessage)
103
- code(cli.extensionCheck.commands)
104
- }
105
- } catch {
106
- warn(cli.extensionCheck.errorMessage)
107
- code(cli.extensionCheck.commands)
108
- }
109
- }
1
+ import { select } from '@inquirer/prompts'
2
+ import { execa } from 'execa'
3
+ import fse from 'fs-extra'
4
+ import path from 'path'
5
+ import { fileURLToPath } from 'url'
6
+ import { code, commandExists, header, info, success, warn } from '../../utils/exec.js'
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+ const PLATFORMS_PRESET_PATH = path.resolve(__dirname, '../../presets/platforms.json')
10
+ const platformsPreset = await fse.readJson(PLATFORMS_PRESET_PATH)
11
+
12
+
13
+ export async function checkPlatform(platform) {
14
+ const preset = platformsPreset.find(p => p.value === platform) || platformsPreset[0]
15
+ if (!preset.cli) {
16
+ header(`Step 4, Checking ${preset.name} CLI`)
17
+ info('No platform integration selected, skipping CLI checks.')
18
+ success(`Platform: ${preset.name}`)
19
+ return
20
+ }
21
+ await checkPlatformCli(preset)
22
+ }
23
+
24
+ export async function choosePlatform() {
25
+ header('Step 3, Version control platform')
26
+
27
+ const backlogChoices = platformsPreset.filter(p => !p.repoOnly)
28
+ const backlogPlatform = await select({
29
+ message: 'Where is your backlog (work items / issues)?',
30
+ choices: backlogChoices.map(p => ({ name: p.name, value: p.value })),
31
+ })
32
+
33
+ const backlogPreset = platformsPreset.find(p => p.value === backlogPlatform)
34
+ success(`Backlog platform: ${backlogPreset?.name || backlogPlatform}`)
35
+
36
+ let repoPlatform = backlogPlatform
37
+ if (backlogPlatform !== 'none') {
38
+ // Exclude backlog-only platforms (e.g. Jira) from repo choices
39
+ const repoChoices = platformsPreset.filter(p => !p.backlogOnly)
40
+ repoPlatform = await select({
41
+ message: 'Where is your code repository (PRs / code reviews)?',
42
+ choices: repoChoices.map(p => ({ name: p.name, value: p.value })),
43
+ default: backlogPreset.backlogOnly ? 'github' : backlogPlatform,
44
+ })
45
+ const repoPreset = platformsPreset.find(p => p.value === repoPlatform)
46
+ success(`Repo platform: ${repoPreset?.name || repoPlatform}`)
47
+ }
48
+
49
+ // Check CLIs for both platforms (deduped)
50
+ const platformsToCheck = [...new Set([backlogPlatform, repoPlatform])].filter(p => p !== 'none')
51
+ for (const p of platformsToCheck) {
52
+ const preset = platformsPreset.find(x => x.value === p)
53
+ if (preset?.cli) await checkPlatformCli(preset)
54
+ }
55
+
56
+ return { backlogPlatform, repoPlatform }
57
+ }
58
+
59
+ async function checkPlatformCli(platformPreset) {
60
+ const { cli } = platformPreset
61
+ header(`Step 4, Checking ${platformPreset.name} CLI`)
62
+
63
+ const hasCli = await commandExists(cli.command)
64
+ if (!hasCli) {
65
+ warn(`${cli.displayName} not found.`)
66
+ info(`Install from: ${cli.installUrl}`)
67
+ if (cli.authCheck?.commands?.length) {
68
+ console.log()
69
+ info('After installing, authenticate with:')
70
+ code(cli.authCheck.commands)
71
+ }
72
+ return
73
+ }
74
+ success(`${cli.displayName} available`)
75
+
76
+ if (cli.authCheck) await runAuthCheck(cli)
77
+ if (cli.extensionCheck) await runExtensionCheck(cli)
78
+ }
79
+
80
+ async function runAuthCheck(cli) {
81
+ try {
82
+ const result = await execa(cli.command, cli.authCheck.args, { reject: false })
83
+ if (result.exitCode === 0) {
84
+ success(`${cli.displayName} authenticated`)
85
+ } else {
86
+ warn(cli.authCheck.notAuthenticatedMessage)
87
+ code(cli.authCheck.commands)
88
+ }
89
+ } catch {
90
+ warn(`Could not check ${cli.command} auth status.`)
91
+ }
92
+ }
93
+
94
+ async function runExtensionCheck(cli) {
95
+ try {
96
+ const result = await execa(cli.command, cli.extensionCheck.args, { reject: false })
97
+ const hasExtension = result.stdout && result.stdout.includes(cli.extensionCheck.expectedOutput)
98
+
99
+ if (hasExtension) {
100
+ success(`${cli.extensionCheck.expectedOutput} extension installed`)
101
+ } else {
102
+ warn(cli.extensionCheck.missingMessage)
103
+ code(cli.extensionCheck.commands)
104
+ }
105
+ } catch {
106
+ warn(cli.extensionCheck.errorMessage)
107
+ code(cli.extensionCheck.commands)
108
+ }
109
+ }