@plainconceptsplatform/workflows 0.4.34 → 0.5.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 (73) hide show
  1. package/README.md +88 -88
  2. package/loops/actions/agent-output.cjs +17 -17
  3. package/loops/actions/audit-close/action.yml +24 -0
  4. package/loops/actions/classify-route/action.yml +1 -3
  5. package/loops/actions/classify-route/classify-route.sh +40 -35
  6. package/loops/actions/update-changelog/action.yml +113 -0
  7. package/loops/actions/validate-merge-gate-output/action.yml +40 -40
  8. package/loops/actions/validate-refine-output/validate-refine-output.sh +16 -3
  9. package/loops/actions/validate-review-output/action.yml +35 -35
  10. package/loops/actions/validate-triage-output/action.yml +36 -36
  11. package/loops/actions/verify-route-matrix/verify-route-matrix.sh +91 -36
  12. package/loops/scripts/compile-agent-workflows.mjs +195 -6
  13. package/loops/templates/agentics/agentics-checks.yml +86 -86
  14. package/loops/templates/agentics/agentics-maintenance.yml +121 -121
  15. package/loops/templates/ci/app-ci-dotnet-next.yml +268 -171
  16. package/loops/templates/ci/app-ci-node-monorepo.yml +208 -178
  17. package/loops/templates/issues/bug_report.yml +109 -109
  18. package/loops/templates/issues/feature_request.yml +75 -75
  19. package/loops/templates/opencode/opencode.ci.json +49 -47
  20. package/loops/templates/opencode/opencode.ci.json.md +46 -41
  21. package/loops/templates/release/github-release.yml +30 -30
  22. package/loops/workflows/agent-apply-review.md +465 -443
  23. package/loops/workflows/agent-audit.md +213 -197
  24. package/loops/workflows/agent-implement.md +538 -414
  25. package/loops/workflows/agent-merge-gate.md +730 -584
  26. package/loops/workflows/agent-refine.md +609 -418
  27. package/loops/workflows/agent-release.md +258 -0
  28. package/loops/workflows/agent-triage.md +447 -439
  29. package/loops/workflows/authorize-bot-work.yml +85 -82
  30. package/loops/workflows/shared/opencode-ci.md +206 -197
  31. package/loops/workflows/shared/platform-defaults.md +19 -16
  32. package/loops/workflows/work-router.yml +862 -632
  33. package/package.json +7 -8
  34. package/dist/action-validation.test.d.ts +0 -1
  35. package/dist/action-validation.test.js +0 -84
  36. package/dist/catalog-installation.d.ts +0 -30
  37. package/dist/catalog-installation.js +0 -379
  38. package/dist/catalog-installation.test.d.ts +0 -1
  39. package/dist/catalog-installation.test.js +0 -448
  40. package/dist/catalog-listing.d.ts +0 -13
  41. package/dist/catalog-listing.js +0 -70
  42. package/dist/catalog-listing.test.d.ts +0 -1
  43. package/dist/catalog-listing.test.js +0 -150
  44. package/dist/index.d.ts +0 -2
  45. package/dist/index.js +0 -218
  46. package/dist/index.test.d.ts +0 -1
  47. package/dist/index.test.js +0 -273
  48. package/dist/repository-inspection.d.ts +0 -23
  49. package/dist/repository-inspection.js +0 -113
  50. package/dist/repository-inspection.test.d.ts +0 -1
  51. package/dist/repository-inspection.test.js +0 -77
  52. package/dist/repository-state.d.ts +0 -18
  53. package/dist/repository-state.js +0 -77
  54. package/dist/repository-state.test.d.ts +0 -1
  55. package/dist/repository-state.test.js +0 -96
  56. package/dist/route-processing.d.ts +0 -6
  57. package/dist/route-processing.js +0 -150
  58. package/dist/route-processing.test.d.ts +0 -1
  59. package/dist/route-processing.test.js +0 -350
  60. package/dist/stack-defaults.d.ts +0 -11
  61. package/dist/stack-defaults.js +0 -104
  62. package/dist/stack-defaults.test.d.ts +0 -1
  63. package/dist/stack-defaults.test.js +0 -266
  64. package/dist/tui.d.ts +0 -25
  65. package/dist/tui.js +0 -287
  66. package/dist/tui.test.d.ts +0 -1
  67. package/dist/tui.test.js +0 -246
  68. package/dist/workflow-catalog.d.ts +0 -25
  69. package/dist/workflow-catalog.js +0 -59
  70. package/dist/workflow-catalog.test.d.ts +0 -1
  71. package/dist/workflow-catalog.test.js +0 -29
  72. package/loops/workflows/agent-direct.md +0 -375
  73. package/loops/workflows/agent-propose.md +0 -342
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "0.4.34",
3
+ "version": "0.5.1",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",
@@ -25,13 +25,6 @@
25
25
  "loops",
