@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +131 -21
  3. package/ROADMAP.md +38 -0
  4. package/SECURITY.md +33 -0
  5. package/bin/structor.mjs +561 -29
  6. package/contrib/self-harness/files/README.md +32 -0
  7. package/contrib/self-harness/files/ai/AGENTS.md +35 -0
  8. package/contrib/self-harness/files/ai/ARCHITECTURE.md +38 -0
  9. package/contrib/self-harness/files/ai/HUB.md +59 -0
  10. package/contrib/self-harness/files/ai/PRODUCT.md +36 -0
  11. package/contrib/self-harness/files/ai/QUALITY.md +31 -0
  12. package/contrib/self-harness/files/ai/context.md +38 -0
  13. package/contrib/self-harness/files/scripts/check-workspace.mjs +72 -0
  14. package/contrib/self-harness/harness.config.json +37 -0
  15. package/docs/CONTRIBUTOR-SETUP.md +45 -0
  16. package/docs/INIT.md +55 -2
  17. package/docs/public-launch.md +150 -0
  18. package/examples/anthropic-only/harness.config.json +26 -0
  19. package/examples/frontend-backend/harness.config.json +8 -8
  20. package/examples/generated-harness-tree.md +432 -0
  21. package/examples/openai-and-anthropic/harness.config.json +7 -7
  22. package/examples/single-repo/harness.config.json +7 -7
  23. package/harness.config.example.json +1 -1
  24. package/package.json +12 -4
  25. package/schemas/contract-manifest.schema.json +0 -1
  26. package/schemas/harness-config.schema.json +5 -2
  27. package/scripts/check-config.mjs +20 -31
  28. package/scripts/check-examples.mjs +146 -0
  29. package/scripts/check-placeholders.mjs +2 -0
  30. package/scripts/check-public-hygiene.mjs +249 -0
  31. package/scripts/check-schemas.mjs +42 -0
  32. package/scripts/check-template-files.mjs +15 -98
  33. package/scripts/generated-harness-contract.mjs +416 -0
  34. package/scripts/init-harness.mjs +227 -139
  35. package/scripts/lib.mjs +462 -12
  36. package/scripts/rendered-config.mjs +109 -0
  37. package/scripts/setup-contributor.mjs +125 -0
  38. package/scripts/smoke-template.mjs +260 -73
  39. package/template/AGENTS.md.tpl +4 -2
  40. package/template/README.md.tpl +5 -0
  41. package/template/ai/CODEX-HOOKS.md.tpl +1 -1
  42. package/template/ai/HARNESS-ENGINEERING.md.tpl +5 -2
  43. package/template/ai/HARNESS.md.tpl +4 -1
  44. package/template/ai/contracts/codex-hooks.contract.json.tpl +58 -1
  45. package/template/ai/contracts/codex-hooks.md.tpl +6 -0
  46. package/template/ai/contracts/release-flow.md.tpl +1 -1
  47. package/template/ai/templates/fixtures/issues/valid-ready.md.tpl +3 -1
  48. package/template/ai/templates/issue-template.md.tpl +3 -1
  49. package/template/ai/workspace/LOCAL-STACK.md.tpl +1 -1
  50. package/template/ai/workspace/SYSTEM-MAP.md.tpl +2 -2
  51. package/template/consumer/AGENTS.md.tpl +4 -4
  52. package/template/consumer/CLAUDE.md.tpl +4 -4
  53. package/template/scripts/bootstrap-workspace.mjs.tpl +11 -25
  54. package/template/scripts/check-claude-compatibility.mjs.tpl +62 -9
  55. package/template/scripts/check-codex-hooks.mjs.tpl +262 -20
  56. package/template/scripts/check-template-governance.mjs.tpl +2 -114
  57. package/template/scripts/check-workspace.mjs.tpl +27 -103
  58. package/template/scripts/check-worktree-bootstrap-fixtures.mjs.tpl +12 -0
  59. package/template/scripts/generate-html-views.mjs.tpl +357 -56
  60. package/template/scripts/generated-harness-contract.mjs.tpl +1 -0
  61. package/template/scripts/hooks/lib/codex-hooks-core.mjs.tpl +14 -3
  62. package/template/scripts/lib/path-safety.mjs.tpl +87 -0
  63. package/template/scripts/lib/worktree-bootstrap.mjs.tpl +16 -13
  64. package/template/scripts/validate-governance.mjs.tpl +52 -36
  65. package/schemas/task-brief.schema.json +0 -37
@@ -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 { assertSafeOutputRoot, exists, validateConfigShape } from "./lib.mjs";
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
- const consumerPathPrefix = "consumer/";
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 consumerList(consumers) {
60
- return consumers.map((consumer) => `- ${consumer.name}: ${consumer.purpose}`).join("\n");
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 consumerNames(consumers) {
70
- return JSON.stringify(consumers.map((consumer) => consumer.name));
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 booleanLiteral(value) {
86
- return value ? "true" : "false";
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
- codexHooks: config.models.openai && (config.clientSupport?.codex?.hooks ?? true),
92
- claudeRules: config.models.anthropic && (config.clientSupport?.claude?.rules ?? true),
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
- const support = clientSupport(config);
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 writeRenderedFile(sourceRelative, targetRoot, values, options) {
147
- const sourcePath = path.join(repoRoot, "template", sourceRelative);
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
- console.log(`would create ${targetPath}`);
154
- return;
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(config, harnessRoot, options) {
169
- for (const consumer of config.consumers) {
170
- const consumerRoot = path.resolve(path.dirname(path.resolve(options.config)), consumer.path);
171
- if (!(await exists(consumerRoot))) {
172
- throw new Error(`Consumer repo path does not exist: ${consumerRoot}`);
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 [targetRelative, sourceRelative] of entrypoints) {
192
- const sourcePath = path.join(repoRoot, "template", "consumer", sourceRelative);
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
- console.log(`would create consumer entrypoint ${targetPath}`);
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 main() {
213
- const options = parseArgs(process.argv.slice(2));
214
- const configPath = path.resolve(options.config);
215
- const config = JSON.parse(await readFile(configPath, "utf8"));
216
- const errors = await validateConfigShape(config, options.config);
217
- if (errors.length > 0) {
218
- throw new Error(errors.join("\n"));
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
- const configDir = path.dirname(configPath);
222
- const outputPath = options.output ?? config.output.path;
223
- const outputRoot = path.resolve(configDir, outputPath);
224
- const consumerRepos = config.consumers.map((consumer) => path.resolve(configDir, consumer.path));
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
- const support = clientSupport(config);
234
- const values = {
235
- PROJECT_NAME: config.project.name,
236
- PROJECT_SLUG: config.project.slug,
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
- for (const sourceRelative of await collectTemplateFiles()) {
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, options);
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 (!options.dryRun) {
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
- if (options.installConsumerEntrypoints) {
263
- await installConsumerEntrypoints(config, outputRoot, { ...options, config: configPath });
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
- main().catch((error) => {
268
- console.error(error instanceof Error ? error.message : String(error));
269
- process.exit(1);
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
+ }