project-tiny-context-harness 0.2.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +399 -0
- package/assets/README.md +455 -0
- package/assets/README.zh-CN.md +131 -0
- package/assets/agents/.gitkeep +1 -0
- package/assets/agents/AGENTS_CORE.md +58 -0
- package/assets/context_templates/architecture.md +31 -0
- package/assets/context_templates/area.md +31 -0
- package/assets/context_templates/context.toml +27 -0
- package/assets/context_templates/deployment.md +35 -0
- package/assets/context_templates/global.md +53 -0
- package/assets/context_templates/verification.md +31 -0
- package/assets/github/.gitkeep +1 -0
- package/assets/github/harness.yml +37 -0
- package/assets/make/.gitkeep +1 -0
- package/assets/make/sdlc-harness.mk +39 -0
- package/assets/skills/context_development_engineer/SKILL.md +86 -0
- package/assets/skills/context_full_project_export/SKILL.md +55 -0
- package/assets/skills/context_product_plan/SKILL.md +85 -0
- package/assets/skills/context_uiux_design/SKILL.md +110 -0
- package/assets/tools/validate_context.py +276 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +12 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +16 -0
- package/dist/commands/export-context.d.ts +1 -0
- package/dist/commands/export-context.js +149 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.js +33 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +108 -0
- package/dist/commands/package-source.d.ts +1 -0
- package/dist/commands/package-source.js +24 -0
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +14 -0
- package/dist/commands/upgrade.d.ts +1 -0
- package/dist/commands/upgrade.js +7 -0
- package/dist/commands/validate.d.ts +1 -0
- package/dist/commands/validate.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/config.js +52 -0
- package/dist/lib/constants.d.ts +3 -0
- package/dist/lib/constants.js +3 -0
- package/dist/lib/context-export.d.ts +21 -0
- package/dist/lib/context-export.js +845 -0
- package/dist/lib/context-manifest.d.ts +3 -0
- package/dist/lib/context-manifest.js +103 -0
- package/dist/lib/context-templates.d.ts +5 -0
- package/dist/lib/context-templates.js +204 -0
- package/dist/lib/design-md.d.ts +2 -0
- package/dist/lib/design-md.js +132 -0
- package/dist/lib/doctor.d.ts +6 -0
- package/dist/lib/doctor.js +41 -0
- package/dist/lib/fs.d.ts +8 -0
- package/dist/lib/fs.js +56 -0
- package/dist/lib/harness-root.d.ts +9 -0
- package/dist/lib/harness-root.js +50 -0
- package/dist/lib/init.d.ts +5 -0
- package/dist/lib/init.js +65 -0
- package/dist/lib/managed-file.d.ts +19 -0
- package/dist/lib/managed-file.js +21 -0
- package/dist/lib/migrations.d.ts +11 -0
- package/dist/lib/migrations.js +180 -0
- package/dist/lib/package-json-config.d.ts +2 -0
- package/dist/lib/package-json-config.js +37 -0
- package/dist/lib/package-source.d.ts +8 -0
- package/dist/lib/package-source.js +124 -0
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +11 -0
- package/dist/lib/schema-guard.d.ts +3 -0
- package/dist/lib/schema-guard.js +28 -0
- package/dist/lib/sync-engine.d.ts +7 -0
- package/dist/lib/sync-engine.js +350 -0
- package/dist/lib/types.d.ts +21 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/upgrade.d.ts +1 -0
- package/dist/lib/upgrade.js +21 -0
- package/dist/lib/validators.d.ts +5 -0
- package/dist/lib/validators.js +459 -0
- package/dist/lib/yaml.d.ts +2 -0
- package/dist/lib/yaml.js +7 -0
- package/migrations/README.md +3 -0
- package/package.json +68 -0
- package/source-mappings.yaml +25 -0
package/dist/lib/init.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { CONTEXT_MANIFEST_PATH, defaultContextManifestTemplate } from "./context-manifest.js";
|
|
3
|
+
import { architectureContextTemplate, areaContextTemplate, globalContextTemplate, verificationContextTemplate } from "./context-templates.js";
|
|
4
|
+
import { writeConfigIfMissing } from "./config.js";
|
|
5
|
+
import { createDesignMdIfMissing, DESIGN_MD_PATH } from "./design-md.js";
|
|
6
|
+
import { harnessConfigPath } from "./harness-root.js";
|
|
7
|
+
import { ensureDir, pathExists, writeTextIfChanged } from "./fs.js";
|
|
8
|
+
import { assertSupportedSchema } from "./schema-guard.js";
|
|
9
|
+
import { runSync } from "./sync-engine.js";
|
|
10
|
+
export async function runInit(projectRoot, options) {
|
|
11
|
+
const report = [];
|
|
12
|
+
await assertSupportedSchema(projectRoot, "init");
|
|
13
|
+
const existingEntries = await projectHasExistingFiles(projectRoot);
|
|
14
|
+
if (existingEntries && !options.adopt && !options.force) {
|
|
15
|
+
report.push("Project is not empty; continuing with non-destructive init. Use --adopt to mark this as an existing project adoption.");
|
|
16
|
+
}
|
|
17
|
+
const configPath = await harnessConfigPath(projectRoot);
|
|
18
|
+
if (await writeConfigIfMissing(projectRoot)) {
|
|
19
|
+
report.push(`created ${configPath}`);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
report.push(`kept existing ${configPath}`);
|
|
23
|
+
}
|
|
24
|
+
await createProjectContext(projectRoot, report);
|
|
25
|
+
await createDesignMd(projectRoot, report);
|
|
26
|
+
const syncReport = await runSync(projectRoot);
|
|
27
|
+
report.push(`sync changed=${syncReport.changed.length} skipped=${syncReport.skipped.length} blocked=${syncReport.blocked.length}`);
|
|
28
|
+
report.push(options.adopt ? "adopt mode complete" : "init complete");
|
|
29
|
+
return report;
|
|
30
|
+
}
|
|
31
|
+
async function createDesignMd(projectRoot, report) {
|
|
32
|
+
if (await createDesignMdIfMissing(projectRoot)) {
|
|
33
|
+
report.push(`created ${DESIGN_MD_PATH}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function projectHasExistingFiles(projectRoot) {
|
|
37
|
+
const markers = ["README.md", "src", "pyproject.toml", "go.mod"];
|
|
38
|
+
for (const marker of markers) {
|
|
39
|
+
if (await pathExists(path.join(projectRoot, marker))) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
async function createProjectContext(projectRoot, report) {
|
|
46
|
+
const areasRoot = path.join(projectRoot, "project_context", "areas");
|
|
47
|
+
await ensureDir(areasRoot);
|
|
48
|
+
const files = [
|
|
49
|
+
[CONTEXT_MANIFEST_PATH, defaultContextManifestTemplate()],
|
|
50
|
+
["project_context/global.md", globalContextTemplate()],
|
|
51
|
+
["project_context/architecture.md", architectureContextTemplate()],
|
|
52
|
+
["project_context/areas/main.md", areaContextTemplate("main")],
|
|
53
|
+
["project_context/areas/main/verification.md", verificationContextTemplate("main")]
|
|
54
|
+
];
|
|
55
|
+
for (const [relative, content] of files) {
|
|
56
|
+
const target = path.join(projectRoot, relative);
|
|
57
|
+
if (await pathExists(target)) {
|
|
58
|
+
report.push(`kept existing ${relative}`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (await writeTextIfChanged(target, content)) {
|
|
62
|
+
report.push(`created ${relative}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ManagedBlockMarkers {
|
|
2
|
+
start: string;
|
|
3
|
+
end: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const MANAGED_BLOCK_START = "<!-- pjsdlc:sdlc-harness:begin -->";
|
|
6
|
+
export declare const MANAGED_BLOCK_END = "<!-- pjsdlc:sdlc-harness:end -->";
|
|
7
|
+
export declare const LEGACY_MANAGED_BLOCK_START = "<!-- sdlc-harness:begin -->";
|
|
8
|
+
export declare const LEGACY_MANAGED_BLOCK_END = "<!-- sdlc-harness:end -->";
|
|
9
|
+
export declare const MAKEFILE_BLOCK_START = "# pjsdlc:sdlc-harness:make:begin";
|
|
10
|
+
export declare const MAKEFILE_BLOCK_END = "# pjsdlc:sdlc-harness:make:end";
|
|
11
|
+
export declare const LEGACY_MAKEFILE_BLOCK_START = "# sdlc-harness:make:begin";
|
|
12
|
+
export declare const LEGACY_MAKEFILE_BLOCK_END = "# sdlc-harness:make:end";
|
|
13
|
+
export declare const GITHUB_WORKFLOW_BLOCK_START = "# pjsdlc:sdlc-harness:github-workflow:begin";
|
|
14
|
+
export declare const GITHUB_WORKFLOW_BLOCK_END = "# pjsdlc:sdlc-harness:github-workflow:end";
|
|
15
|
+
export declare const MANAGED_METADATA_START = "<!-- pjsdlc:sdlc-harness-managed";
|
|
16
|
+
export declare const LEGACY_MANAGED_METADATA_START = "<!-- sdlc-harness-managed";
|
|
17
|
+
export declare const MANAGED_METADATA_END = "-->";
|
|
18
|
+
export declare const AGENTS_BLOCK_MARKERS: ManagedBlockMarkers[];
|
|
19
|
+
export declare const MAKEFILE_BLOCK_MARKERS: ManagedBlockMarkers[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const MANAGED_BLOCK_START = "<!-- pjsdlc:sdlc-harness:begin -->";
|
|
2
|
+
export const MANAGED_BLOCK_END = "<!-- pjsdlc:sdlc-harness:end -->";
|
|
3
|
+
export const LEGACY_MANAGED_BLOCK_START = "<!-- sdlc-harness:begin -->";
|
|
4
|
+
export const LEGACY_MANAGED_BLOCK_END = "<!-- sdlc-harness:end -->";
|
|
5
|
+
export const MAKEFILE_BLOCK_START = "# pjsdlc:sdlc-harness:make:begin";
|
|
6
|
+
export const MAKEFILE_BLOCK_END = "# pjsdlc:sdlc-harness:make:end";
|
|
7
|
+
export const LEGACY_MAKEFILE_BLOCK_START = "# sdlc-harness:make:begin";
|
|
8
|
+
export const LEGACY_MAKEFILE_BLOCK_END = "# sdlc-harness:make:end";
|
|
9
|
+
export const GITHUB_WORKFLOW_BLOCK_START = "# pjsdlc:sdlc-harness:github-workflow:begin";
|
|
10
|
+
export const GITHUB_WORKFLOW_BLOCK_END = "# pjsdlc:sdlc-harness:github-workflow:end";
|
|
11
|
+
export const MANAGED_METADATA_START = "<!-- pjsdlc:sdlc-harness-managed";
|
|
12
|
+
export const LEGACY_MANAGED_METADATA_START = "<!-- sdlc-harness-managed";
|
|
13
|
+
export const MANAGED_METADATA_END = "-->";
|
|
14
|
+
export const AGENTS_BLOCK_MARKERS = [
|
|
15
|
+
{ start: MANAGED_BLOCK_START, end: MANAGED_BLOCK_END },
|
|
16
|
+
{ start: LEGACY_MANAGED_BLOCK_START, end: LEGACY_MANAGED_BLOCK_END }
|
|
17
|
+
];
|
|
18
|
+
export const MAKEFILE_BLOCK_MARKERS = [
|
|
19
|
+
{ start: MAKEFILE_BLOCK_START, end: MAKEFILE_BLOCK_END },
|
|
20
|
+
{ start: LEGACY_MAKEFILE_BLOCK_START, end: LEGACY_MAKEFILE_BLOCK_END }
|
|
21
|
+
];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface Migration {
|
|
2
|
+
from: string;
|
|
3
|
+
to: string;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const migrations: Migration[];
|
|
7
|
+
export interface MigrationReport {
|
|
8
|
+
changed: string[];
|
|
9
|
+
skipped: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function runMigrations(projectRoot: string): Promise<MigrationReport>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import { CONTEXT_MANIFEST_PATH, contextManifestFromExistingAreas } from "./context-manifest.js";
|
|
4
|
+
import { architectureContextTemplate, areaContextTemplate, globalContextTemplate, verificationContextTemplate } from "./context-templates.js";
|
|
5
|
+
import { defaultConfig, readConfig } from "./config.js";
|
|
6
|
+
import { createDesignMdIfMissing, DESIGN_MD_PATH } from "./design-md.js";
|
|
7
|
+
import { ensureDir, listFiles, pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
8
|
+
import { harnessConfigPath, harnessRoot } from "./harness-root.js";
|
|
9
|
+
import { stringifyYaml } from "./yaml.js";
|
|
10
|
+
export const migrations = [];
|
|
11
|
+
export async function runMigrations(projectRoot) {
|
|
12
|
+
const report = { changed: [], skipped: [] };
|
|
13
|
+
const root = await harnessRoot(projectRoot);
|
|
14
|
+
await migrateConfig(projectRoot, root, report);
|
|
15
|
+
await migrateLegacyModulesToAreas(projectRoot, report);
|
|
16
|
+
await migrateBaseProjectContext(projectRoot, report);
|
|
17
|
+
await migrateContextManifest(projectRoot, report);
|
|
18
|
+
await migrateManifestModulePaths(projectRoot, report);
|
|
19
|
+
await migrateDesignMd(projectRoot, report);
|
|
20
|
+
return report;
|
|
21
|
+
}
|
|
22
|
+
async function migrateBaseProjectContext(projectRoot, report) {
|
|
23
|
+
await ensureDir(path.join(projectRoot, "project_context", "areas"));
|
|
24
|
+
const files = [
|
|
25
|
+
["project_context/global.md", globalContextTemplate()],
|
|
26
|
+
["project_context/architecture.md", architectureContextTemplate()],
|
|
27
|
+
["project_context/areas/main.md", areaContextTemplate("main")],
|
|
28
|
+
["project_context/areas/main/verification.md", verificationContextTemplate("main")]
|
|
29
|
+
];
|
|
30
|
+
for (const [relative, content] of files) {
|
|
31
|
+
const target = path.join(projectRoot, ...relative.split("/"));
|
|
32
|
+
if (await pathExists(target)) {
|
|
33
|
+
report.skipped.push(relative);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (await writeTextIfChanged(target, content)) {
|
|
37
|
+
report.changed.push(relative);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
report.skipped.push(relative);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
await migrateGlobalContextSections(projectRoot, report);
|
|
44
|
+
}
|
|
45
|
+
async function migrateGlobalContextSections(projectRoot, report) {
|
|
46
|
+
const relative = "project_context/global.md";
|
|
47
|
+
const target = path.join(projectRoot, ...relative.split("/"));
|
|
48
|
+
if (!(await pathExists(target))) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const original = await readText(target);
|
|
52
|
+
const rewritten = rewriteLegacyModuleReferences(original);
|
|
53
|
+
const additions = [];
|
|
54
|
+
if (!hasHeading(rewritten, "Architecture Context")) {
|
|
55
|
+
additions.push("## Architecture Context", "", "- See `project_context/architecture.md` for the restrained architecture context.", "");
|
|
56
|
+
}
|
|
57
|
+
if (!hasHeading(rewritten, "Context Graph")) {
|
|
58
|
+
additions.push("## Context Graph", "", "- See `project_context/context.toml` for area/context_unit roles, read policy and boundary metadata.", "");
|
|
59
|
+
}
|
|
60
|
+
if (!hasHeading(rewritten, "Context Index")) {
|
|
61
|
+
additions.push("## Context Index", "", "- See `project_context/context.toml` for the current area and context node list.", "");
|
|
62
|
+
}
|
|
63
|
+
if (additions.length === 0 && rewritten === original) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const next = additions.length === 0 ? rewritten : `${rewritten.replace(/\s*$/, "\n\n")}${additions.join("\n")}`;
|
|
67
|
+
if (await writeTextIfChanged(target, next)) {
|
|
68
|
+
report.changed.push(`${relative}#schema-v4-sections`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function migrateContextManifest(projectRoot, report) {
|
|
72
|
+
const manifestPath = path.join(projectRoot, CONTEXT_MANIFEST_PATH);
|
|
73
|
+
if (await pathExists(manifestPath)) {
|
|
74
|
+
report.skipped.push(CONTEXT_MANIFEST_PATH);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (await writeTextIfChanged(manifestPath, await contextManifestFromExistingAreas(projectRoot))) {
|
|
78
|
+
report.changed.push(CONTEXT_MANIFEST_PATH);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
report.skipped.push(CONTEXT_MANIFEST_PATH);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function migrateLegacyModulesToAreas(projectRoot, report) {
|
|
85
|
+
const modulesRoot = path.join(projectRoot, "project_context", "modules");
|
|
86
|
+
const areasRoot = path.join(projectRoot, "project_context", "areas");
|
|
87
|
+
const moduleFiles = (await listFiles(modulesRoot)).filter((file) => file.endsWith(".md")).sort();
|
|
88
|
+
if (moduleFiles.length === 0) {
|
|
89
|
+
report.skipped.push("project_context/modules");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await ensureDir(areasRoot);
|
|
93
|
+
for (const source of moduleFiles) {
|
|
94
|
+
const relativeToModules = path.relative(modulesRoot, source);
|
|
95
|
+
const target = path.join(areasRoot, relativeToModules);
|
|
96
|
+
const sourceRelative = `project_context/modules/${relativeToModules.split(path.sep).join("/")}`;
|
|
97
|
+
const targetRelative = `project_context/areas/${relativeToModules.split(path.sep).join("/")}`;
|
|
98
|
+
if (await pathExists(target)) {
|
|
99
|
+
report.skipped.push(`${sourceRelative} -> ${targetRelative}`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
await ensureDir(path.dirname(target));
|
|
103
|
+
await fs.rename(source, target);
|
|
104
|
+
report.changed.push(`${sourceRelative} -> ${targetRelative}`);
|
|
105
|
+
}
|
|
106
|
+
const remainingFiles = await listFiles(modulesRoot);
|
|
107
|
+
if (remainingFiles.length === 0 && await pathExists(modulesRoot)) {
|
|
108
|
+
await fs.rm(modulesRoot, { recursive: true, force: true });
|
|
109
|
+
report.changed.push("removed project_context/modules");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function migrateManifestModulePaths(projectRoot, report) {
|
|
113
|
+
const manifestPath = path.join(projectRoot, CONTEXT_MANIFEST_PATH);
|
|
114
|
+
if (!(await pathExists(manifestPath))) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const original = await readText(manifestPath);
|
|
118
|
+
const next = ensureManifestDefaultArea(rewriteLegacyModuleReferences(original));
|
|
119
|
+
if (next !== original && await writeTextIfChanged(manifestPath, next)) {
|
|
120
|
+
report.changed.push(`${CONTEXT_MANIFEST_PATH}#areas-paths`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function migrateDesignMd(projectRoot, report) {
|
|
124
|
+
if (await createDesignMdIfMissing(projectRoot)) {
|
|
125
|
+
report.changed.push(DESIGN_MD_PATH);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
report.skipped.push(DESIGN_MD_PATH);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function migrateConfig(projectRoot, root, report) {
|
|
132
|
+
const relativeConfigPath = await harnessConfigPath(projectRoot);
|
|
133
|
+
const configPath = path.join(projectRoot, relativeConfigPath);
|
|
134
|
+
if (!(await pathExists(configPath))) {
|
|
135
|
+
report.skipped.push(relativeConfigPath);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const config = await readConfig(projectRoot);
|
|
139
|
+
const current = defaultConfig(root);
|
|
140
|
+
config.core = current.core;
|
|
141
|
+
config.managed_files = current.managed_files;
|
|
142
|
+
config.never_overwrite = Array.from(new Set([...current.never_overwrite, ...config.never_overwrite]));
|
|
143
|
+
if (await writeTextIfChanged(configPath, stringifyYaml(config))) {
|
|
144
|
+
report.changed.push(relativeConfigPath);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
report.skipped.push(relativeConfigPath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function hasHeading(content, heading) {
|
|
151
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
152
|
+
return new RegExp(`^##\\s+${escaped}\\s*$`, "im").test(content);
|
|
153
|
+
}
|
|
154
|
+
function rewriteLegacyModuleReferences(content) {
|
|
155
|
+
return content
|
|
156
|
+
.replace(/project_context\/modules\//g, "project_context/areas/")
|
|
157
|
+
.replace(/\(modules\//g, "(areas/");
|
|
158
|
+
}
|
|
159
|
+
function ensureManifestDefaultArea(content) {
|
|
160
|
+
if (/^\s*default\s*=\s*true\s*$/im.test(content)) {
|
|
161
|
+
return content;
|
|
162
|
+
}
|
|
163
|
+
const lines = content.split(/\r?\n/);
|
|
164
|
+
const firstAreaIndex = lines.findIndex((line) => line.trim() === "[[areas]]");
|
|
165
|
+
if (firstAreaIndex === -1) {
|
|
166
|
+
return content;
|
|
167
|
+
}
|
|
168
|
+
let nextTableIndex = lines.findIndex((line, index) => index > firstAreaIndex && /^\s*\[\[/.test(line));
|
|
169
|
+
if (nextTableIndex === -1) {
|
|
170
|
+
nextTableIndex = lines.length;
|
|
171
|
+
}
|
|
172
|
+
for (let index = firstAreaIndex + 1; index < nextTableIndex; index += 1) {
|
|
173
|
+
if (/^\s*default\s*=/.test(lines[index])) {
|
|
174
|
+
lines[index] = "default = true";
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
lines.splice(nextTableIndex, 0, "default = true");
|
|
179
|
+
return lines.join("\n");
|
|
180
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
3
|
+
import { normalizeHarnessFolderName } from "./harness-root.js";
|
|
4
|
+
export async function packageHarnessRoot(projectRoot) {
|
|
5
|
+
const packagePath = path.join(projectRoot, "package.json");
|
|
6
|
+
if (!(await pathExists(packagePath))) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const packageJson = parsePackageJson(await readText(packagePath));
|
|
10
|
+
const config = packageJson.sdlcHarness;
|
|
11
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const value = config.harnessFolderName;
|
|
15
|
+
return typeof value === "string" && value.trim() ? normalizeHarnessFolderName(value) : undefined;
|
|
16
|
+
}
|
|
17
|
+
export async function writePackageHarnessRoot(projectRoot, folderName) {
|
|
18
|
+
const normalized = normalizeHarnessFolderName(folderName);
|
|
19
|
+
const packagePath = path.join(projectRoot, "package.json");
|
|
20
|
+
const packageJson = (await pathExists(packagePath)) ? parsePackageJson(await readText(packagePath)) : {};
|
|
21
|
+
const existingConfig = packageJson.sdlcHarness;
|
|
22
|
+
const nextConfig = existingConfig && typeof existingConfig === "object" && !Array.isArray(existingConfig)
|
|
23
|
+
? { ...existingConfig, harnessFolderName: normalized }
|
|
24
|
+
: { harnessFolderName: normalized };
|
|
25
|
+
const next = {
|
|
26
|
+
...packageJson,
|
|
27
|
+
sdlcHarness: nextConfig
|
|
28
|
+
};
|
|
29
|
+
return writeTextIfChanged(packagePath, `${JSON.stringify(next, null, 2)}\n`);
|
|
30
|
+
}
|
|
31
|
+
function parsePackageJson(content) {
|
|
32
|
+
const parsed = JSON.parse(content);
|
|
33
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
34
|
+
throw new Error("package.json must contain a JSON object");
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface PackageSourceSyncReport {
|
|
2
|
+
changed: string[];
|
|
3
|
+
}
|
|
4
|
+
export interface PackageSourceCheckReport {
|
|
5
|
+
drift: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function syncSource(projectRoot: string): Promise<PackageSourceSyncReport>;
|
|
8
|
+
export declare function checkSource(projectRoot: string): Promise<PackageSourceCheckReport>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ensureDir, listFiles, pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
5
|
+
import { AGENTS_BLOCK_MARKERS } from "./managed-file.js";
|
|
6
|
+
import { SOURCE_MAPPINGS_PATH } from "./paths.js";
|
|
7
|
+
import { parseYaml } from "./yaml.js";
|
|
8
|
+
export async function syncSource(projectRoot) {
|
|
9
|
+
const report = { changed: [] };
|
|
10
|
+
for (const mapping of await readSourceMappings(projectRoot)) {
|
|
11
|
+
const changed = await applyMapping(projectRoot, mapping);
|
|
12
|
+
report.changed.push(...changed);
|
|
13
|
+
}
|
|
14
|
+
return report;
|
|
15
|
+
}
|
|
16
|
+
export async function checkSource(projectRoot) {
|
|
17
|
+
const drift = [];
|
|
18
|
+
for (const mapping of await readSourceMappings(projectRoot)) {
|
|
19
|
+
const expected = await renderMapping(projectRoot, mapping);
|
|
20
|
+
const target = path.join(projectRoot, mapping.target);
|
|
21
|
+
if (typeof expected === "string") {
|
|
22
|
+
const existing = (await pathExists(target)) ? await readText(target) : "";
|
|
23
|
+
if (normalize(existing) !== normalize(expected)) {
|
|
24
|
+
drift.push(mapping.target);
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const sourceHashes = new Map();
|
|
29
|
+
for (const item of expected) {
|
|
30
|
+
sourceHashes.set(item.relative, hash(item.content));
|
|
31
|
+
const targetFile = path.join(target, item.relative);
|
|
32
|
+
const existing = (await pathExists(targetFile)) ? await readText(targetFile) : "";
|
|
33
|
+
if (hash(existing) !== hash(item.content)) {
|
|
34
|
+
drift.push(`${mapping.target}/${item.relative}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const targetFiles = await listFiles(target);
|
|
38
|
+
for (const targetFile of targetFiles) {
|
|
39
|
+
if (path.basename(targetFile) === ".gitkeep") {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const relative = path.relative(target, targetFile);
|
|
43
|
+
if (!sourceHashes.has(relative)) {
|
|
44
|
+
drift.push(`${mapping.target}/${relative}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { drift };
|
|
49
|
+
}
|
|
50
|
+
async function readSourceMappings(projectRoot) {
|
|
51
|
+
const mappingPath = path.join(projectRoot, SOURCE_MAPPINGS_PATH);
|
|
52
|
+
const parsed = parseYaml(await readText(mappingPath));
|
|
53
|
+
return parsed.source_mappings ?? [];
|
|
54
|
+
}
|
|
55
|
+
async function applyMapping(projectRoot, mapping) {
|
|
56
|
+
const target = path.join(projectRoot, mapping.target);
|
|
57
|
+
const rendered = await renderMapping(projectRoot, mapping);
|
|
58
|
+
if (typeof rendered === "string") {
|
|
59
|
+
return (await writeTextIfChanged(target, rendered)) ? [mapping.target] : [];
|
|
60
|
+
}
|
|
61
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
62
|
+
await ensureDir(target);
|
|
63
|
+
const changed = [];
|
|
64
|
+
for (const item of rendered) {
|
|
65
|
+
const targetFile = path.join(target, item.relative);
|
|
66
|
+
if (await writeTextIfChanged(targetFile, item.content)) {
|
|
67
|
+
changed.push(`${mapping.target}/${item.relative}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return changed;
|
|
71
|
+
}
|
|
72
|
+
async function renderMapping(projectRoot, mapping) {
|
|
73
|
+
const source = path.join(projectRoot, mapping.source);
|
|
74
|
+
if (mapping.mode === "copy-file") {
|
|
75
|
+
return readText(source);
|
|
76
|
+
}
|
|
77
|
+
if (mapping.mode === "copy-tree") {
|
|
78
|
+
const files = await listFiles(source);
|
|
79
|
+
const rendered = [];
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
if (path.basename(file) === ".gitkeep") {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const relative = path.relative(source, file);
|
|
85
|
+
if (isExcluded(relative, mapping.exclude ?? [])) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
rendered.push({ relative, content: await readText(file) });
|
|
89
|
+
}
|
|
90
|
+
return rendered;
|
|
91
|
+
}
|
|
92
|
+
if (mapping.mode === "extract-managed-block") {
|
|
93
|
+
const content = await readText(source);
|
|
94
|
+
for (const markers of AGENTS_BLOCK_MARKERS) {
|
|
95
|
+
const start = content.indexOf(markers.start);
|
|
96
|
+
const end = content.indexOf(markers.end);
|
|
97
|
+
if (start >= 0 && end > start) {
|
|
98
|
+
return `${content.slice(start + markers.start.length, end).trim()}\n`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return content;
|
|
102
|
+
}
|
|
103
|
+
if (mapping.mode === "extract-harness-targets") {
|
|
104
|
+
return readText(source);
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`Unsupported source mapping mode: ${mapping.mode}`);
|
|
107
|
+
}
|
|
108
|
+
function isExcluded(relativePath, patterns) {
|
|
109
|
+
const normalized = relativePath.split(path.sep).join("/");
|
|
110
|
+
return patterns.some((pattern) => {
|
|
111
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
112
|
+
if (normalizedPattern.endsWith("/**")) {
|
|
113
|
+
const prefix = normalizedPattern.slice(0, -3);
|
|
114
|
+
return normalized === prefix || normalized.startsWith(`${prefix}/`);
|
|
115
|
+
}
|
|
116
|
+
return normalized === normalizedPattern;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function normalize(content) {
|
|
120
|
+
return content.replace(/\r\n/g, "\n").trimEnd();
|
|
121
|
+
}
|
|
122
|
+
function hash(content) {
|
|
123
|
+
return createHash("sha256").update(normalize(content)).digest("hex");
|
|
124
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const SOURCE_MAPPINGS_PATH = "packages/sdlc-harness/source-mappings.yaml";
|
|
2
|
+
export declare const DEFAULT_HARNESS_ROOT = ".agent";
|
|
3
|
+
export declare const HARNESS_JSON_CONFIG_PATH = "sdlc-harness.config.json";
|
|
4
|
+
export declare function packageRoot(): string;
|
|
5
|
+
export declare function packageAssetPath(...segments: string[]): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
export const SOURCE_MAPPINGS_PATH = "packages/sdlc-harness/source-mappings.yaml";
|
|
4
|
+
export const DEFAULT_HARNESS_ROOT = ".agent";
|
|
5
|
+
export const HARNESS_JSON_CONFIG_PATH = "sdlc-harness.config.json";
|
|
6
|
+
export function packageRoot() {
|
|
7
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
|
+
}
|
|
9
|
+
export function packageAssetPath(...segments) {
|
|
10
|
+
return path.join(packageRoot(), "assets", ...segments);
|
|
11
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function assertSupportedSchema(projectRoot: string, commandName: string): Promise<void>;
|
|
2
|
+
export declare function unsupportedSchemaMessage(schemaVersion: string, commandName: string): string | undefined;
|
|
3
|
+
export declare function schemaMajor(schemaVersion: string): number | undefined;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readConfig } from "./config.js";
|
|
2
|
+
import { CANONICAL_NPX_COMMAND, CURRENT_SCHEMA_VERSION } from "./constants.js";
|
|
3
|
+
export async function assertSupportedSchema(projectRoot, commandName) {
|
|
4
|
+
const config = await readConfig(projectRoot);
|
|
5
|
+
const message = unsupportedSchemaMessage(config.core.schema_version, commandName);
|
|
6
|
+
if (message) {
|
|
7
|
+
throw new Error(message);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function unsupportedSchemaMessage(schemaVersion, commandName) {
|
|
11
|
+
const projectMajor = schemaMajor(schemaVersion);
|
|
12
|
+
const supportedMajor = schemaMajor(CURRENT_SCHEMA_VERSION);
|
|
13
|
+
if (projectMajor === undefined || supportedMajor === undefined || projectMajor <= supportedMajor) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return [
|
|
17
|
+
`unsupported Harness schema version ${schemaVersion}; this CLI supports schema ${CURRENT_SCHEMA_VERSION}`,
|
|
18
|
+
`Refusing to run ${commandName} because older CLI versions must not rewrite newer projects`,
|
|
19
|
+
`Use the canonical latest CLI: ${CANONICAL_NPX_COMMAND} ${commandName}`
|
|
20
|
+
].join(". ");
|
|
21
|
+
}
|
|
22
|
+
export function schemaMajor(schemaVersion) {
|
|
23
|
+
const match = /^(\d+)/.exec(String(schemaVersion).trim());
|
|
24
|
+
if (!match) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return Number.parseInt(match[1], 10);
|
|
28
|
+
}
|