26
26
  "README.md"
27
27
  ],
28
- "scripts": {
29
- "build": "tsc --project tsconfig.json",
30
- "test": "vitest run",
31
- "typecheck": "tsc --noEmit --project tsconfig.json",
32
- "release": "pnpm build && pnpm test && npm publish --access public",
33
- "prepack": "node ./scripts/copy-loops.mjs"
34
- },
35
28
  "engines": {
36
29
  "node": ">=20.19"
37
30
  },
@@ -40,5 +33,11 @@
40
33
  "typescript": "^5.8.0",
41
34
  "vitest": "^3.0.0",
42
35
  "yaml": "^2.9.0"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc --project tsconfig.json",
39
+ "test": "vitest run",
40
+ "typecheck": "tsc --noEmit --project tsconfig.json",
41
+ "release": "pnpm build && pnpm test && npm publish --access public"
43
42
  }
44
43
  }
@@ -1 +0,0 @@
1
- export {};
@@ -1,84 +0,0 @@
1
- import { readFile, readdir } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { describe, expect, it } from "vitest";
4
- import { parse } from "yaml";
5
- import { catalogSourcePath } from "./catalog-installation.js";
6
- async function collectActionYmlFiles(actionsDir) {
7
- const entries = await readdir(actionsDir, { withFileTypes: true });
8
- const results = [];
9
- for (const entry of entries) {
10
- if (entry.isDirectory()) {
11
- for (const sub of await readdir(join(actionsDir, entry.name), { withFileTypes: true })) {
12
- if (sub.isFile() && sub.name === "action.yml") {
13
- results.push(join(actionsDir, entry.name, sub.name));
14
- }
15
- }
16
- }
17
- }
18
- return results;
19
- }
20
- async function loadManifests() {
21
- const actionsDir = join(catalogSourcePath(), "actions");
22
- const files = await collectActionYmlFiles(actionsDir);
23
- return Promise.all(files.map(async (file) => {
24
- const content = await readFile(file, "utf8");
25
- return { file, content, manifest: parse(content) };
26
- }));
27
- }
28
- describe("action.yml manifest validation", () => {
29
- it("finds action.yml files under loops/actions/", async () => {
30
- const manifests = await loadManifests();
31
- expect(manifests.length).toBeGreaterThanOrEqual(1);
32
- });
33
- it("every action.yml parses as valid YAML", async () => {
34
- for (const { file, content } of await loadManifests()) {
35
- expect(() => parse(content), `${file}: failed to parse as YAML`).not.toThrow();
36
- }
37
- });
38
- it("every action.yml has runs.using === composite", async () => {
39
- for (const { file, manifest } of await loadManifests()) {
40
- expect(manifest.runs?.using, `${file}: runs.using is not "composite"`).toBe("composite");
41
- }
42
- });
43
- it("every action.yml has at least one step in runs.steps", async () => {
44
- for (const { file, manifest } of await loadManifests()) {
45
- expect(manifest.runs?.steps, `${file}: runs.steps is undefined`).toBeDefined();
46
- expect(manifest.runs.steps.length, `${file}: runs.steps is empty — file may be truncated`).toBeGreaterThanOrEqual(1);
47
- }
48
- });
49
- it("every step with shell: also has run:", async () => {
50
- for (const { file, manifest } of await loadManifests()) {
51
- const steps = manifest.runs?.steps ?? [];
52
- for (const [index, step] of steps.entries()) {
53
- const label = step.name ?? step.id ?? `step ${index}`;
54
- if (step.shell !== undefined) {
55
- expect(step.run, `${file}: step "${label}" has shell: but no run:`).toBeDefined();
56
- expect(typeof step.run, `${file}: step "${label}" run is not a string`).toBe("string");
57
- expect(step.run.trim().length, `${file}: step "${label}" run is empty`).toBeGreaterThan(0);
58
- }
59
- }
60
- }
61
- });
62
- it("every step with env: also has run: or uses:", async () => {
63
- for (const { file, manifest } of await loadManifests()) {
64
- const steps = manifest.runs?.steps ?? [];
65
- for (const [index, step] of steps.entries()) {
66
- const label = step.name ?? step.id ?? `step ${index}`;
67
- if (step.env !== undefined) {
68
- expect(step.run !== undefined || step.uses !== undefined, `${file}: step "${label}" has env: but no run: or uses:`).toBe(true);
69
- }
70
- }
71
- }
72
- });
73
- it("no action.yml is truncated (last step has run: or uses: with non-empty value)", async () => {
74
- for (const { file, manifest } of await loadManifests()) {
75
- const steps = manifest.runs?.steps ?? [];
76
- expect(steps.length, `${file}: no steps found — file may be truncated`).toBeGreaterThanOrEqual(1);
77
- const lastStep = steps[steps.length - 1];
78
- const hasTerminal = (lastStep.run !== undefined && lastStep.run.trim().length > 0) ||
79
- (lastStep.uses !== undefined && lastStep.uses.trim().length > 0) ||
80
- (lastStep.with !== undefined);
81
- expect(hasTerminal, `${file}: last step appears truncated`).toBe(true);
82
- }
83
- });
84
- });
@@ -1,30 +0,0 @@
1
- import { type RouteName, type TemplateName } from "./workflow-catalog.js";
2
- import type { RepositoryInspection } from "./repository-inspection.js";
3
- export interface CatalogInstallResult {
4
- readonly installed: readonly string[];
5
- readonly conflicts: readonly string[];
6
- }
7
- export interface CatalogInstallOptions {
8
- readonly force?: boolean;
9
- readonly sourcePath?: string;
10
- readonly selectedRoutes?: readonly RouteName[];
11
- readonly inspection?: RepositoryInspection;
12
- readonly compile?: (repositoryPath: string) => Promise<void>;
13
- }
14
- interface CatalogFile {
15
- readonly source: string;
16
- readonly target: string;
17
- readonly managed: boolean;
18
- }
19
- export declare function mandatoryFileSpecs(sourcePath: string): CatalogFile[];
20
- export declare function catalogSourcePath(modulePath?: string): string;
21
- export declare function installCatalog(repositoryPath: string, options?: CatalogInstallOptions): Promise<CatalogInstallResult>;
22
- export declare function installTemplate(repositoryPath: string, template: TemplateName, options?: CatalogInstallOptions): Promise<CatalogInstallResult>;
23
- export declare function installMandatoryFiles(repositoryPath: string, options?: CatalogInstallOptions): Promise<CatalogInstallResult>;
24
- export declare function installedRoutes(repositoryPath: string): Promise<RouteName[]>;
25
- export declare function removeRouteFiles(repositoryPath: string, routes: readonly RouteName[]): Promise<string[]>;
26
- export declare function isTemplateName(value: string): value is TemplateName;
27
- export declare function ensurePreCommitHook(repositoryPath: string): Promise<void>;
28
- export declare function runCompileIfAvailable(repositoryPath: string): Promise<void>;
29
- export declare function exists(path: string): Promise<boolean>;
30
- export {};
@@ -1,379 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { access, copyFile, cp, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
3
- import { constants } from "node:fs";
4
- import { dirname, join, relative, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { promisify } from "node:util";
7
- import { parse as parseYaml } from "yaml";
8
- import { catalogTemplates, mandatoryFiles, routeNames, templateNames, workflowRoutes } from "./workflow-catalog.js";
9
- import { processRoutes, excludedWorkerFiles } from "./route-processing.js";
10
- import { generateOpencodeCi, generateOpencodeConfig, generateStackDefaults, injectStackEnv } from "./stack-defaults.js";
11
- const execFileAsync = promisify(execFile);
12
- const sourceMappings = [
13
- ["actions", ".github/actions"],
14
- ["workflows", ".github/workflows"],
15
- ["scripts", "scripts"],
16
- ];
17
- export function mandatoryFileSpecs(sourcePath) {
18
- return mandatoryFiles.map((spec) => ({
19
- source: join(sourcePath, spec.source),
20
- target: spec.target,
21
- managed: true,
22
- }));
23
- }
24
- export function catalogSourcePath(modulePath = fileURLToPath(import.meta.url)) {
25
- return resolve(dirname(modulePath), "..", "loops");
26
- }
27
- export async function installCatalog(repositoryPath, options = {}) {
28
- const sourcePath = options.sourcePath ?? catalogSourcePath();
29
- const selectedRoutes = options.selectedRoutes ?? routeNames;
30
- const allFiles = [...await catalogFiles(sourcePath), ...mandatoryFileSpecs(sourcePath)];
31
- const deduplicated = allFiles.filter((file, index) => allFiles.findIndex((f) => f.target === file.target) === index).sort((left, right) => left.target.localeCompare(right.target));
32
- const excluded = excludedWorkerFiles(selectedRoutes);
33
- const filtered = deduplicated.filter((file) => {
34
- const fileName = file.target.split("/").pop() ?? "";
35
- return !excluded.has(fileName);
36
- });
37
- const fileContents = new Map();
38
- for (const file of filtered) {
39
- fileContents.set(file.target, await readFile(file.source, "utf8"));
40
- }
41
- let processedContents = processRoutes(fileContents, selectedRoutes);
42
- if (options.inspection !== undefined) {
43
- const defaults = generateStackDefaults(options.inspection);
44
- processedContents = injectStackIntoWorkers(processedContents, defaults);
45
- processedContents = transformOpencodeFiles(processedContents, options.inspection);
46
- }
47
- processedContents = await preserveConsumerWorkerEnv(repositoryPath, processedContents);
48
- const updates = [...processedContents.entries()]
49
- .filter(([target]) => filtered.find((file) => file.target === target)?.managed ?? true)
50
- .map(([target, content]) => ({ target, content }));
51
- const conflicts = await conflictingTargets(repositoryPath, updates);
52
- if (conflicts.length > 0 && !options.force)
53
- return { installed: [], conflicts };
54
- const stagedLocks = await validateStagedCatalog(repositoryPath, updates, options.compile);
55
- await applyTransaction(repositoryPath, [...updates, ...stagedLocks, await preCommitHookUpdate(repositoryPath)]);
56
- return { installed: [...processedContents.keys()].sort(), conflicts };
57
- }
58
- function injectStackIntoWorkers(files, defaults) {
59
- const result = new Map(files);
60
- for (const [key, content] of result) {
61
- if (key.startsWith(".github/workflows/agent-") && key.endsWith(".md")) {
62
- result.set(key, injectStackEnv(content, defaults));
63
- }
64
- }
65
- return result;
66
- }
67
- async function preserveConsumerWorkerEnv(repositoryPath, files) {
68
- const result = new Map(files);
69
- for (const [target, content] of result) {
70
- if (!target.startsWith(".github/workflows/agent-") || !target.endsWith(".md"))
71
- continue;
72
- const existingPath = join(repositoryPath, target);
73
- if (!await exists(existingPath))
74
- continue;
75
- result.set(target, mergeWorkerEnv(content, await readFile(existingPath, "utf8")));
76
- }
77
- return result;
78
- }
79
- function mergeWorkerEnv(packageContent, consumerContent) {
80
- const consumerEnv = workerEnvValues(consumerContent);
81
- let result = packageContent.replace(/^ ([A-Z][A-Z0-9_]*): .+$/gm, (line, key) => consumerEnv.has(key) ? ` ${key}: ${consumerEnv.get(key)}` : line);
82
- const endpoint = engineEndpoint(consumerContent);
83
- if (endpoint !== undefined) {
84
- result = result.replace(/^ OPENAI_BASE_URL: .+$/m, ` OPENAI_BASE_URL: ${endpoint}`);
85
- }
86
- return result;
87
- }
88
- function workerEnvValues(content) {
89
- const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/m.exec(content)?.[1];
90
- const envBlock = frontmatter === undefined ? undefined : /^env:\r?\n((?: .*\r?\n?)*)/m.exec(frontmatter)?.[1];
91
- const values = new Map();
92
- if (envBlock === undefined)
93
- return values;
94
- for (const line of envBlock.split(/\r?\n/)) {
95
- const match = /^ ([A-Z][A-Z0-9_]*): (.+)$/.exec(line);
96
- if (match !== null)
97
- values.set(match[1], match[2]);
98
- }
99
- return values;
100
- }
101
- function engineEndpoint(content) {
102
- return /^ OPENAI_BASE_URL: (.+)$/m.exec(content)?.[1];
103
- }
104
- function transformOpencodeFiles(files, inspection) {
105
- const result = new Map(files);
106
- for (const [key, content] of result) {
107
- if (key.endsWith("opencode-ci.md")) {
108
- result.set(key, generateOpencodeCi(content, inspection));
109
- }
110
- else if (key === "opencode.ci.json") {
111
- result.set(key, generateOpencodeConfig(content, inspection));
112
- }
113
- }
114
- return result;
115
- }
116
- export async function installTemplate(repositoryPath, template, options = {}) {
117
- const sourcePath = options.sourcePath ?? catalogSourcePath();
118
- const meta = catalogTemplateMeta(template);
119
- const source = join(sourcePath, "templates", meta.directory, meta.file);
120
- const target = meta.target;
121
- const destination = join(repositoryPath, target);
122
- const conflicts = await exists(destination) && !(await filesMatch(source, destination)) ? [target] : [];
123
- if (conflicts.length > 0 && !options.force)
124
- return { installed: [], conflicts };
125
- await mkdir(dirname(destination), { recursive: true });
126
- await copyFile(source, destination);
127
- if (options.inspection !== undefined && template === "opencode.ci.json") {
128
- const baseContent = await readFile(source, "utf8");
129
- const transformed = generateOpencodeConfig(baseContent, options.inspection);
130
- await writeFile(destination, transformed, "utf8");
131
- }
132
- try {
133
- await runCompileIfAvailable(repositoryPath);
134
- }
135
- catch {
136
- // compile failure is non-fatal
137
- }
138
- return { installed: [target], conflicts };
139
- }
140
- export async function installMandatoryFiles(repositoryPath, options = {}) {
141
- const sourcePath = options.sourcePath ?? catalogSourcePath();
142
- const files = mandatoryFileSpecs(sourcePath).sort((left, right) => left.target.localeCompare(right.target));
143
- const conflicts = (await Promise.all(files.map(async (file) => {
144
- const destination = join(repositoryPath, file.target);
145
- return await exists(destination) && !(await filesMatch(file.source, destination)) ? file.target : undefined;
146
- }))).filter((file) => file !== undefined);
147
- if (conflicts.length > 0 && !options.force)
148
- return { installed: [], conflicts };
149
- const updates = await Promise.all(files.map(async (file) => ({ target: file.target, content: await readFile(file.source, "utf8") })));
150
- await applyTransaction(repositoryPath, [...updates, await preCommitHookUpdate(repositoryPath)]);
151
- return { installed: files.map((file) => file.target), conflicts };
152
- }
153
- export async function installedRoutes(repositoryPath) {
154
- const found = await Promise.all(workflowRoutes.map(async (route) => (await exists(join(repositoryPath, ".github", "workflows", route.worker))) ? route.name : undefined));
155
- return found.filter((name) => name !== undefined);
156
- }
157
- export async function removeRouteFiles(repositoryPath, routes) {
158
- const workerByRoute = new Map(workflowRoutes.map((route) => [route.name, route.worker]));
159
- const removed = [];
160
- for (const route of routes) {
161
- const worker = workerByRoute.get(route);
162
- if (worker === undefined)
163
- continue;
164
- const lock = worker.replace(/\.md$/, ".lock.yml");
165
- for (const file of [worker, lock]) {
166
- const destination = join(repositoryPath, ".github", "workflows", file);
167
- if (await exists(destination)) {
168
- await rm(destination, { force: true });
169
- removed.push(`.github/workflows/${file}`);
170
- }
171
- }
172
- }
173
- return removed.sort();
174
- }
175
- export function isTemplateName(value) {
176
- return templateNames.includes(value);
177
- }
178
- export async function ensurePreCommitHook(repositoryPath) {
179
- const update = await preCommitHookUpdate(repositoryPath);
180
- await applyTransaction(repositoryPath, [update]);
181
- }
182
- async function preCommitHookUpdate(repositoryPath) {
183
- const target = ".husky/pre-commit";
184
- const hookPath = join(repositoryPath, target);
185
- const compileLine = "node scripts/compile-agent-workflows.mjs";
186
- const stageLine = "git add -- .github/workflows/*.lock.yml";
187
- const actionLockLine = "[ ! -f .github/actions/actions-lock.json ] || git add -- .github/actions/actions-lock.json";
188
- const managedLines = `if git diff --cached --name-only -- .github | grep -q .; then\n ${compileLine}\n ${stageLine}\n ${actionLockLine}\nfi\n`;
189
- if (!await exists(hookPath)) {
190
- return { target, content: managedLines };
191
- }
192
- const content = (await readFile(hookPath, "utf8"))
193
- .replace("pnpm exec if git diff --cached --name-only -- .github | grep -q .; then", "if git diff --cached --name-only -- .github | grep -q .; then");
194
- if (content.includes("compile-agent-workflows")) {
195
- const legacyLines = `${compileLine}\n${stageLine}\n${actionLockLine}\n`;
196
- if (content.includes(managedLines))
197
- return { target, content };
198
- if (content.includes(legacyLines))
199
- return { target, content: content.replace(legacyLines, managedLines) };
200
- const suffix = content.endsWith("\n") || content === "" ? "" : "\n";
201
- return { target, content: `${content}${suffix}${managedLines}` };
202
- }
203
- return { target, content: content.endsWith("\n") || content === ""
204
- ? `${content}${managedLines}`
205
- : `${content}\n${managedLines}` };
206
- }
207
- export async function runCompileIfAvailable(repositoryPath) {
208
- const script = join(repositoryPath, "scripts", "compile-agent-workflows.mjs");
209
- if (await exists(script)) {
210
- await execFileAsync("node", [script, "--force"], { cwd: repositoryPath });
211
- }
212
- }
213
- async function conflictingTargets(repositoryPath, updates) {
214
- const conflicts = (await Promise.all(updates.map(async ({ target, content }) => {
215
- const destination = join(repositoryPath, target);
216
- if (!await exists(destination))
217
- return undefined;
218
- return (await readFile(destination, "utf8")) === content ? undefined : target;
219
- }))).filter((target) => target !== undefined);
220
- return [...new Set(conflicts)].sort();
221
- }
222
- async function validateStagedCatalog(repositoryPath, updates, compileOverride) {
223
- const temporaryRoot = join(repositoryPath, ".opencode", ".tmp");
224
- await mkdir(temporaryRoot, { recursive: true });
225
- const stagingPath = await mkdtemp(join(temporaryRoot, "workflows-"));
226
- try {
227
- await copyCompilationInputs(repositoryPath, stagingPath);
228
- await writeUpdates(stagingPath, updates);
229
- await initializeStagingRepository(stagingPath);
230
- const compiler = compileOverride ?? await packageCompiler(stagingPath);
231
- if (compiler === undefined)
232
- return [];
233
- await compiler(stagingPath);
234
- return await generatedFiles(stagingPath);
235
- }
236
- finally {
237
- await rm(stagingPath, { force: true, recursive: true });
238
- }
239
- }
240
- async function copyCompilationInputs(repositoryPath, stagingPath) {
241
- const githubPath = join(repositoryPath, ".github");
242
- if (await exists(githubPath))
243
- await cp(githubPath, join(stagingPath, ".github"), { recursive: true });
244
- }
245
- async function initializeStagingRepository(stagingPath) {
246
- await execFileAsync("git", ["init", "--quiet"], { cwd: stagingPath, windowsHide: true });
247
- }
248
- async function packageCompiler(repositoryPath) {
249
- const scriptPath = join(repositoryPath, "scripts", "compile-agent-workflows.mjs");
250
- if (!await exists(scriptPath))
251
- return undefined;
252
- const content = await readFile(scriptPath, "utf8");
253
- return content.includes("gh aw compile") ? runCompileIfAvailable : undefined;
254
- }
255
- async function generatedFiles(repositoryPath) {
256
- const workflowPath = join(repositoryPath, ".github", "workflows");
257
- const updates = [];
258
- if (await exists(workflowPath)) {
259
- for (const file of await filesIn(workflowPath)) {
260
- if (!file.endsWith(".lock.yml"))
261
- continue;
262
- updates.push({ target: `.github/workflows/${file.replaceAll("\\", "/")}`, content: await readFile(join(workflowPath, file), "utf8") });
263
- }
264
- }
265
- const actionsLock = join(repositoryPath, ".github", "actions", "actions-lock.json");
266
- if (await exists(actionsLock)) {
267
- updates.push({ target: ".github/actions/actions-lock.json", content: await readFile(actionsLock, "utf8") });
268
- }
269
- return updates.sort((left, right) => left.target.localeCompare(right.target));
270
- }
271
- function validateActionManifests(updates) {
272
- for (const { target, content } of updates) {
273
- if (!target.endsWith("action.yml"))
274
- continue;
275
- let manifest;
276
- try {
277
- manifest = parseYaml(content);
278
- }
279
- catch {
280
- throw new Error(`${target}: invalid YAML — failed to parse action manifest`);
281
- }
282
- if (manifest?.runs?.using !== "composite")
283
- continue;
284
- const steps = manifest.runs?.steps;
285
- if (!Array.isArray(steps) || steps.length === 0) {
286
- throw new Error(`${target}: composite action has no steps — file may be truncated`);
287
- }
288
- for (const [index, step] of steps.entries()) {
289
- const label = step.name ?? step.id ?? `step ${index}`;
290
- if (step.shell !== undefined && (typeof step.run !== "string" || step.run.trim() === "")) {
291
- throw new Error(`${target}: step "${label}" has shell: but no run: — manifest is invalid or truncated`);
292
- }
293
- }
294
- }
295
- }
296
- async function applyTransaction(repositoryPath, updates) {
297
- const uniqueUpdates = [...new Map(updates.map((update) => [update.target, update])).values()];
298
- const rollback = await Promise.all(uniqueUpdates.map(async ({ target }) => {
299
- const path = join(repositoryPath, target);
300
- return { target, existed: await exists(path), content: await exists(path) ? await readFile(path, "utf8") : undefined };
301
- }));
302
- try {
303
- await writeUpdates(repositoryPath, uniqueUpdates);
304
- validateActionManifests(uniqueUpdates);
305
- }
306
- catch (error) {
307
- await Promise.all(rollback.map(async ({ target, existed, content }) => {
308
- if (existed && content !== undefined) {
309
- await writeUpdates(repositoryPath, [{ target, content }]);
310
- }
311
- else {
312
- await rm(join(repositoryPath, target), { force: true });
313
- }
314
- }));
315
- throw error;
316
- }
317
- }
318
- async function writeUpdates(repositoryPath, updates) {
319
- await Promise.all(updates.map(async ({ target, content }) => {
320
- const destination = join(repositoryPath, target);
321
- await mkdir(dirname(destination), { recursive: true });
322
- await writeFile(destination, content, "utf8");
323
- }));
324
- }
325
- function catalogTemplateMeta(template) {
326
- const entry = catalogTemplates.find((item) => item.name === template);
327
- if (entry === undefined)
328
- throw new Error(`Unknown template: ${template}`);
329
- const directory = template.startsWith("opencode") ? "opencode" : template.startsWith("app-ci-") ? "ci" : template === "github-release" ? "release" : template === "visual-evidence" ? "visual-evidence" : template === "bug-report" || template === "feature-request" ? "issues" : "agentics";
330
- const isWorkflow = entry.file.endsWith(".yml");
331
- const inferredTarget = template === "app-ci-dotnet-next"
332
- ? ".github/workflows/app-ci.yml"
333
- : isWorkflow ? `.github/workflows/${entry.file}` : entry.file;
334
- const target = entry.target ?? inferredTarget;
335
- return { directory, file: entry.file, target };
336
- }
337
- async function catalogFiles(sourcePath) {
338
- const files = [];
339
- for (const [sourceDirectory, targetDirectory] of sourceMappings) {
340
- for (const file of await filesIn(join(sourcePath, sourceDirectory))) {
341
- if (isGeneratedFile(file))
342
- continue;
343
- files.push({
344
- source: join(sourcePath, sourceDirectory, file),
345
- target: `${targetDirectory}/${file.replaceAll("\\", "/")}`,
346
- managed: true,
347
- });
348
- }
349
- }
350
- return files.sort((left, right) => left.target.localeCompare(right.target));
351
- }
352
- function isGeneratedFile(file) {
353
- const normalized = file.replaceAll("\\", "/");
354
- return normalized.endsWith(".lock.yml") || normalized.endsWith("actions-lock.json");
355
- }
356
- async function filesIn(path) {
357
- const entries = await readdir(path, { recursive: true, withFileTypes: true });
358
- return entries
359
- .filter((entry) => entry.isFile())
360
- .map((entry) => entry.parentPath === undefined ? entry.name : relative(path, join(entry.parentPath, entry.name)));
361
- }
362
- async function filesMatch(source, destination) {
363
- try {
364
- const [sourceContent, destinationContent] = await Promise.all([readFile(source), readFile(destination)]);
365
- return sourceContent.equals(destinationContent);
366
- }
367
- catch {
368
- return false;
369
- }
370
- }
371
- export async function exists(path) {
372
- try {
373
- await access(path, constants.F_OK);
374
- return true;
375
- }
376
- catch {
377
- return false;
378
- }
379
- }
@@ -1 +0,0 @@
1
- export {};