forge-workflow 0.0.3 → 0.0.5
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/.claude/commands/dev.md +340 -314
- package/.claude/commands/plan.md +521 -478
- package/.claude/commands/premerge.md +176 -179
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +442 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +164 -134
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +48 -77
- package/.claude/commands/validate.md +282 -237
- package/.claude/commands/verify.md +221 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +105 -105
- package/.claude/scripts/greptile-resolve.sh +526 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +337 -311
- package/.cline/workflows/plan.md +518 -475
- package/.cline/workflows/premerge.md +173 -176
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +439 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +161 -131
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +45 -74
- package/.cline/workflows/validate.md +279 -234
- package/.cline/workflows/verify.md +218 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +340 -314
- package/.codex/skills/plan/SKILL.md +521 -478
- package/.codex/skills/premerge/SKILL.md +176 -179
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +442 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +164 -134
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +48 -77
- package/.codex/skills/validate/SKILL.md +282 -237
- package/.codex/skills/verify/SKILL.md +221 -221
- package/.cursor/commands/dev.md +337 -311
- package/.cursor/commands/plan.md +518 -475
- package/.cursor/commands/premerge.md +173 -176
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +439 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +161 -131
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +45 -74
- package/.cursor/commands/validate.md +279 -234
- package/.cursor/commands/verify.md +218 -218
- package/.cursor/rules/permissions-guidance.mdc +37 -37
- package/.forge/hooks/check-tdd.js +240 -240
- package/.github/PLUGIN_TEMPLATE.json +32 -32
- package/.github/prompts/dev.prompt.md +342 -316
- package/.github/prompts/plan.prompt.md +523 -480
- package/.github/prompts/premerge.prompt.md +178 -181
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +444 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +166 -136
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +50 -79
- package/.github/prompts/validate.prompt.md +284 -239
- package/.github/prompts/verify.prompt.md +223 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +341 -315
- package/.kilocode/workflows/plan.md +522 -479
- package/.kilocode/workflows/premerge.md +177 -180
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +443 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +165 -135
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +49 -78
- package/.kilocode/workflows/validate.md +283 -238
- package/.kilocode/workflows/verify.md +222 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +340 -314
- package/.opencode/commands/plan.md +521 -478
- package/.opencode/commands/premerge.md +176 -179
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +442 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +164 -134
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +48 -77
- package/.opencode/commands/validate.md +282 -237
- package/.opencode/commands/verify.md +221 -221
- package/.roo/commands/dev.md +341 -315
- package/.roo/commands/plan.md +522 -479
- package/.roo/commands/premerge.md +177 -180
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +443 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +165 -135
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +49 -78
- package/.roo/commands/validate.md +283 -238
- package/.roo/commands/verify.md +222 -222
- package/AGENTS.md +175 -169
- package/CLAUDE.md +100 -99
- package/LICENSE +21 -21
- package/README.md +429 -414
- package/bin/forge-cmd.js +313 -313
- package/bin/{forge-validate.js → forge-preflight.js} +309 -303
- package/bin/forge.js +4596 -4232
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -0
- package/docs/ENHANCED_ONBOARDING.md +602 -602
- package/docs/EXAMPLES.md +482 -482
- package/docs/GREPTILE_SETUP.md +400 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
- package/docs/ROADMAP.md +359 -359
- package/docs/SETUP.md +663 -632
- package/docs/TOOLCHAIN.md +630 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1058
- package/lefthook.yml +39 -39
- package/lib/agents/README.md +198 -198
- package/lib/agents/claude.plugin.json +28 -28
- package/lib/agents/cline.plugin.json +22 -22
- package/lib/agents/codex.plugin.json +19 -19
- package/lib/agents/copilot.plugin.json +24 -24
- package/lib/agents/cursor.plugin.json +25 -25
- package/lib/agents/kilocode.plugin.json +22 -22
- package/lib/agents/opencode.plugin.json +20 -20
- package/lib/agents/roo.plugin.json +23 -23
- package/lib/agents-config.js +2112 -2112
- package/lib/beads-health-check.js +143 -0
- package/lib/beads-setup.js +341 -0
- package/lib/beads-sync-scaffold.js +260 -0
- package/lib/commands/dev.js +513 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/validate.js +602 -602
- package/lib/context-merge.js +359 -359
- package/lib/dep-guard/analyzer.js +294 -294
- package/lib/dep-guard/behavior-detector.js +98 -98
- package/lib/dep-guard/contract-detector.js +162 -162
- package/lib/dep-guard/import-detector.js +498 -498
- package/lib/dep-guard/path-utils.js +13 -13
- package/lib/dep-guard/rubric.js +120 -120
- package/lib/dep-guard/task-parser.js +318 -318
- package/lib/detect-agent.js +191 -0
- package/lib/detect-worktree.js +47 -0
- package/lib/file-hash.js +26 -0
- package/lib/husky-migration.js +450 -0
- package/lib/lefthook-check.js +65 -0
- package/lib/pat-setup.js +207 -0
- package/lib/plugin-catalog.js +350 -350
- package/lib/plugin-manager.js +166 -166
- package/lib/plugin-recommender.js +141 -141
- package/lib/project-discovery.js +491 -491
- package/lib/setup-action-log.js +139 -0
- package/lib/setup-summary-renderer.js +106 -0
- package/lib/setup-utils.js +96 -0
- package/lib/setup.js +192 -118
- package/lib/smart-merge.js +64 -0
- package/lib/symlink-utils.js +81 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -129
- package/scripts/beads-context.sh +291 -0
- package/scripts/beads-context.test.js +563 -0
- package/scripts/behavioral-judge.sh +378 -0
- package/scripts/benchmark.js +85 -0
- package/scripts/branch-protection.js +183 -0
- package/scripts/check-agents.js +172 -0
- package/scripts/commitlint.js +42 -0
- package/scripts/conflict-detect.sh +323 -0
- package/scripts/dep-guard-analyze.js +71 -0
- package/scripts/dep-guard.sh +811 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +399 -0
- package/scripts/github-beads-sync/comment.mjs +64 -0
- package/scripts/github-beads-sync/config.mjs +148 -0
- package/scripts/github-beads-sync/github-api.mjs +131 -0
- package/scripts/github-beads-sync/index.mjs +332 -0
- package/scripts/github-beads-sync/label-mapper.mjs +54 -0
- package/scripts/github-beads-sync/mapping.mjs +78 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
- package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
- package/scripts/github-beads-sync/run-bd.mjs +159 -0
- package/scripts/github-beads-sync/sanitize.mjs +121 -0
- package/scripts/github-beads-sync.config.json +26 -0
- package/scripts/improve-command.js +375 -0
- package/scripts/lib/eval-runner.js +229 -0
- package/scripts/lib/eval-schema.js +135 -0
- package/scripts/lib/eval-storage.js +78 -0
- package/scripts/lib/grading.js +203 -0
- package/scripts/lib/transcript-parser.js +63 -0
- package/scripts/lint.js +47 -0
- package/scripts/migrate-to-bun-test.js +412 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +782 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +460 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +44 -0
- package/scripts/validate.sh +94 -0
- package/skills/parallel-deep-research/SKILL.md +108 -108
- package/skills/parallel-deep-research/evals/README.md +27 -27
- package/skills/parallel-deep-research/evals/evals.json +62 -62
- package/skills/sonarcloud-analysis/SKILL.md +171 -171
- package/skills/sonarcloud-analysis/evals/README.md +27 -27
- package/skills/sonarcloud-analysis/evals/evals.json +50 -50
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
- package/docs/WORKFLOW.md +0 -400
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized action log for setup operations.
|
|
3
|
+
*
|
|
4
|
+
* Collects file-level actions (created, skipped, merged, etc.) during
|
|
5
|
+
* `forge setup` so the CLI can display a structured summary at the end.
|
|
6
|
+
*
|
|
7
|
+
* @module setup-action-log
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Map of directory prefixes to human-readable agent names. */
|
|
11
|
+
const AGENT_PREFIXES = {
|
|
12
|
+
'.claude/': 'Claude Code',
|
|
13
|
+
'.cursor/': 'Cursor',
|
|
14
|
+
'.windsurf/': 'Windsurf',
|
|
15
|
+
'.cline/': 'Cline',
|
|
16
|
+
'.codex/': 'Codex',
|
|
17
|
+
'.opencode/': 'OpenCode',
|
|
18
|
+
'.kilocode/': 'Kilocode',
|
|
19
|
+
'.roo/': 'Roo Code',
|
|
20
|
+
'.github/prompts/': 'GitHub Copilot'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detect the agent name from a file path.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} filePath - Relative file path (e.g. `.claude/settings.json`)
|
|
27
|
+
* @returns {string} Agent name or `'General'` if no agent prefix matches
|
|
28
|
+
*/
|
|
29
|
+
function detectAgent(filePath) {
|
|
30
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
31
|
+
for (const [prefix, name] of Object.entries(AGENT_PREFIXES)) {
|
|
32
|
+
if (normalized.startsWith(prefix)) {
|
|
33
|
+
return name;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return 'General';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Strip the agent directory prefix from a file path.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} filePath - Relative file path
|
|
43
|
+
* @returns {string} Path with the leading agent directory removed
|
|
44
|
+
*/
|
|
45
|
+
function stripAgentPrefix(filePath) {
|
|
46
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
47
|
+
for (const prefix of Object.keys(AGENT_PREFIXES)) {
|
|
48
|
+
if (normalized.startsWith(prefix)) {
|
|
49
|
+
return normalized.slice(prefix.length);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return normalized;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class SetupActionLog {
|
|
56
|
+
constructor() {
|
|
57
|
+
/** @type {Array<{file: string, action: string, detail: string|null}>} */
|
|
58
|
+
this.actions = [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Record a setup action.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} file - Relative file path that was acted on
|
|
65
|
+
* @param {string} action - One of: created, skipped, merged, conflict, removed, force-created
|
|
66
|
+
* @param {string|null} [detail=null] - Optional human-readable detail
|
|
67
|
+
*/
|
|
68
|
+
add(file, action, detail = null) {
|
|
69
|
+
this.actions.push({ file, action, detail });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get counts grouped by action type.
|
|
74
|
+
*
|
|
75
|
+
* @returns {Record<string, number>} e.g. `{ created: 4, skipped: 2 }`
|
|
76
|
+
*/
|
|
77
|
+
getSummary() {
|
|
78
|
+
const counts = {};
|
|
79
|
+
for (const { action } of this.actions) {
|
|
80
|
+
counts[action] = (counts[action] || 0) + 1;
|
|
81
|
+
}
|
|
82
|
+
return counts;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the full ordered list of actions.
|
|
87
|
+
*
|
|
88
|
+
* @returns {Array<{file: string, action: string, detail: string|null}>}
|
|
89
|
+
*/
|
|
90
|
+
getVerbose() {
|
|
91
|
+
return this.actions;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Group files by detected agent name.
|
|
96
|
+
*
|
|
97
|
+
* Each agent key maps to an object whose keys are action types and whose
|
|
98
|
+
* values are arrays of file paths (with the agent prefix stripped).
|
|
99
|
+
*
|
|
100
|
+
* @returns {Record<string, Record<string, string[]>>}
|
|
101
|
+
*/
|
|
102
|
+
getAgentSummary() {
|
|
103
|
+
const agents = {};
|
|
104
|
+
for (const { file, action } of this.actions) {
|
|
105
|
+
const agent = detectAgent(file);
|
|
106
|
+
const stripped = stripAgentPrefix(file);
|
|
107
|
+
|
|
108
|
+
if (!agents[agent]) {
|
|
109
|
+
agents[agent] = {};
|
|
110
|
+
}
|
|
111
|
+
if (!agents[agent][action]) {
|
|
112
|
+
agents[agent][action] = [];
|
|
113
|
+
}
|
|
114
|
+
agents[agent][action].push(stripped);
|
|
115
|
+
}
|
|
116
|
+
return agents;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Filter actions by type.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} action - The action type to filter on
|
|
123
|
+
* @returns {Array<{file: string, action: string, detail: string|null}>}
|
|
124
|
+
*/
|
|
125
|
+
getByAction(action) {
|
|
126
|
+
return this.actions.filter(a => a.action === action);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Total number of recorded actions.
|
|
131
|
+
*
|
|
132
|
+
* @returns {number}
|
|
133
|
+
*/
|
|
134
|
+
get length() {
|
|
135
|
+
return this.actions.length;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { SetupActionLog };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a clean summary from SetupActionLog data.
|
|
3
|
+
*
|
|
4
|
+
* Default mode: 3-line concise output.
|
|
5
|
+
* Verbose mode: file-by-file detail grouped by agent.
|
|
6
|
+
*
|
|
7
|
+
* @module setup-summary-renderer
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Capitalize the first letter of a string.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} str
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function capitalize(str) {
|
|
17
|
+
if (!str) return str;
|
|
18
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Render setup summary as a string.
|
|
23
|
+
*
|
|
24
|
+
* @param {import('./setup-action-log').SetupActionLog} actionLog - The populated action log
|
|
25
|
+
* @param {string[]} agentNames - List of configured agent slugs (e.g. ['claude', 'cursor'])
|
|
26
|
+
* @param {boolean} verbose - Whether to show file-by-file detail
|
|
27
|
+
* @returns {string} The formatted summary output
|
|
28
|
+
*/
|
|
29
|
+
function renderSetupSummary(actionLog, agentNames, verbose) {
|
|
30
|
+
if (verbose) {
|
|
31
|
+
return renderVerbose(actionLog, agentNames);
|
|
32
|
+
}
|
|
33
|
+
return renderDefault(actionLog, agentNames);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Render the default 3-line concise summary.
|
|
38
|
+
*
|
|
39
|
+
* @param {import('./setup-action-log').SetupActionLog} actionLog
|
|
40
|
+
* @param {string[]} agentNames
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
function renderDefault(actionLog, agentNames) {
|
|
44
|
+
const agentCount = agentNames.length;
|
|
45
|
+
const agentLabel = agentCount === 1 ? '1 agent' : `${agentCount} agents`;
|
|
46
|
+
const agentList = agentNames.length > 0 ? ` (${agentNames.join(', ')})` : '';
|
|
47
|
+
|
|
48
|
+
const summary = actionLog.getSummary();
|
|
49
|
+
|
|
50
|
+
// Build counts line — only include non-zero actions
|
|
51
|
+
const displayOrder = ['created', 'skipped', 'merged', 'force-created', 'updated', 'conflict', 'removed'];
|
|
52
|
+
const parts = [];
|
|
53
|
+
for (const action of displayOrder) {
|
|
54
|
+
if (summary[action] && summary[action] > 0) {
|
|
55
|
+
parts.push(`${capitalize(action)}: ${summary[action]} ${summary[action] === 1 ? 'file' : 'files'}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Include any actions not in displayOrder
|
|
59
|
+
for (const [action, count] of Object.entries(summary)) {
|
|
60
|
+
if (!displayOrder.includes(action) && count > 0) {
|
|
61
|
+
parts.push(`${capitalize(action)}: ${count} ${count === 1 ? 'file' : 'files'}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const lines = [];
|
|
66
|
+
lines.push(`Forge setup complete — ${agentLabel} configured${agentList}`);
|
|
67
|
+
|
|
68
|
+
if (parts.length > 0) {
|
|
69
|
+
lines.push(` ${parts.join(' | ')}`);
|
|
70
|
+
} else {
|
|
71
|
+
lines.push(' 0 files changed');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
lines.push(' Run forge setup --verbose to see all files');
|
|
75
|
+
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Render verbose file-by-file output grouped by agent.
|
|
81
|
+
*
|
|
82
|
+
* @param {import('./setup-action-log').SetupActionLog} actionLog
|
|
83
|
+
* @param {string[]} _agentNames - Not used in verbose (agents come from log data)
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function renderVerbose(actionLog, _agentNames) {
|
|
87
|
+
const agentSummary = actionLog.getAgentSummary();
|
|
88
|
+
const lines = [];
|
|
89
|
+
|
|
90
|
+
for (const [agent, actions] of Object.entries(agentSummary)) {
|
|
91
|
+
for (const [action, files] of Object.entries(actions)) {
|
|
92
|
+
const fileCount = files.length;
|
|
93
|
+
const fileLabel = fileCount === 1 ? '1 file' : `${fileCount} files`;
|
|
94
|
+
const fileList = files.join(', ');
|
|
95
|
+
lines.push(`${agent}: ${fileList} (${fileLabel}) [${action}]`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (lines.length === 0) {
|
|
100
|
+
return 'No file operations recorded.';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { renderSetupSummary };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foundational utilities for `forge setup` command.
|
|
3
|
+
*
|
|
4
|
+
* - `ActionCollector` — collects planned/completed file actions for dry-run
|
|
5
|
+
* summaries and normal-mode logging.
|
|
6
|
+
* - `isNonInteractive()` — detects CI environments and piped stdin so
|
|
7
|
+
* interactive prompts can fall back to defaults.
|
|
8
|
+
*
|
|
9
|
+
* @module setup-utils
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Collects file-level actions during setup.
|
|
14
|
+
*
|
|
15
|
+
* Used by `--dry-run` to collect planned actions and by normal mode
|
|
16
|
+
* to log what was actually done.
|
|
17
|
+
*/
|
|
18
|
+
class ActionCollector {
|
|
19
|
+
constructor() {
|
|
20
|
+
/** @type {Array<{type: string, path: string, description: string}>} */
|
|
21
|
+
this._actions = [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Record a setup action.
|
|
26
|
+
*
|
|
27
|
+
* @param {'create'|'modify'|'skip'} type - Action type
|
|
28
|
+
* @param {string} path - File path that was (or would be) acted on
|
|
29
|
+
* @param {string} description - Human-readable explanation
|
|
30
|
+
*/
|
|
31
|
+
add(type, path, description) {
|
|
32
|
+
this._actions.push({ type, path, description });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Return a copy of all recorded actions.
|
|
37
|
+
*
|
|
38
|
+
* @returns {Array<{type: string, path: string, description: string}>}
|
|
39
|
+
*/
|
|
40
|
+
list() {
|
|
41
|
+
return [...this._actions];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Print a formatted summary of all recorded actions to stdout.
|
|
46
|
+
*/
|
|
47
|
+
print() {
|
|
48
|
+
if (this._actions.length === 0) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const icons = {
|
|
53
|
+
create: '+',
|
|
54
|
+
modify: '~',
|
|
55
|
+
skip: '-'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
for (const { type, path, description } of this._actions) {
|
|
59
|
+
const icon = icons[type] || '?';
|
|
60
|
+
process.stdout.write(` [${icon}] ${type.padEnd(6)} ${path} — ${description}\n`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Detect whether the current environment is non-interactive.
|
|
67
|
+
*
|
|
68
|
+
* Returns `true` if any of:
|
|
69
|
+
* - `process.env.CI` is truthy (non-empty)
|
|
70
|
+
* - `process.env.GITHUB_ACTIONS` exists
|
|
71
|
+
* - `process.env.GITLAB_CI` exists
|
|
72
|
+
* - `process.stdin.isTTY` is falsy
|
|
73
|
+
* - `--non-interactive` flag was passed in `process.argv`
|
|
74
|
+
*
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
function isNonInteractive() {
|
|
78
|
+
if (process.env.CI && process.env.CI !== '') {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (process.env.GITHUB_ACTIONS !== undefined) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (process.env.GITLAB_CI !== undefined) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (!process.stdin.isTTY) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (process.argv.includes('--non-interactive')) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { ActionCollector, isNonInteractive };
|
package/lib/setup.js
CHANGED
|
@@ -1,118 +1,192 @@
|
|
|
1
|
-
const fs = require('node:fs');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Save setup state to .forge/setup-state.json
|
|
6
|
-
* @param {string} projectPath - Path to the project root
|
|
7
|
-
* @param {Object} state - Setup state object
|
|
8
|
-
* @param {string} state.version - Forge version
|
|
9
|
-
* @param {string[]} state.completed_steps - List of completed steps
|
|
10
|
-
* @param {string[]} state.pending_steps - List of pending steps
|
|
11
|
-
* @param {string} [state.last_run] - ISO timestamp of last run
|
|
12
|
-
* @returns {Promise<void>}
|
|
13
|
-
*/
|
|
14
|
-
async function saveSetupState(projectPath, state) {
|
|
15
|
-
const forgeDir = path.join(projectPath, '.forge');
|
|
16
|
-
const setupStatePath = path.join(forgeDir, 'setup-state.json');
|
|
17
|
-
|
|
18
|
-
// Ensure .forge directory exists
|
|
19
|
-
await fs.promises.mkdir(forgeDir, { recursive: true });
|
|
20
|
-
|
|
21
|
-
// Add last_run timestamp if not provided
|
|
22
|
-
if (!state.last_run) {
|
|
23
|
-
state.last_run = new Date().toISOString();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Write state as JSON
|
|
27
|
-
await fs.promises.writeFile(setupStatePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Load setup state from .forge/setup-state.json
|
|
32
|
-
* @param {string} projectPath - Path to the project root
|
|
33
|
-
* @returns {Promise<Object|null>} Setup state object, or null if not found or invalid
|
|
34
|
-
*/
|
|
35
|
-
async function loadSetupState(projectPath) {
|
|
36
|
-
const setupStatePath = path.join(projectPath, '.forge', 'setup-state.json');
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const content = await fs.promises.readFile(setupStatePath, 'utf-8');
|
|
40
|
-
return JSON.parse(content);
|
|
41
|
-
} catch (_error) {
|
|
42
|
-
// File doesn't exist or invalid JSON
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Check if setup is complete (no pending steps)
|
|
49
|
-
* @param {string} projectPath - Path to the project root
|
|
50
|
-
* @returns {Promise<boolean>} True if setup complete, false otherwise
|
|
51
|
-
*/
|
|
52
|
-
async function isSetupComplete(projectPath) {
|
|
53
|
-
const state = await loadSetupState(projectPath);
|
|
54
|
-
|
|
55
|
-
if (!state) {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Setup is complete if pending_steps is empty
|
|
60
|
-
return state.pending_steps.length === 0;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Get the next pending step
|
|
65
|
-
* @param {string} projectPath - Path to the project root
|
|
66
|
-
* @returns {Promise<string|null>} Next step name, or null if none
|
|
67
|
-
*/
|
|
68
|
-
async function getNextStep(projectPath) {
|
|
69
|
-
const state = await loadSetupState(projectPath);
|
|
70
|
-
|
|
71
|
-
if (!state || !state.pending_steps || state.pending_steps.length === 0) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Return first pending step
|
|
76
|
-
return state.pending_steps[0];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Mark a step as complete (move from pending to completed)
|
|
81
|
-
* @param {string} projectPath - Path to the project root
|
|
82
|
-
* @param {string} stepName - Name of the step to mark complete
|
|
83
|
-
* @returns {Promise<void>}
|
|
84
|
-
*/
|
|
85
|
-
async function markStepComplete(projectPath, stepName) {
|
|
86
|
-
let state = await loadSetupState(projectPath);
|
|
87
|
-
|
|
88
|
-
// If no state exists, create initial state
|
|
89
|
-
if (!state) {
|
|
90
|
-
state = {
|
|
91
|
-
version: '1.6.0',
|
|
92
|
-
completed_steps: [],
|
|
93
|
-
pending_steps: []
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Remove from pending_steps if present
|
|
98
|
-
state.pending_steps = state.pending_steps.filter(step => step !== stepName);
|
|
99
|
-
|
|
100
|
-
// Add to completed_steps if not already there
|
|
101
|
-
if (!state.completed_steps.includes(stepName)) {
|
|
102
|
-
state.completed_steps.push(stepName);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Update last_run timestamp
|
|
106
|
-
state.last_run = new Date().toISOString();
|
|
107
|
-
|
|
108
|
-
// Save updated state
|
|
109
|
-
await saveSetupState(projectPath, state);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Save setup state to .forge/setup-state.json
|
|
6
|
+
* @param {string} projectPath - Path to the project root
|
|
7
|
+
* @param {Object} state - Setup state object
|
|
8
|
+
* @param {string} state.version - Forge version
|
|
9
|
+
* @param {string[]} state.completed_steps - List of completed steps
|
|
10
|
+
* @param {string[]} state.pending_steps - List of pending steps
|
|
11
|
+
* @param {string} [state.last_run] - ISO timestamp of last run
|
|
12
|
+
* @returns {Promise<void>}
|
|
13
|
+
*/
|
|
14
|
+
async function saveSetupState(projectPath, state) {
|
|
15
|
+
const forgeDir = path.join(projectPath, '.forge');
|
|
16
|
+
const setupStatePath = path.join(forgeDir, 'setup-state.json');
|
|
17
|
+
|
|
18
|
+
// Ensure .forge directory exists
|
|
19
|
+
await fs.promises.mkdir(forgeDir, { recursive: true });
|
|
20
|
+
|
|
21
|
+
// Add last_run timestamp if not provided
|
|
22
|
+
if (!state.last_run) {
|
|
23
|
+
state.last_run = new Date().toISOString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Write state as JSON
|
|
27
|
+
await fs.promises.writeFile(setupStatePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load setup state from .forge/setup-state.json
|
|
32
|
+
* @param {string} projectPath - Path to the project root
|
|
33
|
+
* @returns {Promise<Object|null>} Setup state object, or null if not found or invalid
|
|
34
|
+
*/
|
|
35
|
+
async function loadSetupState(projectPath) {
|
|
36
|
+
const setupStatePath = path.join(projectPath, '.forge', 'setup-state.json');
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const content = await fs.promises.readFile(setupStatePath, 'utf-8');
|
|
40
|
+
return JSON.parse(content);
|
|
41
|
+
} catch (_error) {
|
|
42
|
+
// File doesn't exist or invalid JSON
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if setup is complete (no pending steps)
|
|
49
|
+
* @param {string} projectPath - Path to the project root
|
|
50
|
+
* @returns {Promise<boolean>} True if setup complete, false otherwise
|
|
51
|
+
*/
|
|
52
|
+
async function isSetupComplete(projectPath) {
|
|
53
|
+
const state = await loadSetupState(projectPath);
|
|
54
|
+
|
|
55
|
+
if (!state) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Setup is complete if pending_steps is empty
|
|
60
|
+
return state.pending_steps.length === 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the next pending step
|
|
65
|
+
* @param {string} projectPath - Path to the project root
|
|
66
|
+
* @returns {Promise<string|null>} Next step name, or null if none
|
|
67
|
+
*/
|
|
68
|
+
async function getNextStep(projectPath) {
|
|
69
|
+
const state = await loadSetupState(projectPath);
|
|
70
|
+
|
|
71
|
+
if (!state || !state.pending_steps || state.pending_steps.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Return first pending step
|
|
76
|
+
return state.pending_steps[0];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Mark a step as complete (move from pending to completed)
|
|
81
|
+
* @param {string} projectPath - Path to the project root
|
|
82
|
+
* @param {string} stepName - Name of the step to mark complete
|
|
83
|
+
* @returns {Promise<void>}
|
|
84
|
+
*/
|
|
85
|
+
async function markStepComplete(projectPath, stepName) {
|
|
86
|
+
let state = await loadSetupState(projectPath);
|
|
87
|
+
|
|
88
|
+
// If no state exists, create initial state
|
|
89
|
+
if (!state) {
|
|
90
|
+
state = {
|
|
91
|
+
version: '1.6.0',
|
|
92
|
+
completed_steps: [],
|
|
93
|
+
pending_steps: []
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Remove from pending_steps if present
|
|
98
|
+
state.pending_steps = state.pending_steps.filter(step => step !== stepName);
|
|
99
|
+
|
|
100
|
+
// Add to completed_steps if not already there
|
|
101
|
+
if (!state.completed_steps.includes(stepName)) {
|
|
102
|
+
state.completed_steps.push(stepName);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Update last_run timestamp
|
|
106
|
+
state.last_run = new Date().toISOString();
|
|
107
|
+
|
|
108
|
+
// Save updated state
|
|
109
|
+
await saveSetupState(projectPath, state);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Scaffold GitHub-Beads sync files into a target project.
|
|
114
|
+
* Copies workflow, config, and mapping template — never overwrites existing files.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} projectPath - Target project root
|
|
117
|
+
* @param {string} pkgDir - Forge package directory (source of template files)
|
|
118
|
+
* @returns {Promise<{created: string[], skipped: string[]}>}
|
|
119
|
+
*/
|
|
120
|
+
async function scaffoldGithubBeadsSync(projectPath, pkgDir) {
|
|
121
|
+
const created = [];
|
|
122
|
+
const skipped = [];
|
|
123
|
+
|
|
124
|
+
// Sync script modules that workflows depend on at runtime
|
|
125
|
+
const syncScripts = [
|
|
126
|
+
'config.mjs', 'mapping.mjs', 'comment.mjs', 'github-api.mjs',
|
|
127
|
+
'sanitize.mjs', 'run-bd.mjs', 'label-mapper.mjs', 'index.mjs',
|
|
128
|
+
'reverse-sync.mjs', 'reverse-sync-cli.mjs',
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const files = [
|
|
132
|
+
// Phase 1: GitHub → Beads
|
|
133
|
+
{
|
|
134
|
+
src: path.join(pkgDir, '.github', 'workflows', 'github-to-beads.yml'),
|
|
135
|
+
dest: path.join('.github', 'workflows', 'github-to-beads.yml'),
|
|
136
|
+
},
|
|
137
|
+
// Phase 2: Beads → GitHub
|
|
138
|
+
{
|
|
139
|
+
src: path.join(pkgDir, '.github', 'workflows', 'beads-to-github.yml'),
|
|
140
|
+
dest: path.join('.github', 'workflows', 'beads-to-github.yml'),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
src: path.join(pkgDir, 'scripts', 'github-beads-sync.config.json'),
|
|
144
|
+
dest: path.join('scripts', 'github-beads-sync.config.json'),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
src: null, // generated in-place (empty mapping template)
|
|
148
|
+
dest: path.join('.github', 'beads-mapping.json'),
|
|
149
|
+
content: '{}',
|
|
150
|
+
},
|
|
151
|
+
// Sync script modules
|
|
152
|
+
...syncScripts.map((name) => ({
|
|
153
|
+
src: path.join(pkgDir, 'scripts', 'github-beads-sync', name),
|
|
154
|
+
dest: path.join('scripts', 'github-beads-sync', name),
|
|
155
|
+
})),
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
const destPath = path.join(projectPath, file.dest);
|
|
160
|
+
|
|
161
|
+
// Never overwrite existing files — preserve user customizations
|
|
162
|
+
if (fs.existsSync(destPath)) {
|
|
163
|
+
skipped.push(file.dest);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Ensure parent directory exists
|
|
168
|
+
const destDir = path.dirname(destPath);
|
|
169
|
+
await fs.promises.mkdir(destDir, { recursive: true });
|
|
170
|
+
|
|
171
|
+
if (file.src) {
|
|
172
|
+
// Copy from template
|
|
173
|
+
await fs.promises.copyFile(file.src, destPath);
|
|
174
|
+
} else {
|
|
175
|
+
// Write generated content
|
|
176
|
+
await fs.promises.writeFile(destPath, file.content, 'utf-8');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
created.push(file.dest);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { created, skipped };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
saveSetupState,
|
|
187
|
+
loadSetupState,
|
|
188
|
+
isSetupComplete,
|
|
189
|
+
getNextStep,
|
|
190
|
+
markStepComplete,
|
|
191
|
+
scaffoldGithubBeadsSync
|
|
192
|
+
};
|