@plainconceptsplatform/workflows 0.1.2 → 0.2.0
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/README.md +40 -11
- package/dist/catalog-installation.d.ts +11 -2
- package/dist/catalog-installation.js +53 -9
- package/dist/catalog-installation.test.js +198 -16
- package/dist/catalog-listing.d.ts +13 -0
- package/dist/catalog-listing.js +68 -0
- package/dist/catalog-listing.test.d.ts +1 -0
- package/dist/catalog-listing.test.js +137 -0
- package/dist/index.js +76 -12
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +112 -0
- package/dist/repository-inspection.test.d.ts +1 -0
- package/dist/repository-inspection.test.js +77 -0
- package/dist/tui.d.ts +25 -0
- package/dist/tui.js +254 -0
- package/dist/tui.test.d.ts +1 -0
- package/dist/tui.test.js +246 -0
- package/dist/workflow-catalog.d.ts +15 -8
- package/dist/workflow-catalog.js +25 -15
- package/dist/workflow-catalog.test.js +18 -3
- package/loops/actions/add-issue-labels/action.yml +30 -29
- package/loops/actions/agent-output.cjs +1 -0
- package/loops/actions/apply-agent-bundle/action.yml +24 -23
- package/loops/actions/apply-agent-bundle/apply-bundle.sh +1 -0
- package/loops/actions/apply-agent-comments/action.yml +42 -41
- package/loops/actions/apply-agent-labels/action.yml +55 -54
- package/loops/actions/apply-agent-output/action.yml +108 -107
- package/loops/actions/audit-close/action.yml +104 -103
- package/loops/actions/classify-route/action.yml +91 -90
- package/loops/actions/classify-route/classify-route.sh +1 -0
- package/loops/actions/cleanup-artifacts/action.yml +65 -64
- package/loops/actions/close-agent-issues/action.yml +43 -42
- package/loops/actions/create-agent-issues/action.yml +52 -51
- package/loops/actions/create-issue-comment/action.yml +29 -28
- package/loops/actions/download-agent-output/action.yml +53 -52
- package/loops/actions/identify-gate-subject/action.yml +97 -96
- package/loops/actions/link-pr-to-issue/action.yml +40 -39
- package/loops/actions/list-open-issues/action.yml +33 -32
- package/loops/actions/load-issue-context/action.yml +43 -42
- package/loops/actions/merge-agent-pr/action.yml +36 -35
- package/loops/actions/push-agent-branch/action.yml +45 -44
- package/loops/actions/remove-issue-labels/action.yml +35 -34
- package/loops/actions/stale-recovery/action.yml +288 -287
- package/loops/actions/update-agent-issues/action.yml +58 -57
- package/loops/actions/validate-refine-output/action.yml +44 -43
- package/loops/actions/validate-refine-output/validate-refine-output.sh +1 -0
- package/loops/actions/verify-composite-actions/action.yml +9 -8
- package/loops/actions/verify-composite-actions/verify-composite-actions.sh +1 -0
- package/loops/actions/verify-refine-output/action.yml +9 -8
- package/loops/actions/verify-refine-output/verify-refine-output.sh +1 -0
- package/loops/actions/verify-route-matrix/action.yml +9 -8
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +1 -0
- package/loops/scripts/compile-agent-workflows.mjs +29 -13
- package/loops/templates/agentics/agentics-checks.yml +86 -0
- package/loops/templates/agentics/agentics-maintenance.yml +121 -0
- package/loops/templates/ci/app-ci-dotnet-next.yml +167 -0
- package/loops/templates/ci/app-ci-node-monorepo.yml +178 -0
- package/loops/templates/opencode/opencode.ci.json +46 -0
- package/loops/templates/opencode/opencode.ci.json.md +41 -0
- package/loops/workflows/agent-apply-review.md +9 -10
- package/loops/workflows/agent-audit.md +6 -7
- package/loops/workflows/agent-direct.md +7 -8
- package/loops/workflows/agent-implement.md +7 -8
- package/loops/workflows/agent-merge-gate.md +9 -10
- package/loops/workflows/agent-propose.md +7 -8
- package/loops/workflows/agent-refine.md +5 -7
- package/loops/workflows/shared/opencode-ci.md +105 -1
- package/loops/workflows/shared/platform-defaults.md +1 -0
- package/loops/workflows/work-router.yml +1 -0
- package/package.json +2 -2
- package/loops/aw/repo-config.md +0 -12
package/README.md
CHANGED
|
@@ -1,30 +1,59 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Workflows CLI
|
|
2
2
|
|
|
3
3
|
Install and update shared GitHub Agentic Workflows for Plain Concepts Platform repositories.
|
|
4
4
|
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
The primary entrypoint is the interactive TUI. Run it with no arguments:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @plainconceptsplatform/workflows
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The TUI lists all routes and templates with install status. Arrow keys navigate, space toggles, Enter installs. Selecting any route installs the full managed catalog plus mandatory `opencode.ci.json` and `scripts/compile-agent-workflows.mjs`. Selecting only templates still installs those mandatory files.
|
|
14
|
+
|
|
5
15
|
## Install
|
|
6
16
|
|
|
7
17
|
Before installing workflows, install and configure [`PlainConceptsPlatform/opencode-onboard`](https://github.com/PlainConceptsPlatform/opencode-onboard) in the consumer repository. Loop workers invoke the skills and commands it provides. Verify the required skills and commands are available before compiling workflows.
|
|
8
18
|
|
|
9
|
-
For
|
|
19
|
+
For non-interactive use (advanced):
|
|
10
20
|
|
|
11
21
|
```bash
|
|
12
|
-
npx @plainconceptsplatform/workflows@latest
|
|
13
|
-
npx @plainconceptsplatform/workflows@latest
|
|
14
|
-
npx @plainconceptsplatform/workflows@latest
|
|
22
|
+
npx @plainconceptsplatform/workflows@latest init
|
|
23
|
+
npx @plainconceptsplatform/workflows@latest add
|
|
24
|
+
npx @plainconceptsplatform/workflows@latest update
|
|
15
25
|
```
|
|
16
26
|
|
|
17
27
|
For a project-local development dependency:
|
|
18
28
|
|
|
19
29
|
```bash
|
|
20
30
|
pnpm add -D @plainconceptsplatform/workflows
|
|
21
|
-
pnpm exec
|
|
22
|
-
pnpm exec
|
|
31
|
+
pnpm exec workflows # launch interactive TUI
|
|
32
|
+
pnpm exec workflows init
|
|
33
|
+
pnpm exec workflows add
|
|
23
34
|
```
|
|
24
35
|
|
|
25
|
-
`init`
|
|
36
|
+
`init` inspects the repository and reports its stack and visibility. It does not create or manage repository configuration or a manifest.
|
|
37
|
+
|
|
38
|
+
`add` installs package-owned files including the mandatory `opencode.ci.json` and `scripts/compile-agent-workflows.mjs`. It stops when a managed file differs. Use `pnpm exec workflows update --force` only when you intend to replace managed workflow files.
|
|
39
|
+
|
|
40
|
+
Install optional standalone templates with `add --template`. Available templates are `agentics-checks`, `agentics-maintenance`, `app-ci-dotnet-next`, and `app-ci-node-monorepo`. CI templates are stack-specific copies, not a combined template. Edit their top-level `env:` values for repository paths, package names, and commands.
|
|
41
|
+
|
|
42
|
+
## List and search
|
|
26
43
|
|
|
27
|
-
|
|
44
|
+
List all available workflows, routes, and templates with install status:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx --yes --package @plainconceptsplatform/workflows@latest workflows list
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Each entry is marked `[x]` when the corresponding `.github/workflows/agent-*.md` file already exists in the current directory, or `[ ]` when it is not yet installed.
|
|
51
|
+
|
|
52
|
+
Search by name or description:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx --yes --package @plainconceptsplatform/workflows@latest workflows search "ci"
|
|
56
|
+
```
|
|
28
57
|
|
|
29
58
|
## Manual installation
|
|
30
59
|
|
|
@@ -32,10 +61,10 @@ The package includes `loops/`, a copyable equivalent of `.github/`:
|
|
|
32
61
|
|
|
33
62
|
- `loops/actions/` maps to `.github/actions/`
|
|
34
63
|
- `loops/workflows/` maps to `.github/workflows/`
|
|
35
|
-
- `loops/aw/repo-config.md` maps to `.github/workflows/shared/repo-config.md`
|
|
36
64
|
- `loops/scripts/` maps to `scripts/`
|
|
37
65
|
|
|
38
|
-
Copy these files manually if you do not use the CLI.
|
|
66
|
+
Copy these files manually if you do not use the CLI. Each worker is self-contained. Edit its
|
|
67
|
+
top-level `env:` defaults directly for consumer-specific endpoint, model, labels, paths, and checks.
|
|
39
68
|
|
|
40
69
|
## Compile
|
|
41
70
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type TemplateName } from "./workflow-catalog.js";
|
|
2
2
|
export interface CatalogInstallResult {
|
|
3
3
|
readonly installed: readonly string[];
|
|
4
4
|
readonly conflicts: readonly string[];
|
|
@@ -7,6 +7,15 @@ export interface CatalogInstallOptions {
|
|
|
7
7
|
readonly force?: boolean;
|
|
8
8
|
readonly sourcePath?: string;
|
|
9
9
|
}
|
|
10
|
+
interface CatalogFile {
|
|
11
|
+
readonly source: string;
|
|
12
|
+
readonly target: string;
|
|
13
|
+
readonly managed: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function mandatoryFileSpecs(sourcePath: string): CatalogFile[];
|
|
10
16
|
export declare function catalogSourcePath(modulePath?: string): string;
|
|
11
17
|
export declare function installCatalog(repositoryPath: string, options?: CatalogInstallOptions): Promise<CatalogInstallResult>;
|
|
12
|
-
export
|
|
18
|
+
export declare function installTemplate(repositoryPath: string, template: TemplateName, options?: CatalogInstallOptions): Promise<CatalogInstallResult>;
|
|
19
|
+
export declare function installMandatoryFiles(repositoryPath: string, options?: CatalogInstallOptions): Promise<CatalogInstallResult>;
|
|
20
|
+
export declare function isTemplateName(value: string): value is TemplateName;
|
|
21
|
+
export {};
|
|
@@ -2,34 +2,83 @@ import { access, copyFile, mkdir, readFile, readdir } from "node:fs/promises";
|
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import { catalogTemplates, mandatoryFiles, templateNames } from "./workflow-catalog.js";
|
|
6
6
|
const sourceMappings = [
|
|
7
7
|
["actions", ".github/actions"],
|
|
8
8
|
["workflows", ".github/workflows"],
|
|
9
9
|
["scripts", "scripts"],
|
|
10
10
|
];
|
|
11
|
+
export function mandatoryFileSpecs(sourcePath) {
|
|
12
|
+
return mandatoryFiles.map((spec) => ({
|
|
13
|
+
source: join(sourcePath, spec.source),
|
|
14
|
+
target: spec.target,
|
|
15
|
+
managed: true,
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
11
18
|
export function catalogSourcePath(modulePath = fileURLToPath(import.meta.url)) {
|
|
12
19
|
return resolve(dirname(modulePath), "..", "loops");
|
|
13
20
|
}
|
|
14
21
|
export async function installCatalog(repositoryPath, options = {}) {
|
|
15
22
|
const sourcePath = options.sourcePath ?? catalogSourcePath();
|
|
16
|
-
const files = await catalogFiles(sourcePath);
|
|
17
|
-
const
|
|
23
|
+
const files = [...await catalogFiles(sourcePath), ...mandatoryFileSpecs(sourcePath)];
|
|
24
|
+
const deduplicated = files.filter((file, index) => files.findIndex((f) => f.target === file.target) === index).sort((left, right) => left.target.localeCompare(right.target));
|
|
25
|
+
const managedFiles = deduplicated.filter((file) => file.managed);
|
|
18
26
|
const conflicts = (await Promise.all(managedFiles.map(async (file) => {
|
|
19
27
|
const destination = join(repositoryPath, file.target);
|
|
20
28
|
return await exists(destination) && !(await filesMatch(file.source, destination)) ? file.target : undefined;
|
|
21
29
|
}))).filter((file) => file !== undefined);
|
|
22
30
|
if (conflicts.length > 0 && !options.force)
|
|
23
31
|
return { installed: [], conflicts };
|
|
24
|
-
await Promise.all(
|
|
32
|
+
await Promise.all(deduplicated.map(async (file) => {
|
|
25
33
|
const destination = join(repositoryPath, file.target);
|
|
26
34
|
if (!file.managed && await exists(destination))
|
|
27
35
|
return;
|
|
28
36
|
await mkdir(dirname(destination), { recursive: true });
|
|
29
37
|
await copyFile(file.source, destination);
|
|
30
38
|
}));
|
|
39
|
+
return { installed: deduplicated.map((file) => file.target), conflicts };
|
|
40
|
+
}
|
|
41
|
+
export async function installTemplate(repositoryPath, template, options = {}) {
|
|
42
|
+
const sourcePath = options.sourcePath ?? catalogSourcePath();
|
|
43
|
+
const meta = catalogTemplateMeta(template);
|
|
44
|
+
const source = join(sourcePath, "templates", meta.directory, meta.file);
|
|
45
|
+
const target = meta.target;
|
|
46
|
+
const destination = join(repositoryPath, target);
|
|
47
|
+
const conflicts = await exists(destination) && !(await filesMatch(source, destination)) ? [target] : [];
|
|
48
|
+
if (conflicts.length > 0 && !options.force)
|
|
49
|
+
return { installed: [], conflicts };
|
|
50
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
51
|
+
await copyFile(source, destination);
|
|
52
|
+
return { installed: [target], conflicts };
|
|
53
|
+
}
|
|
54
|
+
export async function installMandatoryFiles(repositoryPath, options = {}) {
|
|
55
|
+
const sourcePath = options.sourcePath ?? catalogSourcePath();
|
|
56
|
+
const files = mandatoryFileSpecs(sourcePath).sort((left, right) => left.target.localeCompare(right.target));
|
|
57
|
+
const conflicts = (await Promise.all(files.map(async (file) => {
|
|
58
|
+
const destination = join(repositoryPath, file.target);
|
|
59
|
+
return await exists(destination) && !(await filesMatch(file.source, destination)) ? file.target : undefined;
|
|
60
|
+
}))).filter((file) => file !== undefined);
|
|
61
|
+
if (conflicts.length > 0 && !options.force)
|
|
62
|
+
return { installed: [], conflicts };
|
|
63
|
+
await Promise.all(files.map(async (file) => {
|
|
64
|
+
const destination = join(repositoryPath, file.target);
|
|
65
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
66
|
+
await copyFile(file.source, destination);
|
|
67
|
+
}));
|
|
31
68
|
return { installed: files.map((file) => file.target), conflicts };
|
|
32
69
|
}
|
|
70
|
+
export function isTemplateName(value) {
|
|
71
|
+
return templateNames.includes(value);
|
|
72
|
+
}
|
|
73
|
+
function catalogTemplateMeta(template) {
|
|
74
|
+
const entry = catalogTemplates.find((item) => item.name === template);
|
|
75
|
+
if (entry === undefined)
|
|
76
|
+
throw new Error(`Unknown template: ${template}`);
|
|
77
|
+
const directory = template.startsWith("opencode") ? "opencode" : template.startsWith("app-ci-") ? "ci" : "agentics";
|
|
78
|
+
const isWorkflow = entry.file.endsWith(".yml");
|
|
79
|
+
const target = isWorkflow ? `.github/workflows/${entry.file}` : entry.file;
|
|
80
|
+
return { directory, file: entry.file, target };
|
|
81
|
+
}
|
|
33
82
|
async function catalogFiles(sourcePath) {
|
|
34
83
|
const files = [];
|
|
35
84
|
for (const [sourceDirectory, targetDirectory] of sourceMappings) {
|
|
@@ -43,10 +92,6 @@ async function catalogFiles(sourcePath) {
|
|
|
43
92
|
});
|
|
44
93
|
}
|
|
45
94
|
}
|
|
46
|
-
const repositoryConfigSource = join(sourcePath, "aw", "repo-config.md");
|
|
47
|
-
if (await exists(repositoryConfigSource)) {
|
|
48
|
-
files.push({ source: repositoryConfigSource, target: repositoryConfigRelativePath, managed: false });
|
|
49
|
-
}
|
|
50
95
|
return files.sort((left, right) => left.target.localeCompare(right.target));
|
|
51
96
|
}
|
|
52
97
|
function isGeneratedFile(file) {
|
|
@@ -77,4 +122,3 @@ async function exists(path) {
|
|
|
77
122
|
return false;
|
|
78
123
|
}
|
|
79
124
|
}
|
|
80
|
-
export { repositoryConfigRelativePath };
|
|
@@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { catalogSourcePath, installCatalog,
|
|
5
|
+
import { catalogSourcePath, installCatalog, installMandatoryFiles, installTemplate } from "./catalog-installation.js";
|
|
6
6
|
const temporaryDirectories = [];
|
|
7
7
|
afterEach(async () => {
|
|
8
8
|
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })));
|
|
@@ -15,13 +15,14 @@ describe("catalog installation", () => {
|
|
|
15
15
|
});
|
|
16
16
|
expect(catalogSourcePath(join(packageDirectory, "dist", "catalog-installation.js"))).toBe(join(packageDirectory, "loops"));
|
|
17
17
|
});
|
|
18
|
-
it("installs package-owned loops files and
|
|
18
|
+
it("installs package-owned loops files including mandatory opencode.ci.json and compile script", async () => {
|
|
19
19
|
const sourcePath = await createDirectory({
|
|
20
20
|
"actions/check/action.yml": "name: Check\n",
|
|
21
21
|
"workflows/agent-check.md": "# Check\n",
|
|
22
22
|
"workflows/shared/defaults.md": "defaults\n",
|
|
23
|
-
"scripts/compile.mjs": "console.log('compile');\n",
|
|
24
|
-
"
|
|
23
|
+
"scripts/compile-agent-workflows.mjs": "console.log('compile');\n",
|
|
24
|
+
"scripts/compile.mjs": "console.log('old compile');\n",
|
|
25
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
25
26
|
"workflows/agent-check.lock.yml": "generated\n",
|
|
26
27
|
"actions/actions-lock.json": "generated\n",
|
|
27
28
|
});
|
|
@@ -31,56 +32,234 @@ describe("catalog installation", () => {
|
|
|
31
32
|
".github/actions/check/action.yml",
|
|
32
33
|
".github/workflows/agent-check.md",
|
|
33
34
|
".github/workflows/shared/defaults.md",
|
|
34
|
-
|
|
35
|
+
"opencode.ci.json",
|
|
36
|
+
"scripts/compile-agent-workflows.mjs",
|
|
35
37
|
"scripts/compile.mjs",
|
|
36
38
|
],
|
|
37
39
|
conflicts: [],
|
|
38
40
|
});
|
|
39
|
-
|
|
41
|
+
});
|
|
42
|
+
it("copies ownership headers for base workflows and selected templates", async () => {
|
|
43
|
+
const header = "# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-check.md. Update with `workflows update --force`; consumer edits may be overwritten.\n";
|
|
44
|
+
const templateHeader = "# Managed by @plainconceptsplatform/workflows. Source: loops/templates/agentics/agentics-checks.yml. Update with `workflows update --force`; consumer edits may be overwritten.\n";
|
|
45
|
+
const sourcePath = await createDirectory({
|
|
46
|
+
"actions/check/action.yml": "# Managed by @plainconceptsplatform/workflows. Source: loops/actions/check/action.yml. Update with `workflows update --force`; consumer edits may be overwritten.\nname: Check\n",
|
|
47
|
+
"workflows/agent-check.md": `---\n${header}# Check\n`,
|
|
48
|
+
"scripts/compile-agent-workflows.mjs": "// Managed by @plainconceptsplatform/workflows. Source: loops/scripts/compile-agent-workflows.mjs. Update with `workflows update --force`; consumer edits may be overwritten.\n",
|
|
49
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
50
|
+
"templates/agentics/agentics-checks.yml": `${templateHeader}name: Agentics checks\n`,
|
|
51
|
+
});
|
|
52
|
+
const repositoryPath = await createDirectory({});
|
|
53
|
+
await installCatalog(repositoryPath, { sourcePath });
|
|
54
|
+
await installTemplate(repositoryPath, "agentics-checks", { sourcePath });
|
|
55
|
+
await expect(readFile(join(repositoryPath, ".github/workflows/agent-check.md"), "utf8")).resolves.toMatch(new RegExp(`^---\\n${escapeRegularExpression(header)}`));
|
|
56
|
+
await expect(readFile(join(repositoryPath, ".github/workflows/agentics-checks.yml"), "utf8")).resolves.toMatch(new RegExp(`^${escapeRegularExpression(templateHeader)}`));
|
|
40
57
|
});
|
|
41
58
|
it("does not report identical managed files as conflicts", async () => {
|
|
42
59
|
const sourcePath = await createDirectory({
|
|
43
60
|
"actions/check/action.yml": "name: Check\n",
|
|
44
61
|
"workflows/agent-check.md": "# Check\n",
|
|
45
|
-
"scripts/compile.mjs": "compile\n",
|
|
62
|
+
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
63
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
46
64
|
});
|
|
47
65
|
const repositoryPath = await createDirectory({ ".github/workflows/agent-check.md": "# Check\n" });
|
|
48
66
|
await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toMatchObject({ conflicts: [] });
|
|
49
67
|
});
|
|
50
|
-
it("
|
|
68
|
+
it("does not manage legacy repository configuration files", async () => {
|
|
69
|
+
const sourcePath = await createDirectory({
|
|
70
|
+
"actions/check/action.yml": "name: Check\n",
|
|
71
|
+
"workflows/agent-check.md": "# Check\n",
|
|
72
|
+
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
73
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
74
|
+
});
|
|
75
|
+
const repositoryPath = await createDirectory({
|
|
76
|
+
".github/workflows/shared/repo-config.md": "legacy consumer config\n",
|
|
77
|
+
});
|
|
78
|
+
await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toMatchObject({
|
|
79
|
+
installed: [
|
|
80
|
+
".github/actions/check/action.yml",
|
|
81
|
+
".github/workflows/agent-check.md",
|
|
82
|
+
"opencode.ci.json",
|
|
83
|
+
"scripts/compile-agent-workflows.mjs",
|
|
84
|
+
],
|
|
85
|
+
conflicts: [],
|
|
86
|
+
});
|
|
87
|
+
await expect(readFile(join(repositoryPath, ".github/workflows/shared/repo-config.md"), "utf8")).resolves.toBe("legacy consumer config\n");
|
|
88
|
+
});
|
|
89
|
+
it("requires force for different managed files", async () => {
|
|
51
90
|
const sourcePath = await createDirectory({
|
|
52
91
|
"actions/check/action.yml": "package action\n",
|
|
53
92
|
"workflows/agent-check.md": "package workflow\n",
|
|
54
|
-
"scripts/compile.mjs": "package script\n",
|
|
55
|
-
"
|
|
93
|
+
"scripts/compile-agent-workflows.mjs": "package script\n",
|
|
94
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
56
95
|
});
|
|
57
96
|
const repositoryPath = await createDirectory({
|
|
58
97
|
".github/actions/check/action.yml": "consumer action\n",
|
|
59
98
|
".github/workflows/agent-check.md": "consumer workflow\n",
|
|
60
|
-
"scripts/compile.mjs": "consumer script\n",
|
|
61
|
-
|
|
99
|
+
"scripts/compile-agent-workflows.mjs": "consumer script\n",
|
|
100
|
+
"opencode.ci.json": "{ \"model\": \"consumer-model\" }\n",
|
|
62
101
|
});
|
|
63
102
|
await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toEqual({
|
|
64
103
|
installed: [],
|
|
65
104
|
conflicts: [
|
|
66
105
|
".github/actions/check/action.yml",
|
|
67
106
|
".github/workflows/agent-check.md",
|
|
68
|
-
"
|
|
107
|
+
"opencode.ci.json",
|
|
108
|
+
"scripts/compile-agent-workflows.mjs",
|
|
69
109
|
],
|
|
70
110
|
});
|
|
71
111
|
await expect(installCatalog(repositoryPath, { force: true, sourcePath })).resolves.toMatchObject({
|
|
72
112
|
conflicts: [
|
|
73
113
|
".github/actions/check/action.yml",
|
|
74
114
|
".github/workflows/agent-check.md",
|
|
75
|
-
"
|
|
115
|
+
"opencode.ci.json",
|
|
116
|
+
"scripts/compile-agent-workflows.mjs",
|
|
76
117
|
],
|
|
77
118
|
});
|
|
78
119
|
await expect(readFile(join(repositoryPath, ".github/actions/check/action.yml"), "utf8")).resolves.toBe("package action\n");
|
|
79
|
-
|
|
120
|
+
});
|
|
121
|
+
it("installs templates only when explicitly selected", async () => {
|
|
122
|
+
const sourcePath = await createDirectory({
|
|
123
|
+
"actions/check/action.yml": "name: Check\n",
|
|
124
|
+
"workflows/agent-check.md": "# Check\n",
|
|
125
|
+
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
126
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
127
|
+
"templates/agentics/agentics-checks.yml": "name: Agentics checks\n",
|
|
128
|
+
"templates/ci/app-ci-node-monorepo.yml": "name: Node CI\n",
|
|
129
|
+
});
|
|
130
|
+
const repositoryPath = await createDirectory({});
|
|
131
|
+
await installCatalog(repositoryPath, { sourcePath });
|
|
132
|
+
await expect(readFile(join(repositoryPath, ".github/workflows/agentics-checks.yml"), "utf8")).rejects.toThrow();
|
|
133
|
+
await expect(installTemplate(repositoryPath, "agentics-checks", { sourcePath })).resolves.toEqual({
|
|
134
|
+
installed: [".github/workflows/agentics-checks.yml"],
|
|
135
|
+
conflicts: [],
|
|
136
|
+
});
|
|
137
|
+
await expect(installTemplate(repositoryPath, "app-ci-node-monorepo", { sourcePath })).resolves.toEqual({
|
|
138
|
+
installed: [".github/workflows/app-ci-node-monorepo.yml"],
|
|
139
|
+
conflicts: [],
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
it("installCatalog installs mandatory opencode.ci.json and compile script alongside catalog files", async () => {
|
|
143
|
+
const sourcePath = await createDirectory({
|
|
144
|
+
"actions/check/action.yml": "name: Check\n",
|
|
145
|
+
"workflows/agent-check.md": "# Check\n",
|
|
146
|
+
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
147
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
148
|
+
});
|
|
149
|
+
const repositoryPath = await createDirectory({});
|
|
150
|
+
const result = await installCatalog(repositoryPath, { sourcePath });
|
|
151
|
+
expect(result.installed).toContain("opencode.ci.json");
|
|
152
|
+
expect(result.installed).toContain("scripts/compile-agent-workflows.mjs");
|
|
153
|
+
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-2\" }\n");
|
|
154
|
+
await expect(readFile(join(repositoryPath, "scripts/compile-agent-workflows.mjs"), "utf8")).resolves.toBe("compile\n");
|
|
155
|
+
});
|
|
156
|
+
it("installMandatoryFiles installs opencode.ci.json and compile script without catalog files", async () => {
|
|
157
|
+
const sourcePath = await createDirectory({
|
|
158
|
+
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
159
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
160
|
+
});
|
|
161
|
+
const repositoryPath = await createDirectory({});
|
|
162
|
+
const result = await installMandatoryFiles(repositoryPath, { sourcePath });
|
|
163
|
+
expect(result.installed).toEqual(["opencode.ci.json", "scripts/compile-agent-workflows.mjs"]);
|
|
164
|
+
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-2\" }\n");
|
|
165
|
+
await expect(readFile(join(repositoryPath, "scripts/compile-agent-workflows.mjs"), "utf8")).resolves.toBe("compile\n");
|
|
166
|
+
});
|
|
167
|
+
it("installMandatoryFiles reports conflicts on differing files", async () => {
|
|
168
|
+
const sourcePath = await createDirectory({
|
|
169
|
+
"scripts/compile-agent-workflows.mjs": "package script\n",
|
|
170
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
171
|
+
});
|
|
172
|
+
const repositoryPath = await createDirectory({
|
|
173
|
+
"opencode.ci.json": "{ \"model\": \"consumer\" }\n",
|
|
174
|
+
"scripts/compile-agent-workflows.mjs": "consumer script\n",
|
|
175
|
+
});
|
|
176
|
+
await expect(installMandatoryFiles(repositoryPath, { sourcePath })).resolves.toEqual({
|
|
177
|
+
installed: [],
|
|
178
|
+
conflicts: ["opencode.ci.json", "scripts/compile-agent-workflows.mjs"],
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
it("installMandatoryFiles overwrites with force", async () => {
|
|
182
|
+
const sourcePath = await createDirectory({
|
|
183
|
+
"scripts/compile-agent-workflows.mjs": "package script\n",
|
|
184
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
185
|
+
});
|
|
186
|
+
const repositoryPath = await createDirectory({
|
|
187
|
+
"opencode.ci.json": "{ \"model\": \"consumer\" }\n",
|
|
188
|
+
"scripts/compile-agent-workflows.mjs": "consumer script\n",
|
|
189
|
+
});
|
|
190
|
+
await expect(installMandatoryFiles(repositoryPath, { force: true, sourcePath })).resolves.toMatchObject({
|
|
191
|
+
installed: ["opencode.ci.json", "scripts/compile-agent-workflows.mjs"],
|
|
192
|
+
});
|
|
193
|
+
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-2\" }\n");
|
|
194
|
+
});
|
|
195
|
+
it("installMandatoryFiles does not install catalog workflow files", async () => {
|
|
196
|
+
const sourcePath = await createDirectory({
|
|
197
|
+
"actions/check/action.yml": "name: Check\n",
|
|
198
|
+
"workflows/agent-check.md": "# Check\n",
|
|
199
|
+
"scripts/compile-agent-workflows.mjs": "compile\n",
|
|
200
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
201
|
+
});
|
|
202
|
+
const repositoryPath = await createDirectory({});
|
|
203
|
+
await installMandatoryFiles(repositoryPath, { sourcePath });
|
|
204
|
+
await expect(readFile(join(repositoryPath, ".github/workflows/agent-check.md"), "utf8")).rejects.toThrow();
|
|
205
|
+
});
|
|
206
|
+
it("installCatalog deduplicates compile script in both scripts/ and mandatory files", async () => {
|
|
207
|
+
const sourcePath = await createDirectory({
|
|
208
|
+
"actions/check/action.yml": "name: Check\n",
|
|
209
|
+
"workflows/agent-check.md": "# Check\n",
|
|
210
|
+
"scripts/compile-agent-workflows.mjs": "same compile script\n",
|
|
211
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
212
|
+
});
|
|
213
|
+
const repositoryPath = await createDirectory({});
|
|
214
|
+
const result = await installCatalog(repositoryPath, { sourcePath });
|
|
215
|
+
const compileEntries = result.installed.filter((path) => path === "scripts/compile-agent-workflows.mjs");
|
|
216
|
+
expect(compileEntries).toHaveLength(1);
|
|
217
|
+
});
|
|
218
|
+
it("installs the opencode.ci.json template to the repository root", async () => {
|
|
219
|
+
const sourcePath = await createDirectory({
|
|
220
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
221
|
+
});
|
|
222
|
+
const repositoryPath = await createDirectory({});
|
|
223
|
+
await expect(installTemplate(repositoryPath, "opencode.ci.json", { sourcePath })).resolves.toEqual({
|
|
224
|
+
installed: ["opencode.ci.json"],
|
|
225
|
+
conflicts: [],
|
|
226
|
+
});
|
|
227
|
+
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-2\" }\n");
|
|
228
|
+
});
|
|
229
|
+
it("requires force to replace the opencode.ci.json template", async () => {
|
|
230
|
+
const sourcePath = await createDirectory({
|
|
231
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
232
|
+
});
|
|
233
|
+
const repositoryPath = await createDirectory({
|
|
234
|
+
"opencode.ci.json": "{ \"model\": \"consumer-model\" }\n",
|
|
235
|
+
});
|
|
236
|
+
await expect(installTemplate(repositoryPath, "opencode.ci.json", { sourcePath })).resolves.toEqual({
|
|
237
|
+
installed: [],
|
|
238
|
+
conflicts: ["opencode.ci.json"],
|
|
239
|
+
});
|
|
240
|
+
await expect(installTemplate(repositoryPath, "opencode.ci.json", { force: true, sourcePath })).resolves.toMatchObject({
|
|
241
|
+
installed: ["opencode.ci.json"],
|
|
242
|
+
});
|
|
243
|
+
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-2\" }\n");
|
|
244
|
+
});
|
|
245
|
+
it("requires force to replace a selected template", async () => {
|
|
246
|
+
const sourcePath = await createDirectory({
|
|
247
|
+
"templates/agentics/agentics-checks.yml": "package template\n",
|
|
248
|
+
});
|
|
249
|
+
const repositoryPath = await createDirectory({
|
|
250
|
+
".github/workflows/agentics-checks.yml": "consumer template\n",
|
|
251
|
+
});
|
|
252
|
+
await expect(installTemplate(repositoryPath, "agentics-checks", { sourcePath })).resolves.toEqual({
|
|
253
|
+
installed: [],
|
|
254
|
+
conflicts: [".github/workflows/agentics-checks.yml"],
|
|
255
|
+
});
|
|
256
|
+
await expect(installTemplate(repositoryPath, "agentics-checks", { force: true, sourcePath })).resolves.toMatchObject({
|
|
257
|
+
installed: [".github/workflows/agentics-checks.yml"],
|
|
258
|
+
});
|
|
80
259
|
});
|
|
81
260
|
});
|
|
82
261
|
async function createDirectory(files) {
|
|
83
|
-
const directory = await mkdtemp(join(tmpdir(), "
|
|
262
|
+
const directory = await mkdtemp(join(tmpdir(), "workflows-"));
|
|
84
263
|
temporaryDirectories.push(directory);
|
|
85
264
|
await Promise.all(Object.entries(files).map(async ([relativePath, content]) => {
|
|
86
265
|
const path = join(directory, relativePath);
|
|
@@ -90,3 +269,6 @@ async function createDirectory(files) {
|
|
|
90
269
|
}));
|
|
91
270
|
return directory;
|
|
92
271
|
}
|
|
272
|
+
function escapeRegularExpression(value) {
|
|
273
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
274
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CatalogEntry {
|
|
2
|
+
readonly kind: "route" | "template";
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly description: string;
|
|
5
|
+
readonly file: string;
|
|
6
|
+
readonly installed: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ListOptions {
|
|
9
|
+
readonly installedPath?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function listCatalog(options?: ListOptions): Promise<readonly CatalogEntry[]>;
|
|
12
|
+
export declare function searchCatalog(entries: readonly CatalogEntry[], query: string): readonly CatalogEntry[];
|
|
13
|
+
export declare function formatCatalog(entries: readonly CatalogEntry[]): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { access, constants } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { catalogTemplates, workflowRoutes } from "./workflow-catalog.js";
|
|
4
|
+
export async function listCatalog(options = {}) {
|
|
5
|
+
const basePath = options.installedPath ?? process.cwd();
|
|
6
|
+
const routes = await Promise.all(workflowRoutes.map(async (route) => ({
|
|
7
|
+
kind: "route",
|
|
8
|
+
name: route.name,
|
|
9
|
+
description: route.description,
|
|
10
|
+
file: route.worker,
|
|
11
|
+
installed: await isFileInstalled(basePath, route.worker),
|
|
12
|
+
})));
|
|
13
|
+
const templates = await Promise.all(catalogTemplates.map(async (template) => ({
|
|
14
|
+
kind: "template",
|
|
15
|
+
name: template.name,
|
|
16
|
+
description: template.description,
|
|
17
|
+
file: template.file,
|
|
18
|
+
installed: await isFileInstalled(basePath, template.file),
|
|
19
|
+
})));
|
|
20
|
+
return [...routes, ...templates];
|
|
21
|
+
}
|
|
22
|
+
export function searchCatalog(entries, query) {
|
|
23
|
+
const normalized = query.toLowerCase().trim();
|
|
24
|
+
if (normalized === "")
|
|
25
|
+
return [];
|
|
26
|
+
return entries.filter((entry) => entry.name.toLowerCase().includes(normalized) ||
|
|
27
|
+
entry.description.toLowerCase().includes(normalized));
|
|
28
|
+
}
|
|
29
|
+
export function formatCatalog(entries) {
|
|
30
|
+
if (entries.length === 0)
|
|
31
|
+
return "No workflows matched the search query.";
|
|
32
|
+
const routes = entries.filter((entry) => entry.kind === "route");
|
|
33
|
+
const templates = entries.filter((entry) => entry.kind === "template");
|
|
34
|
+
const lines = [];
|
|
35
|
+
if (routes.length > 0) {
|
|
36
|
+
lines.push("Workflows:");
|
|
37
|
+
for (const entry of routes) {
|
|
38
|
+
lines.push(formatEntry(entry));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (templates.length > 0) {
|
|
42
|
+
if (lines.length > 0)
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push("Templates:");
|
|
45
|
+
for (const entry of templates) {
|
|
46
|
+
lines.push(formatEntry(entry));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
function formatEntry(entry) {
|
|
52
|
+
const mark = entry.installed ? "[x]" : "[ ]";
|
|
53
|
+
return ` ${mark} ${entry.name} — ${entry.description}`;
|
|
54
|
+
}
|
|
55
|
+
async function isFileInstalled(basePath, workerOrTemplateFile) {
|
|
56
|
+
const installedPath = templateInstallPath(basePath, workerOrTemplateFile);
|
|
57
|
+
try {
|
|
58
|
+
await access(installedPath, constants.F_OK);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function templateInstallPath(basePath, file) {
|
|
66
|
+
const isRootTemplate = file.endsWith(".json");
|
|
67
|
+
return isRootTemplate ? join(basePath, file) : join(basePath, ".github", "workflows", file);
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|