@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,189 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Node/TypeScript/pnpm migration adapter — detects Node+TS+pnpm projects, derives bindings, copies code into forge turborepo (RFC-0546).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not import from @warpgogol/* — this module is portable.</item>
|
|
6
|
+
<item>Do not transform business logic — copy only.</item>
|
|
7
|
+
<item>Do not overwrite forge-protected files.</item>
|
|
8
|
+
</non-goals>
|
|
9
|
+
</MODULE_CONTRACT>
|
|
10
|
+
<CHANGE_SUMMARY>
|
|
11
|
+
<item>RFC-0546: initial node-typescript-pnpm migration adapter.</item>
|
|
12
|
+
<item>RFC-0547: implement postSetup with git init / format-patch + git am.</item>
|
|
13
|
+
</CHANGE_SUMMARY>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import type { MigrationAdapter, AdapterAnalysis, MigrationResult, Conflict } from "../types.ts";
|
|
19
|
+
import { FORGE_PROTECTED_PATHS, DEFAULT_EXCLUDE_PATTERNS } from "../types.ts";
|
|
20
|
+
import { runPostSetup } from "../git-utils.ts";
|
|
21
|
+
|
|
22
|
+
interface PackageJson {
|
|
23
|
+
name?: string;
|
|
24
|
+
scripts?: Record<string, string>;
|
|
25
|
+
dependencies?: Record<string, string>;
|
|
26
|
+
devDependencies?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readPackageJson(sourceDir: string): PackageJson | null {
|
|
30
|
+
const pkgPath = path.join(sourceDir, "package.json");
|
|
31
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf8")) as PackageJson;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const nodeTypescriptPnpmAdapter: MigrationAdapter = {
|
|
40
|
+
id: "node-typescript-pnpm",
|
|
41
|
+
|
|
42
|
+
detect(sourceDir: string): boolean {
|
|
43
|
+
const hasPackageJson = fs.existsSync(path.join(sourceDir, "package.json"));
|
|
44
|
+
const hasTsconfig = fs.existsSync(path.join(sourceDir, "tsconfig.json"));
|
|
45
|
+
const hasPnpmLock = fs.existsSync(path.join(sourceDir, "pnpm-lock.yaml"));
|
|
46
|
+
return hasPackageJson && hasTsconfig && hasPnpmLock;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
analyze(sourceDir: string): AdapterAnalysis {
|
|
50
|
+
const pkg = readPackageJson(sourceDir) ?? {};
|
|
51
|
+
const scripts = pkg.scripts ?? {};
|
|
52
|
+
|
|
53
|
+
const typecheck =
|
|
54
|
+
scripts["build:check"] ?? ((scripts["typecheck"] ?? scripts["tsc"]) ? `tsc --noEmit` : null);
|
|
55
|
+
const test = scripts["test"] ?? null;
|
|
56
|
+
const scopedBuild = scripts["build"] ?? null;
|
|
57
|
+
|
|
58
|
+
const appName = pkg.name
|
|
59
|
+
? pkg.name
|
|
60
|
+
.replace(/^@[^/]+\//, "")
|
|
61
|
+
.replace(/[^a-z0-9-]/gi, "-")
|
|
62
|
+
.toLowerCase()
|
|
63
|
+
: path
|
|
64
|
+
.basename(sourceDir)
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/[^a-z0-9-]/gi, "-");
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
stack: ["typescript", "node"],
|
|
70
|
+
packageManager: "pnpm",
|
|
71
|
+
bindings: { typecheck, test, scopedBuild },
|
|
72
|
+
placement: "apps",
|
|
73
|
+
appName,
|
|
74
|
+
excludePatterns: [...DEFAULT_EXCLUDE_PATTERNS],
|
|
75
|
+
gitHistory: fs.existsSync(path.join(sourceDir, ".git")),
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
migrate(sourceDir: string, targetDir: string, analysis: AdapterAnalysis): MigrationResult {
|
|
80
|
+
const destDir = path.join(targetDir, analysis.placement, analysis.appName);
|
|
81
|
+
const filesCopied: string[] = [];
|
|
82
|
+
const filesSkipped: string[] = [];
|
|
83
|
+
const conflicts: Conflict[] = [];
|
|
84
|
+
|
|
85
|
+
copyDirectory(
|
|
86
|
+
sourceDir,
|
|
87
|
+
destDir,
|
|
88
|
+
sourceDir,
|
|
89
|
+
analysis.excludePatterns,
|
|
90
|
+
filesCopied,
|
|
91
|
+
filesSkipped,
|
|
92
|
+
conflicts,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
let workspaceUpdated = false;
|
|
96
|
+
const workspacePath = path.join(targetDir, "pnpm-workspace.yaml");
|
|
97
|
+
if (fs.existsSync(workspacePath)) {
|
|
98
|
+
const content = fs.readFileSync(workspacePath, "utf8");
|
|
99
|
+
const entry = ` - ${analysis.placement}/${analysis.appName}`;
|
|
100
|
+
if (!content.includes(entry)) {
|
|
101
|
+
const updated = content.trimEnd() + "\n" + entry + "\n";
|
|
102
|
+
fs.writeFileSync(workspacePath, updated);
|
|
103
|
+
workspaceUpdated = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const turboPath = path.join(targetDir, "turbo.json");
|
|
108
|
+
if (fs.existsSync(turboPath)) {
|
|
109
|
+
try {
|
|
110
|
+
const turbo = JSON.parse(fs.readFileSync(turboPath, "utf8")) as Record<string, unknown>;
|
|
111
|
+
if (turbo && typeof turbo === "object") {
|
|
112
|
+
fs.writeFileSync(turboPath, JSON.stringify(turbo, null, 2) + "\n");
|
|
113
|
+
workspaceUpdated = true;
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// turbo.json parse failure — skip update
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { filesCopied, filesSkipped, conflicts, workspaceUpdated };
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
postSetup(sourceDir: string, targetDir: string, analysis: AdapterAnalysis): void {
|
|
124
|
+
runPostSetup(sourceDir, targetDir, analysis);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
function isForgeProtected(relPath: string): boolean {
|
|
129
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
130
|
+
return FORGE_PROTECTED_PATHS.some((p) => normalized === p || normalized.startsWith(p + "/"));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isExcluded(relPath: string, patterns: string[]): boolean {
|
|
134
|
+
const parts = relPath.split(path.sep);
|
|
135
|
+
return patterns.some((p) => parts.includes(p));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function copyDirectory(
|
|
139
|
+
src: string,
|
|
140
|
+
dest: string,
|
|
141
|
+
rootSrc: string,
|
|
142
|
+
excludePatterns: string[],
|
|
143
|
+
filesCopied: string[],
|
|
144
|
+
filesSkipped: string[],
|
|
145
|
+
conflicts: Conflict[],
|
|
146
|
+
): void {
|
|
147
|
+
if (!fs.existsSync(dest)) {
|
|
148
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const srcPath = path.join(src, entry.name);
|
|
154
|
+
const destPath = path.join(dest, entry.name);
|
|
155
|
+
const relPath = path.relative(rootSrc, srcPath);
|
|
156
|
+
|
|
157
|
+
if (entry.isDirectory()) {
|
|
158
|
+
if (isExcluded(relPath, excludePatterns)) continue;
|
|
159
|
+
copyDirectory(
|
|
160
|
+
srcPath,
|
|
161
|
+
destPath,
|
|
162
|
+
rootSrc,
|
|
163
|
+
excludePatterns,
|
|
164
|
+
filesCopied,
|
|
165
|
+
filesSkipped,
|
|
166
|
+
conflicts,
|
|
167
|
+
);
|
|
168
|
+
} else if (entry.isFile()) {
|
|
169
|
+
if (isExcluded(relPath, excludePatterns)) continue;
|
|
170
|
+
|
|
171
|
+
if (isForgeProtected(relPath)) {
|
|
172
|
+
filesSkipped.push(relPath);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (fs.existsSync(destPath)) {
|
|
177
|
+
conflicts.push({
|
|
178
|
+
path: relPath,
|
|
179
|
+
sourceExists: true,
|
|
180
|
+
forgeExists: true,
|
|
181
|
+
resolution: "source-wins",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fs.copyFileSync(srcPath, destPath);
|
|
186
|
+
filesCopied.push(relPath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Phaser/pnpm migration adapter — detects Phaser+pnpm projects, derives bindings, copies code into forge turborepo (RFC-0546).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not import from @warpgogol/* — this module is portable.</item>
|
|
6
|
+
<item>Do not transform business logic — copy only.</item>
|
|
7
|
+
<item>Do not overwrite forge-protected files.</item>
|
|
8
|
+
</non-goals>
|
|
9
|
+
</MODULE_CONTRACT>
|
|
10
|
+
<CHANGE_SUMMARY>
|
|
11
|
+
<item>RFC-0546: initial phaser-pnpm migration adapter.</item>
|
|
12
|
+
<item>RFC-0547: implement postSetup with git init / format-patch + git am.</item>
|
|
13
|
+
</CHANGE_SUMMARY>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import type { MigrationAdapter, AdapterAnalysis, MigrationResult, Conflict } from "../types.ts";
|
|
19
|
+
import { FORGE_PROTECTED_PATHS, DEFAULT_EXCLUDE_PATTERNS } from "../types.ts";
|
|
20
|
+
import { runPostSetup } from "../git-utils.ts";
|
|
21
|
+
|
|
22
|
+
interface PackageJson {
|
|
23
|
+
name?: string;
|
|
24
|
+
scripts?: Record<string, string>;
|
|
25
|
+
dependencies?: Record<string, string>;
|
|
26
|
+
devDependencies?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readPackageJson(sourceDir: string): PackageJson | null {
|
|
30
|
+
const pkgPath = path.join(sourceDir, "package.json");
|
|
31
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf8")) as PackageJson;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const phaserPnpmAdapter: MigrationAdapter = {
|
|
40
|
+
id: "phaser-pnpm",
|
|
41
|
+
|
|
42
|
+
detect(sourceDir: string): boolean {
|
|
43
|
+
const pkg = readPackageJson(sourceDir);
|
|
44
|
+
if (!pkg) return false;
|
|
45
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
46
|
+
return "phaser" in deps;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
analyze(sourceDir: string): AdapterAnalysis {
|
|
50
|
+
const pkg = readPackageJson(sourceDir) ?? {};
|
|
51
|
+
const scripts = pkg.scripts ?? {};
|
|
52
|
+
|
|
53
|
+
const typecheck =
|
|
54
|
+
scripts["build:check"] ?? ((scripts["typecheck"] ?? scripts["tsc"]) ? `tsc --noEmit` : null);
|
|
55
|
+
const test = scripts["test"] ?? null;
|
|
56
|
+
const scopedBuild = scripts["build"] ?? null;
|
|
57
|
+
|
|
58
|
+
const appName = pkg.name
|
|
59
|
+
? pkg.name
|
|
60
|
+
.replace(/^@[^/]+\//, "")
|
|
61
|
+
.replace(/[^a-z0-9-]/gi, "-")
|
|
62
|
+
.toLowerCase()
|
|
63
|
+
: path
|
|
64
|
+
.basename(sourceDir)
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/[^a-z0-9-]/gi, "-");
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
stack: ["typescript", "phaser"],
|
|
70
|
+
packageManager: "pnpm",
|
|
71
|
+
bindings: { typecheck, test, scopedBuild },
|
|
72
|
+
placement: "apps",
|
|
73
|
+
appName,
|
|
74
|
+
excludePatterns: [...DEFAULT_EXCLUDE_PATTERNS],
|
|
75
|
+
gitHistory: fs.existsSync(path.join(sourceDir, ".git")),
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
migrate(sourceDir: string, targetDir: string, analysis: AdapterAnalysis): MigrationResult {
|
|
80
|
+
const destDir = path.join(targetDir, analysis.placement, analysis.appName);
|
|
81
|
+
const filesCopied: string[] = [];
|
|
82
|
+
const filesSkipped: string[] = [];
|
|
83
|
+
const conflicts: Conflict[] = [];
|
|
84
|
+
|
|
85
|
+
copyDirectory(
|
|
86
|
+
sourceDir,
|
|
87
|
+
destDir,
|
|
88
|
+
sourceDir,
|
|
89
|
+
analysis.excludePatterns,
|
|
90
|
+
filesCopied,
|
|
91
|
+
filesSkipped,
|
|
92
|
+
conflicts,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
let workspaceUpdated = false;
|
|
96
|
+
const workspacePath = path.join(targetDir, "pnpm-workspace.yaml");
|
|
97
|
+
if (fs.existsSync(workspacePath)) {
|
|
98
|
+
const content = fs.readFileSync(workspacePath, "utf8");
|
|
99
|
+
const entry = ` - ${analysis.placement}/${analysis.appName}`;
|
|
100
|
+
if (!content.includes(entry)) {
|
|
101
|
+
const updated = content.trimEnd() + "\n" + entry + "\n";
|
|
102
|
+
fs.writeFileSync(workspacePath, updated);
|
|
103
|
+
workspaceUpdated = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const turboPath = path.join(targetDir, "turbo.json");
|
|
108
|
+
if (fs.existsSync(turboPath)) {
|
|
109
|
+
try {
|
|
110
|
+
const turbo = JSON.parse(fs.readFileSync(turboPath, "utf8")) as Record<string, unknown>;
|
|
111
|
+
if (turbo && typeof turbo === "object") {
|
|
112
|
+
fs.writeFileSync(turboPath, JSON.stringify(turbo, null, 2) + "\n");
|
|
113
|
+
workspaceUpdated = true;
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// turbo.json parse failure — skip update
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { filesCopied, filesSkipped, conflicts, workspaceUpdated };
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
postSetup(sourceDir: string, targetDir: string, analysis: AdapterAnalysis): void {
|
|
124
|
+
runPostSetup(sourceDir, targetDir, analysis);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
function isForgeProtected(relPath: string): boolean {
|
|
129
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
130
|
+
return FORGE_PROTECTED_PATHS.some((p) => normalized === p || normalized.startsWith(p + "/"));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isExcluded(relPath: string, patterns: string[]): boolean {
|
|
134
|
+
const parts = relPath.split(path.sep);
|
|
135
|
+
return patterns.some((p) => parts.includes(p));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function copyDirectory(
|
|
139
|
+
src: string,
|
|
140
|
+
dest: string,
|
|
141
|
+
rootSrc: string,
|
|
142
|
+
excludePatterns: string[],
|
|
143
|
+
filesCopied: string[],
|
|
144
|
+
filesSkipped: string[],
|
|
145
|
+
conflicts: Conflict[],
|
|
146
|
+
): void {
|
|
147
|
+
if (!fs.existsSync(dest)) {
|
|
148
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const srcPath = path.join(src, entry.name);
|
|
154
|
+
const destPath = path.join(dest, entry.name);
|
|
155
|
+
const relPath = path.relative(rootSrc, srcPath);
|
|
156
|
+
|
|
157
|
+
if (entry.isDirectory()) {
|
|
158
|
+
if (isExcluded(relPath, excludePatterns)) continue;
|
|
159
|
+
copyDirectory(
|
|
160
|
+
srcPath,
|
|
161
|
+
destPath,
|
|
162
|
+
rootSrc,
|
|
163
|
+
excludePatterns,
|
|
164
|
+
filesCopied,
|
|
165
|
+
filesSkipped,
|
|
166
|
+
conflicts,
|
|
167
|
+
);
|
|
168
|
+
} else if (entry.isFile()) {
|
|
169
|
+
if (isExcluded(relPath, excludePatterns)) continue;
|
|
170
|
+
|
|
171
|
+
if (isForgeProtected(relPath)) {
|
|
172
|
+
filesSkipped.push(relPath);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (fs.existsSync(destPath)) {
|
|
177
|
+
conflicts.push({
|
|
178
|
+
path: relPath,
|
|
179
|
+
sourceExists: true,
|
|
180
|
+
forgeExists: true,
|
|
181
|
+
resolution: "source-wins",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fs.copyFileSync(srcPath, destPath);
|
|
186
|
+
filesCopied.push(relPath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Migration-adapter registry — built-in adapters + config-discovered external adapters (RFC-0546).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not import from @warpgogol/* — this module is portable.</item>
|
|
6
|
+
<item>Do not apply migration here — selection only.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0546: initial migration-adapter registry with built-in adapters.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { MigrationAdapter } from "./types.ts";
|
|
15
|
+
import { nodeTypescriptPnpmAdapter } from "./node-typescript-pnpm/index.ts";
|
|
16
|
+
import { phaserPnpmAdapter } from "./phaser-pnpm/index.ts";
|
|
17
|
+
import type { ForgeConfig } from "../config/forge-config.ts";
|
|
18
|
+
|
|
19
|
+
const BUILT_IN_ADAPTERS: MigrationAdapter[] = [nodeTypescriptPnpmAdapter, phaserPnpmAdapter];
|
|
20
|
+
|
|
21
|
+
export function getAdapters(config?: ForgeConfig): MigrationAdapter[] {
|
|
22
|
+
const adapters = [...BUILT_IN_ADAPTERS];
|
|
23
|
+
|
|
24
|
+
const configAdapters = config?.migrationAdapters ?? [];
|
|
25
|
+
for (const entry of configAdapters) {
|
|
26
|
+
if (entry.module) {
|
|
27
|
+
// External adapter — would require dynamic import at runtime.
|
|
28
|
+
// The skill handles dynamic import; registry returns built-ins only for now.
|
|
29
|
+
// External adapters with module field are resolved by the skill at runtime.
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
// Built-in adapter referenced by id only — already in BUILT_IN_ADAPTERS
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return adapters;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function detectAdapter(sourceDir: string, config?: ForgeConfig): MigrationAdapter | null {
|
|
39
|
+
const adapters = getAdapters(config);
|
|
40
|
+
const matches = adapters.filter((a) => {
|
|
41
|
+
try {
|
|
42
|
+
return a.detect(sourceDir);
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (matches.length === 0) return null;
|
|
49
|
+
return matches[0];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function detectAdapters(sourceDir: string, config?: ForgeConfig): MigrationAdapter[] {
|
|
53
|
+
const adapters = getAdapters(config);
|
|
54
|
+
return adapters.filter((a) => {
|
|
55
|
+
try {
|
|
56
|
+
return a.detect(sourceDir);
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Migration-adapter type contracts — interfaces for stack-specific code migration into forge projects (RFC-0546).</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not import from @warpgogol/* — this module is portable.</item>
|
|
6
|
+
<item>Do not implement adapter logic here — only type definitions.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>RFC-0546: initial migration-adapter type contracts (MigrationAdapter, AdapterAnalysis, MigrationResult, Conflict).</item>
|
|
11
|
+
<item>RFC-0547: remove .git from DEFAULT_EXCLUDE_PATTERNS — git history handled by postSetup via format-patch + git am.</item>
|
|
12
|
+
<item>Re-add .git to DEFAULT_EXCLUDE_PATTERNS — copyDirectory copies into apps/<appName>/, not the project root; copying .git there creates a nested repo. Git history is handled by postSetup at the project root.</item>
|
|
13
|
+
<item>Add .env* to untracked-file transfer guarantee — the adapter copies ALL files on disk (including git-ignored), not just git-tracked files.</item>
|
|
14
|
+
</CHANGE_SUMMARY>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface MigrationAdapter {
|
|
18
|
+
id: string;
|
|
19
|
+
detect(sourceDir: string): boolean;
|
|
20
|
+
analyze(sourceDir: string): AdapterAnalysis;
|
|
21
|
+
migrate(sourceDir: string, targetDir: string, analysis: AdapterAnalysis): MigrationResult;
|
|
22
|
+
postSetup(sourceDir: string, targetDir: string, analysis: AdapterAnalysis): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AdapterAnalysis {
|
|
26
|
+
stack: string[];
|
|
27
|
+
packageManager: string;
|
|
28
|
+
bindings: {
|
|
29
|
+
typecheck: string | null;
|
|
30
|
+
test: string | null;
|
|
31
|
+
scopedBuild: string | null;
|
|
32
|
+
};
|
|
33
|
+
placement: "apps" | "packages";
|
|
34
|
+
appName: string;
|
|
35
|
+
excludePatterns: string[];
|
|
36
|
+
gitHistory: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MigrationResult {
|
|
40
|
+
filesCopied: string[];
|
|
41
|
+
filesSkipped: string[];
|
|
42
|
+
conflicts: Conflict[];
|
|
43
|
+
workspaceUpdated: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface Conflict {
|
|
47
|
+
path: string;
|
|
48
|
+
sourceExists: boolean;
|
|
49
|
+
forgeExists: boolean;
|
|
50
|
+
resolution: "forge-wins" | "source-wins";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const FORGE_PROTECTED_PATHS = [
|
|
54
|
+
"forge.yaml",
|
|
55
|
+
".agents",
|
|
56
|
+
"docs/rfcs",
|
|
57
|
+
"docs/adrs",
|
|
58
|
+
"PREFERENCES.md",
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
export const DEFAULT_EXCLUDE_PATTERNS = [
|
|
62
|
+
"node_modules",
|
|
63
|
+
"dist",
|
|
64
|
+
".next",
|
|
65
|
+
".cache",
|
|
66
|
+
".turbo",
|
|
67
|
+
".git",
|
|
68
|
+
] as const;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { detectWorkspaceType, discoverWorkspaces } from "../workspace-discovery.ts";
|
|
3
|
+
import { GENERATED_MARKER } from "../../utils/index.ts";
|
|
4
|
+
import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
tempDir = await mkdtemp(join(tmpdir(), "forge-ws-discovery-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("detectWorkspaceType", () => {
|
|
19
|
+
it("returns null when no package.json exists", () => {
|
|
20
|
+
expect(detectWorkspaceType(tempDir)).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("detects 'app' when astro.config.mjs exists", async () => {
|
|
24
|
+
await writeFile(join(tempDir, "package.json"), '{"name":"app"}', "utf8");
|
|
25
|
+
await writeFile(join(tempDir, "astro.config.mjs"), "export default {}", "utf8");
|
|
26
|
+
expect(detectWorkspaceType(tempDir)).toBe("app");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("detects 'app' when astro.config.ts exists", async () => {
|
|
30
|
+
await writeFile(join(tempDir, "package.json"), '{"name":"app"}', "utf8");
|
|
31
|
+
await writeFile(join(tempDir, "astro.config.ts"), "export default {}", "utf8");
|
|
32
|
+
expect(detectWorkspaceType(tempDir)).toBe("app");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("detects 'service' when Dockerfile exists", async () => {
|
|
36
|
+
await writeFile(join(tempDir, "package.json"), '{"name":"svc"}', "utf8");
|
|
37
|
+
await writeFile(join(tempDir, "Dockerfile"), "FROM node:20", "utf8");
|
|
38
|
+
expect(detectWorkspaceType(tempDir)).toBe("service");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("detects 'service' when service.config.yaml exists", async () => {
|
|
42
|
+
await writeFile(join(tempDir, "package.json"), '{"name":"svc"}', "utf8");
|
|
43
|
+
await writeFile(join(tempDir, "service.config.yaml"), "name: svc", "utf8");
|
|
44
|
+
expect(detectWorkspaceType(tempDir)).toBe("service");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("defaults to 'package' when package.json exists but no markers", async () => {
|
|
48
|
+
await writeFile(join(tempDir, "package.json"), '{"name":"pkg"}', "utf8");
|
|
49
|
+
expect(detectWorkspaceType(tempDir)).toBe("package");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("uses profile workspaceTypes when provided", async () => {
|
|
53
|
+
await writeFile(join(tempDir, "package.json"), '{"name":"pkg","dependencies":{"phaser":"^3"}}', "utf8");
|
|
54
|
+
const wst = [{ id: "game", detect: { packageJsonDep: "phaser" }, skills: [] }];
|
|
55
|
+
expect(detectWorkspaceType(tempDir, wst)).toBe("game");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns null when profile workspaceTypes provided but no match", async () => {
|
|
59
|
+
await writeFile(join(tempDir, "package.json"), '{"name":"pkg"}', "utf8");
|
|
60
|
+
const wst = [{ id: "game", detect: { packageJsonDep: "phaser" }, skills: [] }];
|
|
61
|
+
expect(detectWorkspaceType(tempDir, wst)).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("profile workspaceTypes with glob detection", async () => {
|
|
65
|
+
await writeFile(join(tempDir, "package.json"), '{"name":"pkg"}', "utf8");
|
|
66
|
+
await writeFile(join(tempDir, "game.config"), "config", "utf8");
|
|
67
|
+
const wst = [{ id: "game", detect: { glob: "game.*" }, skills: [] }];
|
|
68
|
+
expect(detectWorkspaceType(tempDir, wst)).toBe("game");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("profile workspaceTypes with contains detection", async () => {
|
|
72
|
+
await writeFile(join(tempDir, "package.json"), '{"name":"pkg"}', "utf8");
|
|
73
|
+
await writeFile(join(tempDir, "marker.txt"), "marker", "utf8");
|
|
74
|
+
const wst = [{ id: "custom", detect: { contains: "marker.txt" }, skills: [] }];
|
|
75
|
+
expect(detectWorkspaceType(tempDir, wst)).toBe("custom");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("discoverWorkspaces", () => {
|
|
80
|
+
it("returns empty for directory with no workspaces", () => {
|
|
81
|
+
expect(discoverWorkspaces(tempDir)).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("discovers nested workspaces", async () => {
|
|
85
|
+
const appsDir = join(tempDir, "apps");
|
|
86
|
+
const appDir = join(appsDir, "my-app");
|
|
87
|
+
await mkdir(appDir, { recursive: true });
|
|
88
|
+
await writeFile(join(appDir, "package.json"), '{"name":"my-app"}', "utf8");
|
|
89
|
+
await writeFile(join(appDir, "astro.config.mjs"), "export default {}", "utf8");
|
|
90
|
+
|
|
91
|
+
const results = discoverWorkspaces(tempDir);
|
|
92
|
+
expect(results).toHaveLength(1);
|
|
93
|
+
expect(results[0].type).toBe("app");
|
|
94
|
+
expect(results[0].path).toContain("my-app");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("skips node_modules, .git, dist, .turbo, .cache, .agents", async () => {
|
|
98
|
+
for (const skipDir of ["node_modules", ".git", "dist", ".turbo", ".cache", ".agents"]) {
|
|
99
|
+
const dir = join(tempDir, skipDir, "sub");
|
|
100
|
+
await mkdir(dir, { recursive: true });
|
|
101
|
+
await writeFile(join(dir, "package.json"), '{"name":"skip"}', "utf8");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const results = discoverWorkspaces(tempDir);
|
|
105
|
+
expect(results).toHaveLength(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("detects hasAgentsMd and isGenerated", async () => {
|
|
109
|
+
const pkgDir = join(tempDir, "packages", "my-pkg");
|
|
110
|
+
await mkdir(pkgDir, { recursive: true });
|
|
111
|
+
await writeFile(join(pkgDir, "package.json"), '{"name":"my-pkg"}', "utf8");
|
|
112
|
+
await writeFile(
|
|
113
|
+
join(pkgDir, "AGENTS.md"),
|
|
114
|
+
`<!-- ${GENERATED_MARKER} -->\n# Test`,
|
|
115
|
+
"utf8",
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const results = discoverWorkspaces(tempDir);
|
|
119
|
+
expect(results).toHaveLength(1);
|
|
120
|
+
expect(results[0].hasAgentsMd).toBe(true);
|
|
121
|
+
expect(results[0].isGenerated).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("reports hasAgentsMd=false when no AGENTS.md", async () => {
|
|
125
|
+
const pkgDir = join(tempDir, "packages", "my-pkg");
|
|
126
|
+
await mkdir(pkgDir, { recursive: true });
|
|
127
|
+
await writeFile(join(pkgDir, "package.json"), '{"name":"my-pkg"}', "utf8");
|
|
128
|
+
|
|
129
|
+
const results = discoverWorkspaces(tempDir);
|
|
130
|
+
expect(results).toHaveLength(1);
|
|
131
|
+
expect(results[0].hasAgentsMd).toBe(false);
|
|
132
|
+
expect(results[0].isGenerated).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|