@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.
- package/LICENSE +21 -21
- package/README.md +455 -454
- package/cli/commands/join.js +259 -259
- package/cli/commands/migrate.js +310 -310
- package/cli/commands/shared.js +27 -27
- package/cli/commands/single.js +79 -79
- package/cli/commands/wizard.js +134 -134
- package/cli/fragments/ops-backlog/az.md +28 -28
- package/cli/fragments/ops-backlog/gh.md +28 -28
- package/cli/fragments/ops-backlog/jira.md +28 -28
- package/cli/fragments/ops-review/az.md +62 -62
- package/cli/fragments/ops-review/gh.md +51 -51
- package/cli/fragments/ops-review/gl.md +55 -55
- package/cli/fragments/ops-ship/gh.md +67 -67
- package/cli/fragments/ops-ship/gl.md +84 -84
- package/cli/index.js +116 -116
- package/cli/presets/clean.json +21 -21
- package/cli/presets/models.json +68 -68
- package/cli/presets/optimization.json +32 -32
- package/cli/presets/platforms.json +76 -76
- package/cli/presets/source.json +23 -23
- package/cli/steps/clean/index.js +120 -120
- package/cli/steps/copy/agents.js +120 -120
- package/cli/steps/copy/fullstack-engineer.js +89 -89
- package/cli/steps/copy/opencode-json.js +324 -324
- package/cli/steps/copy/skills.js +316 -316
- package/cli/steps/models/format.js +88 -88
- package/cli/steps/models/index.js +64 -64
- package/cli/steps/openspec/index.js +136 -136
- package/cli/steps/optimization/codegraph.js +141 -141
- package/cli/steps/optimization/detect.js +66 -66
- package/cli/steps/optimization/humanizer.js +17 -17
- package/cli/steps/optimization/index.js +158 -158
- package/cli/steps/optimization/memory.js +91 -91
- package/cli/steps/optimization/patch-guardrails.js +111 -111
- package/cli/steps/optimization/simple-english.js +17 -17
- package/cli/steps/optimization/skills-lock.js +30 -30
- package/cli/steps/platform/index.js +109 -109
- package/cli/steps/source/index.js +123 -123
- package/cli/utils/agent-color.js +111 -111
- package/cli/utils/copy.js +134 -134
- package/cli/utils/exec-spinner.js +47 -47
- package/cli/utils/exec.js +134 -134
- package/cli/utils/legacy-check.js +30 -30
- package/cli/utils/models-cache.js +58 -58
- package/cli/utils/models-pricing.js +42 -42
- package/cli/utils/paths.js +81 -81
- package/cli/utils/process.js +3 -3
- package/cli/utils/terminal.js +6 -6
- package/cli/utils/update-manifest.js +75 -75
- package/harness/.opencode/_gitignore +10 -10
- package/harness/.opencode/commands/ops-backlog.md +10 -10
- package/harness/.opencode/commands/ops-review.md +10 -10
- package/harness/.opencode/commands/ops-ship.md +9 -9
- package/harness/.opencode/commands/plan-archive.md +7 -7
- package/harness/.opencode/commands/plan-explore.md +11 -11
- package/harness/.opencode/commands/plan-propose.md +11 -11
- package/harness/.opencode/commands/plan-quick.md +7 -7
- package/harness/.opencode/commands/plan-story.md +11 -11
- package/harness/.opencode/commands/repo-audit.md +7 -7
- package/harness/.opencode/commands/repo-help.md +5 -5
- package/harness/.opencode/package.json +5 -5
- package/harness/.opencode/plugins/pc-commit-coauthor.js +41 -0
- package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
- package/harness/.opencode/plugins/pc-subagent-tiers.js +367 -367
- package/harness/.opencode/plugins/pc-system-reminders.js +386 -386
- package/harness/.opencode/scripts/check-diff.mjs +252 -244
- package/harness/.opencode/scripts/code-rules.mjs +288 -288
- package/harness/ARCHITECTURE.md +13 -13
- package/harness/DESIGN.md +13 -13
- package/harness/opencode.jsonc +36 -36
- package/harness/openspec/config.yaml +20 -20
- package/harness/skills-lock.json +17 -17
- package/package.json +14 -12
- package/skills/agent-harness-cli/SKILL.md +86 -86
- package/skills/pc-guardrails-generic/SKILL.md +53 -53
- package/skills/pc-guardrails-project/SKILL.md +8 -8
- package/skills/pc-make-architecture/structure-template.md +64 -64
- package/skills/pc-make-engineer/SKILL.md +59 -59
- package/skills/pc-make-engineer/template.md +36 -36
- package/skills/pc-make-guardrails/category-reference.md +112 -112
- package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
- package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
- package/skills/pc-ops-ship/SKILL.md +22 -22
- package/skills/pc-plan-apply/SKILL.md +88 -86
- package/skills/pc-plan-apply/simple-mode.md +21 -21
- package/skills/pc-plan-explore/SKILL.md +21 -21
- package/skills/pc-plan-goal/SKILL.md +91 -91
- package/skills/pc-plan-goal/failure-policy.md +30 -30
- package/skills/pc-plan-goal/output-mode.md +10 -10
- package/skills/pc-plan-propose/SKILL.md +125 -125
- package/skills/pc-plan-propose/task-annotation.md +39 -39
- package/skills/pc-plan-quick/SKILL.md +46 -46
- package/skills/pc-repo-audit/SKILL.md +44 -44
- package/skills/pc-repo-help/SKILL.md +87 -87
- package/skills/pc-repo-verify/SKILL.md +150 -150
package/cli/utils/copy.js
CHANGED
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
import fse from 'fs-extra'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import { canUpdateManagedFile, hashComparableFile, readUpdateManifest, recordManagedFile, writeUpdateManifest } from './update-manifest.js'
|
|
4
|
-
|
|
5
|
-
// Folders never copied (skills handled separately by installSkills, .bootstrap is internal tooling)
|
|
6
|
-
const ALWAYS_EXCLUDE = ['.bootstrap', 'skills', 'node_modules']
|
|
7
|
-
|
|
8
|
-
// Files never overwritten even with forceOverwrite: user owns these.
|
|
9
|
-
// The CLI ships templates, but init / the wizard / the user populate them
|
|
10
|
-
// with project-specific content. Overwriting on update would destroy user work.
|
|
11
|
-
const NEVER_OVERWRITE = [
|
|
12
|
-
`openspec${path.sep}config.yaml`,
|
|
13
|
-
'opencode.jsonc',
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
const MARKER_COMMANDS = new Set(['.opencode/commands/ops-review.md', '.opencode/commands/ops-backlog.md'])
|
|
17
|
-
|
|
18
|
-
// Where a project may legitimately keep the doc instead of the repository root.
|
|
19
|
-
// Foundations puts both under ai/, and projects that follow it point their own
|
|
20
|
-
// AGENTS.md there.
|
|
21
|
-
const ROOT_DOCS = new Set(['ARCHITECTURE.md', 'DESIGN.md'])
|
|
22
|
-
const ALTERNATE_DOC_DIRS = ['ai', 'docs']
|
|
23
|
-
|
|
24
|
-
// Seeding the "NOT GENERATED YET, run /make-architecture" placeholder next to a
|
|
25
|
-
// real architecture document is worse than seeding nothing: pc-make-guardrails
|
|
26
|
-
// reads the root file as its primary source, and an agent that finds the
|
|
27
|
-
// placeholder there concludes the project has no architecture written down.
|
|
28
|
-
async function documentedElsewhere(destDir, fileName) {
|
|
29
|
-
if (!ROOT_DOCS.has(fileName)) return false
|
|
30
|
-
for (const directory of ALTERNATE_DOC_DIRS) {
|
|
31
|
-
if (await fse.pathExists(path.join(destDir, directory, fileName))) return true
|
|
32
|
-
}
|
|
33
|
-
return false
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const MANAGED_DIRS = ['.opencode/plugins/', '.opencode/scripts/']
|
|
37
|
-
|
|
38
|
-
function isTemplateTest(relativePath) {
|
|
39
|
-
const normalized = relativePath.split(path.sep).join('/')
|
|
40
|
-
return MANAGED_DIRS.some(dir => normalized.startsWith(dir)) && normalized.endsWith('.test.js')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function isManagedContentPath(relativePath) {
|
|
44
|
-
const normalized = relativePath.split(path.sep).join('/')
|
|
45
|
-
if (MANAGED_DIRS.some(dir => normalized.startsWith(dir))) return true
|
|
46
|
-
if (normalized.startsWith('.opencode/commands/') && !MARKER_COMMANDS.has(normalized)) return true
|
|
47
|
-
return normalized === 'openspec/specs/.gitkeep' || normalized === 'openspec/changes/archive/.gitkeep'
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Copy content/ directory to destination.
|
|
52
|
-
* Excludes:
|
|
53
|
-
* - .agents/skills and .opencode/skills (handled separately)
|
|
54
|
-
* - .bootstrap (internal tooling)
|
|
55
|
-
* - node_modules
|
|
56
|
-
* - DESIGN.md and ARCHITECTURE.md if ctx says they already exist (preserve user's files)
|
|
57
|
-
* @param {string} contentDir - absolute path to content/
|
|
58
|
-
* @param {string} destDir - absolute path to destination (project root)
|
|
59
|
-
* @param {'azure'|'github'} platform
|
|
60
|
-
* @param {{ hasDesign?: boolean, hasArchitecture?: boolean, updateMode?: boolean }} ctx
|
|
61
|
-
*/
|
|
62
|
-
export async function copyContent(contentDir, destDir, platform, ctx = {}) {
|
|
63
|
-
const manifest = ctx.updateMode ? await readUpdateManifest(destDir) : null
|
|
64
|
-
await fse.copy(contentDir, destDir, {
|
|
65
|
-
overwrite: ctx.updateMode || ctx.forceOverwrite || false,
|
|
66
|
-
filter: async (src) => {
|
|
67
|
-
const rel = path.relative(contentDir, src)
|
|
68
|
-
const parts = rel.split(path.sep)
|
|
69
|
-
if (parts.some(part => ALWAYS_EXCLUDE.includes(part))) return false
|
|
70
|
-
if (isTemplateTest(rel)) return false
|
|
71
|
-
if (ctx.hasDesign && rel === 'DESIGN.md') return false
|
|
72
|
-
if (ctx.hasArchitecture && rel === 'ARCHITECTURE.md') return false
|
|
73
|
-
if (await documentedElsewhere(destDir, rel)) return false
|
|
74
|
-
// User-owned config files are never overwritten, even with forceOverwrite.
|
|
75
|
-
// The update command calls writeModelsToConfigs separately to set the
|
|
76
|
-
// model field in opencode.jsonc without destroying user additions.
|
|
77
|
-
if (NEVER_OVERWRITE.includes(rel)) return false
|
|
78
|
-
if (!ctx.updateMode) return true
|
|
79
|
-
|
|
80
|
-
const stat = await fse.stat(src)
|
|
81
|
-
if (stat.isDirectory()) return true
|
|
82
|
-
if (!isManagedContentPath(rel)) {
|
|
83
|
-
return !(await fse.pathExists(path.join(destDir, rel)))
|
|
84
|
-
}
|
|
85
|
-
return canUpdateManagedFile(rel, destDir, manifest)
|
|
86
|
-
},
|
|
87
|
-
})
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export async function recordManagedContent(contentDir, destDir, { updateMode = false } = {}) {
|
|
91
|
-
const manifest = await readUpdateManifest(destDir)
|
|
92
|
-
|
|
93
|
-
async function walk(directory) {
|
|
94
|
-
for (const entry of await fse.readdir(directory, { withFileTypes: true })) {
|
|
95
|
-
const sourcePath = path.join(directory, entry.name)
|
|
96
|
-
const relativePath = path.relative(contentDir, sourcePath)
|
|
97
|
-
if (entry.isDirectory()) {
|
|
98
|
-
await walk(sourcePath)
|
|
99
|
-
continue
|
|
100
|
-
}
|
|
101
|
-
if (isTemplateTest(relativePath)) continue
|
|
102
|
-
if (!isManagedContentPath(relativePath)) continue
|
|
103
|
-
|
|
104
|
-
const destinationPath = path.join(destDir, relativePath)
|
|
105
|
-
if (!await fse.pathExists(destinationPath)) continue
|
|
106
|
-
// Comparable, not raw: this runs after the patchers, so a managed file's
|
|
107
|
-
// destination never equals its source byte for byte once anything has
|
|
108
|
-
// been injected into a marker pair. Comparing raw hashes here meant no
|
|
109
|
-
// entry was ever recorded for those files, which left them permanently
|
|
110
|
-
// indistinguishable from files a project had edited by hand.
|
|
111
|
-
const sourceHash = await hashComparableFile(sourcePath)
|
|
112
|
-
const destinationHash = await hashComparableFile(destinationPath)
|
|
113
|
-
const manifestPath = relativePath.split(path.sep).join('/')
|
|
114
|
-
const previousHash = manifest.files?.[manifestPath]
|
|
115
|
-
if (!updateMode || destinationHash === sourceHash || destinationHash === previousHash) {
|
|
116
|
-
await recordManagedFile(manifest, relativePath, sourcePath)
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
await walk(contentDir)
|
|
122
|
-
await writeUpdateManifest(manifest, destDir)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export async function findAiFiles(dir, files) {
|
|
126
|
-
const found = []
|
|
127
|
-
for (const file of files) {
|
|
128
|
-
const fullPath = path.join(dir, file)
|
|
129
|
-
if (await fse.pathExists(fullPath)) {
|
|
130
|
-
found.push(fullPath)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return found
|
|
134
|
-
}
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { canUpdateManagedFile, hashComparableFile, readUpdateManifest, recordManagedFile, writeUpdateManifest } from './update-manifest.js'
|
|
4
|
+
|
|
5
|
+
// Folders never copied (skills handled separately by installSkills, .bootstrap is internal tooling)
|
|
6
|
+
const ALWAYS_EXCLUDE = ['.bootstrap', 'skills', 'node_modules']
|
|
7
|
+
|
|
8
|
+
// Files never overwritten even with forceOverwrite: user owns these.
|
|
9
|
+
// The CLI ships templates, but init / the wizard / the user populate them
|
|
10
|
+
// with project-specific content. Overwriting on update would destroy user work.
|
|
11
|
+
const NEVER_OVERWRITE = [
|
|
12
|
+
`openspec${path.sep}config.yaml`,
|
|
13
|
+
'opencode.jsonc',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
const MARKER_COMMANDS = new Set(['.opencode/commands/ops-review.md', '.opencode/commands/ops-backlog.md'])
|
|
17
|
+
|
|
18
|
+
// Where a project may legitimately keep the doc instead of the repository root.
|
|
19
|
+
// Foundations puts both under ai/, and projects that follow it point their own
|
|
20
|
+
// AGENTS.md there.
|
|
21
|
+
const ROOT_DOCS = new Set(['ARCHITECTURE.md', 'DESIGN.md'])
|
|
22
|
+
const ALTERNATE_DOC_DIRS = ['ai', 'docs']
|
|
23
|
+
|
|
24
|
+
// Seeding the "NOT GENERATED YET, run /make-architecture" placeholder next to a
|
|
25
|
+
// real architecture document is worse than seeding nothing: pc-make-guardrails
|
|
26
|
+
// reads the root file as its primary source, and an agent that finds the
|
|
27
|
+
// placeholder there concludes the project has no architecture written down.
|
|
28
|
+
async function documentedElsewhere(destDir, fileName) {
|
|
29
|
+
if (!ROOT_DOCS.has(fileName)) return false
|
|
30
|
+
for (const directory of ALTERNATE_DOC_DIRS) {
|
|
31
|
+
if (await fse.pathExists(path.join(destDir, directory, fileName))) return true
|
|
32
|
+
}
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MANAGED_DIRS = ['.opencode/plugins/', '.opencode/scripts/']
|
|
37
|
+
|
|
38
|
+
function isTemplateTest(relativePath) {
|
|
39
|
+
const normalized = relativePath.split(path.sep).join('/')
|
|
40
|
+
return MANAGED_DIRS.some(dir => normalized.startsWith(dir)) && normalized.endsWith('.test.js')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isManagedContentPath(relativePath) {
|
|
44
|
+
const normalized = relativePath.split(path.sep).join('/')
|
|
45
|
+
if (MANAGED_DIRS.some(dir => normalized.startsWith(dir))) return true
|
|
46
|
+
if (normalized.startsWith('.opencode/commands/') && !MARKER_COMMANDS.has(normalized)) return true
|
|
47
|
+
return normalized === 'openspec/specs/.gitkeep' || normalized === 'openspec/changes/archive/.gitkeep'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Copy content/ directory to destination.
|
|
52
|
+
* Excludes:
|
|
53
|
+
* - .agents/skills and .opencode/skills (handled separately)
|
|
54
|
+
* - .bootstrap (internal tooling)
|
|
55
|
+
* - node_modules
|
|
56
|
+
* - DESIGN.md and ARCHITECTURE.md if ctx says they already exist (preserve user's files)
|
|
57
|
+
* @param {string} contentDir - absolute path to content/
|
|
58
|
+
* @param {string} destDir - absolute path to destination (project root)
|
|
59
|
+
* @param {'azure'|'github'} platform
|
|
60
|
+
* @param {{ hasDesign?: boolean, hasArchitecture?: boolean, updateMode?: boolean }} ctx
|
|
61
|
+
*/
|
|
62
|
+
export async function copyContent(contentDir, destDir, platform, ctx = {}) {
|
|
63
|
+
const manifest = ctx.updateMode ? await readUpdateManifest(destDir) : null
|
|
64
|
+
await fse.copy(contentDir, destDir, {
|
|
65
|
+
overwrite: ctx.updateMode || ctx.forceOverwrite || false,
|
|
66
|
+
filter: async (src) => {
|
|
67
|
+
const rel = path.relative(contentDir, src)
|
|
68
|
+
const parts = rel.split(path.sep)
|
|
69
|
+
if (parts.some(part => ALWAYS_EXCLUDE.includes(part))) return false
|
|
70
|
+
if (isTemplateTest(rel)) return false
|
|
71
|
+
if (ctx.hasDesign && rel === 'DESIGN.md') return false
|
|
72
|
+
if (ctx.hasArchitecture && rel === 'ARCHITECTURE.md') return false
|
|
73
|
+
if (await documentedElsewhere(destDir, rel)) return false
|
|
74
|
+
// User-owned config files are never overwritten, even with forceOverwrite.
|
|
75
|
+
// The update command calls writeModelsToConfigs separately to set the
|
|
76
|
+
// model field in opencode.jsonc without destroying user additions.
|
|
77
|
+
if (NEVER_OVERWRITE.includes(rel)) return false
|
|
78
|
+
if (!ctx.updateMode) return true
|
|
79
|
+
|
|
80
|
+
const stat = await fse.stat(src)
|
|
81
|
+
if (stat.isDirectory()) return true
|
|
82
|
+
if (!isManagedContentPath(rel)) {
|
|
83
|
+
return !(await fse.pathExists(path.join(destDir, rel)))
|
|
84
|
+
}
|
|
85
|
+
return canUpdateManagedFile(rel, destDir, manifest)
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function recordManagedContent(contentDir, destDir, { updateMode = false } = {}) {
|
|
91
|
+
const manifest = await readUpdateManifest(destDir)
|
|
92
|
+
|
|
93
|
+
async function walk(directory) {
|
|
94
|
+
for (const entry of await fse.readdir(directory, { withFileTypes: true })) {
|
|
95
|
+
const sourcePath = path.join(directory, entry.name)
|
|
96
|
+
const relativePath = path.relative(contentDir, sourcePath)
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
await walk(sourcePath)
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
if (isTemplateTest(relativePath)) continue
|
|
102
|
+
if (!isManagedContentPath(relativePath)) continue
|
|
103
|
+
|
|
104
|
+
const destinationPath = path.join(destDir, relativePath)
|
|
105
|
+
if (!await fse.pathExists(destinationPath)) continue
|
|
106
|
+
// Comparable, not raw: this runs after the patchers, so a managed file's
|
|
107
|
+
// destination never equals its source byte for byte once anything has
|
|
108
|
+
// been injected into a marker pair. Comparing raw hashes here meant no
|
|
109
|
+
// entry was ever recorded for those files, which left them permanently
|
|
110
|
+
// indistinguishable from files a project had edited by hand.
|
|
111
|
+
const sourceHash = await hashComparableFile(sourcePath)
|
|
112
|
+
const destinationHash = await hashComparableFile(destinationPath)
|
|
113
|
+
const manifestPath = relativePath.split(path.sep).join('/')
|
|
114
|
+
const previousHash = manifest.files?.[manifestPath]
|
|
115
|
+
if (!updateMode || destinationHash === sourceHash || destinationHash === previousHash) {
|
|
116
|
+
await recordManagedFile(manifest, relativePath, sourcePath)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await walk(contentDir)
|
|
122
|
+
await writeUpdateManifest(manifest, destDir)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function findAiFiles(dir, files) {
|
|
126
|
+
const found = []
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const fullPath = path.join(dir, file)
|
|
129
|
+
if (await fse.pathExists(fullPath)) {
|
|
130
|
+
found.push(fullPath)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return found
|
|
134
|
+
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
import ora from 'ora'
|
|
3
|
-
|
|
4
|
-
// ── Screen / step state ──────────────────────────────────────────────────────
|
|
5
|
-
|
|
6
|
-
const previousSteps = []; // up to 2 completed steps, each is an array of lines
|
|
7
|
-
let currentStepLines = []; // lines accumulated in the current step
|
|
8
|
-
let stepSpinner = null; // ora spinner shown while step is working
|
|
9
|
-
|
|
10
|
-
export function appendLine(line) {
|
|
11
|
-
currentStepLines.push(line);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function stopSpinner() {
|
|
15
|
-
if (stepSpinner) {
|
|
16
|
-
stepSpinner.stop();
|
|
17
|
-
stepSpinner = null;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function startSpinner(text = 'working...') {
|
|
22
|
-
stopSpinner();
|
|
23
|
-
stepSpinner = ora({ text: chalk.dim(text), color: 'red' }).start();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function redraw() {
|
|
27
|
-
if (process.stdout.isTTY) console.clear();
|
|
28
|
-
|
|
29
|
-
// Show up to 2 previous steps dimmed
|
|
30
|
-
for (const stepLines of previousSteps) {
|
|
31
|
-
for (const line of stepLines) {
|
|
32
|
-
process.stdout.write(`${chalk.dim(line)}\n`);
|
|
33
|
-
}
|
|
34
|
-
process.stdout.write('\n');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Current step output
|
|
38
|
-
for (const line of currentStepLines) {
|
|
39
|
-
process.stdout.write(`${line}\n`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function rotateStep() {
|
|
44
|
-
previousSteps.push(currentStepLines);
|
|
45
|
-
if (previousSteps.length > 2) previousSteps.shift();
|
|
46
|
-
currentStepLines = [];
|
|
47
|
-
}
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
|
|
4
|
+
// ── Screen / step state ──────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const previousSteps = []; // up to 2 completed steps, each is an array of lines
|
|
7
|
+
let currentStepLines = []; // lines accumulated in the current step
|
|
8
|
+
let stepSpinner = null; // ora spinner shown while step is working
|
|
9
|
+
|
|
10
|
+
export function appendLine(line) {
|
|
11
|
+
currentStepLines.push(line);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function stopSpinner() {
|
|
15
|
+
if (stepSpinner) {
|
|
16
|
+
stepSpinner.stop();
|
|
17
|
+
stepSpinner = null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function startSpinner(text = 'working...') {
|
|
22
|
+
stopSpinner();
|
|
23
|
+
stepSpinner = ora({ text: chalk.dim(text), color: 'red' }).start();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function redraw() {
|
|
27
|
+
if (process.stdout.isTTY) console.clear();
|
|
28
|
+
|
|
29
|
+
// Show up to 2 previous steps dimmed
|
|
30
|
+
for (const stepLines of previousSteps) {
|
|
31
|
+
for (const line of stepLines) {
|
|
32
|
+
process.stdout.write(`${chalk.dim(line)}\n`);
|
|
33
|
+
}
|
|
34
|
+
process.stdout.write('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Current step output
|
|
38
|
+
for (const line of currentStepLines) {
|
|
39
|
+
process.stdout.write(`${line}\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function rotateStep() {
|
|
44
|
+
previousSteps.push(currentStepLines);
|
|
45
|
+
if (previousSteps.length > 2) previousSteps.shift();
|
|
46
|
+
currentStepLines = [];
|
|
47
|
+
}
|
package/cli/utils/exec.js
CHANGED
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
import { execa } from 'execa'
|
|
3
|
-
import ora from 'ora'
|
|
4
|
-
import { appendLine, redraw, rotateStep, startSpinner, stopSpinner } from './exec-spinner.js'
|
|
5
|
-
import { MARKERS } from './terminal.js';
|
|
6
|
-
|
|
7
|
-
// ── Public API ───────────────────────────────────────────────────────────────
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Run a shell command with a spinner.
|
|
11
|
-
* Returns { success, stdout, stderr }
|
|
12
|
-
*/
|
|
13
|
-
export async function run(command, args = [], { label, cwd = process.cwd() } = {}) {
|
|
14
|
-
const spinner = ora(label ?? `${command} ${args.join(' ')}`).start();
|
|
15
|
-
try {
|
|
16
|
-
const result = await execa(command, args, { cwd, reject: false });
|
|
17
|
-
if (result.exitCode === 0) {
|
|
18
|
-
spinner.succeed();
|
|
19
|
-
} else {
|
|
20
|
-
spinner.fail();
|
|
21
|
-
}
|
|
22
|
-
return { success: result.exitCode === 0, stdout: result.stdout, stderr: result.stderr };
|
|
23
|
-
} catch (err) {
|
|
24
|
-
spinner.fail();
|
|
25
|
-
return { success: false, stdout: '', stderr: err.message };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Check if a command is available on PATH.
|
|
31
|
-
* Returns true/false.
|
|
32
|
-
*/
|
|
33
|
-
export async function commandExists(command) {
|
|
34
|
-
try {
|
|
35
|
-
const result = await execa(command, ['--version'], { reject: false });
|
|
36
|
-
return result.exitCode === 0;
|
|
37
|
-
} catch {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Print a section header, clears screen, shows previous step dimmed, starts new step.
|
|
44
|
-
*/
|
|
45
|
-
export function header(text) {
|
|
46
|
-
rotateStep();
|
|
47
|
-
|
|
48
|
-
const line1 = '';
|
|
49
|
-
const line2 = chalk.bold.hex('#fe3d57')(`━━ ${text}`);
|
|
50
|
-
const line3 = '';
|
|
51
|
-
|
|
52
|
-
appendLine(line1);
|
|
53
|
-
appendLine(line2);
|
|
54
|
-
appendLine(line3);
|
|
55
|
-
|
|
56
|
-
redraw();
|
|
57
|
-
|
|
58
|
-
startSpinner('working...');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Restart the step spinner after prompts or logs.
|
|
63
|
-
*/
|
|
64
|
-
export function loading(text = 'working...') {
|
|
65
|
-
startSpinner(text);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Print a success line.
|
|
70
|
-
*/
|
|
71
|
-
export function success(text) {
|
|
72
|
-
stopSpinner();
|
|
73
|
-
const line = chalk.green(`${MARKERS.OK_PREFIX}${text}`);
|
|
74
|
-
appendLine(line);
|
|
75
|
-
console.log(line);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Print a warning line.
|
|
80
|
-
*/
|
|
81
|
-
export function warn(text) {
|
|
82
|
-
stopSpinner();
|
|
83
|
-
const line = chalk.yellow(`${MARKERS.WARN_PREFIX}${text}`);
|
|
84
|
-
appendLine(line);
|
|
85
|
-
console.log(line);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Print an error line.
|
|
90
|
-
*/
|
|
91
|
-
export function error(text) {
|
|
92
|
-
stopSpinner();
|
|
93
|
-
const line = chalk.red(`${MARKERS.ERROR_PREFIX}${text}`);
|
|
94
|
-
appendLine(line);
|
|
95
|
-
console.log(line);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Print an info line.
|
|
100
|
-
*/
|
|
101
|
-
export function info(text) {
|
|
102
|
-
stopSpinner();
|
|
103
|
-
const line = chalk.dim(`${MARKERS.EMPTY}${text}`);
|
|
104
|
-
appendLine(line);
|
|
105
|
-
console.log(line);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Print an action prompt line (white bold, requires user interaction).
|
|
110
|
-
*/
|
|
111
|
-
export function prompt(text) {
|
|
112
|
-
stopSpinner();
|
|
113
|
-
const line = chalk.bold(`${MARKERS.EMPTY}${text}`);
|
|
114
|
-
appendLine(line);
|
|
115
|
-
console.log(line);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Print a code block.
|
|
120
|
-
*/
|
|
121
|
-
export function code(lines) {
|
|
122
|
-
stopSpinner();
|
|
123
|
-
appendLine('');
|
|
124
|
-
console.log();
|
|
125
|
-
for (const line of lines) {
|
|
126
|
-
const formatted = chalk.bgGray.white(
|
|
127
|
-
`${MARKERS.EMPTY}${line}${MARKERS.EMPTY}`,
|
|
128
|
-
);
|
|
129
|
-
appendLine(formatted);
|
|
130
|
-
console.log(formatted);
|
|
131
|
-
}
|
|
132
|
-
appendLine('');
|
|
133
|
-
console.log();
|
|
134
|
-
}
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { execa } from 'execa'
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import { appendLine, redraw, rotateStep, startSpinner, stopSpinner } from './exec-spinner.js'
|
|
5
|
+
import { MARKERS } from './terminal.js';
|
|
6
|
+
|
|
7
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Run a shell command with a spinner.
|
|
11
|
+
* Returns { success, stdout, stderr }
|
|
12
|
+
*/
|
|
13
|
+
export async function run(command, args = [], { label, cwd = process.cwd() } = {}) {
|
|
14
|
+
const spinner = ora(label ?? `${command} ${args.join(' ')}`).start();
|
|
15
|
+
try {
|
|
16
|
+
const result = await execa(command, args, { cwd, reject: false });
|
|
17
|
+
if (result.exitCode === 0) {
|
|
18
|
+
spinner.succeed();
|
|
19
|
+
} else {
|
|
20
|
+
spinner.fail();
|
|
21
|
+
}
|
|
22
|
+
return { success: result.exitCode === 0, stdout: result.stdout, stderr: result.stderr };
|
|
23
|
+
} catch (err) {
|
|
24
|
+
spinner.fail();
|
|
25
|
+
return { success: false, stdout: '', stderr: err.message };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if a command is available on PATH.
|
|
31
|
+
* Returns true/false.
|
|
32
|
+
*/
|
|
33
|
+
export async function commandExists(command) {
|
|
34
|
+
try {
|
|
35
|
+
const result = await execa(command, ['--version'], { reject: false });
|
|
36
|
+
return result.exitCode === 0;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Print a section header, clears screen, shows previous step dimmed, starts new step.
|
|
44
|
+
*/
|
|
45
|
+
export function header(text) {
|
|
46
|
+
rotateStep();
|
|
47
|
+
|
|
48
|
+
const line1 = '';
|
|
49
|
+
const line2 = chalk.bold.hex('#fe3d57')(`━━ ${text}`);
|
|
50
|
+
const line3 = '';
|
|
51
|
+
|
|
52
|
+
appendLine(line1);
|
|
53
|
+
appendLine(line2);
|
|
54
|
+
appendLine(line3);
|
|
55
|
+
|
|
56
|
+
redraw();
|
|
57
|
+
|
|
58
|
+
startSpinner('working...');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Restart the step spinner after prompts or logs.
|
|
63
|
+
*/
|
|
64
|
+
export function loading(text = 'working...') {
|
|
65
|
+
startSpinner(text);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Print a success line.
|
|
70
|
+
*/
|
|
71
|
+
export function success(text) {
|
|
72
|
+
stopSpinner();
|
|
73
|
+
const line = chalk.green(`${MARKERS.OK_PREFIX}${text}`);
|
|
74
|
+
appendLine(line);
|
|
75
|
+
console.log(line);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Print a warning line.
|
|
80
|
+
*/
|
|
81
|
+
export function warn(text) {
|
|
82
|
+
stopSpinner();
|
|
83
|
+
const line = chalk.yellow(`${MARKERS.WARN_PREFIX}${text}`);
|
|
84
|
+
appendLine(line);
|
|
85
|
+
console.log(line);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Print an error line.
|
|
90
|
+
*/
|
|
91
|
+
export function error(text) {
|
|
92
|
+
stopSpinner();
|
|
93
|
+
const line = chalk.red(`${MARKERS.ERROR_PREFIX}${text}`);
|
|
94
|
+
appendLine(line);
|
|
95
|
+
console.log(line);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Print an info line.
|
|
100
|
+
*/
|
|
101
|
+
export function info(text) {
|
|
102
|
+
stopSpinner();
|
|
103
|
+
const line = chalk.dim(`${MARKERS.EMPTY}${text}`);
|
|
104
|
+
appendLine(line);
|
|
105
|
+
console.log(line);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Print an action prompt line (white bold, requires user interaction).
|
|
110
|
+
*/
|
|
111
|
+
export function prompt(text) {
|
|
112
|
+
stopSpinner();
|
|
113
|
+
const line = chalk.bold(`${MARKERS.EMPTY}${text}`);
|
|
114
|
+
appendLine(line);
|
|
115
|
+
console.log(line);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Print a code block.
|
|
120
|
+
*/
|
|
121
|
+
export function code(lines) {
|
|
122
|
+
stopSpinner();
|
|
123
|
+
appendLine('');
|
|
124
|
+
console.log();
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
const formatted = chalk.bgGray.white(
|
|
127
|
+
`${MARKERS.EMPTY}${line}${MARKERS.EMPTY}`,
|
|
128
|
+
);
|
|
129
|
+
appendLine(formatted);
|
|
130
|
+
console.log(formatted);
|
|
131
|
+
}
|
|
132
|
+
appendLine('');
|
|
133
|
+
console.log();
|
|
134
|
+
}
|