@structor-dev/cli 0.1.0 → 0.2.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/CHANGELOG.md +56 -0
- package/README.md +131 -21
- package/ROADMAP.md +38 -0
- package/SECURITY.md +33 -0
- package/bin/structor.mjs +561 -29
- package/contrib/self-harness/files/README.md +32 -0
- package/contrib/self-harness/files/ai/AGENTS.md +35 -0
- package/contrib/self-harness/files/ai/ARCHITECTURE.md +38 -0
- package/contrib/self-harness/files/ai/HUB.md +59 -0
- package/contrib/self-harness/files/ai/PRODUCT.md +36 -0
- package/contrib/self-harness/files/ai/QUALITY.md +31 -0
- package/contrib/self-harness/files/ai/context.md +38 -0
- package/contrib/self-harness/files/scripts/check-workspace.mjs +72 -0
- package/contrib/self-harness/harness.config.json +37 -0
- package/docs/CONTRIBUTOR-SETUP.md +45 -0
- package/docs/INIT.md +55 -2
- package/docs/public-launch.md +150 -0
- package/examples/anthropic-only/harness.config.json +26 -0
- package/examples/frontend-backend/harness.config.json +8 -8
- package/examples/generated-harness-tree.md +432 -0
- package/examples/openai-and-anthropic/harness.config.json +7 -7
- package/examples/single-repo/harness.config.json +7 -7
- package/harness.config.example.json +1 -1
- package/package.json +12 -4
- package/schemas/contract-manifest.schema.json +0 -1
- package/schemas/harness-config.schema.json +5 -2
- package/scripts/check-config.mjs +20 -31
- package/scripts/check-examples.mjs +146 -0
- package/scripts/check-placeholders.mjs +2 -0
- package/scripts/check-public-hygiene.mjs +249 -0
- package/scripts/check-schemas.mjs +42 -0
- package/scripts/check-template-files.mjs +15 -98
- package/scripts/generated-harness-contract.mjs +416 -0
- package/scripts/init-harness.mjs +227 -139
- package/scripts/lib.mjs +462 -12
- package/scripts/rendered-config.mjs +109 -0
- package/scripts/setup-contributor.mjs +125 -0
- package/scripts/smoke-template.mjs +260 -73
- package/template/AGENTS.md.tpl +4 -2
- package/template/README.md.tpl +5 -0
- package/template/ai/CODEX-HOOKS.md.tpl +1 -1
- package/template/ai/HARNESS-ENGINEERING.md.tpl +5 -2
- package/template/ai/HARNESS.md.tpl +4 -1
- package/template/ai/contracts/codex-hooks.contract.json.tpl +58 -1
- package/template/ai/contracts/codex-hooks.md.tpl +6 -0
- package/template/ai/contracts/release-flow.md.tpl +1 -1
- package/template/ai/templates/fixtures/issues/valid-ready.md.tpl +3 -1
- package/template/ai/templates/issue-template.md.tpl +3 -1
- package/template/ai/workspace/LOCAL-STACK.md.tpl +1 -1
- package/template/ai/workspace/SYSTEM-MAP.md.tpl +2 -2
- package/template/consumer/AGENTS.md.tpl +4 -4
- package/template/consumer/CLAUDE.md.tpl +4 -4
- package/template/scripts/bootstrap-workspace.mjs.tpl +11 -25
- package/template/scripts/check-claude-compatibility.mjs.tpl +62 -9
- package/template/scripts/check-codex-hooks.mjs.tpl +262 -20
- package/template/scripts/check-template-governance.mjs.tpl +2 -114
- package/template/scripts/check-workspace.mjs.tpl +27 -103
- package/template/scripts/check-worktree-bootstrap-fixtures.mjs.tpl +12 -0
- package/template/scripts/generate-html-views.mjs.tpl +357 -56
- package/template/scripts/generated-harness-contract.mjs.tpl +1 -0
- package/template/scripts/hooks/lib/codex-hooks-core.mjs.tpl +14 -3
- package/template/scripts/lib/path-safety.mjs.tpl +87 -0
- package/template/scripts/lib/worktree-bootstrap.mjs.tpl +16 -13
- package/template/scripts/validate-governance.mjs.tpl +52 -36
- package/schemas/task-brief.schema.json +0 -37
package/scripts/init-harness.mjs
CHANGED
|
@@ -2,9 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
5
6
|
import path from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import {
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import {
|
|
9
|
+
assertSafeWriteTarget,
|
|
10
|
+
exists,
|
|
11
|
+
resolveHarnessConfig,
|
|
12
|
+
} from "./lib.mjs";
|
|
13
|
+
import {
|
|
14
|
+
consumerEntrypointsForSettings,
|
|
15
|
+
freshRenderScriptTemplatesForSettings,
|
|
16
|
+
shouldRenderTemplate as shouldRenderContractTemplate,
|
|
17
|
+
trustedGeneratedScriptTemplatesForSettings,
|
|
18
|
+
} from "./generated-harness-contract.mjs";
|
|
19
|
+
import {
|
|
20
|
+
consumerEntrypointValues,
|
|
21
|
+
harnessTemplateValues,
|
|
22
|
+
renderedGeneratedScriptHashes,
|
|
23
|
+
} from "./rendered-config.mjs";
|
|
8
24
|
|
|
9
25
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
26
|
|
|
@@ -15,15 +31,9 @@ const dryRunArg = "--dry-run";
|
|
|
15
31
|
const forceArg = "--force";
|
|
16
32
|
const installConsumerEntrypointsArg = "--install-consumer-entrypoints";
|
|
17
33
|
const allowAbsoluteOutputArg = "--allow-absolute-output";
|
|
34
|
+
const allowTemplateRepoConsumerArg = "--allow-template-repo-consumer";
|
|
18
35
|
|
|
19
|
-
|
|
20
|
-
const anthropicPathPrefix = ".claude/";
|
|
21
|
-
const codexHookPathPrefix = ".codex/";
|
|
22
|
-
const scriptRulesPath = "scripts/check-claude-compatibility.mjs.tpl";
|
|
23
|
-
const scriptCodexPath = "scripts/check-codex-hooks.mjs.tpl";
|
|
24
|
-
const scriptHooksPath = "scripts/hooks/";
|
|
25
|
-
|
|
26
|
-
function parseArgs(argv) {
|
|
36
|
+
export function parseArgs(argv) {
|
|
27
37
|
const options = {
|
|
28
38
|
config: configFileDefault,
|
|
29
39
|
output: null,
|
|
@@ -31,6 +41,7 @@ function parseArgs(argv) {
|
|
|
31
41
|
force: false,
|
|
32
42
|
installConsumerEntrypoints: false,
|
|
33
43
|
allowAbsoluteOutput: false,
|
|
44
|
+
allowTemplateRepoConsumer: false,
|
|
34
45
|
};
|
|
35
46
|
|
|
36
47
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -41,13 +52,14 @@ function parseArgs(argv) {
|
|
|
41
52
|
else if (arg === forceArg) options.force = true;
|
|
42
53
|
else if (arg === installConsumerEntrypointsArg) options.installConsumerEntrypoints = true;
|
|
43
54
|
else if (arg === allowAbsoluteOutputArg) options.allowAbsoluteOutput = true;
|
|
55
|
+
else if (arg === allowTemplateRepoConsumerArg) options.allowTemplateRepoConsumer = true;
|
|
44
56
|
else throw new Error(`Unknown argument: ${arg}`);
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
return options;
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
function render(content, values) {
|
|
62
|
+
export function render(content, values) {
|
|
51
63
|
return content.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => {
|
|
52
64
|
if (!(key in values)) {
|
|
53
65
|
throw new Error(`No value provided for template placeholder {{${key}}}`);
|
|
@@ -56,70 +68,24 @@ function render(content, values) {
|
|
|
56
68
|
});
|
|
57
69
|
}
|
|
58
70
|
|
|
59
|
-
function
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function validationList(validation) {
|
|
64
|
-
const entries = Object.entries(validation ?? {});
|
|
65
|
-
if (entries.length === 0) return "- No local validation commands documented yet.";
|
|
66
|
-
return entries.map(([name, command]) => `- ${name}: \`${command}\``).join("\n");
|
|
71
|
+
function sha256(content) {
|
|
72
|
+
return createHash("sha256").update(content).digest("hex");
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
function
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function consumerConfig(consumers, configDir, outputRoot) {
|
|
74
|
-
const workspaceRoot = path.dirname(outputRoot);
|
|
75
|
-
const normalizedConsumers = consumers.map((consumer) => {
|
|
76
|
-
const consumerRoot = path.resolve(configDir, consumer.path);
|
|
77
|
-
return {
|
|
78
|
-
...consumer,
|
|
79
|
-
workspacePath: path.relative(workspaceRoot, consumerRoot).replaceAll(path.sep, "/") || ".",
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
|
-
return JSON.stringify(normalizedConsumers, null, 2);
|
|
75
|
+
function relativePath(from, to) {
|
|
76
|
+
return path.relative(from, to).replaceAll(path.sep, "/") || ".";
|
|
83
77
|
}
|
|
84
78
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function clientSupport(config) {
|
|
79
|
+
async function packageMetadata() {
|
|
80
|
+
const packageJson = JSON.parse(await readFile(path.join(repoRoot, "package.json"), "utf8"));
|
|
90
81
|
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
claudeHooks: config.models.anthropic && (config.clientSupport?.claude?.hooks ?? false),
|
|
94
|
-
claudeSkills: config.models.anthropic && (config.clientSupport?.claude?.skills ?? false),
|
|
82
|
+
name: packageJson.name,
|
|
83
|
+
version: packageJson.version,
|
|
95
84
|
};
|
|
96
85
|
}
|
|
97
86
|
|
|
98
|
-
function shouldRenderTemplate(sourceRelative, config) {
|
|
99
|
-
|
|
100
|
-
const claudePath = "workspace/.claude/";
|
|
101
|
-
const openaiWorkspacePath = "workspace/AGENTS.md.tpl";
|
|
102
|
-
const claudeWorkspacePath = "workspace/CLAUDE.md.tpl";
|
|
103
|
-
|
|
104
|
-
if (sourceRelative.startsWith(consumerPathPrefix)) return false;
|
|
105
|
-
if (!config.models.anthropic && sourceRelative.startsWith(anthropicPathPrefix)) return false;
|
|
106
|
-
if (!support.claudeRules && sourceRelative.startsWith(`${anthropicPathPrefix}rules/`)) return false;
|
|
107
|
-
if (!support.claudeHooks && sourceRelative.startsWith(`${anthropicPathPrefix}hooks/`)) return false;
|
|
108
|
-
if (!support.claudeSkills && sourceRelative.startsWith(`${anthropicPathPrefix}skills/`)) return false;
|
|
109
|
-
if (!support.codexHooks && sourceRelative.startsWith(codexHookPathPrefix)) return false;
|
|
110
|
-
if (!support.codexHooks && sourceRelative.startsWith(scriptHooksPath)) return false;
|
|
111
|
-
if (!support.codexHooks && sourceRelative === scriptCodexPath) return false;
|
|
112
|
-
if (!support.codexHooks && sourceRelative === "ai/contracts/codex-hooks.contract.json.tpl") return false;
|
|
113
|
-
if (!config.models.anthropic && sourceRelative === scriptRulesPath) return false;
|
|
114
|
-
if (!config.models.openai && sourceRelative === openaiWorkspacePath) return false;
|
|
115
|
-
if (!config.models.anthropic && sourceRelative === claudeWorkspacePath) return false;
|
|
116
|
-
if (!config.models.anthropic && sourceRelative.startsWith(claudePath)) return false;
|
|
117
|
-
if (!support.claudeRules && sourceRelative.startsWith(`${claudePath}rules/`)) return false;
|
|
118
|
-
if (!config.models.openai && sourceRelative === "AGENTS.md.tpl") return false;
|
|
119
|
-
if (!config.models.anthropic && sourceRelative === "CLAUDE.md.tpl") return false;
|
|
120
|
-
if (!config.models.openai && sourceRelative.startsWith("ai/model-overlays/openai/")) return false;
|
|
121
|
-
if (!config.models.anthropic && sourceRelative.startsWith("ai/model-overlays/anthropic/")) return false;
|
|
122
|
-
return true;
|
|
87
|
+
export function shouldRenderTemplate(sourceRelative, config) {
|
|
88
|
+
return shouldRenderContractTemplate(sourceRelative, config);
|
|
123
89
|
}
|
|
124
90
|
|
|
125
91
|
async function collectTemplateFiles() {
|
|
@@ -143,128 +109,250 @@ async function collectTemplateFiles() {
|
|
|
143
109
|
return files.sort();
|
|
144
110
|
}
|
|
145
111
|
|
|
146
|
-
async function
|
|
147
|
-
const
|
|
112
|
+
async function generatedScriptHashes(templateFiles, config, values) {
|
|
113
|
+
const hashes = {};
|
|
114
|
+
const trustedScriptTemplates = new Set(trustedGeneratedScriptTemplatesForSettings(config));
|
|
115
|
+
|
|
116
|
+
for (const sourceRelative of templateFiles) {
|
|
117
|
+
if (!trustedScriptTemplates.has(sourceRelative)) continue;
|
|
118
|
+
|
|
119
|
+
const sourcePath = path.join(repoRoot, "template", sourceRelative);
|
|
120
|
+
const targetRelative = sourceRelative.replace(/\.tpl$/, "");
|
|
121
|
+
hashes[targetRelative] = sha256(render(await readFile(sourcePath, "utf8"), values));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return renderedGeneratedScriptHashes(hashes);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function writeRenderedFile(sourceRelative, targetRoot, values, options, templateRoot = path.join(repoRoot, "template")) {
|
|
128
|
+
const sourcePath = path.join(templateRoot, sourceRelative);
|
|
148
129
|
const targetRelative = sourceRelative.replace(/\.tpl$/, "");
|
|
149
130
|
const targetPath = path.join(targetRoot, targetRelative);
|
|
150
131
|
const content = render(await readFile(sourcePath, "utf8"), values);
|
|
151
132
|
|
|
152
133
|
if (options.dryRun) {
|
|
153
|
-
|
|
154
|
-
|
|
134
|
+
const action = (await exists(targetPath)) ? (options.force ? "overwrite" : "skip existing") : "create";
|
|
135
|
+
console.log(`would ${action} ${targetPath}`);
|
|
136
|
+
return { action: "dry-run", rendered: false, targetPath, targetRelative };
|
|
155
137
|
}
|
|
156
138
|
|
|
157
139
|
if ((await exists(targetPath)) && !options.force) {
|
|
158
140
|
console.log(`skipped existing ${targetPath}`);
|
|
159
|
-
return;
|
|
141
|
+
return { action: "skipped", rendered: false, targetPath, targetRelative };
|
|
160
142
|
}
|
|
161
143
|
|
|
162
144
|
const existed = await exists(targetPath);
|
|
145
|
+
await assertSafeWriteTarget({
|
|
146
|
+
targetPath,
|
|
147
|
+
rootPath: targetRoot,
|
|
148
|
+
label: `Generated harness file ${targetRelative}`,
|
|
149
|
+
});
|
|
163
150
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
164
151
|
await writeFile(targetPath, content);
|
|
165
152
|
console.log(`${existed ? "wrote" : "created"} ${targetPath}`);
|
|
153
|
+
return { action: existed ? "wrote" : "created", rendered: true, targetPath, targetRelative };
|
|
166
154
|
}
|
|
167
155
|
|
|
168
|
-
async function installConsumerEntrypoints(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
156
|
+
export async function installConsumerEntrypoints(resolvedConfig, options) {
|
|
157
|
+
const { config, outputRoot: harnessRoot, support, consumers } = resolvedConfig;
|
|
158
|
+
const entrypoints = consumerEntrypointsForSettings({
|
|
159
|
+
models: config.models,
|
|
160
|
+
clientSupport: support,
|
|
161
|
+
});
|
|
162
|
+
const records = [];
|
|
163
|
+
|
|
164
|
+
for (const resolvedConsumer of consumers) {
|
|
165
|
+
const consumer = resolvedConsumer.config;
|
|
166
|
+
const consumerRoot = resolvedConsumer.confirmedRoot ?? resolvedConsumer.root;
|
|
174
167
|
|
|
175
168
|
const harnessRelativePath = path.relative(consumerRoot, harnessRoot).replaceAll(path.sep, "/") || ".";
|
|
176
|
-
const values =
|
|
177
|
-
PROJECT_NAME: config.project.name,
|
|
178
|
-
CONSUMER_NAME: consumer.name,
|
|
179
|
-
CONSUMER_PURPOSE: consumer.purpose,
|
|
180
|
-
CONSUMER_VALIDATION_LIST: validationList(consumer.validation),
|
|
181
|
-
HARNESS_RELATIVE_PATH: harnessRelativePath,
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const entrypoints = [];
|
|
185
|
-
if (config.models.openai) entrypoints.push(["AGENTS.md", "AGENTS.md.tpl"]);
|
|
186
|
-
if (config.models.anthropic) {
|
|
187
|
-
entrypoints.push(["CLAUDE.md", "CLAUDE.md.tpl"]);
|
|
188
|
-
entrypoints.push([path.join(".claude", "CLAUDE.md"), path.join(".claude", "CLAUDE.md.tpl")]);
|
|
189
|
-
}
|
|
169
|
+
const values = consumerEntrypointValues(config, consumer, harnessRelativePath);
|
|
190
170
|
|
|
191
|
-
for (const
|
|
192
|
-
const
|
|
171
|
+
for (const entrypoint of entrypoints) {
|
|
172
|
+
const targetRelative = entrypoint.path;
|
|
173
|
+
const sourcePath = path.join(repoRoot, "template", entrypoint.template);
|
|
193
174
|
const targetPath = path.join(consumerRoot, targetRelative);
|
|
194
175
|
const content = render(await readFile(sourcePath, "utf8"), values);
|
|
176
|
+
const record = {
|
|
177
|
+
consumer: consumer.name,
|
|
178
|
+
consumerPath: consumer.path,
|
|
179
|
+
path: targetRelative,
|
|
180
|
+
rendered: false,
|
|
181
|
+
};
|
|
195
182
|
|
|
196
183
|
if (options.dryRun) {
|
|
197
|
-
|
|
184
|
+
const action = (await exists(targetPath)) ? (options.force ? "overwrite" : "skip existing") : "create";
|
|
185
|
+
console.log(`would ${action} consumer entrypoint ${targetPath}`);
|
|
186
|
+
records.push({ ...record, action: "dry-run" });
|
|
198
187
|
continue;
|
|
199
188
|
}
|
|
200
189
|
if ((await exists(targetPath)) && !options.force) {
|
|
201
190
|
console.log(`skipped existing consumer entrypoint ${targetPath}`);
|
|
191
|
+
records.push({ ...record, action: "skipped" });
|
|
202
192
|
continue;
|
|
203
193
|
}
|
|
204
194
|
|
|
195
|
+
await assertSafeWriteTarget({
|
|
196
|
+
targetPath,
|
|
197
|
+
rootPath: consumerRoot,
|
|
198
|
+
label: `Consumer entrypoint ${targetRelative}`,
|
|
199
|
+
});
|
|
205
200
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
206
201
|
await writeFile(targetPath, content);
|
|
207
202
|
console.log(`wrote consumer entrypoint ${targetPath}`);
|
|
203
|
+
records.push({ ...record, action: "wrote", rendered: true });
|
|
208
204
|
}
|
|
209
205
|
}
|
|
206
|
+
|
|
207
|
+
return records;
|
|
210
208
|
}
|
|
211
209
|
|
|
212
|
-
async function
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
210
|
+
async function writeGenerationManifest({
|
|
211
|
+
config,
|
|
212
|
+
configContent,
|
|
213
|
+
configPath,
|
|
214
|
+
consumerEntrypoints,
|
|
215
|
+
generatedFiles,
|
|
216
|
+
outputRoot,
|
|
217
|
+
resolvedConfig,
|
|
218
|
+
support,
|
|
219
|
+
}) {
|
|
220
|
+
const manifestPath = path.join(outputRoot, ".structor", "manifest.json");
|
|
221
|
+
const metadata = await packageMetadata();
|
|
222
|
+
const manifest = {
|
|
223
|
+
generatorName: metadata.name,
|
|
224
|
+
generatorVersion: metadata.version,
|
|
225
|
+
generatedAt: new Date().toISOString(),
|
|
226
|
+
config: {
|
|
227
|
+
path: relativePath(resolvedConfig.workspaceRoot, configPath),
|
|
228
|
+
sha256: sha256(configContent),
|
|
229
|
+
project: {
|
|
230
|
+
name: config.project.name,
|
|
231
|
+
slug: config.project.slug,
|
|
232
|
+
harnessRepoName: config.project.harnessRepoName,
|
|
233
|
+
},
|
|
234
|
+
models: {
|
|
235
|
+
openai: Boolean(config.models.openai),
|
|
236
|
+
anthropic: Boolean(config.models.anthropic),
|
|
237
|
+
},
|
|
238
|
+
clientSupport: support,
|
|
239
|
+
consumers: config.consumers.map((consumer) => ({
|
|
240
|
+
name: consumer.name,
|
|
241
|
+
path: consumer.path,
|
|
242
|
+
purpose: consumer.purpose,
|
|
243
|
+
})),
|
|
244
|
+
},
|
|
245
|
+
files: generatedFiles.map((file) => ({
|
|
246
|
+
path: file.targetRelative,
|
|
247
|
+
action: file.action,
|
|
248
|
+
rendered: file.rendered,
|
|
249
|
+
})),
|
|
250
|
+
consumerEntrypoints,
|
|
251
|
+
};
|
|
220
252
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
assertSafeOutputRoot({
|
|
226
|
-
outputPath,
|
|
227
|
-
outputRoot,
|
|
228
|
-
repoRoot,
|
|
229
|
-
workspaceRoot: configDir,
|
|
230
|
-
consumerRepos,
|
|
231
|
-
allowAbsoluteOutput: options.allowAbsoluteOutput,
|
|
253
|
+
await assertSafeWriteTarget({
|
|
254
|
+
targetPath: manifestPath,
|
|
255
|
+
rootPath: outputRoot,
|
|
256
|
+
label: "Generation manifest .structor/manifest.json",
|
|
232
257
|
});
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
HARNESS_REPO_NAME: config.project.harnessRepoName,
|
|
238
|
-
CONSUMER_REPOS_LIST: consumerList(config.consumers),
|
|
239
|
-
CONSUMER_REPO_NAMES_JSON: consumerNames(config.consumers),
|
|
240
|
-
CONSUMER_CONFIG_JSON: consumerConfig(config.consumers, configDir, outputRoot),
|
|
241
|
-
PRIMARY_CONSUMER_NAME: config.consumers[0].name,
|
|
242
|
-
MODEL_OPENAI_ENABLED: booleanLiteral(config.models.openai),
|
|
243
|
-
MODEL_ANTHROPIC_ENABLED: booleanLiteral(config.models.anthropic),
|
|
244
|
-
CLIENT_CODEX_HOOKS_ENABLED: booleanLiteral(support.codexHooks),
|
|
245
|
-
CLIENT_CLAUDE_RULES_ENABLED: booleanLiteral(support.claudeRules),
|
|
246
|
-
CLIENT_CLAUDE_HOOKS_ENABLED: booleanLiteral(support.claudeHooks),
|
|
247
|
-
CLIENT_CLAUDE_SKILLS_ENABLED: booleanLiteral(support.claudeSkills),
|
|
248
|
-
};
|
|
258
|
+
await mkdir(path.dirname(manifestPath), { recursive: true });
|
|
259
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
260
|
+
console.log(`wrote ${manifestPath}`);
|
|
261
|
+
}
|
|
249
262
|
|
|
250
|
-
|
|
263
|
+
export async function generateHarness(config, {
|
|
264
|
+
configPath = null,
|
|
265
|
+
configContent = null,
|
|
266
|
+
configDir = configPath ? path.dirname(path.resolve(configPath)) : process.cwd(),
|
|
267
|
+
outputPath = config.output.path,
|
|
268
|
+
dryRun = false,
|
|
269
|
+
force = false,
|
|
270
|
+
installConsumerEntrypoints: shouldInstallConsumerEntrypoints = false,
|
|
271
|
+
allowAbsoluteOutput = false,
|
|
272
|
+
allowTemplateRepoConsumer = false,
|
|
273
|
+
} = {}) {
|
|
274
|
+
const manifestConfigContent = configContent
|
|
275
|
+
?? (configPath ? await readFile(path.resolve(configPath), "utf8") : `${JSON.stringify(config, null, 2)}\n`);
|
|
276
|
+
const resolvedConfig = await resolveHarnessConfig(config, {
|
|
277
|
+
label: configPath ?? "harness config",
|
|
278
|
+
configPath,
|
|
279
|
+
configDir,
|
|
280
|
+
outputPath,
|
|
281
|
+
allowAbsoluteOutput,
|
|
282
|
+
requireExistingConsumers: shouldInstallConsumerEntrypoints,
|
|
283
|
+
allowTemplateRepoConsumer,
|
|
284
|
+
});
|
|
285
|
+
const { outputRoot, support } = resolvedConfig;
|
|
286
|
+
const values = harnessTemplateValues(config, support, resolvedConfig.consumers, outputRoot);
|
|
287
|
+
values.GENERATED_HARNESS_CONTRACT_MODULE = await readFile(
|
|
288
|
+
path.join(repoRoot, "scripts/generated-harness-contract.mjs"),
|
|
289
|
+
"utf8",
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const templateFiles = await collectTemplateFiles();
|
|
293
|
+
values.GENERATED_SCRIPT_HASHES_JSON = await generatedScriptHashes(templateFiles, config, values);
|
|
294
|
+
const freshRenderScriptTemplates = new Set(freshRenderScriptTemplatesForSettings(config));
|
|
295
|
+
|
|
296
|
+
let renderedHtmlViewsScript = false;
|
|
297
|
+
const generatedFiles = [];
|
|
298
|
+
for (const sourceRelative of templateFiles) {
|
|
251
299
|
if (!shouldRenderTemplate(sourceRelative, config)) continue;
|
|
252
|
-
await writeRenderedFile(sourceRelative, outputRoot, values,
|
|
300
|
+
const result = await writeRenderedFile(sourceRelative, outputRoot, values, { dryRun, force });
|
|
301
|
+
generatedFiles.push(result);
|
|
302
|
+
if (freshRenderScriptTemplates.has(sourceRelative) && result.rendered) {
|
|
303
|
+
renderedHtmlViewsScript = true;
|
|
304
|
+
}
|
|
253
305
|
}
|
|
254
306
|
|
|
255
|
-
if (!
|
|
307
|
+
if (!dryRun && renderedHtmlViewsScript) {
|
|
256
308
|
execFileSync(process.execPath, [path.join(outputRoot, "scripts/generate-html-views.mjs")], {
|
|
257
309
|
cwd: outputRoot,
|
|
258
310
|
stdio: "inherit",
|
|
259
311
|
});
|
|
312
|
+
} else if (!dryRun) {
|
|
313
|
+
console.log("skipped HTML view generation because scripts/generate-html-views.mjs was not freshly rendered");
|
|
260
314
|
}
|
|
261
315
|
|
|
262
|
-
|
|
263
|
-
await installConsumerEntrypoints(
|
|
316
|
+
const consumerEntrypoints = shouldInstallConsumerEntrypoints
|
|
317
|
+
? await installConsumerEntrypoints(resolvedConfig, { dryRun, force, config: configPath })
|
|
318
|
+
: [];
|
|
319
|
+
|
|
320
|
+
if (!dryRun) {
|
|
321
|
+
await writeGenerationManifest({
|
|
322
|
+
config,
|
|
323
|
+
configContent: manifestConfigContent,
|
|
324
|
+
configPath,
|
|
325
|
+
consumerEntrypoints,
|
|
326
|
+
generatedFiles,
|
|
327
|
+
outputRoot,
|
|
328
|
+
resolvedConfig,
|
|
329
|
+
support,
|
|
330
|
+
});
|
|
264
331
|
}
|
|
332
|
+
|
|
333
|
+
return resolvedConfig;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function main() {
|
|
337
|
+
const options = parseArgs(process.argv.slice(2));
|
|
338
|
+
const configPath = path.resolve(options.config);
|
|
339
|
+
const configContent = await readFile(configPath, "utf8");
|
|
340
|
+
const config = JSON.parse(configContent);
|
|
341
|
+
await generateHarness(config, {
|
|
342
|
+
configPath,
|
|
343
|
+
configContent,
|
|
344
|
+
outputPath: options.output ?? config.output.path,
|
|
345
|
+
dryRun: options.dryRun,
|
|
346
|
+
force: options.force,
|
|
347
|
+
installConsumerEntrypoints: options.installConsumerEntrypoints,
|
|
348
|
+
allowAbsoluteOutput: options.allowAbsoluteOutput,
|
|
349
|
+
allowTemplateRepoConsumer: options.allowTemplateRepoConsumer,
|
|
350
|
+
});
|
|
265
351
|
}
|
|
266
352
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
353
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
354
|
+
main().catch((error) => {
|
|
355
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
356
|
+
process.exit(1);
|
|
357
|
+
});
|
|
358
|
+
}
|