@guidobuilds/forge-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Lex Christopherson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # Forge
2
+
3
+ Forge is a personal project for working with code agents more reliably.
4
+
5
+ It started as a way to get better results from OpenCode, and now also supports Codex and Claude Code. The idea is to give agents a lightweight operating model for turning vague software requests into smaller, safer, verifiable changes without adding a heavy process around them.
6
+
7
+ Forge is experimental. It is shaped by hands-on use, and the workflow may change as I learn what works and what does not.
8
+
9
+ ## Why It Exists
10
+
11
+ Code agents are useful, but they often fail in predictable ways: they start coding too early, lose context between steps, overbuild, or make changes without a clear verification path.
12
+
13
+ Forge is my attempt to make that work more disciplined:
14
+
15
+ - clarify intent before implementation when it matters
16
+ - choose the smallest safe workflow for each request
17
+ - keep long-running context in durable project notes
18
+ - separate orchestration from execution
19
+ - make verification part of the work, not an afterthought
20
+
21
+ Forge is intentionally minimal. It is not a plugin marketplace, a new IDE, or a replacement for your agent. It is just a small workflow layer for getting agents to pause, inspect, plan when needed, and verify their work.
22
+
23
+ ## What It Does
24
+
25
+ Forge adds a structured agent workflow for:
26
+
27
+ - inspect an existing codebase before making changes
28
+ - produce design notes for ambiguous or high-risk work
29
+ - turn approved direction into an executable plan
30
+ - implement focused changes with a minimum-change bias
31
+ - run or document validation after implementation
32
+ - preserve important decisions and follow-ups under `.forge/`
33
+
34
+ For simple requests, Forge should stay out of the way and take the shortest safe path. For larger changes, it can slow the process down just enough to reduce rework and bad assumptions.
35
+
36
+ ## How It Works
37
+
38
+ Forge uses a thin orchestrator and a single worker model.
39
+
40
+ The orchestrator decides how much process a request needs. The worker does the actual inspection, design, planning, building, operating, or verification work. This keeps the user conversation focused while still giving the agent a repeatable execution pattern.
41
+
42
+ Typical routes include:
43
+
44
+ ```text
45
+ inspect -> build -> verify
46
+ inspect -> design -> plan -> build -> verify
47
+ build -> verify
48
+ ```
49
+
50
+ There is no mandatory lifecycle. Forge tries to choose the lightest safe path based on the task, risk, and available context.
51
+
52
+ ## Durable Context
53
+
54
+ When a task benefits from persistent context, Forge writes notes under `.forge/<feature-slug>/`.
55
+
56
+ These notes are useful for:
57
+
58
+ - resuming work across agent sessions
59
+ - reviewing the reasoning behind a change
60
+ - keeping implementation aligned with approved decisions
61
+ - making follow-up work easier to delegate
62
+
63
+ Small, obvious changes do not need ceremony. The goal is to use durable artifacts only when they reduce ambiguity or risk.
64
+
65
+ ## Supported Agents
66
+
67
+ Forge currently installs support for:
68
+
69
+ - OpenCode
70
+ - Codex
71
+ - Claude Code
72
+
73
+ The same operating model is shared across all supported agents so the workflow stays mostly consistent even when the underlying tool changes.
74
+
75
+ ## Installation
76
+
77
+ The primary installer is the npm CLI:
78
+
79
+ ```sh
80
+ npx forge-ai install
81
+ ```
82
+
83
+ The installer prompts for the target agent platform and whether Forge should be installed globally for your user or locally for the current project.
84
+
85
+ To update an existing install:
86
+
87
+ ```sh
88
+ npx forge-ai update
89
+ ```
90
+
91
+ For non-interactive environments:
92
+
93
+ ```sh
94
+ npx forge-ai install --platform all --scope user --yes
95
+ ```
96
+
97
+ Preview the files without writing them:
98
+
99
+ ```sh
100
+ npx forge-ai install --platform all --scope user --dry-run
101
+ ```
102
+
103
+ Validate a local Forge source tree:
104
+
105
+ ```sh
106
+ npx forge-ai validate --source .
107
+ ```
108
+
109
+ ## Local Development
110
+
111
+ From a local checkout:
112
+
113
+ ```sh
114
+ npm install
115
+ npm run build
116
+ node bin/forge-ai.mjs install --source . --platform all --scope user
117
+ ```
118
+
119
+ To preview local output without writing:
120
+
121
+ ```sh
122
+ node bin/forge-ai.mjs install --source . --platform all --scope project --dry-run
123
+ ```
124
+
125
+ ## Updating
126
+
127
+ Run the npm updater:
128
+
129
+ ```sh
130
+ npx forge-ai update
131
+ ```
132
+
133
+ Forge replaces its managed agent and skill definitions in your supported agent configuration directories.
134
+
135
+ ## Uninstalling
136
+
137
+ Remove Forge from the agent configuration directories for OpenCode, Codex, or Claude Code by deleting the installed Forge agent and skill entries.
138
+
139
+ If you installed Forge for multiple tools, repeat the removal for each one you no longer want to use.
140
+
141
+ ## Project Status
142
+
143
+ Forge is experimental and personal.
144
+
145
+ There is no promise that the workflow will stay stable or that every agent/tool combination will keep working the same way. I am using it, changing it, and keeping the parts that make agent work better in practice.
146
+
147
+ Feedback, issues, and pull requests are welcome, especially when they come from real usage.
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: forge-worker
3
+ description: Forge universal worker for inspect, design, plan, build, operate, and verify work
4
+ claude:
5
+ permissions:
6
+ tools: [TodoWrite, Read, Write, Edit, Bash, Glob, Grep, LS, MultiEdit, WebFetch]
7
+ opencode:
8
+ mode: subagent
9
+ permissions:
10
+ todowrite: true
11
+ read: true
12
+ write: true
13
+ edit: true
14
+ bash: true
15
+ glob: true
16
+ grep: true
17
+ list: true
18
+ patch: true
19
+ skill: true
20
+ webfetch: true
21
+ ---
22
+
23
+ You are the Forge worker.
24
+
25
+ Load and follow the `forge-worker` skill before doing work.
26
+
27
+ You are the only worker type in Forge. The orchestrator may launch multiple instances of you in parallel or sequence.
28
+
29
+ ## Inputs
30
+ - Orchestrator prompt with the assigned subgoal, expected boundaries, and any approval context.
31
+ - Optional: `.forge/<feature-slug>/explore.md`
32
+ - Optional: `.forge/<feature-slug>/design.md`
33
+ - Optional: `.forge/<feature-slug>/plan.md`
34
+ - Optional: `.forge/<feature-slug>/build-log.md`
35
+
36
+ The skill defines routing by work type, artifact guidance, approval handling, bounded execution, escalation rules, and validation expectations.
37
+
38
+ ## Contract (strict)
39
+ Return only:
40
+
41
+ ```text
42
+ STATUS: success|partial|blocked
43
+ WORK_TYPE: inspect|design|plan|build|operate|verify|mixed
44
+ FEATURE_SLUG: <kebab-case>
45
+ ARTIFACTS:
46
+ - <path or None>
47
+ SUMMARY:
48
+ - <brief point>
49
+ NEXT_RECOMMENDED: inspect|design|plan|build|operate|verify|ask-user|none
50
+ RISKS:
51
+ - <risk or None>
52
+ QUESTIONS:
53
+ 1) <question>
54
+ 2) <question>
55
+ ```
56
+
57
+ Include `QUESTIONS` only when blocked.
58
+
59
+ Do not interact directly with the user. Escalate open decisions back to the orchestrator through the contract.
60
+ Do not add extra format outside the defined worker contract.
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: forge
3
+ description: Forge orchestrator with dynamic runtime routing and a single worker type
4
+ claude:
5
+ permissions:
6
+ tools: [Task, AskUserQuestion, TodoWrite]
7
+ opencode:
8
+ mode: primary
9
+ permissions:
10
+ task: true
11
+ question: true
12
+ todowrite: true
13
+ ---
14
+
15
+ # Role
16
+ You are Forge, the Forge orchestrator.
17
+
18
+ Load and follow the `using-forge` skill before routing work.
19
+
20
+ You are a coordinator, not an executor.
21
+
22
+ The `using-forge` skill owns runtime routing, operating principles, approval heuristics, artifact conventions, concurrency guidance, and shared definitions.
23
+
24
+ ## Orchestrator rules
25
+ - Never do worker work inline.
26
+ - Never do non-development execution work inline.
27
+ - Delegate all technical and operational work to Forge workers.
28
+ - Keep one thin thread with the user.
29
+ - Choose the lightest safe routing permitted by the skill.
30
+ - Enforce the Forge worker contract strictly.
31
+
32
+ ## Worker model
33
+ - `forge-worker` is the only worker type in Forge.
34
+ - You may launch one worker instance for a bounded task.
35
+ - You may launch multiple `forge-worker` instances in sequence when one result should shape the next delegation.
36
+ - You may launch multiple `forge-worker` instances in parallel when subgoals are sufficiently independent.
37
+ - Keep each worker invocation narrowly scoped so multiple instances do not collide on the same ownership or files unless deliberate.
38
+
39
+ ## Contract enforcement
40
+ Each worker response must include:
41
+
42
+ ```text
43
+ STATUS: success|partial|blocked
44
+ WORK_TYPE: inspect|design|plan|build|operate|verify|mixed
45
+ FEATURE_SLUG: <kebab-case>
46
+ ARTIFACTS:
47
+ - <path or None>
48
+ SUMMARY:
49
+ - <point>
50
+ NEXT_RECOMMENDED: inspect|design|plan|build|operate|verify|ask-user|none
51
+ RISKS:
52
+ - <risk or None>
53
+ QUESTIONS:
54
+ 1) <question>
55
+ ```
56
+
57
+ `QUESTIONS` appears only when `STATUS: blocked`.
58
+
59
+ If output is malformed:
60
+ 1) request one reformat retry with same task_id
61
+ 2) if malformed again, stop with actionable error
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../dist/src/cli.js';
3
+
4
+ main().then((code) => { process.exitCode = code; }, (error) => {
5
+ console.error(error);
6
+ process.exitCode = 1;
7
+ });
@@ -0,0 +1,32 @@
1
+ import { stringifyYaml } from '../frontmatter.js';
2
+ import { diagnostic } from '../diagnostics.js';
3
+ import { isRecord, stringList } from './shared.js';
4
+ export function renderClaudeAgent(agent) {
5
+ const diagnostics = [];
6
+ const fm = { name: agent.name, description: agent.description };
7
+ if (agent.claude?.model)
8
+ fm.model = agent.claude.model;
9
+ const permissions = agent.claude?.permissions;
10
+ const tools = isRecord(permissions) ? stringList(permissions.tools) : stringList(permissions);
11
+ if (tools)
12
+ fm.tools = tools;
13
+ else if (permissions !== undefined)
14
+ diagnostics.push(diagnostic('info', 'CLAUDE_AGENT_TOOLS_IGNORED', `Claude agent permissions must be a tools string list for ${agent.name}`, { platform: 'claude' }));
15
+ return { content: `${stringifyYaml(fm)}${agent.definition}\n`, diagnostics };
16
+ }
17
+ export function renderClaudeSkill(skill) {
18
+ const diagnostics = [];
19
+ const fm = { name: skill.name, description: skill.description };
20
+ const permissions = skill.claude?.permissions;
21
+ const allowedTools = isRecord(permissions) ? stringList(permissions['allowed-tools']) : undefined;
22
+ if (allowedTools) {
23
+ fm['allowed-tools'] = allowedTools;
24
+ diagnostics.push(diagnostic('warning', 'CLAUDE_SKILL_ALLOWED_TOOLS', `Claude skill allowed-tools preapproves tools but does not universally restrict them for ${skill.name}`, { platform: 'claude' }));
25
+ }
26
+ else if (permissions !== undefined) {
27
+ diagnostics.push(diagnostic('info', 'CLAUDE_SKILL_PERMISSIONS_IGNORED', `Claude skill permissions are not emitted for ${skill.name}`, { platform: 'claude' }));
28
+ }
29
+ if (skill.claude?.model)
30
+ diagnostics.push(diagnostic('info', 'CLAUDE_SKILL_MODEL_IGNORED', `Claude skill model is not emitted for ${skill.name}`, { platform: 'claude' }));
31
+ return { content: `${stringifyYaml(fm)}${skill.instructions}\n`, diagnostics };
32
+ }
@@ -0,0 +1,29 @@
1
+ import { stringifyYaml } from '../frontmatter.js';
2
+ import { diagnostic } from '../diagnostics.js';
3
+ import { isRecord, tomlString } from './shared.js';
4
+ const safeSandboxModes = new Set(['read-only', 'workspace-write']);
5
+ export function renderCodexAgent(agent) {
6
+ const diagnostics = [diagnostic('info', 'CODEX_PARTIAL_AGENT_SUPPORT', `Codex agent output is partial and does not generate AGENTS.md or profiles for ${agent.name}`, { platform: 'codex' })];
7
+ const lines = [`name = ${tomlString(agent.name)}`, `description = ${tomlString(agent.description)}`, `developer_instructions = ${tomlString(agent.definition)}`];
8
+ if (agent.codex?.model)
9
+ lines.push(`model = ${tomlString(agent.codex.model)}`);
10
+ const permissions = agent.codex?.permissions;
11
+ if (isRecord(permissions) && typeof permissions.sandbox_mode === 'string') {
12
+ if (safeSandboxModes.has(permissions.sandbox_mode))
13
+ lines.push(`sandbox_mode = ${tomlString(permissions.sandbox_mode)}`);
14
+ else
15
+ diagnostics.push(diagnostic('warning', 'CODEX_UNSAFE_SANDBOX_IGNORED', `Unsafe Codex sandbox_mode ignored for ${agent.name}`, { platform: 'codex' }));
16
+ }
17
+ else if (permissions !== undefined) {
18
+ diagnostics.push(diagnostic('info', 'CODEX_AGENT_PERMISSIONS_IGNORED', `Codex agent permissions are not emitted for ${agent.name}`, { platform: 'codex' }));
19
+ }
20
+ return { content: `${lines.join('\n')}\n`, diagnostics };
21
+ }
22
+ export function renderCodexSkill(skill) {
23
+ const diagnostics = [];
24
+ if (skill.codex?.permissions)
25
+ diagnostics.push(diagnostic('info', 'CODEX_SKILL_PERMISSIONS_IGNORED', `Codex skill permissions are not emitted for ${skill.name}`, { platform: 'codex' }));
26
+ if (skill.codex?.model)
27
+ diagnostics.push(diagnostic('info', 'CODEX_SKILL_MODEL_IGNORED', `Codex skill model is not emitted for ${skill.name}`, { platform: 'codex' }));
28
+ return { content: `${stringifyYaml({ name: skill.name, description: skill.description })}${skill.instructions}\n`, diagnostics };
29
+ }
@@ -0,0 +1,20 @@
1
+ import { stringifyYaml } from '../frontmatter.js';
2
+ import { diagnostic } from '../diagnostics.js';
3
+ export function renderOpenCodeAgent(agent) {
4
+ const fm = { description: agent.description };
5
+ if (agent.opencode?.mode)
6
+ fm.mode = agent.opencode.mode;
7
+ if (agent.opencode?.model)
8
+ fm.model = agent.opencode.model;
9
+ if (agent.opencode?.permissions)
10
+ fm.permission = agent.opencode.permissions;
11
+ return { content: `${stringifyYaml(fm)}${agent.definition}\n`, diagnostics: [] };
12
+ }
13
+ export function renderOpenCodeSkill(skill) {
14
+ const diagnostics = [];
15
+ if (skill.opencode?.permissions)
16
+ diagnostics.push(diagnostic('info', 'OPENCODE_SKILL_PERMISSIONS_IGNORED', `OpenCode skill permissions are not emitted for ${skill.name}`, { platform: 'opencode' }));
17
+ if (skill.opencode?.model)
18
+ diagnostics.push(diagnostic('info', 'OPENCODE_SKILL_MODEL_IGNORED', `OpenCode skill model is not emitted for ${skill.name}`, { platform: 'opencode' }));
19
+ return { content: `${stringifyYaml({ name: skill.name, description: skill.description })}${skill.instructions}\n`, diagnostics };
20
+ }
@@ -0,0 +1,9 @@
1
+ export function isRecord(value) {
2
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
3
+ }
4
+ export function stringList(value) {
5
+ return Array.isArray(value) && value.every((item) => typeof item === 'string' && /^[A-Za-z0-9_*.,:-]+$/.test(item)) ? value : undefined;
6
+ }
7
+ export function tomlString(value) {
8
+ return JSON.stringify(value);
9
+ }
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ import * as p from '@clack/prompts';
3
+ import pc from 'picocolors';
4
+ import { readFileSync } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { formatDiagnostic, hasErrors } from './diagnostics.js';
8
+ import { buildWritePlan, parsePlatform, parseScope } from './processor.js';
9
+ import { writeOutputs } from './writer.js';
10
+ export async function main(argv = process.argv.slice(2), promptIO = {}) {
11
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
12
+ showUsage();
13
+ return 0;
14
+ }
15
+ if (argv[0] === '--version' || argv[0] === '-v') {
16
+ console.log(readPackageVersion());
17
+ return 0;
18
+ }
19
+ const parsed = parseArgs(argv);
20
+ if ('error' in parsed) {
21
+ console.error(parsed.error);
22
+ return 1;
23
+ }
24
+ const options = parsed.options;
25
+ const command = normalizeCommand(options.command);
26
+ if (!command) {
27
+ console.error(`Unknown command ${options.command ?? ''}`);
28
+ showUsage();
29
+ return 1;
30
+ }
31
+ const install = command === 'install' || command === 'update';
32
+ if (install && !options.sourceExplicit)
33
+ options.source = bundledSourceRoot();
34
+ if (command === 'update' || options.yes)
35
+ options.force = true;
36
+ const interactive = install && isInteractivePrompt(promptIO);
37
+ if (interactive)
38
+ p.intro(`${pc.bold('Forge AI')} ${pc.dim(command === 'update' ? 'updater' : 'installer')}`, clackIO(promptIO));
39
+ if (install) {
40
+ const prompted = await promptForMissingInstallOptions(options, promptIO);
41
+ if (!prompted) {
42
+ if (interactive)
43
+ p.cancel('Cancelled', clackIO(promptIO));
44
+ return 1;
45
+ }
46
+ }
47
+ let plan = await buildWritePlan({ source: options.source, platform: options.platform, scope: options.scope, checkCollisions: install && !options.dryRun, force: options.force });
48
+ if (install && !options.dryRun && !options.force && canOfferUpdate(plan.diagnostics)) {
49
+ const accepted = await promptForUpdate(plan, promptIO);
50
+ if (accepted === undefined) {
51
+ if (interactive)
52
+ p.cancel('Cancelled', clackIO(promptIO));
53
+ return 1;
54
+ }
55
+ if (accepted) {
56
+ options.force = true;
57
+ plan = await buildWritePlan({ source: options.source, platform: options.platform, scope: options.scope, checkCollisions: true, force: true });
58
+ }
59
+ }
60
+ printPlan(command, plan.sourceCount, plan.files, plan.diagnostics);
61
+ if (hasErrors(plan.diagnostics)) {
62
+ if (interactive)
63
+ p.outro(pc.red('Forge was not installed.'), clackIO(promptIO));
64
+ return 1;
65
+ }
66
+ if (install && !options.dryRun) {
67
+ if (interactive) {
68
+ const spinner = p.spinner(clackIO(promptIO));
69
+ spinner.start(options.force ? 'Updating Forge files' : 'Installing Forge files');
70
+ try {
71
+ await writeOutputs(plan.files);
72
+ spinner.stop(`Wrote ${plan.files.length} file(s).`);
73
+ }
74
+ catch (error) {
75
+ spinner.error('Failed to write Forge files');
76
+ throw error;
77
+ }
78
+ }
79
+ else {
80
+ await writeOutputs(plan.files);
81
+ console.log(`Wrote ${plan.files.length} file(s).`);
82
+ }
83
+ }
84
+ else if (install && interactive) {
85
+ p.log.info(`Dry run only. ${plan.files.length} file(s) would be written.`, clackIO(promptIO));
86
+ }
87
+ if (install && interactive) {
88
+ p.outro(options.dryRun ? pc.cyan('Dry run complete.') : pc.green('Forge is ready.'), clackIO(promptIO));
89
+ }
90
+ return 0;
91
+ }
92
+ function parseArgs(argv) {
93
+ const options = { command: argv[0], platform: 'all', scope: 'user', source: '.', dryRun: false, force: false, yes: false, platformExplicit: false, scopeExplicit: false, sourceExplicit: false };
94
+ for (let index = 1; index < argv.length; index += 1) {
95
+ const arg = argv[index];
96
+ if (arg === '--dry-run')
97
+ options.dryRun = true;
98
+ else if (arg === '--force')
99
+ options.force = true;
100
+ else if (arg === '--yes' || arg === '-y')
101
+ options.yes = true;
102
+ else if (arg === '--platform') {
103
+ const value = argv[++index];
104
+ const platform = value ? parsePlatform(value) : undefined;
105
+ if (!platform)
106
+ return { error: `Invalid --platform ${value ?? ''}` };
107
+ options.platform = platform;
108
+ options.platformExplicit = true;
109
+ }
110
+ else if (arg === '--scope') {
111
+ const value = argv[++index];
112
+ const scope = value ? parseScope(value) : undefined;
113
+ if (!scope)
114
+ return { error: `Invalid --scope ${value ?? ''}` };
115
+ options.scope = scope;
116
+ options.scopeExplicit = true;
117
+ }
118
+ else if (arg === '--source') {
119
+ const value = argv[++index];
120
+ if (!value)
121
+ return { error: 'Missing --source value' };
122
+ options.source = value;
123
+ options.sourceExplicit = true;
124
+ }
125
+ else {
126
+ return { error: `Unknown argument ${arg}` };
127
+ }
128
+ }
129
+ const command = normalizeCommand(options.command);
130
+ if (command === 'validate' && (options.dryRun || options.force || options.yes || options.scopeExplicit))
131
+ return { error: 'validate only accepts --platform and --source' };
132
+ return { options };
133
+ }
134
+ async function promptForMissingInstallOptions(options, promptIO) {
135
+ if (options.yes)
136
+ return true;
137
+ if (options.platformExplicit && options.scopeExplicit)
138
+ return true;
139
+ if (!isInteractivePrompt(promptIO))
140
+ return true;
141
+ const io = clackIO(promptIO);
142
+ if (!options.platformExplicit) {
143
+ const platform = await p.select({
144
+ message: 'Install Forge for which coding agent?',
145
+ initialValue: 'all',
146
+ options: [
147
+ { value: 'all', label: 'All supported agents', hint: 'OpenCode, Codex, and Claude Code' },
148
+ { value: 'opencode', label: 'OpenCode' },
149
+ { value: 'codex', label: 'Codex' },
150
+ { value: 'claude', label: 'Claude Code' }
151
+ ],
152
+ ...io
153
+ });
154
+ if (p.isCancel(platform))
155
+ return false;
156
+ options.platform = platform;
157
+ }
158
+ if (!options.scopeExplicit) {
159
+ const scope = await p.select({
160
+ message: 'Where should Forge be installed?',
161
+ initialValue: 'user',
162
+ options: [
163
+ { value: 'user', label: 'User', hint: 'Available in every project' },
164
+ { value: 'project', label: 'Project', hint: 'Only this repository' }
165
+ ],
166
+ ...io
167
+ });
168
+ if (p.isCancel(scope))
169
+ return false;
170
+ options.scope = scope;
171
+ }
172
+ return true;
173
+ }
174
+ async function promptForUpdate(plan, promptIO) {
175
+ if (!isInteractivePrompt(promptIO))
176
+ return false;
177
+ const existing = plan.diagnostics.filter((item) => item.code === 'DESTINATION_EXISTS');
178
+ const count = existing.length;
179
+ p.log.warn(`${count} Forge output${count === 1 ? '' : 's'} already exist.`, clackIO(promptIO));
180
+ const accepted = await p.confirm({
181
+ message: 'Update the existing Forge files?',
182
+ active: 'Update',
183
+ inactive: 'Cancel',
184
+ initialValue: false,
185
+ ...clackIO(promptIO)
186
+ });
187
+ if (p.isCancel(accepted))
188
+ return undefined;
189
+ return accepted;
190
+ }
191
+ function normalizeCommand(command) {
192
+ if (command === 'install' || command === 'i')
193
+ return 'install';
194
+ if (command === 'update' || command === 'upgrade')
195
+ return 'update';
196
+ if (command === 'validate')
197
+ return 'validate';
198
+ return undefined;
199
+ }
200
+ function canOfferUpdate(diagnostics) {
201
+ const errors = diagnostics.filter((item) => item.severity === 'error');
202
+ return errors.length > 0 && errors.every((item) => item.code === 'DESTINATION_EXISTS');
203
+ }
204
+ function isInteractivePrompt(promptIO) {
205
+ const env = promptIO.env ?? process.env;
206
+ const interactive = promptIO.isInteractive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
207
+ return interactive && env.CI !== 'true';
208
+ }
209
+ function clackIO(promptIO) {
210
+ return { input: promptIO.input, output: promptIO.output };
211
+ }
212
+ function bundledSourceRoot() {
213
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
214
+ }
215
+ function readPackageVersion() {
216
+ try {
217
+ const packageJson = JSON.parse(readFileSync(path.join(bundledSourceRoot(), 'package.json'), 'utf8'));
218
+ return typeof packageJson.version === 'string' ? packageJson.version : '0.0.0';
219
+ }
220
+ catch {
221
+ return '0.0.0';
222
+ }
223
+ }
224
+ function showUsage() {
225
+ console.log('Usage: forge-ai install [--platform opencode|claude|codex|all] [--scope user|project] [--source <dir>] [--dry-run] [--force] [--yes]');
226
+ console.log(' forge-ai update [--platform opencode|claude|codex|all] [--scope user|project] [--source <dir>] [--dry-run] [--yes]');
227
+ console.log(' forge-ai validate [--platform opencode|claude|codex|all] [--source <dir>]');
228
+ }
229
+ function printPlan(command, sourceCount, files, diagnostics) {
230
+ console.log(`${command}: ${sourceCount} source(s), ${files.length} output(s)`);
231
+ for (const file of files)
232
+ console.log(`- ${file.platform} ${file.kind} ${file.name} -> ${file.path}`);
233
+ for (const item of diagnostics)
234
+ console.log(formatDiagnostic(item));
235
+ }
236
+ if (import.meta.url === `file://${process.argv[1]}`) {
237
+ main().then((code) => { process.exitCode = code; }, (error) => { console.error(error); process.exitCode = 1; });
238
+ }
@@ -0,0 +1,14 @@
1
+ export function diagnostic(severity, code, message, extras = {}) {
2
+ return { severity, code, message, ...extras };
3
+ }
4
+ export function hasErrors(diagnostics) {
5
+ return diagnostics.some((diagnostic) => diagnostic.severity === 'error');
6
+ }
7
+ export function formatDiagnostic(diagnostic) {
8
+ const location = diagnostic.sourcePath ? ` ${diagnostic.sourcePath}` : '';
9
+ const platform = diagnostic.platform ? ` [${diagnostic.platform}]` : '';
10
+ return `${diagnostic.severity.toUpperCase()} ${diagnostic.code}${platform}${location}: ${diagnostic.message}`;
11
+ }
12
+ export function info(code, message, platform) {
13
+ return diagnostic('info', code, message, platform ? { platform } : {});
14
+ }