@warpgogol/forge 2.8.0 → 2.8.2
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/AGENTS.md +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/os/session/handlers/validate.d.ts.map +1 -1
- package/dist/os/session/handlers/validate.js +23 -1
- package/dist/os/session/handlers/validate.js.map +1 -1
- package/dist/os/session/types.d.ts +31 -1
- package/dist/os/session/types.d.ts.map +1 -1
- package/dist/os/session/types.js +7 -0
- package/dist/os/session/types.js.map +1 -1
- package/os/adr/adr.module.ts +158 -0
- package/os/adr/frontmatter-io.ts +80 -0
- package/os/adr/handlers/archive.ts +244 -0
- package/os/adr/handlers/implement-stamp.ts +331 -0
- package/os/adr/handlers/list-create.ts +207 -0
- package/os/adr/handlers/validate.test.ts +232 -0
- package/os/adr/handlers/validate.ts +429 -0
- package/os/adr/index.ts +33 -0
- package/os/adr/types.ts +141 -0
- package/os/audit/audit.module.ts +48 -0
- package/os/audit/frontmatter-io.ts +84 -0
- package/os/audit/handlers/archive.ts +239 -0
- package/os/audit/index.ts +23 -0
- package/os/audit/types.ts +29 -0
- package/os/compass/compass.module.ts +186 -0
- package/os/compass/handlers/compass-audit-handler.ts +382 -0
- package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
- package/os/compass/handlers/compass-inventory-handler.ts +294 -0
- package/os/compass/handlers/compass-inventory.ts +521 -0
- package/os/compass/handlers/git-revision.ts +137 -0
- package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
- package/os/compass/handlers/resolve-scan-root.ts +79 -0
- package/os/compass/index.ts +33 -0
- package/os/core/core.module.ts +948 -0
- package/os/core/handlers/assets-check.ts +122 -0
- package/os/core/handlers/assets-helpers.ts +174 -0
- package/os/core/handlers/assets-list.ts +99 -0
- package/os/core/handlers/build.ts +155 -0
- package/os/core/handlers/determinism-check.ts +354 -0
- package/os/core/handlers/dev.ts +127 -0
- package/os/core/handlers/invariant-engine.test.ts +681 -0
- package/os/core/handlers/knowledge-compact.ts +248 -0
- package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
- package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
- package/os/core/handlers/note-link-validate.test.ts +117 -0
- package/os/core/handlers/note-orphan-detect.test.ts +94 -0
- package/os/core/handlers/package-health.test.ts +226 -0
- package/os/core/handlers/package-health.ts +231 -0
- package/os/core/handlers/pinned-check.ts +200 -0
- package/os/core/handlers/pinned-init.ts +333 -0
- package/os/core/handlers/pinned-types.ts +50 -0
- package/os/core/handlers/pinned-validate.ts +301 -0
- package/os/core/handlers/profile-resolve.ts +92 -0
- package/os/core/handlers/release-prepare.ts +294 -0
- package/os/core/handlers/release-publish.ts +211 -0
- package/os/core/handlers/validate.ts +262 -0
- package/os/core/handlers/workspace-deps.ts +55 -0
- package/os/core/index.ts +13 -0
- package/os/exploration/exploration.module.ts +90 -0
- package/os/exploration/frontmatter-io.ts +77 -0
- package/os/exploration/handlers/archive.ts +159 -0
- package/os/exploration/handlers/list.ts +77 -0
- package/os/exploration/handlers/show.ts +107 -0
- package/os/exploration/index.ts +25 -0
- package/os/exploration/types.ts +65 -0
- package/os/mission/handlers/archive.test.ts +391 -0
- package/os/mission/handlers/archive.ts +453 -0
- package/os/mission/index.ts +1 -0
- package/os/mission/mission.module.ts +48 -0
- package/os/mission/types.ts +41 -0
- package/os/naming/index.ts +14 -0
- package/os/naming/naming-convention.test.ts +163 -0
- package/os/naming/naming-convention.ts +378 -0
- package/os/naming/naming.module.ts +36 -0
- package/os/notes/index.ts +10 -0
- package/os/notes/notes.module.ts +109 -0
- package/os/plan/frontmatter-io.ts +82 -0
- package/os/plan/handlers/archive.ts +238 -0
- package/os/plan/index.ts +23 -0
- package/os/plan/plan.module.ts +48 -0
- package/os/plan/types.ts +29 -0
- package/os/program/discovery.ts +294 -0
- package/os/program/handlers/complete.ts +419 -0
- package/os/program/handlers/lease.ts +433 -0
- package/os/program/handlers/seal.ts +332 -0
- package/os/program/handlers/validate.ts +213 -0
- package/os/program/lease.ts +156 -0
- package/os/program/program.module.ts +212 -0
- package/os/program/schemas.ts +218 -0
- package/os/program/state.ts +264 -0
- package/os/rfc/acceptance.ts +344 -0
- package/os/rfc/decision-log.ts +310 -0
- package/os/rfc/dna-trace.ts +280 -0
- package/os/rfc/frontmatter-io.test.ts +132 -0
- package/os/rfc/frontmatter-io.ts +130 -0
- package/os/rfc/handlers/archive.ts +300 -0
- package/os/rfc/handlers/check.ts +238 -0
- package/os/rfc/handlers/implement-stamp.ts +515 -0
- package/os/rfc/handlers/index-graph.ts +237 -0
- package/os/rfc/handlers/lifecycle.test.ts +127 -0
- package/os/rfc/handlers/lifecycle.ts +249 -0
- package/os/rfc/handlers/list-create.ts +296 -0
- package/os/rfc/handlers/pipeline-status.ts +173 -0
- package/os/rfc/handlers/shared.ts +121 -0
- package/os/rfc/handlers/supersede-propose.ts +241 -0
- package/os/rfc/handlers/validate-rules.test.ts +891 -0
- package/os/rfc/handlers/validate-rules.ts +984 -0
- package/os/rfc/handlers/validate.ts +166 -0
- package/os/rfc/handlers.ts +19 -0
- package/os/rfc/index.ts +97 -0
- package/os/rfc/rfc.module.ts +416 -0
- package/os/rfc/types.ts +596 -0
- package/os/rfc/verification-evidence.ts +244 -0
- package/os/session/atif-parser.ts +166 -0
- package/os/session/frontmatter-io.ts +163 -0
- package/os/session/handlers/archive.ts +288 -0
- package/os/session/handlers/list.ts +174 -0
- package/os/session/handlers/save.ts +348 -0
- package/os/session/handlers/validate.ts +250 -0
- package/os/session/index.ts +50 -0
- package/os/session/session.module.ts +126 -0
- package/os/session/types.ts +203 -0
- package/os/spec/live-spec-list-show-validate.test.ts +257 -0
- package/os/spec/live-spec-list.ts +78 -0
- package/os/spec/live-spec-merge.test.ts +261 -0
- package/os/spec/live-spec-merge.ts +413 -0
- package/os/spec/live-spec-show.ts +117 -0
- package/os/spec/live-spec-types.ts +93 -0
- package/os/spec/live-spec-validate.ts +170 -0
- package/os/spec/spec-materialize.ts +390 -0
- package/os/spec/spec-schema.ts +174 -0
- package/os/spec/spec-status.ts +284 -0
- package/os/spec/spec-validate.ts +513 -0
- package/os/spec/spec.module.ts +135 -0
- package/os/werkstatt/handlers/lock.ts +168 -0
- package/os/werkstatt/handlers/schema.ts +47 -0
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
- package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
- package/os/werkstatt/index.ts +39 -0
- package/os/werkstatt/werkstatt.module.ts +62 -0
- package/os/workflow/handlers.ts +304 -0
- package/os/workflow/index.ts +28 -0
- package/os/workflow/types.ts +99 -0
- package/os/workflow/workflow.module.ts +54 -0
- package/package.json +3 -3
- package/profiles/godot-csharp.yaml +4 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/skills/_shared/fo-session-summary.md +132 -16
- package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +5 -0
- package/skills/fo/fo-idea/SKILL.md +1 -1
- package/skills/fo/fo-review/SKILL.md +1 -1
- package/skills/fo/fo-session-retro/SKILL.md +8 -0
- package/skills/fo/fo-session-save/SKILL.md +8 -0
- package/src/cli-output.ts +59 -0
- package/src/config/__tests__/resolve-terminology.test.ts +45 -0
- package/src/config/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -0
- package/src/knowledge/__tests__/promote.test.ts +219 -0
- package/src/knowledge/__tests__/serialize.test.ts +192 -0
- package/src/knowledge/budgets.ts +202 -0
- package/src/knowledge/compact.ts +405 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/parse.ts +227 -0
- package/src/knowledge/promote.ts +156 -0
- package/src/knowledge/schema.ts +112 -0
- package/src/knowledge/serialize.ts +72 -0
- package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
- package/src/migration-adapters/__tests__/registry.test.ts +131 -0
- package/src/migration-adapters/__tests__/types.test.ts +86 -0
- package/src/migration-adapters/git-utils.ts +71 -0
- package/src/migration-adapters/ignored-files.ts +221 -0
- package/src/migration-adapters/index.ts +21 -0
- package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
- package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
- package/src/migration-adapters/registry.ts +61 -0
- package/src/migration-adapters/types.ts +68 -0
- package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
- package/src/onboarding/agents-generate.ts +443 -0
- package/src/onboarding/create.ts +427 -0
- package/src/onboarding/doctor.ts +1297 -0
- package/src/onboarding/init.ts +337 -0
- package/src/onboarding/invariant-engine.ts +448 -0
- package/src/onboarding/memory-scaffold.ts +189 -0
- package/src/onboarding/nested-agents-generate.ts +93 -0
- package/src/onboarding/nested-agents-templates.ts +233 -0
- package/src/onboarding/profile-validate.ts +139 -0
- package/src/onboarding/scaffold-project.ts +299 -0
- package/src/onboarding/scaffold.ts +143 -0
- package/src/onboarding/upgrade.ts +558 -0
- package/src/onboarding/workspace-discovery.ts +167 -0
- package/src/profiles/__tests__/profile-schema.test.ts +228 -0
- package/src/profiles/__tests__/stack-profile.test.ts +190 -0
- package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
- package/src/profiles/profile-schema.ts +350 -0
- package/src/profiles/stack-profile.ts +188 -0
- package/src/profiles/terminology-utils.ts +36 -0
- package/src/registry.ts +185 -0
- package/src/skill-schema.ts +37 -0
- package/src/tests/acceptance-criteria.test.ts +99 -0
- package/src/tests/adr-implement-stamp.test.ts +368 -0
- package/src/tests/agents-generate-domain.test.ts +317 -0
- package/src/tests/agents-generate.test.ts +458 -0
- package/src/tests/bindings-schema.test.ts +314 -0
- package/src/tests/budgets.test.ts +213 -0
- package/src/tests/cli-output.test.ts +196 -0
- package/src/tests/compact.test.ts +796 -0
- package/src/tests/create.test.ts +289 -0
- package/src/tests/doctor-autonomy.test.ts +98 -0
- package/src/tests/doctor-bindings.test.ts +286 -0
- package/src/tests/doctor-domain.test.ts +113 -0
- package/src/tests/exploration-handlers.test.ts +256 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
- package/src/tests/forge-config.test.ts +420 -0
- package/src/tests/fs-atomic.test.ts +96 -0
- package/src/tests/fs.test.ts +103 -0
- package/src/tests/generated-marker.test.ts +161 -0
- package/src/tests/hash.test.ts +37 -0
- package/src/tests/implement-stamp.test.ts +505 -0
- package/src/tests/init-bindings.test.ts +183 -0
- package/src/tests/knowledge-parse.test.ts +307 -0
- package/src/tests/knowledge-pbt.test.ts +164 -0
- package/src/tests/memory-scaffold.test.ts +192 -0
- package/src/tests/migration-adapters.test.ts +480 -0
- package/src/tests/package-files.test.ts +54 -0
- package/src/tests/pinned-check.test.ts +194 -0
- package/src/tests/pinned-init.test.ts +126 -0
- package/src/tests/profile-schema.test.ts +194 -0
- package/src/tests/profile-validate.test.ts +120 -0
- package/src/tests/program-lease.test.ts +245 -0
- package/src/tests/program-paths-extended.test.ts +117 -0
- package/src/tests/program-paths.test.ts +98 -0
- package/src/tests/program-property.test.ts +139 -0
- package/src/tests/program-schemas.test.ts +445 -0
- package/src/tests/program-spec-node.test.ts +250 -0
- package/src/tests/promote.test.ts +481 -0
- package/src/tests/registry.test.ts +73 -0
- package/src/tests/scaffold-project.test.ts +150 -0
- package/src/tests/session-handlers.test.ts +503 -0
- package/src/tests/session-pbt.test.ts +190 -0
- package/src/tests/skill-schema.test.ts +146 -0
- package/src/tests/skill-validate-knowledge.test.ts +26 -0
- package/src/tests/skill-validate.test.ts +254 -0
- package/src/tests/stack-profile.test.ts +221 -0
- package/src/tests/string-utils.test.ts +42 -0
- package/src/tests/upgrade.test.ts +428 -0
- package/src/tests/werkstatt-lock.test.ts +343 -0
- package/src/tests/workspace-discovery-domain.test.ts +112 -0
- package/src/tests/workspace-discovery.test.ts +135 -0
- package/src/types.ts +221 -0
- package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
- package/src/utils/__tests__/fs-trash.test.ts +45 -0
- package/src/utils/fs-atomic.ts +93 -0
- package/src/utils/fs-idempotent.ts +41 -0
- package/src/utils/fs-trash-sync.ts +37 -0
- package/src/utils/fs-trash.ts +24 -0
- package/src/utils/fs.ts +74 -0
- package/src/utils/generated-marker.ts +166 -0
- package/src/utils/hash.ts +19 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/string-utils.ts +19 -0
- package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
- package/src/validators/note-frontmatter-validate.ts +142 -0
- package/src/validators/note-link-validate.ts +145 -0
- package/src/validators/note-orphan-detect.ts +146 -0
- package/src/validators/port-validate.ts +97 -0
- package/src/validators/skill-validate.ts +831 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>forge.scaffold — creates a working pnpm + Turborepo monorepo from a stack profile in the current directory.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not add a --force flag — ever.</item>
|
|
6
|
+
<item>Do not import from @warpgogol/* — this module is portable.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0392: initial forge.scaffold handler.</item>
|
|
11
|
+
<item>RFC-0542: populate nextSteps in forge.scaffold result.</item>
|
|
12
|
+
<item>Add __PROJECT_NAME__ placeholder replacement for workspace files; profiles now include root package.json with scripts (clean, format, test, upgrade-packages) and scripts/clean.mjs.</item>
|
|
13
|
+
</CHANGE_SUMMARY>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { execSync } from "node:child_process";
|
|
19
|
+
import { listStackProfiles, type StackProfile } from "../profiles/stack-profile.ts";
|
|
20
|
+
import { resolveForgeRoot } from "../config/forge-config.ts";
|
|
21
|
+
import type {
|
|
22
|
+
ForgeCommandInput,
|
|
23
|
+
ForgeCommandResult,
|
|
24
|
+
ForgeNextStep,
|
|
25
|
+
ForgeRuntimeContext,
|
|
26
|
+
} from "../types.ts";
|
|
27
|
+
|
|
28
|
+
interface ScaffoldProjectResult {
|
|
29
|
+
command: string;
|
|
30
|
+
status: "pass" | "fail";
|
|
31
|
+
profile: string;
|
|
32
|
+
created: string[];
|
|
33
|
+
installLog: string[];
|
|
34
|
+
errors: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function runScaffoldProject(
|
|
38
|
+
input: ForgeCommandInput,
|
|
39
|
+
context: ForgeRuntimeContext,
|
|
40
|
+
): Promise<ForgeCommandResult<ScaffoldProjectResult>> {
|
|
41
|
+
const { workspaceRoot, logger, outputFormat } = context;
|
|
42
|
+
const profileId = input.flags["profile"] as string | undefined;
|
|
43
|
+
const projectName = input.flags["name"] as string | undefined;
|
|
44
|
+
const templateId = input.flags["template"] as string | undefined;
|
|
45
|
+
|
|
46
|
+
const created: string[] = [];
|
|
47
|
+
const installLog: string[] = [];
|
|
48
|
+
const errors: string[] = [];
|
|
49
|
+
|
|
50
|
+
// Validate required flags
|
|
51
|
+
if (!profileId) {
|
|
52
|
+
return fail("forge.scaffold", "", "Missing required flag: --profile", errors, outputFormat, logger);
|
|
53
|
+
}
|
|
54
|
+
// Derive project name from folder name if not provided (consistent with forge.create)
|
|
55
|
+
const effectiveProjectName = projectName ?? path.basename(workspaceRoot);
|
|
56
|
+
|
|
57
|
+
// Validate kebab-case project name
|
|
58
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(effectiveProjectName)) {
|
|
59
|
+
return fail("forge.scaffold", profileId, `Project name "${effectiveProjectName}" is not kebab-case`, errors, outputFormat, logger);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// RFC-0877: The target directory must be empty (only .git/ tolerated).
|
|
63
|
+
// The empty-directory check is handled by forge.create.
|
|
64
|
+
// forge.scaffold is called from forge.create which already verified the directory is empty.
|
|
65
|
+
|
|
66
|
+
// Load profiles and find the requested one
|
|
67
|
+
let forgeRoot: string;
|
|
68
|
+
if (context.forgeRoot) {
|
|
69
|
+
forgeRoot = context.forgeRoot;
|
|
70
|
+
} else {
|
|
71
|
+
try {
|
|
72
|
+
forgeRoot = resolveForgeRoot(workspaceRoot);
|
|
73
|
+
} catch {
|
|
74
|
+
// In autonomous mode, forgeRoot may be the cwd itself
|
|
75
|
+
forgeRoot = workspaceRoot;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let profiles: StackProfile[];
|
|
80
|
+
try {
|
|
81
|
+
profiles = listStackProfiles(forgeRoot);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return fail("forge.scaffold", profileId, `Failed to load profiles: ${(err as Error).message}`, errors, outputFormat, logger);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const profile = profiles.find((p) => p.id === profileId);
|
|
87
|
+
if (!profile) {
|
|
88
|
+
const available = profiles.map((p) => p.id).join(", ");
|
|
89
|
+
return fail("forge.scaffold", profileId, `Unknown profile "${profileId}". Available: ${available}`, errors, outputFormat, logger);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Create workspace directories
|
|
93
|
+
for (const dir of profile.workspace.dirs) {
|
|
94
|
+
const dirPath = path.join(workspaceRoot, dir);
|
|
95
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
96
|
+
created.push(`${dir}/`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Create workspace files (with __PROJECT_NAME__ placeholder replacement)
|
|
100
|
+
for (const file of profile.workspace.files) {
|
|
101
|
+
const filePath = path.join(workspaceRoot, file.path);
|
|
102
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
103
|
+
const content = file.content.replace(/__PROJECT_NAME__/g, effectiveProjectName);
|
|
104
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
105
|
+
created.push(file.path);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Ensure .npmrc has ignore-workspace-root-check=true before running install commands to avoid:
|
|
109
|
+
// - ERR_PNPM_ADDING_TO_ROOT (pnpm v10/v11 blocks `pnpm add` in workspace root without -w)
|
|
110
|
+
// - ERR_PNPM_IGNORED_BUILDS (pnpm exits non-zero for ignored build scripts)
|
|
111
|
+
// Profiles may already include .npmrc in workspace.files — append the setting if missing.
|
|
112
|
+
const npmrcPath = path.join(workspaceRoot, ".npmrc");
|
|
113
|
+
if (profile.install.length > 0) {
|
|
114
|
+
if (fs.existsSync(npmrcPath)) {
|
|
115
|
+
const existing = fs.readFileSync(npmrcPath, "utf8");
|
|
116
|
+
if (!existing.includes("ignore-workspace-root-check=true")) {
|
|
117
|
+
fs.appendFileSync(npmrcPath, "ignore-workspace-root-check=true\n", "utf8");
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
fs.writeFileSync(npmrcPath, "ignore-workspace-root-check=true\n", "utf8");
|
|
121
|
+
created.push(".npmrc");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create first workspace if defined (use template override if --template is provided)
|
|
126
|
+
let templateError: string | null = null;
|
|
127
|
+
const effectiveFirstWorkspace = (() => {
|
|
128
|
+
if (templateId && profile.templates) {
|
|
129
|
+
const template = profile.templates.find((t) => t.id === templateId);
|
|
130
|
+
if (template) return template.firstWorkspace;
|
|
131
|
+
templateError = `Unknown template "${templateId}". Available: ${profile.templates.map((t) => t.id).join(", ")}`;
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
// Use default template if no --template specified and templates exist
|
|
135
|
+
if (profile.templates && profile.templates.length > 0) {
|
|
136
|
+
const defaultTemplate = profile.templates.find((t) => t.default) ?? profile.templates[0];
|
|
137
|
+
return defaultTemplate.firstWorkspace;
|
|
138
|
+
}
|
|
139
|
+
return profile.firstWorkspace;
|
|
140
|
+
})();
|
|
141
|
+
|
|
142
|
+
if (templateError) {
|
|
143
|
+
return fail("forge.scaffold", profileId, templateError, errors, outputFormat, logger);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (effectiveFirstWorkspace) {
|
|
147
|
+
const wsPath = effectiveFirstWorkspace.path.replace("my-site", effectiveProjectName).replace("my-game", effectiveProjectName);
|
|
148
|
+
const wsDir = path.join(workspaceRoot, wsPath);
|
|
149
|
+
fs.mkdirSync(wsDir, { recursive: true });
|
|
150
|
+
created.push(`${wsPath}/`);
|
|
151
|
+
|
|
152
|
+
for (const file of effectiveFirstWorkspace.files) {
|
|
153
|
+
const filePath = path.join(wsDir, file.path);
|
|
154
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
155
|
+
// Replace placeholder names (quoted and bare)
|
|
156
|
+
const content = file.content
|
|
157
|
+
.replace(/my-site/g, effectiveProjectName)
|
|
158
|
+
.replace(/my-game/g, effectiveProjectName);
|
|
159
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
160
|
+
created.push(`${wsPath}/${file.path}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Run ALL install commands AFTER all files are created (atomicity: no partial state if install fails)
|
|
165
|
+
// Root workspace install commands
|
|
166
|
+
if (outputFormat === "pretty" && profile.install.length > 0) {
|
|
167
|
+
logger.info(`Installing ${profile.install.length} root workspace package(s)...`);
|
|
168
|
+
}
|
|
169
|
+
for (const cmd of profile.install) {
|
|
170
|
+
if (outputFormat === "pretty") {
|
|
171
|
+
logger.info(`Running: ${cmd}`);
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
execSync(cmd, { cwd: workspaceRoot, stdio: "pipe", timeout: 60000 });
|
|
175
|
+
installLog.push(`${cmd} — ok`);
|
|
176
|
+
if (outputFormat === "pretty") {
|
|
177
|
+
logger.success(`Finished: ${cmd}`);
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
const stderr = String((err as { stderr?: Buffer }).stderr ?? "").trim();
|
|
181
|
+
const stdout = String((err as { stdout?: Buffer }).stdout ?? "").trim();
|
|
182
|
+
const details = [stderr, stdout].filter(Boolean).join("\n");
|
|
183
|
+
// pnpm v10 exits non-zero for ERR_PNPM_IGNORED_BUILDS even when install succeeded
|
|
184
|
+
if (details.includes("ERR_PNPM_IGNORED_BUILDS")) {
|
|
185
|
+
installLog.push(`${cmd} — ok (build scripts ignored)`);
|
|
186
|
+
if (outputFormat === "pretty") {
|
|
187
|
+
logger.success(`Finished: ${cmd} (build scripts ignored — run 'pnpm approve-builds' later)`);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
installLog.push(`${cmd} — FAILED: ${details || (err as Error).message}`);
|
|
191
|
+
if (outputFormat === "pretty") {
|
|
192
|
+
logger.warn(`Install failed (non-fatal): ${cmd}`);
|
|
193
|
+
if (details) logger.warn(details);
|
|
194
|
+
logger.warn("Run this command manually after fixing the issue (e.g. configuring .npmrc).");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// First workspace install commands
|
|
201
|
+
if (effectiveFirstWorkspace) {
|
|
202
|
+
const wsPath = effectiveFirstWorkspace.path.replace("my-site", effectiveProjectName).replace("my-game", effectiveProjectName);
|
|
203
|
+
const wsDir = path.join(workspaceRoot, wsPath);
|
|
204
|
+
if (outputFormat === "pretty" && effectiveFirstWorkspace.install.length > 0) {
|
|
205
|
+
logger.info(`Installing first workspace packages in ${effectiveFirstWorkspace.path}...`);
|
|
206
|
+
}
|
|
207
|
+
for (const cmd of effectiveFirstWorkspace.install) {
|
|
208
|
+
if (outputFormat === "pretty") {
|
|
209
|
+
logger.info(`Running: ${cmd}`);
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
execSync(cmd, { cwd: wsDir, stdio: "pipe", timeout: 60000 });
|
|
213
|
+
installLog.push(`${cmd} — ok`);
|
|
214
|
+
if (outputFormat === "pretty") {
|
|
215
|
+
logger.success(`Finished: ${cmd}`);
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
const stderr = String((err as { stderr?: Buffer }).stderr ?? "").trim();
|
|
219
|
+
const stdout = String((err as { stdout?: Buffer }).stdout ?? "").trim();
|
|
220
|
+
const details = [stderr, stdout].filter(Boolean).join("\n");
|
|
221
|
+
// pnpm v10 exits non-zero for ERR_PNPM_IGNORED_BUILDS even when install succeeded
|
|
222
|
+
if (details.includes("ERR_PNPM_IGNORED_BUILDS")) {
|
|
223
|
+
installLog.push(`${cmd} — ok (build scripts ignored)`);
|
|
224
|
+
if (outputFormat === "pretty") {
|
|
225
|
+
logger.success(`Finished: ${cmd} (build scripts ignored — run 'pnpm approve-builds' later)`);
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
errors.push(`Install step failed: ${cmd}\n${details || (err as Error).message}`);
|
|
229
|
+
if (outputFormat === "pretty") {
|
|
230
|
+
logger.error(`Install failed: ${cmd}`);
|
|
231
|
+
if (details) logger.error(details);
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
data: { command: "forge.scaffold", status: "fail", profile: profileId, created, installLog, errors },
|
|
235
|
+
nextSteps: [{ action: "Fix the errors above and re-run forge.scaffold", kind: "required" }],
|
|
236
|
+
exitCode: 1,
|
|
237
|
+
summary: `forge.scaffold: failed — install step "${cmd}" failed`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Format generated files with prettier if it's available
|
|
245
|
+
try {
|
|
246
|
+
if (outputFormat === "pretty") {
|
|
247
|
+
logger.info("Formatting generated files with prettier...");
|
|
248
|
+
}
|
|
249
|
+
execSync("npx prettier --write .", { cwd: workspaceRoot, stdio: "pipe", timeout: 30000 });
|
|
250
|
+
installLog.push("prettier --write . — ok");
|
|
251
|
+
if (outputFormat === "pretty") {
|
|
252
|
+
logger.success("Formatting complete.");
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
// Prettier is optional — don't fail the scaffold if it's not available
|
|
256
|
+
if (outputFormat === "pretty") {
|
|
257
|
+
logger.info("Skipping prettier formatting (not available yet).");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (outputFormat === "pretty") {
|
|
262
|
+
logger.success(`Scaffolded ${profile.displayName} in ${workspaceRoot}`);
|
|
263
|
+
for (const f of created) {
|
|
264
|
+
logger.info(` created: ${f}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const nextSteps: ForgeNextStep[] =
|
|
269
|
+
[
|
|
270
|
+
{ action: "Open the project in Windsurf", kind: "required" },
|
|
271
|
+
{ action: "Run forge.create to deploy skills and docs dirs", kind: "required" },
|
|
272
|
+
];
|
|
273
|
+
return {
|
|
274
|
+
data: { command: "forge.scaffold", status: "pass", profile: profileId, created, installLog, errors },
|
|
275
|
+
nextSteps,
|
|
276
|
+
exitCode: 0,
|
|
277
|
+
summary: `forge.scaffold: OK — scaffolded ${profileId} with ${created.length} files`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function fail(
|
|
282
|
+
command: string,
|
|
283
|
+
profile: string,
|
|
284
|
+
message: string,
|
|
285
|
+
errors: string[],
|
|
286
|
+
outputFormat: string,
|
|
287
|
+
logger: ForgeRuntimeContext["logger"],
|
|
288
|
+
): ForgeCommandResult<ScaffoldProjectResult> {
|
|
289
|
+
errors.push(message);
|
|
290
|
+
if (outputFormat === "pretty") {
|
|
291
|
+
logger.error(message);
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
data: { command, status: "fail", profile, created: [], installLog: [], errors },
|
|
295
|
+
nextSteps: [{ action: "Fix the errors above and re-run forge.scaffold", kind: "required" }],
|
|
296
|
+
exitCode: 1,
|
|
297
|
+
summary: `${command}: failed — ${message}`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>forge.port.scaffold — generates a skeleton for a new skill or command in forge.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not validate the scaffolded content — that is forge.port.validate's job.</item>
|
|
6
|
+
</non-goals>
|
|
7
|
+
</MODULE_CONTRACT>
|
|
8
|
+
<CHANGE_SUMMARY>
|
|
9
|
+
<item>RFC-0374: initial forge.port.scaffold handler.</item>
|
|
10
|
+
</CHANGE_SUMMARY>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { FORGE_SKILLS } from "../registry.ts";
|
|
16
|
+
import { resolveForgeRoot } from "../config/forge-config.ts";
|
|
17
|
+
|
|
18
|
+
interface ScaffoldResult {
|
|
19
|
+
command: string;
|
|
20
|
+
status: "pass" | "fail";
|
|
21
|
+
created: string[];
|
|
22
|
+
errors: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function runScaffold(
|
|
26
|
+
input: { flags: { name?: string; type?: string; category?: string } },
|
|
27
|
+
context: unknown,
|
|
28
|
+
): ScaffoldResult {
|
|
29
|
+
const ctx = context as { workspaceRoot?: string };
|
|
30
|
+
const workspaceRoot = ctx?.workspaceRoot ?? process.cwd();
|
|
31
|
+
let forgeRoot: string;
|
|
32
|
+
try {
|
|
33
|
+
forgeRoot = resolveForgeRoot(workspaceRoot);
|
|
34
|
+
} catch {
|
|
35
|
+
forgeRoot = path.join(workspaceRoot, "packages", "forge");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const name = input.flags?.name;
|
|
39
|
+
const type = input.flags?.type ?? "skill";
|
|
40
|
+
const category = input.flags?.category ?? "shared";
|
|
41
|
+
|
|
42
|
+
const errors: string[] = [];
|
|
43
|
+
const created: string[] = [];
|
|
44
|
+
|
|
45
|
+
if (!name) {
|
|
46
|
+
errors.push("--name flag is required");
|
|
47
|
+
return { command: "forge.port.scaffold", status: "fail", created, errors };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check for existing skill with same name
|
|
51
|
+
if (FORGE_SKILLS.some((s) => s.name === name)) {
|
|
52
|
+
errors.push(`Skill "${name}" already exists in the forge registry`);
|
|
53
|
+
return { command: "forge.port.scaffold", status: "fail", created, errors };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (type === "skill") {
|
|
57
|
+
const skillDir = path.join(forgeRoot, "skills", category, name);
|
|
58
|
+
const skillPath = path.join(skillDir, "SKILL.md");
|
|
59
|
+
|
|
60
|
+
if (fs.existsSync(skillPath)) {
|
|
61
|
+
errors.push(`SKILL.md already exists at ${path.relative(workspaceRoot, skillPath)}`);
|
|
62
|
+
return { command: "forge.port.scaffold", status: "fail", created, errors };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
66
|
+
|
|
67
|
+
const content = `---
|
|
68
|
+
name: ${name}
|
|
69
|
+
description: TODO — one-line description (max 200 chars)
|
|
70
|
+
invocation: user
|
|
71
|
+
category: ${category}
|
|
72
|
+
concerns: document-only
|
|
73
|
+
dependsOn: []
|
|
74
|
+
languagePolicy: ref(PREFERENCES.md)
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
# ${name}
|
|
78
|
+
|
|
79
|
+
Before starting, read \`PREFERENCES.md\` at the repository root. If the file is missing or \`aiLanguage\` is unset, ask the operator once and create the file using the \`my-preferences\` skill semantics.
|
|
80
|
+
|
|
81
|
+
TODO: Write the skill's behavioral instructions here.
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
fs.writeFileSync(skillPath, content, "utf8");
|
|
85
|
+
created.push(path.relative(workspaceRoot, skillPath));
|
|
86
|
+
} else if (type === "command") {
|
|
87
|
+
const cmdDir = path.join(forgeRoot, "os", name);
|
|
88
|
+
const cmdPath = path.join(cmdDir, `${name}.module.ts`);
|
|
89
|
+
|
|
90
|
+
if (fs.existsSync(cmdPath)) {
|
|
91
|
+
errors.push(`Command module already exists at ${path.relative(workspaceRoot, cmdPath)}`);
|
|
92
|
+
return { command: "forge.port.scaffold", status: "fail", created, errors };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fs.mkdirSync(cmdDir, { recursive: true });
|
|
96
|
+
|
|
97
|
+
const content = `/*
|
|
98
|
+
<MODULE_CONTRACT>
|
|
99
|
+
<purpose>TODO — purpose of the ${name} command module.</purpose>
|
|
100
|
+
<non-goals>
|
|
101
|
+
<item>Do not import from @warpgogol/site-kernel.</item>
|
|
102
|
+
</non-goals>
|
|
103
|
+
</MODULE_CONTRACT>
|
|
104
|
+
<CHANGE_SUMMARY>
|
|
105
|
+
<item>RFC-0374: initial ${name} command module.</item>
|
|
106
|
+
</CHANGE_SUMMARY>
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
import type { ForgeModule, ForgeCommandDefinition } from "../../src/forge-module.ts";
|
|
110
|
+
|
|
111
|
+
export const forge${capitalize(name)}Module: ForgeModule = {
|
|
112
|
+
name: "forge-${name}",
|
|
113
|
+
version: "0.1.0",
|
|
114
|
+
register(registry) {
|
|
115
|
+
registry.registerCommand({
|
|
116
|
+
name: "${name}",
|
|
117
|
+
description: "TODO — one-line description",
|
|
118
|
+
scope: "workspace",
|
|
119
|
+
execute() {
|
|
120
|
+
// TODO: implement
|
|
121
|
+
},
|
|
122
|
+
} satisfies ForgeCommandDefinition);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
fs.writeFileSync(cmdPath, content, "utf8");
|
|
128
|
+
created.push(path.relative(workspaceRoot, cmdPath));
|
|
129
|
+
} else {
|
|
130
|
+
errors.push(`--type must be "skill" or "command", got "${type}"`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
command: "forge.port.scaffold",
|
|
135
|
+
status: errors.length === 0 ? "pass" : "fail",
|
|
136
|
+
created,
|
|
137
|
+
errors,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function capitalize(s: string): string {
|
|
142
|
+
return s.split("-").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
143
|
+
}
|