@plainconceptsplatform/workflows 0.1.2 → 0.1.5
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 +28 -10
- package/dist/catalog-installation.d.ts +3 -2
- package/dist/catalog-installation.js +26 -6
- package/dist/catalog-installation.test.js +104 -10
- 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 +62 -11
- 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/workflow-catalog.d.ts +9 -7
- package/dist/workflow-catalog.js +21 -16
- 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 +18 -2
- 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 +1 -0
- 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,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Workflows CLI
|
|
2
2
|
|
|
3
3
|
Install and update shared GitHub Agentic Workflows for Plain Concepts Platform repositories.
|
|
4
4
|
|
|
@@ -9,22 +9,40 @@ Before installing workflows, install and configure [`PlainConceptsPlatform/openc
|
|
|
9
9
|
For one-off use, prefer:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx @plainconceptsplatform/workflows@latest
|
|
13
|
-
npx @plainconceptsplatform/workflows@latest
|
|
14
|
-
npx @plainconceptsplatform/workflows@latest
|
|
12
|
+
npx @plainconceptsplatform/workflows@latest init
|
|
13
|
+
npx @plainconceptsplatform/workflows@latest add
|
|
14
|
+
npx @plainconceptsplatform/workflows@latest update
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
For a project-local development dependency:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
pnpm add -D @plainconceptsplatform/workflows
|
|
21
|
-
pnpm exec
|
|
22
|
-
pnpm exec
|
|
21
|
+
pnpm exec workflows init
|
|
22
|
+
pnpm exec workflows add
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
`init`
|
|
25
|
+
`init` inspects the repository and reports its stack and visibility. It does not create or manage repository configuration or a manifest.
|
|
26
26
|
|
|
27
|
-
`add` installs
|
|
27
|
+
`add` installs package-owned files. It stops when a managed file differs. Use `pnpm exec workflows update --force` only when you intend to replace managed workflow files.
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
## List and search
|
|
32
|
+
|
|
33
|
+
List all available workflows, routes, and templates with install status:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx --yes --package @plainconceptsplatform/workflows@latest workflows list
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
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.
|
|
40
|
+
|
|
41
|
+
Search by name or description:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx --yes --package @plainconceptsplatform/workflows@latest workflows search "ci"
|
|
45
|
+
```
|
|
28
46
|
|
|
29
47
|
## Manual installation
|
|
30
48
|
|
|
@@ -32,10 +50,10 @@ The package includes `loops/`, a copyable equivalent of `.github/`:
|
|
|
32
50
|
|
|
33
51
|
- `loops/actions/` maps to `.github/actions/`
|
|
34
52
|
- `loops/workflows/` maps to `.github/workflows/`
|
|
35
|
-
- `loops/aw/repo-config.md` maps to `.github/workflows/shared/repo-config.md`
|
|
36
53
|
- `loops/scripts/` maps to `scripts/`
|
|
37
54
|
|
|
38
|
-
Copy these files manually if you do not use the CLI.
|
|
55
|
+
Copy these files manually if you do not use the CLI. Each worker is self-contained. Edit its
|
|
56
|
+
top-level `env:` defaults directly for consumer-specific endpoint, model, labels, paths, and checks.
|
|
39
57
|
|
|
40
58
|
## Compile
|
|
41
59
|
|
|
@@ -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[];
|
|
@@ -9,4 +9,5 @@ export interface CatalogInstallOptions {
|
|
|
9
9
|
}
|
|
10
10
|
export declare function catalogSourcePath(modulePath?: string): string;
|
|
11
11
|
export declare function installCatalog(repositoryPath: string, options?: CatalogInstallOptions): Promise<CatalogInstallResult>;
|
|
12
|
-
export
|
|
12
|
+
export declare function installTemplate(repositoryPath: string, template: TemplateName, options?: CatalogInstallOptions): Promise<CatalogInstallResult>;
|
|
13
|
+
export declare function isTemplateName(value: string): value is TemplateName;
|
|
@@ -2,7 +2,7 @@ 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, templateNames } from "./workflow-catalog.js";
|
|
6
6
|
const sourceMappings = [
|
|
7
7
|
["actions", ".github/actions"],
|
|
8
8
|
["workflows", ".github/workflows"],
|
|
@@ -30,6 +30,31 @@ export async function installCatalog(repositoryPath, options = {}) {
|
|
|
30
30
|
}));
|
|
31
31
|
return { installed: files.map((file) => file.target), conflicts };
|
|
32
32
|
}
|
|
33
|
+
export async function installTemplate(repositoryPath, template, options = {}) {
|
|
34
|
+
const sourcePath = options.sourcePath ?? catalogSourcePath();
|
|
35
|
+
const meta = catalogTemplateMeta(template);
|
|
36
|
+
const source = join(sourcePath, "templates", meta.directory, meta.file);
|
|
37
|
+
const target = meta.target;
|
|
38
|
+
const destination = join(repositoryPath, target);
|
|
39
|
+
const conflicts = await exists(destination) && !(await filesMatch(source, destination)) ? [target] : [];
|
|
40
|
+
if (conflicts.length > 0 && !options.force)
|
|
41
|
+
return { installed: [], conflicts };
|
|
42
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
43
|
+
await copyFile(source, destination);
|
|
44
|
+
return { installed: [target], conflicts };
|
|
45
|
+
}
|
|
46
|
+
export function isTemplateName(value) {
|
|
47
|
+
return templateNames.includes(value);
|
|
48
|
+
}
|
|
49
|
+
function catalogTemplateMeta(template) {
|
|
50
|
+
const entry = catalogTemplates.find((item) => item.name === template);
|
|
51
|
+
if (entry === undefined)
|
|
52
|
+
throw new Error(`Unknown template: ${template}`);
|
|
53
|
+
const directory = template.startsWith("opencode") ? "opencode" : template.startsWith("app-ci-") ? "ci" : "agentics";
|
|
54
|
+
const isWorkflow = entry.file.endsWith(".yml");
|
|
55
|
+
const target = isWorkflow ? `.github/workflows/${entry.file}` : entry.file;
|
|
56
|
+
return { directory, file: entry.file, target };
|
|
57
|
+
}
|
|
33
58
|
async function catalogFiles(sourcePath) {
|
|
34
59
|
const files = [];
|
|
35
60
|
for (const [sourceDirectory, targetDirectory] of sourceMappings) {
|
|
@@ -43,10 +68,6 @@ async function catalogFiles(sourcePath) {
|
|
|
43
68
|
});
|
|
44
69
|
}
|
|
45
70
|
}
|
|
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
71
|
return files.sort((left, right) => left.target.localeCompare(right.target));
|
|
51
72
|
}
|
|
52
73
|
function isGeneratedFile(file) {
|
|
@@ -77,4 +98,3 @@ async function exists(path) {
|
|
|
77
98
|
return false;
|
|
78
99
|
}
|
|
79
100
|
}
|
|
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, 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,12 @@ 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
|
|
18
|
+
it("installs package-owned loops files", 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
23
|
"scripts/compile.mjs": "console.log('compile');\n",
|
|
24
|
-
"aw/repo-config.md": "# Repository configuration\n",
|
|
25
24
|
"workflows/agent-check.lock.yml": "generated\n",
|
|
26
25
|
"actions/actions-lock.json": "generated\n",
|
|
27
26
|
});
|
|
@@ -31,12 +30,25 @@ describe("catalog installation", () => {
|
|
|
31
30
|
".github/actions/check/action.yml",
|
|
32
31
|
".github/workflows/agent-check.md",
|
|
33
32
|
".github/workflows/shared/defaults.md",
|
|
34
|
-
repositoryConfigRelativePath,
|
|
35
33
|
"scripts/compile.mjs",
|
|
36
34
|
],
|
|
37
35
|
conflicts: [],
|
|
38
36
|
});
|
|
39
|
-
|
|
37
|
+
});
|
|
38
|
+
it("copies ownership headers for base workflows and selected templates", async () => {
|
|
39
|
+
const header = "# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-check.md. Update with `workflows update --force`; consumer edits may be overwritten.\n";
|
|
40
|
+
const templateHeader = "# Managed by @plainconceptsplatform/workflows. Source: loops/templates/agentics/agentics-checks.yml. Update with `workflows update --force`; consumer edits may be overwritten.\n";
|
|
41
|
+
const sourcePath = await createDirectory({
|
|
42
|
+
"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",
|
|
43
|
+
"workflows/agent-check.md": `---\n${header}# Check\n`,
|
|
44
|
+
"scripts/compile.mjs": "// Managed by @plainconceptsplatform/workflows. Source: loops/scripts/compile.mjs. Update with `workflows update --force`; consumer edits may be overwritten.\n",
|
|
45
|
+
"templates/agentics/agentics-checks.yml": `${templateHeader}name: Agentics checks\n`,
|
|
46
|
+
});
|
|
47
|
+
const repositoryPath = await createDirectory({});
|
|
48
|
+
await installCatalog(repositoryPath, { sourcePath });
|
|
49
|
+
await installTemplate(repositoryPath, "agentics-checks", { sourcePath });
|
|
50
|
+
await expect(readFile(join(repositoryPath, ".github/workflows/agent-check.md"), "utf8")).resolves.toMatch(new RegExp(`^---\\n${escapeRegularExpression(header)}`));
|
|
51
|
+
await expect(readFile(join(repositoryPath, ".github/workflows/agentics-checks.yml"), "utf8")).resolves.toMatch(new RegExp(`^${escapeRegularExpression(templateHeader)}`));
|
|
40
52
|
});
|
|
41
53
|
it("does not report identical managed files as conflicts", async () => {
|
|
42
54
|
const sourcePath = await createDirectory({
|
|
@@ -47,18 +59,35 @@ describe("catalog installation", () => {
|
|
|
47
59
|
const repositoryPath = await createDirectory({ ".github/workflows/agent-check.md": "# Check\n" });
|
|
48
60
|
await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toMatchObject({ conflicts: [] });
|
|
49
61
|
});
|
|
50
|
-
it("
|
|
62
|
+
it("does not manage legacy repository configuration files", async () => {
|
|
63
|
+
const sourcePath = await createDirectory({
|
|
64
|
+
"actions/check/action.yml": "name: Check\n",
|
|
65
|
+
"workflows/agent-check.md": "# Check\n",
|
|
66
|
+
"scripts/compile.mjs": "compile\n",
|
|
67
|
+
});
|
|
68
|
+
const repositoryPath = await createDirectory({
|
|
69
|
+
".github/workflows/shared/repo-config.md": "legacy consumer config\n",
|
|
70
|
+
});
|
|
71
|
+
await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toMatchObject({
|
|
72
|
+
installed: [
|
|
73
|
+
".github/actions/check/action.yml",
|
|
74
|
+
".github/workflows/agent-check.md",
|
|
75
|
+
"scripts/compile.mjs",
|
|
76
|
+
],
|
|
77
|
+
conflicts: [],
|
|
78
|
+
});
|
|
79
|
+
await expect(readFile(join(repositoryPath, ".github/workflows/shared/repo-config.md"), "utf8")).resolves.toBe("legacy consumer config\n");
|
|
80
|
+
});
|
|
81
|
+
it("requires force for different managed files", async () => {
|
|
51
82
|
const sourcePath = await createDirectory({
|
|
52
83
|
"actions/check/action.yml": "package action\n",
|
|
53
84
|
"workflows/agent-check.md": "package workflow\n",
|
|
54
85
|
"scripts/compile.mjs": "package script\n",
|
|
55
|
-
"aw/repo-config.md": "package config\n",
|
|
56
86
|
});
|
|
57
87
|
const repositoryPath = await createDirectory({
|
|
58
88
|
".github/actions/check/action.yml": "consumer action\n",
|
|
59
89
|
".github/workflows/agent-check.md": "consumer workflow\n",
|
|
60
90
|
"scripts/compile.mjs": "consumer script\n",
|
|
61
|
-
[repositoryConfigRelativePath]: "consumer config\n",
|
|
62
91
|
});
|
|
63
92
|
await expect(installCatalog(repositoryPath, { sourcePath })).resolves.toEqual({
|
|
64
93
|
installed: [],
|
|
@@ -76,11 +105,73 @@ describe("catalog installation", () => {
|
|
|
76
105
|
],
|
|
77
106
|
});
|
|
78
107
|
await expect(readFile(join(repositoryPath, ".github/actions/check/action.yml"), "utf8")).resolves.toBe("package action\n");
|
|
79
|
-
|
|
108
|
+
});
|
|
109
|
+
it("installs templates only when explicitly selected", async () => {
|
|
110
|
+
const sourcePath = await createDirectory({
|
|
111
|
+
"actions/check/action.yml": "name: Check\n",
|
|
112
|
+
"workflows/agent-check.md": "# Check\n",
|
|
113
|
+
"scripts/compile.mjs": "compile\n",
|
|
114
|
+
"templates/agentics/agentics-checks.yml": "name: Agentics checks\n",
|
|
115
|
+
"templates/ci/app-ci-node-monorepo.yml": "name: Node CI\n",
|
|
116
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
117
|
+
});
|
|
118
|
+
const repositoryPath = await createDirectory({});
|
|
119
|
+
await installCatalog(repositoryPath, { sourcePath });
|
|
120
|
+
await expect(readFile(join(repositoryPath, ".github/workflows/agentics-checks.yml"), "utf8")).rejects.toThrow();
|
|
121
|
+
await expect(installTemplate(repositoryPath, "agentics-checks", { sourcePath })).resolves.toEqual({
|
|
122
|
+
installed: [".github/workflows/agentics-checks.yml"],
|
|
123
|
+
conflicts: [],
|
|
124
|
+
});
|
|
125
|
+
await expect(installTemplate(repositoryPath, "app-ci-node-monorepo", { sourcePath })).resolves.toEqual({
|
|
126
|
+
installed: [".github/workflows/app-ci-node-monorepo.yml"],
|
|
127
|
+
conflicts: [],
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
it("installs the opencode.ci.json template to the repository root", async () => {
|
|
131
|
+
const sourcePath = await createDirectory({
|
|
132
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
133
|
+
});
|
|
134
|
+
const repositoryPath = await createDirectory({});
|
|
135
|
+
await expect(installTemplate(repositoryPath, "opencode.ci.json", { sourcePath })).resolves.toEqual({
|
|
136
|
+
installed: ["opencode.ci.json"],
|
|
137
|
+
conflicts: [],
|
|
138
|
+
});
|
|
139
|
+
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-2\" }\n");
|
|
140
|
+
});
|
|
141
|
+
it("requires force to replace the opencode.ci.json template", async () => {
|
|
142
|
+
const sourcePath = await createDirectory({
|
|
143
|
+
"templates/opencode/opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }\n",
|
|
144
|
+
});
|
|
145
|
+
const repositoryPath = await createDirectory({
|
|
146
|
+
"opencode.ci.json": "{ \"model\": \"consumer-model\" }\n",
|
|
147
|
+
});
|
|
148
|
+
await expect(installTemplate(repositoryPath, "opencode.ci.json", { sourcePath })).resolves.toEqual({
|
|
149
|
+
installed: [],
|
|
150
|
+
conflicts: ["opencode.ci.json"],
|
|
151
|
+
});
|
|
152
|
+
await expect(installTemplate(repositoryPath, "opencode.ci.json", { force: true, sourcePath })).resolves.toMatchObject({
|
|
153
|
+
installed: ["opencode.ci.json"],
|
|
154
|
+
});
|
|
155
|
+
await expect(readFile(join(repositoryPath, "opencode.ci.json"), "utf8")).resolves.toBe("{ \"model\": \"plainconcepts/glm-5-2\" }\n");
|
|
156
|
+
});
|
|
157
|
+
it("requires force to replace a selected template", async () => {
|
|
158
|
+
const sourcePath = await createDirectory({
|
|
159
|
+
"templates/agentics/agentics-checks.yml": "package template\n",
|
|
160
|
+
});
|
|
161
|
+
const repositoryPath = await createDirectory({
|
|
162
|
+
".github/workflows/agentics-checks.yml": "consumer template\n",
|
|
163
|
+
});
|
|
164
|
+
await expect(installTemplate(repositoryPath, "agentics-checks", { sourcePath })).resolves.toEqual({
|
|
165
|
+
installed: [],
|
|
166
|
+
conflicts: [".github/workflows/agentics-checks.yml"],
|
|
167
|
+
});
|
|
168
|
+
await expect(installTemplate(repositoryPath, "agentics-checks", { force: true, sourcePath })).resolves.toMatchObject({
|
|
169
|
+
installed: [".github/workflows/agentics-checks.yml"],
|
|
170
|
+
});
|
|
80
171
|
});
|
|
81
172
|
});
|
|
82
173
|
async function createDirectory(files) {
|
|
83
|
-
const directory = await mkdtemp(join(tmpdir(), "
|
|
174
|
+
const directory = await mkdtemp(join(tmpdir(), "workflows-"));
|
|
84
175
|
temporaryDirectories.push(directory);
|
|
85
176
|
await Promise.all(Object.entries(files).map(async ([relativePath, content]) => {
|
|
86
177
|
const path = join(directory, relativePath);
|
|
@@ -90,3 +181,6 @@ async function createDirectory(files) {
|
|
|
90
181
|
}));
|
|
91
182
|
return directory;
|
|
92
183
|
}
|
|
184
|
+
function escapeRegularExpression(value) {
|
|
185
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
186
|
+
}
|
|
@@ -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 {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { formatCatalog, listCatalog, searchCatalog } from "./catalog-listing.js";
|
|
6
|
+
import { workflowRoutes, catalogTemplates } from "./workflow-catalog.js";
|
|
7
|
+
const temporaryDirectories = [];
|
|
8
|
+
afterEach(async () => {
|
|
9
|
+
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })));
|
|
10
|
+
});
|
|
11
|
+
describe("catalog listing", () => {
|
|
12
|
+
it("returns all routes and templates", async () => {
|
|
13
|
+
const repositoryPath = await createRepository({});
|
|
14
|
+
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
15
|
+
expect(entries).toHaveLength(workflowRoutes.length + catalogTemplates.length);
|
|
16
|
+
const routeNames = entries.filter((entry) => entry.kind === "route").map((entry) => entry.name);
|
|
17
|
+
const templateNames = entries.filter((entry) => entry.kind === "template").map((entry) => entry.name);
|
|
18
|
+
expect(routeNames).toEqual(["refine", "implement", "direct", "apply-review", "merge-gate", "audit", "propose"]);
|
|
19
|
+
expect(templateNames).toEqual(["agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "opencode.ci.json"]);
|
|
20
|
+
});
|
|
21
|
+
it("reports all entries as not installed in an empty repository", async () => {
|
|
22
|
+
const repositoryPath = await createRepository({});
|
|
23
|
+
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
24
|
+
expect(entries.every((entry) => entry.installed === false)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
it("marks a route as installed when its worker file exists", async () => {
|
|
27
|
+
const repositoryPath = await createRepository({
|
|
28
|
+
".github/workflows/agent-refine.md": "# Refine",
|
|
29
|
+
});
|
|
30
|
+
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
31
|
+
const refineEntry = entries.find((entry) => entry.name === "refine");
|
|
32
|
+
expect(refineEntry).toBeDefined();
|
|
33
|
+
expect(refineEntry.installed).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it("marks a template as installed when its template file exists", async () => {
|
|
36
|
+
const repositoryPath = await createRepository({
|
|
37
|
+
".github/workflows/agentics-checks.yml": "name: Agentics checks",
|
|
38
|
+
});
|
|
39
|
+
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
40
|
+
const checksEntry = entries.find((entry) => entry.name === "agentics-checks");
|
|
41
|
+
expect(checksEntry).toBeDefined();
|
|
42
|
+
expect(checksEntry.installed).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it("marks the opencode.ci.json template as installed when the file exists at repository root", async () => {
|
|
45
|
+
const repositoryPath = await createRepository({
|
|
46
|
+
"opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }",
|
|
47
|
+
});
|
|
48
|
+
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
49
|
+
const opencodeEntry = entries.find((entry) => entry.name === "opencode.ci.json");
|
|
50
|
+
expect(opencodeEntry).toBeDefined();
|
|
51
|
+
expect(opencodeEntry.installed).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
it("reports mixed installed and uninstalled entries", async () => {
|
|
54
|
+
const repositoryPath = await createRepository({
|
|
55
|
+
".github/workflows/agent-refine.md": "# Refine",
|
|
56
|
+
".github/workflows/agent-audit.md": "# Audit",
|
|
57
|
+
".github/workflows/app-ci-dotnet-next.yml": "name: CI",
|
|
58
|
+
"opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }",
|
|
59
|
+
});
|
|
60
|
+
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
61
|
+
expect(entries.filter((entry) => entry.installed).map((entry) => entry.name))
|
|
62
|
+
.toEqual(expect.arrayContaining(["refine", "audit", "app-ci-dotnet-next", "opencode.ci.json"]));
|
|
63
|
+
expect(entries.filter((entry) => !entry.installed)).toHaveLength(workflowRoutes.length + catalogTemplates.length - 4);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("searchCatalog", () => {
|
|
67
|
+
const sampleEntries = [
|
|
68
|
+
{ kind: "route", name: "refine", description: "Refines an issue into a user story.", file: "agent-refine.md", installed: false },
|
|
69
|
+
{ kind: "route", name: "audit", description: "Read-only repository audit.", file: "agent-audit.md", installed: true },
|
|
70
|
+
{ kind: "template", name: "app-ci-dotnet-next", description: "App CI pipeline for .NET + Next.js.", file: "app-ci-dotnet-next.yml", installed: false },
|
|
71
|
+
];
|
|
72
|
+
it("filters by name match", () => {
|
|
73
|
+
const results = searchCatalog(sampleEntries, "audit");
|
|
74
|
+
expect(results).toHaveLength(1);
|
|
75
|
+
expect(results[0].name).toBe("audit");
|
|
76
|
+
});
|
|
77
|
+
it("filters by description match", () => {
|
|
78
|
+
const results = searchCatalog(sampleEntries, "user story");
|
|
79
|
+
expect(results).toHaveLength(1);
|
|
80
|
+
expect(results[0].name).toBe("refine");
|
|
81
|
+
});
|
|
82
|
+
it("is case-insensitive", () => {
|
|
83
|
+
const results = searchCatalog(sampleEntries, "AUDIT");
|
|
84
|
+
expect(results).toHaveLength(1);
|
|
85
|
+
expect(results[0].name).toBe("audit");
|
|
86
|
+
});
|
|
87
|
+
it("returns empty array for no matches", () => {
|
|
88
|
+
const results = searchCatalog(sampleEntries, "nonexistent");
|
|
89
|
+
expect(results).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
it("returns empty array for empty query after trim", () => {
|
|
92
|
+
expect(searchCatalog(sampleEntries, " ")).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
it("returns both routes and templates when the query matches both kinds", () => {
|
|
95
|
+
const entries = [
|
|
96
|
+
{ kind: "route", name: "direct", description: "Executes a CI instruction.", file: "agent-direct.md", installed: false },
|
|
97
|
+
{ kind: "template", name: "app-ci-dotnet-next", description: "App CI pipeline.", file: "app-ci-dotnet-next.yml", installed: true },
|
|
98
|
+
];
|
|
99
|
+
const results = searchCatalog(entries, "CI");
|
|
100
|
+
expect(results).toHaveLength(2);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("formatCatalog", () => {
|
|
104
|
+
it("groups routes under Workflows and templates under Templates", () => {
|
|
105
|
+
const entries = [
|
|
106
|
+
{ kind: "route", name: "refine", description: "Refines a story.", file: "agent-refine.md", installed: true },
|
|
107
|
+
{ kind: "template", name: "app-ci-dotnet-next", description: "CI for .NET.", file: "app-ci-dotnet-next.yml", installed: false },
|
|
108
|
+
];
|
|
109
|
+
const output = formatCatalog(entries);
|
|
110
|
+
expect(output).toContain("Workflows:");
|
|
111
|
+
expect(output).toContain("Templates:");
|
|
112
|
+
expect(output).toContain("[x] refine");
|
|
113
|
+
expect(output).toContain("[ ] app-ci-dotnet-next");
|
|
114
|
+
});
|
|
115
|
+
it("prints a no-match message for an empty result", () => {
|
|
116
|
+
expect(formatCatalog([])).toBe("No workflows matched the search query.");
|
|
117
|
+
});
|
|
118
|
+
it("prints only a Workflows section when no templates match", () => {
|
|
119
|
+
const entries = [
|
|
120
|
+
{ kind: "route", name: "refine", description: "Refines.", file: "agent-refine.md", installed: false },
|
|
121
|
+
];
|
|
122
|
+
const output = formatCatalog(entries);
|
|
123
|
+
expect(output).toContain("Workflows:");
|
|
124
|
+
expect(output).not.toContain("Templates:");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
async function createRepository(files) {
|
|
128
|
+
const repositoryPath = await mkdtemp(join(tmpdir(), "workflows-"));
|
|
129
|
+
temporaryDirectories.push(repositoryPath);
|
|
130
|
+
await Promise.all(Object.entries(files).map(async ([relativePath, content]) => {
|
|
131
|
+
const path = join(repositoryPath, relativePath);
|
|
132
|
+
const { mkdir } = await import("node:fs/promises");
|
|
133
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
134
|
+
await writeFile(path, content, "utf8");
|
|
135
|
+
}));
|
|
136
|
+
return repositoryPath;
|
|
137
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { inspectRepository, parseVisibility, resolveVisibility } from "./repository-inspection.js";
|
|
3
|
-
import { installCatalog } from "./catalog-installation.js";
|
|
4
|
-
import {
|
|
3
|
+
import { installCatalog, installTemplate, isTemplateName } from "./catalog-installation.js";
|
|
4
|
+
import { formatCatalog, listCatalog, searchCatalog } from "./catalog-listing.js";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
const HELP_TEXT = `Workflows CLI — install and manage Plain Concepts Platform agentic workflows.
|
|
8
|
+
|
|
9
|
+
Usage: workflows <command> [options]
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
init Inspect the repository and report its stack and visibility.
|
|
13
|
+
add Install package-owned workflow files into .github/.
|
|
14
|
+
update Alias for add. Use --force to overwrite managed files.
|
|
15
|
+
status Print repository inspection as JSON.
|
|
16
|
+
list List all available workflows and templates with install status.
|
|
17
|
+
search <query> Filter workflows and templates by name or description.
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--visibility public|private Override repository visibility (init only).
|
|
21
|
+
--template <name> Install a standalone template instead of the catalog.
|
|
22
|
+
Templates: agentics-checks, agentics-maintenance,
|
|
23
|
+
app-ci-dotnet-next, app-ci-node-monorepo,
|
|
24
|
+
opencode.ci.json.
|
|
25
|
+
--force Overwrite managed files that differ from the package source.
|
|
26
|
+
-h, --help Show this help text.
|
|
27
|
+
|
|
28
|
+
Installed workflows are marked [x] when the corresponding .github/workflows/agent-*.md
|
|
29
|
+
file exists relative to the current directory.`;
|
|
7
30
|
export async function run(arguments_, repositoryPath = process.cwd()) {
|
|
8
31
|
const [command, ...options] = arguments_;
|
|
9
32
|
if (command === "--help" || command === "-h" || command === undefined) {
|
|
10
|
-
console.log(
|
|
33
|
+
console.log(HELP_TEXT);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
if (command === "list") {
|
|
37
|
+
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
38
|
+
console.log(formatCatalog(entries));
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
if (command === "search") {
|
|
42
|
+
if (options.length === 0 || options.length > 1)
|
|
43
|
+
return fail("search requires exactly one query argument.");
|
|
44
|
+
const allEntries = await listCatalog({ installedPath: repositoryPath });
|
|
45
|
+
const results = searchCatalog(allEntries, options[0]);
|
|
46
|
+
console.log(formatCatalog(results));
|
|
11
47
|
return 0;
|
|
12
48
|
}
|
|
13
49
|
if (command === "init") {
|
|
@@ -16,21 +52,23 @@ export async function run(arguments_, repositoryPath = process.cwd()) {
|
|
|
16
52
|
return fail("--visibility must be public or private.");
|
|
17
53
|
const inspection = await inspectRepository(repositoryPath);
|
|
18
54
|
const resolvedVisibility = await resolveVisibility(repositoryPath, visibility);
|
|
19
|
-
|
|
20
|
-
console.log(JSON.stringify({ command, ...result }, null, 2));
|
|
55
|
+
console.log(JSON.stringify({ command, inspection, visibility: resolvedVisibility }, null, 2));
|
|
21
56
|
return 0;
|
|
22
57
|
}
|
|
23
58
|
if (command === "status") {
|
|
24
59
|
const inspection = await inspectRepository(repositoryPath);
|
|
25
|
-
|
|
26
|
-
console.log(JSON.stringify({ command, inspection, manifest, repositoryConfigPath: repositoryConfigRelativePath }, null, 2));
|
|
60
|
+
console.log(JSON.stringify({ command, inspection }, null, 2));
|
|
27
61
|
return 0;
|
|
28
62
|
}
|
|
29
63
|
if (command === "add" || command === "update") {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
64
|
+
const template = readTemplateOption(options);
|
|
65
|
+
if (template === "invalid")
|
|
66
|
+
return fail(`${command} accepts only --force or --template agentics-checks|agentics-maintenance|app-ci-dotnet-next|app-ci-node-monorepo|opencode.ci.json.`);
|
|
67
|
+
const force = options.includes("--force");
|
|
68
|
+
const result = template === undefined
|
|
69
|
+
? await installCatalog(repositoryPath, { force })
|
|
70
|
+
: await installTemplate(repositoryPath, template, { force });
|
|
71
|
+
if (result.conflicts.length > 0 && !force) {
|
|
34
72
|
console.error(`Catalog conflicts found. Re-run with --force to overwrite package-managed files:\n${result.conflicts.join("\n")}`);
|
|
35
73
|
return 1;
|
|
36
74
|
}
|
|
@@ -46,6 +84,19 @@ function readVisibilityOption(options) {
|
|
|
46
84
|
return "invalid";
|
|
47
85
|
return parseVisibility(options[1]) ?? "invalid";
|
|
48
86
|
}
|
|
87
|
+
function readTemplateOption(options) {
|
|
88
|
+
const templateIndex = options.indexOf("--template");
|
|
89
|
+
if (templateIndex === -1)
|
|
90
|
+
return options.every((option) => option === "--force") ? undefined : "invalid";
|
|
91
|
+
if (templateIndex + 1 >= options.length || options.filter((option) => option === "--template").length !== 1)
|
|
92
|
+
return "invalid";
|
|
93
|
+
if (options.some((option, index) => option !== "--force" && index !== templateIndex && index !== templateIndex + 1))
|
|
94
|
+
return "invalid";
|
|
95
|
+
return templateNameFromOptions(options[templateIndex + 1]);
|
|
96
|
+
}
|
|
97
|
+
function templateNameFromOptions(value) {
|
|
98
|
+
return value !== undefined && isTemplateName(value) ? value : "invalid";
|
|
99
|
+
}
|
|
49
100
|
function fail(message) {
|
|
50
101
|
console.error(message);
|
|
51
102
|
return 1;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|