@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.
- package/README.md +88 -88
- package/loops/actions/agent-output.cjs +17 -17
- package/loops/actions/audit-close/action.yml +24 -0
- package/loops/actions/classify-route/action.yml +1 -3
- package/loops/actions/classify-route/classify-route.sh +40 -35
- package/loops/actions/update-changelog/action.yml +113 -0
- package/loops/actions/validate-merge-gate-output/action.yml +40 -40
- package/loops/actions/validate-refine-output/validate-refine-output.sh +16 -3
- package/loops/actions/validate-review-output/action.yml +35 -35
- package/loops/actions/validate-triage-output/action.yml +36 -36
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +91 -36
- package/loops/scripts/compile-agent-workflows.mjs +195 -6
- package/loops/templates/agentics/agentics-checks.yml +86 -86
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/ci/app-ci-dotnet-next.yml +268 -171
- package/loops/templates/ci/app-ci-node-monorepo.yml +208 -178
- package/loops/templates/issues/bug_report.yml +109 -109
- package/loops/templates/issues/feature_request.yml +75 -75
- package/loops/templates/opencode/opencode.ci.json +49 -47
- package/loops/templates/opencode/opencode.ci.json.md +46 -41
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-apply-review.md +465 -443
- package/loops/workflows/agent-audit.md +213 -197
- package/loops/workflows/agent-implement.md +538 -414
- package/loops/workflows/agent-merge-gate.md +730 -584
- package/loops/workflows/agent-refine.md +609 -418
- package/loops/workflows/agent-release.md +258 -0
- package/loops/workflows/agent-triage.md +447 -439
- package/loops/workflows/authorize-bot-work.yml +85 -82
- package/loops/workflows/shared/opencode-ci.md +206 -197
- package/loops/workflows/shared/platform-defaults.md +19 -16
- package/loops/workflows/work-router.yml +862 -632
- package/package.json +7 -8
- package/dist/action-validation.test.d.ts +0 -1
- package/dist/action-validation.test.js +0 -84
- package/dist/catalog-installation.d.ts +0 -30
- package/dist/catalog-installation.js +0 -379
- package/dist/catalog-installation.test.d.ts +0 -1
- package/dist/catalog-installation.test.js +0 -448
- package/dist/catalog-listing.d.ts +0 -13
- package/dist/catalog-listing.js +0 -70
- package/dist/catalog-listing.test.d.ts +0 -1
- package/dist/catalog-listing.test.js +0 -150
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -218
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.js +0 -273
- package/dist/repository-inspection.d.ts +0 -23
- package/dist/repository-inspection.js +0 -113
- package/dist/repository-inspection.test.d.ts +0 -1
- package/dist/repository-inspection.test.js +0 -77
- package/dist/repository-state.d.ts +0 -18
- package/dist/repository-state.js +0 -77
- package/dist/repository-state.test.d.ts +0 -1
- package/dist/repository-state.test.js +0 -96
- package/dist/route-processing.d.ts +0 -6
- package/dist/route-processing.js +0 -150
- package/dist/route-processing.test.d.ts +0 -1
- package/dist/route-processing.test.js +0 -350
- package/dist/stack-defaults.d.ts +0 -11
- package/dist/stack-defaults.js +0 -104
- package/dist/stack-defaults.test.d.ts +0 -1
- package/dist/stack-defaults.test.js +0 -266
- package/dist/tui.d.ts +0 -25
- package/dist/tui.js +0 -287
- package/dist/tui.test.d.ts +0 -1
- package/dist/tui.test.js +0 -246
- package/dist/workflow-catalog.d.ts +0 -25
- package/dist/workflow-catalog.js +0 -59
- package/dist/workflow-catalog.test.d.ts +0 -1
- package/dist/workflow-catalog.test.js +0 -29
- package/loops/workflows/agent-direct.md +0 -375
- package/loops/workflows/agent-propose.md +0 -342
|
@@ -1,150 +0,0 @@
|
|
|
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", "triage", "apply-review", "merge-gate", "audit", "propose"]);
|
|
19
|
-
expect(templateNames).toEqual(["agentics-checks", "agentics-maintenance", "app-ci-dotnet-next", "app-ci-node-monorepo", "bug-report", "feature-request", "github-release", "opencode.ci.json", "visual-evidence"]);
|
|
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 an issue template as installed when its file exists in .github/ISSUE_TEMPLATE/", async () => {
|
|
45
|
-
const repositoryPath = await createRepository({
|
|
46
|
-
".github/ISSUE_TEMPLATE/bug_report.yml": "name: Bug report",
|
|
47
|
-
".github/ISSUE_TEMPLATE/feature_request.yml": "name: Feature request",
|
|
48
|
-
});
|
|
49
|
-
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
50
|
-
const bugEntry = entries.find((entry) => entry.name === "bug-report");
|
|
51
|
-
const featureEntry = entries.find((entry) => entry.name === "feature-request");
|
|
52
|
-
expect(bugEntry).toBeDefined();
|
|
53
|
-
expect(bugEntry.installed).toBe(true);
|
|
54
|
-
expect(featureEntry).toBeDefined();
|
|
55
|
-
expect(featureEntry.installed).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
it("marks the opencode.ci.json template as installed when the file exists at repository root", async () => {
|
|
58
|
-
const repositoryPath = await createRepository({
|
|
59
|
-
"opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }",
|
|
60
|
-
});
|
|
61
|
-
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
62
|
-
const opencodeEntry = entries.find((entry) => entry.name === "opencode.ci.json");
|
|
63
|
-
expect(opencodeEntry).toBeDefined();
|
|
64
|
-
expect(opencodeEntry.installed).toBe(true);
|
|
65
|
-
});
|
|
66
|
-
it("reports mixed installed and uninstalled entries", async () => {
|
|
67
|
-
const repositoryPath = await createRepository({
|
|
68
|
-
".github/workflows/agent-refine.md": "# Refine",
|
|
69
|
-
".github/workflows/agent-audit.md": "# Audit",
|
|
70
|
-
".github/workflows/app-ci-dotnet-next.yml": "name: CI",
|
|
71
|
-
"opencode.ci.json": "{ \"model\": \"plainconcepts/glm-5-2\" }",
|
|
72
|
-
});
|
|
73
|
-
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
74
|
-
expect(entries.filter((entry) => entry.installed).map((entry) => entry.name))
|
|
75
|
-
.toEqual(expect.arrayContaining(["refine", "audit", "app-ci-dotnet-next", "opencode.ci.json"]));
|
|
76
|
-
expect(entries.filter((entry) => !entry.installed)).toHaveLength(workflowRoutes.length + catalogTemplates.length - 4);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
describe("searchCatalog", () => {
|
|
80
|
-
const sampleEntries = [
|
|
81
|
-
{ kind: "route", name: "refine", description: "Refines an issue into a user story.", file: "agent-refine.md", installed: false },
|
|
82
|
-
{ kind: "route", name: "audit", description: "Read-only repository audit.", file: "agent-audit.md", installed: true },
|
|
83
|
-
{ kind: "template", name: "app-ci-dotnet-next", description: "App CI pipeline for .NET + Next.js.", file: "app-ci-dotnet-next.yml", installed: false },
|
|
84
|
-
];
|
|
85
|
-
it("filters by name match", () => {
|
|
86
|
-
const results = searchCatalog(sampleEntries, "audit");
|
|
87
|
-
expect(results).toHaveLength(1);
|
|
88
|
-
expect(results[0].name).toBe("audit");
|
|
89
|
-
});
|
|
90
|
-
it("filters by description match", () => {
|
|
91
|
-
const results = searchCatalog(sampleEntries, "user story");
|
|
92
|
-
expect(results).toHaveLength(1);
|
|
93
|
-
expect(results[0].name).toBe("refine");
|
|
94
|
-
});
|
|
95
|
-
it("is case-insensitive", () => {
|
|
96
|
-
const results = searchCatalog(sampleEntries, "AUDIT");
|
|
97
|
-
expect(results).toHaveLength(1);
|
|
98
|
-
expect(results[0].name).toBe("audit");
|
|
99
|
-
});
|
|
100
|
-
it("returns empty array for no matches", () => {
|
|
101
|
-
const results = searchCatalog(sampleEntries, "nonexistent");
|
|
102
|
-
expect(results).toHaveLength(0);
|
|
103
|
-
});
|
|
104
|
-
it("returns empty array for empty query after trim", () => {
|
|
105
|
-
expect(searchCatalog(sampleEntries, " ")).toHaveLength(0);
|
|
106
|
-
});
|
|
107
|
-
it("returns both routes and templates when the query matches both kinds", () => {
|
|
108
|
-
const entries = [
|
|
109
|
-
{ kind: "route", name: "direct", description: "Executes a CI instruction.", file: "agent-direct.md", installed: false },
|
|
110
|
-
{ kind: "template", name: "app-ci-dotnet-next", description: "App CI pipeline.", file: "app-ci-dotnet-next.yml", installed: true },
|
|
111
|
-
];
|
|
112
|
-
const results = searchCatalog(entries, "CI");
|
|
113
|
-
expect(results).toHaveLength(2);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
describe("formatCatalog", () => {
|
|
117
|
-
it("groups routes under Workflows and templates under Templates", () => {
|
|
118
|
-
const entries = [
|
|
119
|
-
{ kind: "route", name: "refine", description: "Refines a story.", file: "agent-refine.md", installed: true },
|
|
120
|
-
{ kind: "template", name: "app-ci-dotnet-next", description: "CI for .NET.", file: "app-ci-dotnet-next.yml", installed: false },
|
|
121
|
-
];
|
|
122
|
-
const output = formatCatalog(entries);
|
|
123
|
-
expect(output).toContain("Workflows:");
|
|
124
|
-
expect(output).toContain("Templates:");
|
|
125
|
-
expect(output).toContain("[x] refine");
|
|
126
|
-
expect(output).toContain("[ ] app-ci-dotnet-next");
|
|
127
|
-
});
|
|
128
|
-
it("prints a no-match message for an empty result", () => {
|
|
129
|
-
expect(formatCatalog([])).toBe("No workflows matched the search query.");
|
|
130
|
-
});
|
|
131
|
-
it("prints only a Workflows section when no templates match", () => {
|
|
132
|
-
const entries = [
|
|
133
|
-
{ kind: "route", name: "refine", description: "Refines.", file: "agent-refine.md", installed: false },
|
|
134
|
-
];
|
|
135
|
-
const output = formatCatalog(entries);
|
|
136
|
-
expect(output).toContain("Workflows:");
|
|
137
|
-
expect(output).not.toContain("Templates:");
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
async function createRepository(files) {
|
|
141
|
-
const repositoryPath = await mkdtemp(join(tmpdir(), "workflows-"));
|
|
142
|
-
temporaryDirectories.push(repositoryPath);
|
|
143
|
-
await Promise.all(Object.entries(files).map(async ([relativePath, content]) => {
|
|
144
|
-
const path = join(repositoryPath, relativePath);
|
|
145
|
-
const { mkdir } = await import("node:fs/promises");
|
|
146
|
-
await mkdir(join(path, ".."), { recursive: true });
|
|
147
|
-
await writeFile(path, content, "utf8");
|
|
148
|
-
}));
|
|
149
|
-
return repositoryPath;
|
|
150
|
-
}
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { inspectRepository, parseVisibility, resolveVisibility } from "./repository-inspection.js";
|
|
3
|
-
import { installCatalog, installedRoutes, installTemplate, isTemplateName, removeRouteFiles } from "./catalog-installation.js";
|
|
4
|
-
import { formatCatalog, listCatalog, searchCatalog } from "./catalog-listing.js";
|
|
5
|
-
import { routeNames, templateNames } from "./workflow-catalog.js";
|
|
6
|
-
import { runInteractive } from "./tui.js";
|
|
7
|
-
import { resolve } from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
const HELP_TEXT = `Workflows CLI — install and manage Plain Concepts Platform agentic workflows.
|
|
10
|
-
|
|
11
|
-
Run with no arguments to launch the interactive TUI, the primary way to select and install
|
|
12
|
-
workflows and templates:
|
|
13
|
-
|
|
14
|
-
npx @plainconceptsplatform/workflows
|
|
15
|
-
|
|
16
|
-
Advanced (non-interactive) commands:
|
|
17
|
-
|
|
18
|
-
Usage: workflows <command> [options]
|
|
19
|
-
|
|
20
|
-
Commands:
|
|
21
|
-
(default) Launch the interactive TUI for selecting and installing items.
|
|
22
|
-
init Inspect the repository and report its stack and visibility.
|
|
23
|
-
add [routes] [--template <name>] [--force] Install route workers, a template, or mandatory files.
|
|
24
|
-
remove <routes> [--force] Uninstall route workers and regenerate the router without them.
|
|
25
|
-
update Alias for add.
|
|
26
|
-
status Print repository inspection as JSON.
|
|
27
|
-
list List all available workflows and templates with install status.
|
|
28
|
-
search <query> Filter workflows and templates by name or description.
|
|
29
|
-
|
|
30
|
-
Route names (positional arguments to add):
|
|
31
|
-
refine, implement, direct, triage, apply-review, merge-gate, audit, propose
|
|
32
|
-
|
|
33
|
-
add Mandatory files only (opencode.ci.json, compile script,
|
|
34
|
-
shared imports, actions, router, classifier, route matrix).
|
|
35
|
-
add implement refine direct Installs those route workers plus mandatory files.
|
|
36
|
-
add --template agentics-checks Installs the named template only (no mandatory files).
|
|
37
|
-
add refine --template agentics-checks Installs routes + mandatory + the named template.
|
|
38
|
-
add refine implement --force Forces re-install of routes plus mandatory, overwriting.
|
|
39
|
-
remove propose Uninstalls the propose worker and drops it from the router.
|
|
40
|
-
|
|
41
|
-
add and remove keep the router consistent with what is installed: add unions the requested
|
|
42
|
-
routes with the routes already present, and remove drops the requested routes from that set.
|
|
43
|
-
Both regenerate the router, classifier, and route matrix from the resulting set. Changing the
|
|
44
|
-
route set rewrites the package-owned router, so pass --force to overwrite it.
|
|
45
|
-
|
|
46
|
-
Options:
|
|
47
|
-
--visibility public|private Override repository visibility (init only).
|
|
48
|
-
--template <name> Install a standalone template alongside or instead of routes.
|
|
49
|
-
Templates: agentics-checks, agentics-maintenance,
|
|
50
|
-
app-ci-dotnet-next, app-ci-node-monorepo,
|
|
51
|
-
bug-report, feature-request, github-release,
|
|
52
|
-
opencode.ci.json.
|
|
53
|
-
--force Overwrite managed files that differ from the package source.
|
|
54
|
-
-h, --help Show this help text.
|
|
55
|
-
|
|
56
|
-
Installed workflows are marked [x] when the corresponding .github/workflows/agent-*.md
|
|
57
|
-
file exists relative to the current directory.`;
|
|
58
|
-
export async function run(arguments_, repositoryPath = process.cwd()) {
|
|
59
|
-
const [command, ...options] = arguments_;
|
|
60
|
-
if (command === "--help" || command === "-h") {
|
|
61
|
-
console.log(HELP_TEXT);
|
|
62
|
-
return 0;
|
|
63
|
-
}
|
|
64
|
-
if (command === undefined) {
|
|
65
|
-
const force = options.includes("--force");
|
|
66
|
-
return runInteractive(repositoryPath, { force });
|
|
67
|
-
}
|
|
68
|
-
if (command === "list") {
|
|
69
|
-
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
70
|
-
console.log(formatCatalog(entries));
|
|
71
|
-
return 0;
|
|
72
|
-
}
|
|
73
|
-
if (command === "search") {
|
|
74
|
-
if (options.length === 0 || options.length > 1)
|
|
75
|
-
return fail("search requires exactly one query argument.");
|
|
76
|
-
const allEntries = await listCatalog({ installedPath: repositoryPath });
|
|
77
|
-
const results = searchCatalog(allEntries, options[0]);
|
|
78
|
-
console.log(formatCatalog(results));
|
|
79
|
-
return 0;
|
|
80
|
-
}
|
|
81
|
-
if (command === "init") {
|
|
82
|
-
const visibility = readVisibilityOption(options);
|
|
83
|
-
if (visibility === "invalid")
|
|
84
|
-
return fail("--visibility must be public or private.");
|
|
85
|
-
const inspection = await inspectRepository(repositoryPath);
|
|
86
|
-
const resolvedVisibility = await resolveVisibility(repositoryPath, visibility);
|
|
87
|
-
console.log(JSON.stringify({ command, inspection, visibility: resolvedVisibility }, null, 2));
|
|
88
|
-
return 0;
|
|
89
|
-
}
|
|
90
|
-
if (command === "status") {
|
|
91
|
-
const inspection = await inspectRepository(repositoryPath);
|
|
92
|
-
console.log(JSON.stringify({ command, inspection }, null, 2));
|
|
93
|
-
return 0;
|
|
94
|
-
}
|
|
95
|
-
if (command === "add" || command === "update") {
|
|
96
|
-
const parsed = parseAddOptions(options);
|
|
97
|
-
if (parsed.kind === "invalid")
|
|
98
|
-
return fail(parsed.message);
|
|
99
|
-
const inspection = await inspectRepository(repositoryPath);
|
|
100
|
-
const routes = parsed.routes;
|
|
101
|
-
const template = parsed.template;
|
|
102
|
-
const force = parsed.force;
|
|
103
|
-
const allConflicts = [];
|
|
104
|
-
const allInstalled = [];
|
|
105
|
-
if (routes.length > 0) {
|
|
106
|
-
const selectedRoutes = unionRoutes(routes, await installedRoutes(repositoryPath));
|
|
107
|
-
const result = await installCatalog(repositoryPath, { force, selectedRoutes, inspection });
|
|
108
|
-
allConflicts.push(...result.conflicts);
|
|
109
|
-
allInstalled.push(...result.installed);
|
|
110
|
-
}
|
|
111
|
-
else if (template === undefined) {
|
|
112
|
-
const result = await installCatalog(repositoryPath, { force, selectedRoutes: [], inspection });
|
|
113
|
-
allConflicts.push(...result.conflicts);
|
|
114
|
-
allInstalled.push(...result.installed);
|
|
115
|
-
}
|
|
116
|
-
if (template !== undefined) {
|
|
117
|
-
const result = await installTemplate(repositoryPath, template, { force, inspection });
|
|
118
|
-
allConflicts.push(...result.conflicts);
|
|
119
|
-
allInstalled.push(...result.installed);
|
|
120
|
-
}
|
|
121
|
-
if (allConflicts.length > 0 && !force) {
|
|
122
|
-
console.error(`Catalog conflicts found. Re-run with --force to overwrite package-managed files:\n${allConflicts.join("\n")}`);
|
|
123
|
-
return 1;
|
|
124
|
-
}
|
|
125
|
-
console.log(JSON.stringify({ command, installed: allInstalled.sort(), conflicts: allConflicts }, null, 2));
|
|
126
|
-
return 0;
|
|
127
|
-
}
|
|
128
|
-
if (command === "remove") {
|
|
129
|
-
const parsed = parseAddOptions(options);
|
|
130
|
-
if (parsed.kind === "invalid")
|
|
131
|
-
return fail(parsed.message);
|
|
132
|
-
if (parsed.template !== undefined)
|
|
133
|
-
return fail("remove does not accept --template.");
|
|
134
|
-
if (parsed.routes.length === 0)
|
|
135
|
-
return fail("remove requires at least one route.");
|
|
136
|
-
const inspection = await inspectRepository(repositoryPath);
|
|
137
|
-
const installed = await installedRoutes(repositoryPath);
|
|
138
|
-
const desiredRoutes = installed.filter((route) => !parsed.routes.includes(route));
|
|
139
|
-
const result = await installCatalog(repositoryPath, { force: parsed.force, selectedRoutes: desiredRoutes, inspection });
|
|
140
|
-
if (result.conflicts.length > 0 && !parsed.force) {
|
|
141
|
-
console.error(`Catalog conflicts found. Re-run with --force to overwrite package-managed files:\n${result.conflicts.join("\n")}`);
|
|
142
|
-
return 1;
|
|
143
|
-
}
|
|
144
|
-
const removed = await removeRouteFiles(repositoryPath, parsed.routes);
|
|
145
|
-
console.log(JSON.stringify({ command, installed: [...result.installed].sort(), removed, conflicts: result.conflicts }, null, 2));
|
|
146
|
-
return 0;
|
|
147
|
-
}
|
|
148
|
-
return fail(`Unknown command: ${command}`);
|
|
149
|
-
}
|
|
150
|
-
function unionRoutes(requested, installed) {
|
|
151
|
-
const result = [...requested];
|
|
152
|
-
for (const route of installed) {
|
|
153
|
-
if (!result.includes(route))
|
|
154
|
-
result.push(route);
|
|
155
|
-
}
|
|
156
|
-
return result;
|
|
157
|
-
}
|
|
158
|
-
function readVisibilityOption(options) {
|
|
159
|
-
if (options.length === 0)
|
|
160
|
-
return undefined;
|
|
161
|
-
if (options.length !== 2 || options[0] !== "--visibility")
|
|
162
|
-
return "invalid";
|
|
163
|
-
return parseVisibility(options[1]) ?? "invalid";
|
|
164
|
-
}
|
|
165
|
-
const TEMPLATE_NAMES = templateNames.join("|");
|
|
166
|
-
function parseAddOptions(options) {
|
|
167
|
-
const routes = [];
|
|
168
|
-
let template;
|
|
169
|
-
let templateSeen = false;
|
|
170
|
-
let force = false;
|
|
171
|
-
let i = 0;
|
|
172
|
-
while (i < options.length) {
|
|
173
|
-
const token = options[i];
|
|
174
|
-
if (token === "--force") {
|
|
175
|
-
force = true;
|
|
176
|
-
i++;
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (token === "--template") {
|
|
180
|
-
if (templateSeen)
|
|
181
|
-
return invalid("--template can only be specified once.");
|
|
182
|
-
templateSeen = true;
|
|
183
|
-
if (i + 1 >= options.length)
|
|
184
|
-
return invalid("--template requires a name.");
|
|
185
|
-
const name = options[i + 1];
|
|
186
|
-
if (!isTemplateName(name))
|
|
187
|
-
return invalid(`--template must be one of: ${TEMPLATE_NAMES}.`);
|
|
188
|
-
template = name;
|
|
189
|
-
i += 2;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (token.startsWith("--")) {
|
|
193
|
-
return invalid(`Unknown option: ${token}`);
|
|
194
|
-
}
|
|
195
|
-
if (routeNames.includes(token)) {
|
|
196
|
-
const route = token;
|
|
197
|
-
if (routes.includes(route))
|
|
198
|
-
return invalid(`Duplicate route: ${route}.`);
|
|
199
|
-
routes.push(route);
|
|
200
|
-
i++;
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
return invalid(`Unknown route: ${token}. Valid routes: ${routeNames.join(", ")}.`);
|
|
204
|
-
}
|
|
205
|
-
return { kind: "ok", routes, template, force };
|
|
206
|
-
}
|
|
207
|
-
function invalid(message) {
|
|
208
|
-
return { kind: "invalid", message };
|
|
209
|
-
}
|
|
210
|
-
function fail(message) {
|
|
211
|
-
console.error(message);
|
|
212
|
-
return 1;
|
|
213
|
-
}
|
|
214
|
-
if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
215
|
-
void run(process.argv.slice(2)).then((exitCode) => {
|
|
216
|
-
process.exitCode = exitCode;
|
|
217
|
-
});
|
|
218
|
-
}
|
package/dist/index.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/index.test.js
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
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, vi } from "vitest";
|
|
5
|
-
import { run } from "./index.js";
|
|
6
|
-
import * as catalogInstallation from "./catalog-installation.js";
|
|
7
|
-
const temporaryDirectories = [];
|
|
8
|
-
afterEach(async () => {
|
|
9
|
-
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })));
|
|
10
|
-
vi.restoreAllMocks();
|
|
11
|
-
});
|
|
12
|
-
function mockInstallers() {
|
|
13
|
-
const installCatalog = vi.spyOn(catalogInstallation, "installCatalog").mockResolvedValue({ installed: [], conflicts: [] });
|
|
14
|
-
const installTemplate = vi.spyOn(catalogInstallation, "installTemplate").mockResolvedValue({ installed: [], conflicts: [] });
|
|
15
|
-
return { installCatalog, installTemplate };
|
|
16
|
-
}
|
|
17
|
-
describe("workflows CLI", () => {
|
|
18
|
-
it("prints template names in help", async () => {
|
|
19
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
20
|
-
await expect(run(["--help"])).resolves.toBe(0);
|
|
21
|
-
expect(log).toHaveBeenCalledWith(expect.stringContaining("agentics-checks"));
|
|
22
|
-
expect(log).toHaveBeenCalledWith(expect.stringContaining("app-ci-dotnet-next"));
|
|
23
|
-
expect(log).toHaveBeenCalledWith(expect.stringContaining("opencode.ci.json"));
|
|
24
|
-
log.mockRestore();
|
|
25
|
-
});
|
|
26
|
-
it("prints route names as positional arguments in help", async () => {
|
|
27
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
28
|
-
await expect(run(["--help"])).resolves.toBe(0);
|
|
29
|
-
const output = log.mock.calls[0][0];
|
|
30
|
-
expect(output).toContain("refine, implement, direct, triage, apply-review, merge-gate, audit, propose");
|
|
31
|
-
expect(output).toContain("add [routes]");
|
|
32
|
-
log.mockRestore();
|
|
33
|
-
});
|
|
34
|
-
it("rejects an unsupported template name", async () => {
|
|
35
|
-
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
36
|
-
await expect(run(["add", "--template", "unknown"])).resolves.toBe(1);
|
|
37
|
-
expect(error).toHaveBeenCalledWith("--template must be one of: agentics-checks|agentics-maintenance|app-ci-dotnet-next|app-ci-node-monorepo|bug-report|feature-request|github-release|opencode.ci.json|visual-evidence.");
|
|
38
|
-
error.mockRestore();
|
|
39
|
-
});
|
|
40
|
-
it("rejects an unknown route passed to add", async () => {
|
|
41
|
-
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
42
|
-
await expect(run(["add", "frobnicate"])).resolves.toBe(1);
|
|
43
|
-
expect(error).toHaveBeenCalledWith("Unknown route: frobnicate. Valid routes: refine, implement, direct, triage, apply-review, merge-gate, audit, propose.");
|
|
44
|
-
error.mockRestore();
|
|
45
|
-
});
|
|
46
|
-
it("rejects a duplicate route", async () => {
|
|
47
|
-
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
48
|
-
await expect(run(["add", "refine", "refine"])).resolves.toBe(1);
|
|
49
|
-
expect(error).toHaveBeenCalledWith("Duplicate route: refine.");
|
|
50
|
-
error.mockRestore();
|
|
51
|
-
});
|
|
52
|
-
it("lists all workflows and templates with install status [ ] when none installed", async () => {
|
|
53
|
-
const repositoryPath = await createRepository({});
|
|
54
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
55
|
-
await expect(run(["list"], repositoryPath)).resolves.toBe(0);
|
|
56
|
-
expect(log).toHaveBeenCalledTimes(1);
|
|
57
|
-
const output = log.mock.calls[0][0];
|
|
58
|
-
expect(output).toContain("Workflows:");
|
|
59
|
-
expect(output).toContain("Templates:");
|
|
60
|
-
expect(output).toContain("refine");
|
|
61
|
-
expect(output).toContain("implement");
|
|
62
|
-
expect(output).toContain("agentics-checks");
|
|
63
|
-
expect(output).toContain("app-ci-dotnet-next");
|
|
64
|
-
expect(output).toContain("github-release");
|
|
65
|
-
// None installed: all [ ]
|
|
66
|
-
const installedCount = (output.match(/\[x\]/g) ?? []).length;
|
|
67
|
-
expect(installedCount).toBe(0);
|
|
68
|
-
// 8 routes + 9 templates = 17 entries
|
|
69
|
-
const uninstalledCount = (output.match(/\[ \]/g) ?? []).length;
|
|
70
|
-
expect(uninstalledCount).toBe(17);
|
|
71
|
-
log.mockRestore();
|
|
72
|
-
});
|
|
73
|
-
it("marks installed workflows with [x]", async () => {
|
|
74
|
-
const repositoryPath = await createRepository({
|
|
75
|
-
".github/workflows/agent-refine.md": "# Refine",
|
|
76
|
-
".github/workflows/agentics-checks.yml": "name: Agentics checks",
|
|
77
|
-
});
|
|
78
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
79
|
-
await expect(run(["list"], repositoryPath)).resolves.toBe(0);
|
|
80
|
-
const output = log.mock.calls[0][0];
|
|
81
|
-
const installed = output.match(/(\[x\])/g) ?? [];
|
|
82
|
-
expect(installed.length).toBe(2);
|
|
83
|
-
// refine and agentics-checks should be [x]
|
|
84
|
-
const refineLines = output.split("\n").filter((line) => line.includes("refine") || line.includes("agentics-checks"));
|
|
85
|
-
expect(refineLines.filter((line) => line.includes("[x]"))).toHaveLength(2);
|
|
86
|
-
log.mockRestore();
|
|
87
|
-
});
|
|
88
|
-
it("search filters by name", async () => {
|
|
89
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
90
|
-
await expect(run(["search", "audit"])).resolves.toBe(0);
|
|
91
|
-
const output = log.mock.calls[0][0];
|
|
92
|
-
expect(output).toContain("audit");
|
|
93
|
-
expect(output).not.toContain("Templates:");
|
|
94
|
-
// Only the audit route should be returned — no other route name should appear.
|
|
95
|
-
const visibleRoutes = ["refine", "implement", "direct", "triage", "apply-review", "merge-gate", "propose"];
|
|
96
|
-
for (const route of visibleRoutes) {
|
|
97
|
-
expect(output).not.toContain(`${route} —`);
|
|
98
|
-
}
|
|
99
|
-
log.mockRestore();
|
|
100
|
-
});
|
|
101
|
-
it("search filters by description keyword", async () => {
|
|
102
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
103
|
-
await expect(run(["search", "CI"])).resolves.toBe(0);
|
|
104
|
-
const output = log.mock.calls[0][0];
|
|
105
|
-
expect(output).toContain("dotnet-next");
|
|
106
|
-
expect(output).toContain("node-monorepo");
|
|
107
|
-
expect(output).not.toContain("refine —");
|
|
108
|
-
log.mockRestore();
|
|
109
|
-
});
|
|
110
|
-
it("search with no matches prints a no-match message", async () => {
|
|
111
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
112
|
-
await expect(run(["search", "nonexistent"])).resolves.toBe(0);
|
|
113
|
-
const output = log.mock.calls[0][0];
|
|
114
|
-
expect(output).toBe("No workflows matched the search query.");
|
|
115
|
-
log.mockRestore();
|
|
116
|
-
});
|
|
117
|
-
it("search with no query fails", async () => {
|
|
118
|
-
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
119
|
-
await expect(run(["search"])).resolves.toBe(1);
|
|
120
|
-
expect(error).toHaveBeenCalledWith("search requires exactly one query argument.");
|
|
121
|
-
error.mockRestore();
|
|
122
|
-
});
|
|
123
|
-
it("search with too many arguments fails", async () => {
|
|
124
|
-
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
125
|
-
await expect(run(["search", "a", "b"])).resolves.toBe(1);
|
|
126
|
-
expect(error).toHaveBeenCalledWith("search requires exactly one query argument.");
|
|
127
|
-
error.mockRestore();
|
|
128
|
-
});
|
|
129
|
-
it("add with no routes and no template calls installCatalog with empty selectedRoutes", async () => {
|
|
130
|
-
const { installCatalog, installTemplate } = mockInstallers();
|
|
131
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
132
|
-
await expect(run(["add"])).resolves.toBe(0);
|
|
133
|
-
expect(installCatalog).toHaveBeenCalledOnce();
|
|
134
|
-
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ selectedRoutes: [] }));
|
|
135
|
-
expect(installTemplate).not.toHaveBeenCalled();
|
|
136
|
-
log.mockRestore();
|
|
137
|
-
});
|
|
138
|
-
it("add with explicit routes passes those routes to installCatalog", async () => {
|
|
139
|
-
const { installCatalog, installTemplate } = mockInstallers();
|
|
140
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
141
|
-
await expect(run(["add", "implement", "refine", "direct"])).resolves.toBe(0);
|
|
142
|
-
expect(installCatalog).toHaveBeenCalledOnce();
|
|
143
|
-
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ selectedRoutes: ["implement", "refine", "direct"] }));
|
|
144
|
-
expect(installTemplate).not.toHaveBeenCalled();
|
|
145
|
-
log.mockRestore();
|
|
146
|
-
});
|
|
147
|
-
it("add --force passes force flag to installCatalog", async () => {
|
|
148
|
-
const { installCatalog } = mockInstallers();
|
|
149
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
150
|
-
await expect(run(["add", "--force"])).resolves.toBe(0);
|
|
151
|
-
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ force: true, selectedRoutes: [] }));
|
|
152
|
-
log.mockRestore();
|
|
153
|
-
});
|
|
154
|
-
it("add with routes and --force passes both", async () => {
|
|
155
|
-
const { installCatalog } = mockInstallers();
|
|
156
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
157
|
-
await expect(run(["add", "refine", "implement", "--force"])).resolves.toBe(0);
|
|
158
|
-
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ force: true, selectedRoutes: ["refine", "implement"] }));
|
|
159
|
-
log.mockRestore();
|
|
160
|
-
});
|
|
161
|
-
it("add with routes and template calls both installCatalog and installTemplate", async () => {
|
|
162
|
-
const { installCatalog, installTemplate } = mockInstallers();
|
|
163
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
164
|
-
await expect(run(["add", "refine", "--template", "agentics-checks"])).resolves.toBe(0);
|
|
165
|
-
expect(installCatalog).toHaveBeenCalledOnce();
|
|
166
|
-
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ selectedRoutes: ["refine"] }));
|
|
167
|
-
expect(installTemplate).toHaveBeenCalledOnce();
|
|
168
|
-
expect(installTemplate).toHaveBeenCalledWith(expect.any(String), "agentics-checks", expect.any(Object));
|
|
169
|
-
log.mockRestore();
|
|
170
|
-
});
|
|
171
|
-
it("add with template only calls installTemplate but not installCatalog", async () => {
|
|
172
|
-
const { installCatalog, installTemplate } = mockInstallers();
|
|
173
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
174
|
-
await expect(run(["add", "--template", "agentics-checks"])).resolves.toBe(0);
|
|
175
|
-
expect(installCatalog).not.toHaveBeenCalled();
|
|
176
|
-
expect(installTemplate).toHaveBeenCalledOnce();
|
|
177
|
-
expect(installTemplate).toHaveBeenCalledWith(expect.any(String), "agentics-checks", expect.any(Object));
|
|
178
|
-
log.mockRestore();
|
|
179
|
-
});
|
|
180
|
-
it("add with routes and template and force passes all options", async () => {
|
|
181
|
-
const { installCatalog, installTemplate } = mockInstallers();
|
|
182
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
183
|
-
await expect(run(["add", "refine", "implement", "--template", "agentics-checks", "--force"])).resolves.toBe(0);
|
|
184
|
-
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ force: true, selectedRoutes: ["refine", "implement"] }));
|
|
185
|
-
expect(installTemplate).toHaveBeenCalledWith(expect.any(String), "agentics-checks", expect.objectContaining({ force: true }));
|
|
186
|
-
log.mockRestore();
|
|
187
|
-
});
|
|
188
|
-
it("add with conflict and no force exits 1", async () => {
|
|
189
|
-
const { installCatalog } = mockInstallers();
|
|
190
|
-
installCatalog.mockResolvedValue({ installed: [], conflicts: ["opencode.ci.json"] });
|
|
191
|
-
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
192
|
-
await expect(run(["add"])).resolves.toBe(1);
|
|
193
|
-
expect(error).toHaveBeenCalledWith(expect.stringContaining("Catalog conflicts found"));
|
|
194
|
-
error.mockRestore();
|
|
195
|
-
});
|
|
196
|
-
it("add with conflict and --force still succeeds", async () => {
|
|
197
|
-
const { installCatalog } = mockInstallers();
|
|
198
|
-
installCatalog.mockResolvedValue({ installed: ["opencode.ci.json"], conflicts: ["opencode.ci.json"] });
|
|
199
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
200
|
-
await expect(run(["add", "--force"])).resolves.toBe(0);
|
|
201
|
-
expect(log).toHaveBeenCalled();
|
|
202
|
-
log.mockRestore();
|
|
203
|
-
});
|
|
204
|
-
it("add with --template but missing name produces an error", async () => {
|
|
205
|
-
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
206
|
-
await expect(run(["add", "--template"])).resolves.toBe(1);
|
|
207
|
-
expect(error).toHaveBeenCalledWith("--template requires a name.");
|
|
208
|
-
error.mockRestore();
|
|
209
|
-
});
|
|
210
|
-
it("add with multiple --template flags produces an error", async () => {
|
|
211
|
-
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
212
|
-
await expect(run(["add", "--template", "agentics-checks", "--template", "agentics-maintenance"])).resolves.toBe(1);
|
|
213
|
-
expect(error).toHaveBeenCalledWith("--template can only be specified once.");
|
|
214
|
-
error.mockRestore();
|
|
215
|
-
});
|
|
216
|
-
it("add with unknown flag produces an error", async () => {
|
|
217
|
-
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
218
|
-
await expect(run(["add", "--unknown"])).resolves.toBe(1);
|
|
219
|
-
expect(error).toHaveBeenCalledWith("Unknown option: --unknown");
|
|
220
|
-
error.mockRestore();
|
|
221
|
-
});
|
|
222
|
-
it("add unions requested routes with already-installed routes", async () => {
|
|
223
|
-
const { installCatalog } = mockInstallers();
|
|
224
|
-
const repositoryPath = await createRepository({
|
|
225
|
-
".github/workflows/agent-refine.md": "# Refine",
|
|
226
|
-
".github/workflows/agent-implement.md": "# Implement",
|
|
227
|
-
});
|
|
228
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
229
|
-
await expect(run(["add", "direct"], repositoryPath)).resolves.toBe(0);
|
|
230
|
-
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ selectedRoutes: ["direct", "refine", "implement"] }));
|
|
231
|
-
log.mockRestore();
|
|
232
|
-
});
|
|
233
|
-
it("remove requires at least one route", async () => {
|
|
234
|
-
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
235
|
-
await expect(run(["remove"])).resolves.toBe(1);
|
|
236
|
-
expect(error).toHaveBeenCalledWith("remove requires at least one route.");
|
|
237
|
-
error.mockRestore();
|
|
238
|
-
});
|
|
239
|
-
it("remove rejects the --template flag", async () => {
|
|
240
|
-
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
241
|
-
await expect(run(["remove", "--template", "agentics-checks"])).resolves.toBe(1);
|
|
242
|
-
expect(error).toHaveBeenCalledWith("remove does not accept --template.");
|
|
243
|
-
error.mockRestore();
|
|
244
|
-
});
|
|
245
|
-
it("remove regenerates the router for the remaining routes and deletes the worker", async () => {
|
|
246
|
-
const { installCatalog } = mockInstallers();
|
|
247
|
-
const repositoryPath = await createRepository({
|
|
248
|
-
".github/workflows/agent-refine.md": "# Refine",
|
|
249
|
-
".github/workflows/agent-refine.lock.yml": "generated",
|
|
250
|
-
".github/workflows/agent-implement.md": "# Implement",
|
|
251
|
-
});
|
|
252
|
-
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
253
|
-
await expect(run(["remove", "refine", "--force"], repositoryPath)).resolves.toBe(0);
|
|
254
|
-
expect(installCatalog).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ force: true, selectedRoutes: ["implement"] }));
|
|
255
|
-
const { access } = await import("node:fs/promises");
|
|
256
|
-
const { constants } = await import("node:fs");
|
|
257
|
-
await expect(access(join(repositoryPath, ".github/workflows/agent-refine.md"), constants.F_OK)).rejects.toThrow();
|
|
258
|
-
await expect(access(join(repositoryPath, ".github/workflows/agent-refine.lock.yml"), constants.F_OK)).rejects.toThrow();
|
|
259
|
-
await expect(access(join(repositoryPath, ".github/workflows/agent-implement.md"), constants.F_OK)).resolves.toBeUndefined();
|
|
260
|
-
log.mockRestore();
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
async function createRepository(files) {
|
|
264
|
-
const repositoryPath = await mkdtemp(join(tmpdir(), "workflows-"));
|
|
265
|
-
temporaryDirectories.push(repositoryPath);
|
|
266
|
-
await Promise.all(Object.entries(files).map(async ([relativePath, content]) => {
|
|
267
|
-
const path = join(repositoryPath, relativePath);
|
|
268
|
-
const { mkdir } = await import("node:fs/promises");
|
|
269
|
-
await mkdir(join(path, ".."), { recursive: true });
|
|
270
|
-
await writeFile(path, content, "utf8");
|
|
271
|
-
}));
|
|
272
|
-
return repositoryPath;
|
|
273
|
-
}
|