@warpgogol/forge 2.7.0 → 2.8.1
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/src/config/forge-config.d.ts +1 -1
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +2 -2
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/onboarding/agents-generate.d.ts +2 -1
- package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/agents-generate.js +24 -4
- package/dist/src/onboarding/agents-generate.js.map +1 -1
- package/dist/src/profiles/profile-schema.d.ts +2 -0
- package/dist/src/profiles/profile-schema.d.ts.map +1 -1
- package/dist/src/profiles/profile-schema.js +1 -0
- package/dist/src/profiles/profile-schema.js.map +1 -1
- package/dist/src/profiles/stack-profile.d.ts +1 -0
- package/dist/src/profiles/stack-profile.d.ts.map +1 -1
- package/dist/src/profiles/stack-profile.js +2 -0
- package/dist/src/profiles/stack-profile.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 +5 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/profiles/root-agents-godot.md +133 -0
- 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/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -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/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/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/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 +51 -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 +217 -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/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/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,167 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Workspace discovery for nested AGENTS.md generation (RFC-0611).
|
|
4
|
+
Scans the project root for directories containing package.json and auto-detects
|
|
5
|
+
workspace type (app, service, package) by content markers.</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not scan node_modules, .git, dist, .turbo, .cache, .agents directories.</item>
|
|
8
|
+
<item>Do not read package.json contents — only check existence for workspace detection.</item>
|
|
9
|
+
<item>Do not add new workspace-type detection rules without an amending RFC.</item>
|
|
10
|
+
</non-goals>
|
|
11
|
+
</MODULE_CONTRACT>
|
|
12
|
+
<CHANGE_SUMMARY>
|
|
13
|
+
<item>RFC-0611: initial workspace discovery and type auto-detection.</item>
|
|
14
|
+
<item>RFC-0640: accept optional workspaceTypes from profile for profile-driven detection, falling back to hardcoded detection when absent.</item>
|
|
15
|
+
</CHANGE_SUMMARY>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { hasGeneratedMarker } from "../utils/index.ts";
|
|
21
|
+
import type { ProfileWorkspaceType } from "../profiles/profile-schema.ts";
|
|
22
|
+
|
|
23
|
+
export type WorkspaceType = "app" | "service" | "package" | string;
|
|
24
|
+
|
|
25
|
+
export interface WorkspaceDir {
|
|
26
|
+
path: string;
|
|
27
|
+
type: WorkspaceType;
|
|
28
|
+
hasAgentsMd: boolean;
|
|
29
|
+
isGenerated: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SKIP_DIRS = new Set([
|
|
33
|
+
"node_modules",
|
|
34
|
+
".git",
|
|
35
|
+
"dist",
|
|
36
|
+
".turbo",
|
|
37
|
+
".cache",
|
|
38
|
+
".agents",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export function detectWorkspaceType(
|
|
42
|
+
dirPath: string,
|
|
43
|
+
workspaceTypes?: ProfileWorkspaceType[],
|
|
44
|
+
): WorkspaceType | null {
|
|
45
|
+
if (!fs.existsSync(path.join(dirPath, "package.json"))) return null;
|
|
46
|
+
|
|
47
|
+
// RFC-0640: try profile-driven detection first
|
|
48
|
+
if (workspaceTypes && workspaceTypes.length > 0) {
|
|
49
|
+
for (const wt of workspaceTypes) {
|
|
50
|
+
if (matchesWorkspaceType(dirPath, wt)) {
|
|
51
|
+
return wt.id;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// RFC-0640: when workspaceTypes is present, it fully replaces hardcoded detection
|
|
55
|
+
// Directories matching no profile-declared workspace type are not classified
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fallback: hardcoded detection (software domain)
|
|
60
|
+
if (
|
|
61
|
+
fs.existsSync(path.join(dirPath, "astro.config.mjs")) ||
|
|
62
|
+
fs.existsSync(path.join(dirPath, "astro.config.js")) ||
|
|
63
|
+
fs.existsSync(path.join(dirPath, "astro.config.ts"))
|
|
64
|
+
) {
|
|
65
|
+
return "app";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
fs.existsSync(path.join(dirPath, "Dockerfile")) ||
|
|
70
|
+
fs.existsSync(path.join(dirPath, "service.config.yaml")) ||
|
|
71
|
+
fs.existsSync(path.join(dirPath, "service.config.yml"))
|
|
72
|
+
) {
|
|
73
|
+
return "service";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return "package";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function matchesWorkspaceType(dirPath: string, wt: ProfileWorkspaceType): boolean {
|
|
80
|
+
const { glob: globPattern, contains, packageJsonDep } = wt.detect;
|
|
81
|
+
|
|
82
|
+
if (globPattern) {
|
|
83
|
+
try {
|
|
84
|
+
const entries = fs.readdirSync(dirPath);
|
|
85
|
+
const regex = globToRegex(globPattern);
|
|
86
|
+
if (entries.some((e) => regex.test(e))) return true;
|
|
87
|
+
} catch {
|
|
88
|
+
// dir not readable
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (contains) {
|
|
93
|
+
const filePath = path.join(dirPath, contains);
|
|
94
|
+
if (fs.existsSync(filePath)) return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (packageJsonDep) {
|
|
98
|
+
try {
|
|
99
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(dirPath, "package.json"), "utf8"));
|
|
100
|
+
const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
101
|
+
if (packageJsonDep in deps) return true;
|
|
102
|
+
} catch {
|
|
103
|
+
// package.json not readable
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function discoverWorkspaces(
|
|
111
|
+
workspaceRoot: string,
|
|
112
|
+
workspaceTypes?: ProfileWorkspaceType[],
|
|
113
|
+
): WorkspaceDir[] {
|
|
114
|
+
const results: WorkspaceDir[] = [];
|
|
115
|
+
|
|
116
|
+
function scanDir(dirPath: string, depth: number): void {
|
|
117
|
+
if (depth > 5) return;
|
|
118
|
+
|
|
119
|
+
let entries: fs.Dirent[];
|
|
120
|
+
try {
|
|
121
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
122
|
+
} catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (!entry.isDirectory()) continue;
|
|
128
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
129
|
+
|
|
130
|
+
const childPath = path.join(dirPath, entry.name);
|
|
131
|
+
const type = detectWorkspaceType(childPath, workspaceTypes);
|
|
132
|
+
|
|
133
|
+
if (type !== null) {
|
|
134
|
+
const agentsMdPath = path.join(childPath, "AGENTS.md");
|
|
135
|
+
const hasAgentsMd = fs.existsSync(agentsMdPath);
|
|
136
|
+
let isGenerated = false;
|
|
137
|
+
if (hasAgentsMd) {
|
|
138
|
+
try {
|
|
139
|
+
const content = fs.readFileSync(agentsMdPath, "utf8");
|
|
140
|
+
isGenerated = hasGeneratedMarker(content);
|
|
141
|
+
} catch {
|
|
142
|
+
// unreadable — treat as not generated
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
results.push({
|
|
147
|
+
path: path.relative(workspaceRoot, childPath),
|
|
148
|
+
type,
|
|
149
|
+
hasAgentsMd,
|
|
150
|
+
isGenerated,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
scanDir(childPath, depth + 1);
|
|
154
|
+
} else {
|
|
155
|
+
scanDir(childPath, depth + 1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
scanDir(workspaceRoot, 0);
|
|
161
|
+
return results;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function globToRegex(pattern: string): RegExp {
|
|
165
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
166
|
+
return new RegExp(`^${escaped}$`);
|
|
167
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Domain-neutral profile schema extensions — optional fields that allow a stack profile to declare its domain model (terminology, artifacts, workspace types, invariants, register) without breaking backward compatibility.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not import from @warpgogol/* — this module is portable.</item>
|
|
6
|
+
<item>Do not define enforcement logic for invariants — that lives in the invariant engine module.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0638: initial domain-neutral profile schema extensions with six optional fields.</item>
|
|
11
|
+
<item>RFC-0674: add profileDevServerSchema and devServer field for lifecycle commands.</item>
|
|
12
|
+
<item>RFC-0675: add profileInvariantCheckSchema and check field to profileInvariantSchema for enforcement.</item>
|
|
13
|
+
<item>RFC-0679: add profileAssetSchema and assets field for asset management commands.</item>
|
|
14
|
+
<item>RFC-0680: add profileReleaseSchema and release field for release lifecycle commands.</item>
|
|
15
|
+
<item>RFC-0694: replace html-attribute-pattern with attribute-pattern (elements array) for HTML+JSX support.</item>
|
|
16
|
+
<item>Add prerequisites field for profile-declared system dependency checks (e.g. FFmpeg).</item>
|
|
17
|
+
<item>Add templates field for multi-template profiles (e.g. React + HTML).</item>
|
|
18
|
+
<item>ADR-0043: add scriptDir field for agent-generated script directory convention.</item>
|
|
19
|
+
<item>RFC-0808: add link-resolution, frontmatter-required, path-exclusion check kinds for note vault validation.</item>
|
|
20
|
+
<item>Add gitignore-entry and secret-scan check kinds for godot-csharp profile.</item>
|
|
21
|
+
</CHANGE_SUMMARY>
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Universal terminology key catalog
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export const UNIVERSAL_TERMINOLOGY_KEYS = [
|
|
31
|
+
"artifact",
|
|
32
|
+
"artifactPlural",
|
|
33
|
+
"module",
|
|
34
|
+
"source",
|
|
35
|
+
"output",
|
|
36
|
+
"verify",
|
|
37
|
+
"operator",
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
export const TERMINOLOGY_DEFAULTS: Record<string, string> = {
|
|
41
|
+
artifact: "artifact",
|
|
42
|
+
artifactPlural: "artifacts",
|
|
43
|
+
module: "module",
|
|
44
|
+
source: "source file",
|
|
45
|
+
output: "output",
|
|
46
|
+
verify: "verify",
|
|
47
|
+
operator: "operator",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Profile artifact schema
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export const profileArtifactSchema = z.object({
|
|
55
|
+
id: z.string().min(1),
|
|
56
|
+
extensions: z.array(z.string().min(1)),
|
|
57
|
+
produce: z
|
|
58
|
+
.object({
|
|
59
|
+
command: z.string().min(1),
|
|
60
|
+
output: z.string().optional(),
|
|
61
|
+
})
|
|
62
|
+
.optional(),
|
|
63
|
+
validate: z
|
|
64
|
+
.object({
|
|
65
|
+
command: z.string().min(1),
|
|
66
|
+
outputFormat: z.enum(["plain", "json"]).optional(),
|
|
67
|
+
violationPattern: z.string().optional(),
|
|
68
|
+
})
|
|
69
|
+
.optional(),
|
|
70
|
+
determinism: z
|
|
71
|
+
.object({
|
|
72
|
+
hashable: z.boolean(),
|
|
73
|
+
inputs: z.array(z.string()),
|
|
74
|
+
})
|
|
75
|
+
.optional(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export interface ProfileArtifact {
|
|
79
|
+
id: string;
|
|
80
|
+
extensions: string[];
|
|
81
|
+
produce?: {
|
|
82
|
+
command: string;
|
|
83
|
+
output?: string;
|
|
84
|
+
};
|
|
85
|
+
validate?: {
|
|
86
|
+
command: string;
|
|
87
|
+
outputFormat?: "plain" | "json";
|
|
88
|
+
violationPattern?: string;
|
|
89
|
+
};
|
|
90
|
+
determinism?: {
|
|
91
|
+
hashable: boolean;
|
|
92
|
+
inputs: string[];
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Profile workspace type schema
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
export const profileWorkspaceTypeSchema = z.object({
|
|
101
|
+
id: z.string().min(1),
|
|
102
|
+
detect: z.object({
|
|
103
|
+
glob: z.string().optional(),
|
|
104
|
+
contains: z.string().optional(),
|
|
105
|
+
packageJsonDep: z.string().optional(),
|
|
106
|
+
}),
|
|
107
|
+
skills: z.array(z.string()).optional(),
|
|
108
|
+
agentsMdTemplate: z.string().optional(),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export interface ProfileWorkspaceType {
|
|
112
|
+
id: string;
|
|
113
|
+
detect: {
|
|
114
|
+
glob?: string;
|
|
115
|
+
contains?: string;
|
|
116
|
+
packageJsonDep?: string;
|
|
117
|
+
};
|
|
118
|
+
skills?: string[];
|
|
119
|
+
agentsMdTemplate?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Profile invariant schema
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
export const profileInvariantCheckSchema = z
|
|
127
|
+
.object({
|
|
128
|
+
kind: z.enum([
|
|
129
|
+
"filename-pattern",
|
|
130
|
+
"file-contains",
|
|
131
|
+
"file-not-contains",
|
|
132
|
+
"attribute-pattern",
|
|
133
|
+
"link-resolution",
|
|
134
|
+
"frontmatter-required",
|
|
135
|
+
"path-exclusion",
|
|
136
|
+
"gitignore-entry",
|
|
137
|
+
"secret-scan",
|
|
138
|
+
]),
|
|
139
|
+
glob: z.string().optional(),
|
|
140
|
+
pattern: z.string().optional(),
|
|
141
|
+
negatedPattern: z.string().optional(),
|
|
142
|
+
elements: z.array(z.string()).optional(),
|
|
143
|
+
attribute: z.string().optional(),
|
|
144
|
+
fields: z.array(z.string()).optional(),
|
|
145
|
+
})
|
|
146
|
+
.refine(
|
|
147
|
+
(v) =>
|
|
148
|
+
v.kind !== "attribute-pattern" ||
|
|
149
|
+
(v.elements != null && v.elements.length > 0 && v.attribute != null && v.pattern != null),
|
|
150
|
+
{
|
|
151
|
+
message:
|
|
152
|
+
"elements (non-empty array), attribute, and pattern are required for kind: attribute-pattern",
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
.refine((v) => v.kind !== "frontmatter-required" || (v.fields != null && v.fields.length > 0), {
|
|
156
|
+
message: "fields (non-empty array) is required for kind: frontmatter-required",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
export interface ProfileInvariantCheck {
|
|
160
|
+
kind:
|
|
161
|
+
| "filename-pattern"
|
|
162
|
+
| "file-contains"
|
|
163
|
+
| "file-not-contains"
|
|
164
|
+
| "attribute-pattern"
|
|
165
|
+
| "link-resolution"
|
|
166
|
+
| "frontmatter-required"
|
|
167
|
+
| "path-exclusion"
|
|
168
|
+
| "gitignore-entry"
|
|
169
|
+
| "secret-scan";
|
|
170
|
+
glob?: string;
|
|
171
|
+
pattern?: string;
|
|
172
|
+
negatedPattern?: string;
|
|
173
|
+
elements?: string[];
|
|
174
|
+
attribute?: string;
|
|
175
|
+
fields?: string[];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export const profileInvariantSchema = z.object({
|
|
179
|
+
id: z
|
|
180
|
+
.string()
|
|
181
|
+
.min(1)
|
|
182
|
+
.regex(/^[A-Z]+-\d+$/, "Invariant id must match ^[A-Z]+-\\d+$ (e.g. VIDEO-01)"),
|
|
183
|
+
rule: z.string().min(1),
|
|
184
|
+
severity: z.enum(["error", "warning"]),
|
|
185
|
+
check: profileInvariantCheckSchema.optional(),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
export interface ProfileInvariant {
|
|
189
|
+
id: string;
|
|
190
|
+
rule: string;
|
|
191
|
+
severity: "error" | "warning";
|
|
192
|
+
check?: ProfileInvariantCheck;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Profile dev server schema (RFC-0674)
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
export const profileDevServerSchema = z.object({
|
|
200
|
+
command: z.string().min(1),
|
|
201
|
+
port: z.number().int().positive().optional(),
|
|
202
|
+
readinessTimeout: z.number().int().positive().optional(),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
export interface ProfileDevServer {
|
|
206
|
+
command: string;
|
|
207
|
+
port?: number;
|
|
208
|
+
readinessTimeout?: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Profile asset schema (RFC-0679)
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
export const profileAssetTypeSchema = z.object({
|
|
216
|
+
id: z.string().min(1),
|
|
217
|
+
extensions: z.array(z.string().min(1)),
|
|
218
|
+
referencePattern: z.string().optional(),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
export interface ProfileAssetType {
|
|
222
|
+
id: string;
|
|
223
|
+
extensions: string[];
|
|
224
|
+
referencePattern?: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export const profileAssetSchema = z.object({
|
|
228
|
+
dir: z.string().min(1),
|
|
229
|
+
types: z.array(profileAssetTypeSchema),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
export interface ProfileAsset {
|
|
233
|
+
dir: string;
|
|
234
|
+
types: ProfileAssetType[];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Profile release schema (RFC-0680)
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
export const profileReleaseSchema = z.object({
|
|
242
|
+
target: z.enum(["local", "r2", "s3"]),
|
|
243
|
+
outputDir: z.string().min(1),
|
|
244
|
+
manifestName: z.string().default("release-manifest.json"),
|
|
245
|
+
includeArtifacts: z.array(z.string()).optional(),
|
|
246
|
+
r2: z
|
|
247
|
+
.object({
|
|
248
|
+
bucket: z.string().min(1),
|
|
249
|
+
accountId: z.string().min(1),
|
|
250
|
+
prefix: z.string().default(""),
|
|
251
|
+
})
|
|
252
|
+
.optional(),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
export interface ProfileRelease {
|
|
256
|
+
target: "local" | "r2" | "s3";
|
|
257
|
+
outputDir: string;
|
|
258
|
+
manifestName: string;
|
|
259
|
+
includeArtifacts?: string[];
|
|
260
|
+
r2?: {
|
|
261
|
+
bucket: string;
|
|
262
|
+
accountId: string;
|
|
263
|
+
prefix: string;
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Profile prerequisite schema
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
export const profilePrerequisiteSchema = z.object({
|
|
272
|
+
id: z.string().min(1),
|
|
273
|
+
name: z.string().min(1),
|
|
274
|
+
check: z.string().min(1),
|
|
275
|
+
installHint: z.string().optional(),
|
|
276
|
+
severity: z.enum(["error", "warning"]).default("error"),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
export interface ProfilePrerequisite {
|
|
280
|
+
id: string;
|
|
281
|
+
name: string;
|
|
282
|
+
check: string;
|
|
283
|
+
installHint?: string;
|
|
284
|
+
severity: "error" | "warning";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Profile template schema (multi-template profiles)
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
export const profileTemplateSchema = z.object({
|
|
292
|
+
id: z.string().min(1),
|
|
293
|
+
default: z.boolean().optional(),
|
|
294
|
+
firstWorkspace: z.object({
|
|
295
|
+
path: z.string().min(1),
|
|
296
|
+
files: z.array(
|
|
297
|
+
z.object({
|
|
298
|
+
path: z.string().min(1),
|
|
299
|
+
content: z.string(),
|
|
300
|
+
}),
|
|
301
|
+
),
|
|
302
|
+
install: z.array(z.string()).default([]),
|
|
303
|
+
}),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
export interface ProfileTemplate {
|
|
307
|
+
id: string;
|
|
308
|
+
default?: boolean;
|
|
309
|
+
firstWorkspace: {
|
|
310
|
+
path: string;
|
|
311
|
+
files: Array<{ path: string; content: string }>;
|
|
312
|
+
install: string[];
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Stack profile domain fields
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
export const stackProfileDomainFieldsSchema = z.object({
|
|
321
|
+
domain: z.string().optional(),
|
|
322
|
+
terminology: z.record(z.string(), z.string()).optional(),
|
|
323
|
+
artifacts: z.array(profileArtifactSchema).optional(),
|
|
324
|
+
workspaceTypes: z.array(profileWorkspaceTypeSchema).optional(),
|
|
325
|
+
invariants: z.array(profileInvariantSchema).optional(),
|
|
326
|
+
register: z.enum(["business", "creative"]).optional(),
|
|
327
|
+
devServer: profileDevServerSchema.optional(),
|
|
328
|
+
assets: profileAssetSchema.optional(),
|
|
329
|
+
release: profileReleaseSchema.optional(),
|
|
330
|
+
prerequisites: z.array(profilePrerequisiteSchema).optional(),
|
|
331
|
+
templates: z.array(profileTemplateSchema).optional(),
|
|
332
|
+
scriptDir: z.string().min(1).optional(),
|
|
333
|
+
rootAgentsMdTemplate: z.string().optional(),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
export interface StackProfileDomainFields {
|
|
337
|
+
domain?: string;
|
|
338
|
+
terminology?: Record<string, string>;
|
|
339
|
+
artifacts?: ProfileArtifact[];
|
|
340
|
+
workspaceTypes?: ProfileWorkspaceType[];
|
|
341
|
+
invariants?: ProfileInvariant[];
|
|
342
|
+
register?: "business" | "creative";
|
|
343
|
+
devServer?: ProfileDevServer;
|
|
344
|
+
assets?: ProfileAsset;
|
|
345
|
+
release?: ProfileRelease;
|
|
346
|
+
prerequisites?: ProfilePrerequisite[];
|
|
347
|
+
templates?: ProfileTemplate[];
|
|
348
|
+
scriptDir?: string;
|
|
349
|
+
rootAgentsMdTemplate?: string;
|
|
350
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Stack profile schema, loader, and detector. Defines what a supported stack looks like and how to detect it in an existing project.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not scaffold projects — that is scaffold-project.ts.</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 stack profile module with zod schema, listStackProfiles, detectStack.</item>
|
|
11
|
+
<item>RFC-0638: extended stackProfileSchema with optional domain-neutral fields (domain, terminology, artifacts, workspaceTypes, invariants, register).</item>
|
|
12
|
+
<item>RFC-0674: add devServer field to stackProfileSchema for lifecycle commands.</item>
|
|
13
|
+
<item>RFC-0679: add assets field to stackProfileSchema for asset management commands.</item>
|
|
14
|
+
<item>RFC-0680: add release field to stackProfileSchema for release lifecycle commands.</item>
|
|
15
|
+
<item>ADR-0043: add scriptDir field for agent-generated script directory convention.</item>
|
|
16
|
+
</CHANGE_SUMMARY>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { parse as parseYaml } from "yaml";
|
|
23
|
+
import { stackProfileDomainFieldsSchema, type StackProfileDomainFields } from "./profile-schema.ts";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Schema (forge/stack-profile@1)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export const stackProfileSchema = z.object({
|
|
30
|
+
schema: z.literal("forge/stack-profile@1"),
|
|
31
|
+
id: z.string().min(1),
|
|
32
|
+
displayName: z.string().min(1),
|
|
33
|
+
detect: z.object({
|
|
34
|
+
anyOf: z.array(z.string()).min(1),
|
|
35
|
+
}),
|
|
36
|
+
workspace: z.object({
|
|
37
|
+
dirs: z.array(z.string()).min(1),
|
|
38
|
+
files: z.array(
|
|
39
|
+
z.object({
|
|
40
|
+
path: z.string().min(1),
|
|
41
|
+
content: z.string(),
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
}),
|
|
45
|
+
install: z.array(z.string()).default([]),
|
|
46
|
+
firstWorkspace: z
|
|
47
|
+
.object({
|
|
48
|
+
path: z.string().min(1),
|
|
49
|
+
files: z.array(
|
|
50
|
+
z.object({
|
|
51
|
+
path: z.string().min(1),
|
|
52
|
+
content: z.string(),
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
install: z.array(z.string()).default([]),
|
|
56
|
+
})
|
|
57
|
+
.optional(),
|
|
58
|
+
// RFC-0638: Domain-neutral optional fields
|
|
59
|
+
domain: stackProfileDomainFieldsSchema.shape.domain,
|
|
60
|
+
terminology: stackProfileDomainFieldsSchema.shape.terminology,
|
|
61
|
+
artifacts: stackProfileDomainFieldsSchema.shape.artifacts,
|
|
62
|
+
workspaceTypes: stackProfileDomainFieldsSchema.shape.workspaceTypes,
|
|
63
|
+
invariants: stackProfileDomainFieldsSchema.shape.invariants,
|
|
64
|
+
register: stackProfileDomainFieldsSchema.shape.register,
|
|
65
|
+
// RFC-0674: Dev server declaration for lifecycle commands
|
|
66
|
+
devServer: stackProfileDomainFieldsSchema.shape.devServer,
|
|
67
|
+
// RFC-0679: Asset management declaration
|
|
68
|
+
assets: stackProfileDomainFieldsSchema.shape.assets,
|
|
69
|
+
// RFC-0680: Release lifecycle declaration
|
|
70
|
+
release: stackProfileDomainFieldsSchema.shape.release,
|
|
71
|
+
// Prerequisites: system-level dependency checks (e.g. FFmpeg)
|
|
72
|
+
prerequisites: stackProfileDomainFieldsSchema.shape.prerequisites,
|
|
73
|
+
// Templates: multi-template profiles (e.g. React + HTML)
|
|
74
|
+
templates: stackProfileDomainFieldsSchema.shape.templates,
|
|
75
|
+
// ADR-0043: Agent-generated script directory override
|
|
76
|
+
scriptDir: stackProfileDomainFieldsSchema.shape.scriptDir,
|
|
77
|
+
// Profile-driven root AGENTS.md template
|
|
78
|
+
rootAgentsMdTemplate: stackProfileDomainFieldsSchema.shape.rootAgentsMdTemplate,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export interface ProfileFile {
|
|
82
|
+
path: string;
|
|
83
|
+
content: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface StackProfile extends StackProfileDomainFields {
|
|
87
|
+
schema: "forge/stack-profile@1";
|
|
88
|
+
id: string;
|
|
89
|
+
displayName: string;
|
|
90
|
+
detect: { anyOf: string[] };
|
|
91
|
+
workspace: {
|
|
92
|
+
dirs: string[];
|
|
93
|
+
files: ProfileFile[];
|
|
94
|
+
};
|
|
95
|
+
install: string[];
|
|
96
|
+
firstWorkspace?: {
|
|
97
|
+
path: string;
|
|
98
|
+
files: ProfileFile[];
|
|
99
|
+
install: string[];
|
|
100
|
+
};
|
|
101
|
+
prerequisites?: import("./profile-schema.ts").ProfilePrerequisite[];
|
|
102
|
+
templates?: import("./profile-schema.ts").ProfileTemplate[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Loader
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
export function loadStackProfile(profilePath: string): StackProfile {
|
|
110
|
+
const raw = fs.readFileSync(profilePath, "utf8");
|
|
111
|
+
let parsed: unknown;
|
|
112
|
+
try {
|
|
113
|
+
parsed = parseYaml(raw);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
throw new Error(`Stack profile at ${profilePath} is not valid YAML: ${(err as Error).message}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = stackProfileSchema.safeParse(parsed);
|
|
119
|
+
if (!result.success) {
|
|
120
|
+
const issues = result.error.issues
|
|
121
|
+
.map((i) => ` - ${i.path.join(".")}: ${i.message}`)
|
|
122
|
+
.join("\n");
|
|
123
|
+
throw new Error(`Stack profile at ${profilePath} failed schema validation:\n${issues}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result.data as StackProfile;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function listStackProfiles(forgeRoot: string): StackProfile[] {
|
|
130
|
+
const profilesDir = path.join(forgeRoot, "profiles");
|
|
131
|
+
if (!fs.existsSync(profilesDir)) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const entries = fs.readdirSync(profilesDir);
|
|
136
|
+
const profiles: StackProfile[] = [];
|
|
137
|
+
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
if (entry.endsWith(".yaml") && !entry.startsWith(".")) {
|
|
140
|
+
const profilePath = path.join(profilesDir, entry);
|
|
141
|
+
profiles.push(loadStackProfile(profilePath));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return profiles;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Detector
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
export function detectStack(projectRoot: string, profiles: StackProfile[]): StackProfile | null {
|
|
153
|
+
for (const profile of profiles) {
|
|
154
|
+
if (matchesAnyOf(projectRoot, profile.detect.anyOf)) {
|
|
155
|
+
return profile;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function matchesAnyOf(projectRoot: string, patterns: string[]): boolean {
|
|
162
|
+
for (const pattern of patterns) {
|
|
163
|
+
if (globExists(projectRoot, pattern)) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function globExists(dir: string, pattern: string): boolean {
|
|
171
|
+
// Simple glob: support * and *.ext patterns at the root level
|
|
172
|
+
if (!pattern.includes("*")) {
|
|
173
|
+
return fs.existsSync(path.join(dir, pattern));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const entries = fs.readdirSync(dir);
|
|
178
|
+
const regex = globToRegex(pattern);
|
|
179
|
+
return entries.some((e) => regex.test(e));
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function globToRegex(pattern: string): RegExp {
|
|
186
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
187
|
+
return new RegExp(`^${escaped}$`);
|
|
188
|
+
}
|