@plainconceptsplatform/workflows 0.4.33 → 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 +4 -3
- package/loops/actions/classify-route/classify-route.sh +69 -29
- package/loops/actions/load-issue-context/action.yml +2 -0
- package/loops/actions/update-changelog/action.yml +113 -0
- package/loops/actions/validate-merge-gate-output/action.yml +40 -0
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +34 -0
- package/loops/actions/validate-refine-output/validate-refine-output.sh +16 -3
- package/loops/actions/validate-review-output/action.yml +35 -0
- package/loops/actions/validate-review-output/validate-review-output.sh +30 -0
- package/loops/actions/validate-triage-output/action.yml +36 -0
- package/loops/actions/validate-triage-output/validate-triage-output.sh +36 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +121 -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 -373
- package/loops/workflows/agent-audit.md +213 -197
- package/loops/workflows/agent-implement.md +538 -374
- package/loops/workflows/agent-merge-gate.md +730 -485
- package/loops/workflows/agent-refine.md +609 -379
- package/loops/workflows/agent-release.md +258 -0
- package/loops/workflows/agent-triage.md +447 -0
- package/loops/workflows/authorize-bot-work.yml +85 -81
- 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 -551
- package/package.json +42 -42
- package/dist/catalog-installation.d.ts +0 -30
- package/dist/catalog-installation.js +0 -352
- 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 -342
- 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 -57
- package/dist/workflow-catalog.test.d.ts +0 -1
- package/dist/workflow-catalog.test.js +0 -29
- package/loops/workflows/agent-direct.md +0 -363
- package/loops/workflows/agent-propose.md +0 -342
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { generateOpencodeCi, generateOpencodeConfig, generateStackDefaults, injectStackEnv, } from "./stack-defaults.js";
|
|
3
|
-
function makeInspection(overrides = {}) {
|
|
4
|
-
return {
|
|
5
|
-
repositoryPath: "/repo",
|
|
6
|
-
existingAgentWorkflows: [],
|
|
7
|
-
stackHints: {
|
|
8
|
-
packageJson: false,
|
|
9
|
-
pnpmLockfile: false,
|
|
10
|
-
solutionFiles: [],
|
|
11
|
-
openSpec: false,
|
|
12
|
-
...overrides,
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
describe("generateStackDefaults", () => {
|
|
17
|
-
it("returns .NET verify commands when .slnx is found", () => {
|
|
18
|
-
const defaults = generateStackDefaults(makeInspection({
|
|
19
|
-
solutionFiles: ["app.slnx"],
|
|
20
|
-
}));
|
|
21
|
-
expect(defaults.verifyCommands).toBe("dotnet restore && dotnet build -c Release --no-restore && dotnet test");
|
|
22
|
-
expect(defaults.hasDotnet).toBe(true);
|
|
23
|
-
expect(defaults.hasNodeOnly).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
it("returns pnpm verify when pnpm-lock.yaml is found without .slnx", () => {
|
|
26
|
-
const defaults = generateStackDefaults(makeInspection({
|
|
27
|
-
pnpmLockfile: true,
|
|
28
|
-
}));
|
|
29
|
-
expect(defaults.verifyCommands).toBe("pnpm verify");
|
|
30
|
-
expect(defaults.hasDotnet).toBe(false);
|
|
31
|
-
expect(defaults.hasNodeOnly).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
it("returns both .NET and Web verify commands for a full-stack repo", () => {
|
|
34
|
-
const defaults = generateStackDefaults(makeInspection({
|
|
35
|
-
solutionFiles: ["app.slnx"],
|
|
36
|
-
pnpmLockfile: true,
|
|
37
|
-
}));
|
|
38
|
-
expect(defaults.verifyCommands).toContain(".NET");
|
|
39
|
-
expect(defaults.verifyCommands).toContain("dotnet restore");
|
|
40
|
-
expect(defaults.verifyCommands).toContain("dotnet build -c Release");
|
|
41
|
-
expect(defaults.verifyCommands).toContain("dotnet test");
|
|
42
|
-
expect(defaults.verifyCommands).toContain("Web");
|
|
43
|
-
expect(defaults.verifyCommands).toContain("pnpm lint");
|
|
44
|
-
expect(defaults.verifyCommands).toContain("pnpm test");
|
|
45
|
-
expect(defaults.verifyCommands).toContain("pnpm build");
|
|
46
|
-
expect(defaults.hasDotnet).toBe(true);
|
|
47
|
-
expect(defaults.hasNodeOnly).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
it("returns pnpm verify when neither .slnx nor pnpm-lock.yaml is present", () => {
|
|
50
|
-
const defaults = generateStackDefaults(makeInspection());
|
|
51
|
-
expect(defaults.verifyCommands).toBe("pnpm verify");
|
|
52
|
-
expect(defaults.hasDotnet).toBe(false);
|
|
53
|
-
expect(defaults.hasNodeOnly).toBe(false);
|
|
54
|
-
});
|
|
55
|
-
it("includes a repo rules base string with architecture context", () => {
|
|
56
|
-
const dotnetDefaults = generateStackDefaults(makeInspection({
|
|
57
|
-
solutionFiles: ["app.slnx"],
|
|
58
|
-
}));
|
|
59
|
-
expect(dotnetDefaults.repoRulesBase).toContain("Clean Architecture");
|
|
60
|
-
const nodeDefaults = generateStackDefaults(makeInspection({
|
|
61
|
-
pnpmLockfile: true,
|
|
62
|
-
}));
|
|
63
|
-
expect(nodeDefaults.repoRulesBase).toContain("Node.js");
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
describe("injectStackEnv", () => {
|
|
67
|
-
it("injects VERIFY_COMMANDS into a worker that has an env block", () => {
|
|
68
|
-
const content = `---
|
|
69
|
-
env:
|
|
70
|
-
REPO_RULES: "some rules"
|
|
71
|
-
description: test
|
|
72
|
-
---`;
|
|
73
|
-
const defaults = generateStackDefaults(makeInspection({
|
|
74
|
-
solutionFiles: ["app.slnx"],
|
|
75
|
-
}));
|
|
76
|
-
const result = injectStackEnv(content, defaults);
|
|
77
|
-
expect(result).toContain('VERIFY_COMMANDS: "dotnet restore && dotnet build -c Release --no-restore && dotnet test"');
|
|
78
|
-
});
|
|
79
|
-
it("injects pnpm verification when no .slnx is present", () => {
|
|
80
|
-
const content = `---
|
|
81
|
-
env:
|
|
82
|
-
REPO_RULES: "some rules"
|
|
83
|
-
---`;
|
|
84
|
-
const defaults = generateStackDefaults(makeInspection({
|
|
85
|
-
pnpmLockfile: true,
|
|
86
|
-
}));
|
|
87
|
-
const result = injectStackEnv(content, defaults);
|
|
88
|
-
expect(result).toContain('VERIFY_COMMANDS: "pnpm verify"');
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
const OPENCODE_CI_MD = `---
|
|
92
|
-
env:
|
|
93
|
-
AGENTMEMORY_VERSION: "0.9.28"
|
|
94
|
-
CODEGRAPH_VERSION: "1.5.0"
|
|
95
|
-
description: Shared CI setup.
|
|
96
|
-
|
|
97
|
-
pre-agent-steps:
|
|
98
|
-
- name: Install RTK
|
|
99
|
-
run: |
|
|
100
|
-
rtk --version
|
|
101
|
-
rtk init -g --opencode --auto-patch
|
|
102
|
-
- name: Install opencode plugin dependencies
|
|
103
|
-
run: |
|
|
104
|
-
set -euo pipefail
|
|
105
|
-
if [ ! -f .opencode/package.json ]; then
|
|
106
|
-
echo "No .opencode/package.json, nothing to install"
|
|
107
|
-
exit 0
|
|
108
|
-
fi
|
|
109
|
-
- name: Install workspace dependencies
|
|
110
|
-
run: pnpm install --frozen-lockfile
|
|
111
|
-
- name: Merge the CI-only OpenCode provider into opencode.jsonc
|
|
112
|
-
run: |
|
|
113
|
-
jq -e . "$FRAGMENT" > /dev/null
|
|
114
|
-
---`;
|
|
115
|
-
describe("generateOpencodeCi", () => {
|
|
116
|
-
it("adds NuGet cache and restores detected solution when .slnx is found", () => {
|
|
117
|
-
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
118
|
-
solutionFiles: ["apps/api/Numa.slnx"],
|
|
119
|
-
}));
|
|
120
|
-
expect(result).toContain("Cache NuGet packages");
|
|
121
|
-
expect(result).toContain("Restore .NET dependencies");
|
|
122
|
-
expect(result).toContain("dotnet restore apps/api/Numa.slnx");
|
|
123
|
-
expect(result).toContain("\n - name: Restore .NET dependencies\n");
|
|
124
|
-
expect(result).not.toContain("\n - name: Restore .NET dependencies\n");
|
|
125
|
-
});
|
|
126
|
-
it("uses POSIX separators for .NET restore commands", () => {
|
|
127
|
-
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
128
|
-
solutionFiles: ["apps\\api\\Numa.slnx"],
|
|
129
|
-
}));
|
|
130
|
-
expect(result).toContain("dotnet restore apps/api/Numa.slnx");
|
|
131
|
-
expect(result).not.toContain("dotnet restore apps\\api\\Numa.slnx");
|
|
132
|
-
});
|
|
133
|
-
it("adds the pinned OpenSpec CLI install step when openspec/ directory exists", () => {
|
|
134
|
-
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
135
|
-
openSpec: true,
|
|
136
|
-
}));
|
|
137
|
-
expect(result).toContain("Install OpenSpec CLI");
|
|
138
|
-
expect(result).toContain("@fission-ai/openspec@1.8.0");
|
|
139
|
-
});
|
|
140
|
-
it("adds both NuGet and OpenSpec steps when both are detected", () => {
|
|
141
|
-
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
142
|
-
solutionFiles: ["app.slnx"],
|
|
143
|
-
openSpec: true,
|
|
144
|
-
}));
|
|
145
|
-
expect(result).toContain("Cache NuGet packages");
|
|
146
|
-
expect(result).toContain("Install OpenSpec CLI");
|
|
147
|
-
});
|
|
148
|
-
it("does not add NuGet steps when no .slnx is present", () => {
|
|
149
|
-
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
150
|
-
pnpmLockfile: true,
|
|
151
|
-
}));
|
|
152
|
-
expect(result).not.toContain("Cache NuGet packages");
|
|
153
|
-
expect(result).not.toContain("Restore .NET dependencies");
|
|
154
|
-
});
|
|
155
|
-
it("preserves the original merge step", () => {
|
|
156
|
-
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
157
|
-
solutionFiles: ["app.slnx"],
|
|
158
|
-
openSpec: true,
|
|
159
|
-
}));
|
|
160
|
-
expect(result).toContain("Merge the CI-only OpenCode provider");
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
const OPENCODE_CI_JSON = `{
|
|
164
|
-
"$schema": "https://opencode.ai/config.json",
|
|
165
|
-
"model": "plainconcepts/glm-5-2",
|
|
166
|
-
"plugin": [],
|
|
167
|
-
"default_agent": "ci-workflow-agent",
|
|
168
|
-
"agent": {
|
|
169
|
-
"ci-workflow-agent": {
|
|
170
|
-
"description": "Executes GitHub Agentic Workflow tasks in CI.",
|
|
171
|
-
"mode": "primary",
|
|
172
|
-
"prompt": "You execute the GitHub Agentic Workflow task in the user prompt."
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
"permission": {
|
|
176
|
-
"read": "allow"
|
|
177
|
-
},
|
|
178
|
-
"lsp": {
|
|
179
|
-
"csharp": {
|
|
180
|
-
"disabled": true
|
|
181
|
-
},
|
|
182
|
-
"fsharp": {
|
|
183
|
-
"disabled": true
|
|
184
|
-
},
|
|
185
|
-
"razor": {
|
|
186
|
-
"disabled": true
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
"provider": {
|
|
190
|
-
"plainconcepts": {
|
|
191
|
-
"api": "http://172.30.0.30:10000",
|
|
192
|
-
"options": {
|
|
193
|
-
"apiKey": "awf-openai-proxy"
|
|
194
|
-
},
|
|
195
|
-
"models": {
|
|
196
|
-
"glm-5-2": {
|
|
197
|
-
"name": "GLM 5.2"
|
|
198
|
-
},
|
|
199
|
-
"glm-5-1": {
|
|
200
|
-
"name": "GLM 5.1"
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
`;
|
|
207
|
-
describe("generateOpencodeConfig", () => {
|
|
208
|
-
it("keeps LSP section when .slnx is present", () => {
|
|
209
|
-
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
210
|
-
solutionFiles: ["app.slnx"],
|
|
211
|
-
}));
|
|
212
|
-
const parsed = JSON.parse(result);
|
|
213
|
-
expect(parsed.lsp).toBeDefined();
|
|
214
|
-
expect(parsed.lsp.csharp.disabled).toBe(true);
|
|
215
|
-
expect(parsed.lsp.fsharp.disabled).toBe(true);
|
|
216
|
-
expect(parsed.lsp.razor.disabled).toBe(true);
|
|
217
|
-
});
|
|
218
|
-
it("removes LSP section when no .slnx is present", () => {
|
|
219
|
-
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
220
|
-
pnpmLockfile: true,
|
|
221
|
-
}));
|
|
222
|
-
const parsed = JSON.parse(result);
|
|
223
|
-
expect(parsed.lsp).toBeUndefined();
|
|
224
|
-
});
|
|
225
|
-
it("adds .NET guardrails to agent prompt when .slnx is present and prompt lacks them", () => {
|
|
226
|
-
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
227
|
-
solutionFiles: ["app.slnx"],
|
|
228
|
-
}));
|
|
229
|
-
const parsed = JSON.parse(result);
|
|
230
|
-
expect(parsed.agent["ci-workflow-agent"].prompt).toContain(".NET guardrails");
|
|
231
|
-
expect(parsed.agent["ci-workflow-agent"].prompt).toContain("Clean Architecture");
|
|
232
|
-
});
|
|
233
|
-
it("adds Node/React rules to agent prompt when no .slnx", () => {
|
|
234
|
-
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
235
|
-
pnpmLockfile: true,
|
|
236
|
-
}));
|
|
237
|
-
const parsed = JSON.parse(result);
|
|
238
|
-
expect(parsed.agent["ci-workflow-agent"].prompt).toContain("Node/React rules");
|
|
239
|
-
expect(parsed.agent["ci-workflow-agent"].prompt).toContain("Feature-Sliced Design");
|
|
240
|
-
});
|
|
241
|
-
it("preserves both models in the provider", () => {
|
|
242
|
-
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
243
|
-
solutionFiles: ["app.slnx"],
|
|
244
|
-
}));
|
|
245
|
-
const parsed = JSON.parse(result);
|
|
246
|
-
expect(parsed.provider.plainconcepts.models["glm-5-2"]).toBeDefined();
|
|
247
|
-
expect(parsed.provider.plainconcepts.models["glm-5-1"]).toBeDefined();
|
|
248
|
-
});
|
|
249
|
-
it("preserves the plainconcepts provider and its API URL", () => {
|
|
250
|
-
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
251
|
-
solutionFiles: ["app.slnx"],
|
|
252
|
-
}));
|
|
253
|
-
const parsed = JSON.parse(result);
|
|
254
|
-
expect(parsed.provider.plainconcepts.api).toBe("http://172.30.0.30:10000");
|
|
255
|
-
expect(parsed.provider.plainconcepts.options.apiKey).toBe("awf-openai-proxy");
|
|
256
|
-
});
|
|
257
|
-
it("does not add .NET guardrails twice when already present", () => {
|
|
258
|
-
const withGuardrails = OPENCODE_CI_JSON.replace('"You execute the GitHub Agentic Workflow task in the user prompt."', '"You execute the GitHub Agentic Workflow task in the user prompt.\\n\\n# .NET guardrails\\nFollow Clean Architecture."');
|
|
259
|
-
const result = generateOpencodeConfig(withGuardrails, makeInspection({
|
|
260
|
-
solutionFiles: ["app.slnx"],
|
|
261
|
-
}));
|
|
262
|
-
const parsed = JSON.parse(result);
|
|
263
|
-
const guardrailsCount = (parsed.agent["ci-workflow-agent"].prompt.match(/\.NET guardrails/g) ?? []).length;
|
|
264
|
-
expect(guardrailsCount).toBe(1);
|
|
265
|
-
});
|
|
266
|
-
});
|
package/dist/tui.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { type CatalogEntry } from "./catalog-listing.js";
|
|
2
|
-
export type SelectionStatus = "selecting" | "submitting" | "cancelled";
|
|
3
|
-
export interface SelectionState {
|
|
4
|
-
readonly allItems: readonly CatalogEntry[];
|
|
5
|
-
readonly visibleItems: readonly CatalogEntry[];
|
|
6
|
-
readonly selected: ReadonlySet<string>;
|
|
7
|
-
readonly cursor: number;
|
|
8
|
-
readonly filter: string;
|
|
9
|
-
readonly status: SelectionStatus;
|
|
10
|
-
}
|
|
11
|
-
export interface InteractiveOptions {
|
|
12
|
-
readonly force?: boolean;
|
|
13
|
-
}
|
|
14
|
-
export declare function createSelectionState(entries: readonly CatalogEntry[]): SelectionState;
|
|
15
|
-
export declare function fuzzyMatch(text: string, query: string): boolean;
|
|
16
|
-
export declare function filterItems(items: readonly CatalogEntry[], filter: string): readonly CatalogEntry[];
|
|
17
|
-
export declare function applyFilter(state: SelectionState, filter: string): SelectionState;
|
|
18
|
-
export declare function clearFilter(state: SelectionState): SelectionState;
|
|
19
|
-
export declare function moveCursorUp(state: SelectionState): SelectionState;
|
|
20
|
-
export declare function moveCursorDown(state: SelectionState): SelectionState;
|
|
21
|
-
export declare function toggleSelection(state: SelectionState): SelectionState;
|
|
22
|
-
export declare function submitSelection(state: SelectionState): SelectionState;
|
|
23
|
-
export declare function cancelSelection(state: SelectionState): SelectionState;
|
|
24
|
-
export declare function getItemsToInstall(state: SelectionState, force?: boolean): readonly CatalogEntry[];
|
|
25
|
-
export declare function runInteractive(repositoryPath: string, options?: InteractiveOptions): Promise<number>;
|
package/dist/tui.js
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import * as readline from "node:readline";
|
|
2
|
-
import { formatCatalog, listCatalog } from "./catalog-listing.js";
|
|
3
|
-
import { installCatalog, installMandatoryFiles, installTemplate, isTemplateName, removeRouteFiles } from "./catalog-installation.js";
|
|
4
|
-
import { inspectRepository } from "./repository-inspection.js";
|
|
5
|
-
import { routeNames } from "./workflow-catalog.js";
|
|
6
|
-
const ANSI = {
|
|
7
|
-
clear: "\x1b[2J",
|
|
8
|
-
home: "\x1b[H",
|
|
9
|
-
reset: "\x1b[0m",
|
|
10
|
-
bold: "\x1b[1m",
|
|
11
|
-
dim: "\x1b[2m",
|
|
12
|
-
cyan: "\x1b[36m",
|
|
13
|
-
green: "\x1b[32m",
|
|
14
|
-
grey: "\x1b[90m",
|
|
15
|
-
white: "\x1b[37m",
|
|
16
|
-
hideCursor: "\x1b[?25l",
|
|
17
|
-
showCursor: "\x1b[?25h",
|
|
18
|
-
};
|
|
19
|
-
export function createSelectionState(entries) {
|
|
20
|
-
const selected = new Set(entries.filter((entry) => entry.installed).map((entry) => entry.name));
|
|
21
|
-
return {
|
|
22
|
-
allItems: entries,
|
|
23
|
-
visibleItems: entries,
|
|
24
|
-
selected,
|
|
25
|
-
cursor: 0,
|
|
26
|
-
filter: "",
|
|
27
|
-
status: "selecting",
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
export function fuzzyMatch(text, query) {
|
|
31
|
-
if (query === "")
|
|
32
|
-
return true;
|
|
33
|
-
const haystack = text.toLowerCase();
|
|
34
|
-
const needle = query.toLowerCase();
|
|
35
|
-
let needleIndex = 0;
|
|
36
|
-
for (let i = 0; i < haystack.length && needleIndex < needle.length; i++) {
|
|
37
|
-
if (haystack[i] === needle[needleIndex])
|
|
38
|
-
needleIndex++;
|
|
39
|
-
}
|
|
40
|
-
return needleIndex === needle.length;
|
|
41
|
-
}
|
|
42
|
-
export function filterItems(items, filter) {
|
|
43
|
-
if (filter === "")
|
|
44
|
-
return items;
|
|
45
|
-
return items.filter((entry) => fuzzyMatch(`${entry.name} ${entry.description}`, filter));
|
|
46
|
-
}
|
|
47
|
-
export function applyFilter(state, filter) {
|
|
48
|
-
const visibleItems = filterItems(state.allItems, filter);
|
|
49
|
-
const cursor = visibleItems.length === 0 ? 0 : Math.min(state.cursor, visibleItems.length - 1);
|
|
50
|
-
return { ...state, visibleItems, filter, cursor };
|
|
51
|
-
}
|
|
52
|
-
export function clearFilter(state) {
|
|
53
|
-
return applyFilter(state, "");
|
|
54
|
-
}
|
|
55
|
-
export function moveCursorUp(state) {
|
|
56
|
-
if (state.visibleItems.length === 0)
|
|
57
|
-
return state;
|
|
58
|
-
const cursor = state.cursor === 0 ? state.visibleItems.length - 1 : state.cursor - 1;
|
|
59
|
-
return { ...state, cursor };
|
|
60
|
-
}
|
|
61
|
-
export function moveCursorDown(state) {
|
|
62
|
-
if (state.visibleItems.length === 0)
|
|
63
|
-
return state;
|
|
64
|
-
const cursor = state.cursor === state.visibleItems.length - 1 ? 0 : state.cursor + 1;
|
|
65
|
-
return { ...state, cursor };
|
|
66
|
-
}
|
|
67
|
-
export function toggleSelection(state) {
|
|
68
|
-
if (state.visibleItems.length === 0)
|
|
69
|
-
return state;
|
|
70
|
-
const entry = state.visibleItems[state.cursor];
|
|
71
|
-
if (entry === undefined)
|
|
72
|
-
return state;
|
|
73
|
-
const selected = new Set(state.selected);
|
|
74
|
-
if (selected.has(entry.name)) {
|
|
75
|
-
selected.delete(entry.name);
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
selected.add(entry.name);
|
|
79
|
-
}
|
|
80
|
-
return { ...state, selected };
|
|
81
|
-
}
|
|
82
|
-
export function submitSelection(state) {
|
|
83
|
-
return { ...state, status: "submitting" };
|
|
84
|
-
}
|
|
85
|
-
export function cancelSelection(state) {
|
|
86
|
-
return { ...state, status: "cancelled" };
|
|
87
|
-
}
|
|
88
|
-
export function getItemsToInstall(state, force = false) {
|
|
89
|
-
return state.allItems.filter((entry) => state.selected.has(entry.name) && (force || !entry.installed));
|
|
90
|
-
}
|
|
91
|
-
function renderEntry(entry, highlighted, checked) {
|
|
92
|
-
const indicator = highlighted ? `${ANSI.bold}${ANSI.white}>${ANSI.reset}` : " ";
|
|
93
|
-
if (checked) {
|
|
94
|
-
const checkbox = `${ANSI.green}[x]${ANSI.reset}`;
|
|
95
|
-
const name = highlighted ? `${ANSI.white}${ANSI.bold}${entry.name}${ANSI.reset}` : entry.name;
|
|
96
|
-
const desc = `${ANSI.dim} — ${entry.description}${ANSI.reset}`;
|
|
97
|
-
return `${indicator} ${checkbox} ${name}${desc}`;
|
|
98
|
-
}
|
|
99
|
-
const checkbox = `${ANSI.grey}[ ]${ANSI.reset}`;
|
|
100
|
-
const name = highlighted ? `${ANSI.white}${ANSI.bold}${entry.name}${ANSI.reset}` : entry.name;
|
|
101
|
-
const desc = `${ANSI.dim} — ${entry.description}${ANSI.reset}`;
|
|
102
|
-
return `${indicator} ${checkbox} ${name}${desc}`;
|
|
103
|
-
}
|
|
104
|
-
function render(state) {
|
|
105
|
-
const lines = [];
|
|
106
|
-
lines.push(`${ANSI.cyan}${ANSI.bold}Plain Concepts Platform — Agentic Workflows${ANSI.reset}`);
|
|
107
|
-
lines.push(`${ANSI.dim}Select items to install. Already-installed items are pre-checked.${ANSI.reset}`);
|
|
108
|
-
lines.push("");
|
|
109
|
-
if (state.filter) {
|
|
110
|
-
lines.push(`${ANSI.cyan}Filter:${ANSI.reset} ${state.filter}${ANSI.grey}█${ANSI.reset}`);
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
lines.push(`${ANSI.dim}Type to filter, arrows to navigate, space to toggle, Enter to install${ANSI.reset}`);
|
|
114
|
-
}
|
|
115
|
-
lines.push("");
|
|
116
|
-
const routes = state.visibleItems.filter((entry) => entry.kind === "route");
|
|
117
|
-
const templates = state.visibleItems.filter((entry) => entry.kind === "template");
|
|
118
|
-
let index = 0;
|
|
119
|
-
if (routes.length > 0) {
|
|
120
|
-
lines.push(`${ANSI.cyan}Workflows${ANSI.reset}`);
|
|
121
|
-
for (const entry of routes) {
|
|
122
|
-
lines.push(renderEntry(entry, index === state.cursor, state.selected.has(entry.name)));
|
|
123
|
-
index++;
|
|
124
|
-
}
|
|
125
|
-
if (templates.length > 0)
|
|
126
|
-
lines.push("");
|
|
127
|
-
}
|
|
128
|
-
if (templates.length > 0) {
|
|
129
|
-
lines.push(`${ANSI.cyan}Templates${ANSI.reset}`);
|
|
130
|
-
for (const entry of templates) {
|
|
131
|
-
lines.push(renderEntry(entry, index === state.cursor, state.selected.has(entry.name)));
|
|
132
|
-
index++;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (state.visibleItems.length === 0) {
|
|
136
|
-
lines.push(`${ANSI.grey} No items match the filter.${ANSI.reset}`);
|
|
137
|
-
}
|
|
138
|
-
lines.push("");
|
|
139
|
-
lines.push(`${ANSI.grey}↑↓ navigate space toggle type to filter Enter install Esc clear filter q quit${ANSI.reset}`);
|
|
140
|
-
process.stdout.write(`${ANSI.clear}${ANSI.home}${lines.join("\n")}\n`);
|
|
141
|
-
}
|
|
142
|
-
export async function runInteractive(repositoryPath, options = {}) {
|
|
143
|
-
const force = options.force ?? false;
|
|
144
|
-
if (process.stdin.isTTY !== true) {
|
|
145
|
-
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
146
|
-
console.log(formatCatalog(entries));
|
|
147
|
-
console.log("");
|
|
148
|
-
console.log("Run with a TTY for an interactive selection screen, or use: workflows add [--template <name>] [--force]");
|
|
149
|
-
return 0;
|
|
150
|
-
}
|
|
151
|
-
const entries = await listCatalog({ installedPath: repositoryPath });
|
|
152
|
-
let state = createSelectionState(entries);
|
|
153
|
-
readline.emitKeypressEvents(process.stdin);
|
|
154
|
-
process.stdin.setRawMode(true);
|
|
155
|
-
process.stdin.resume();
|
|
156
|
-
process.stdout.write(ANSI.hideCursor);
|
|
157
|
-
render(state);
|
|
158
|
-
return new Promise((resolve) => {
|
|
159
|
-
const onKeypress = (str, key) => {
|
|
160
|
-
if (key?.ctrl && key?.name === "c") {
|
|
161
|
-
cleanup();
|
|
162
|
-
resolve(0);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
if (str === "q" && state.filter === "" && !key?.ctrl && !key?.meta && !key?.shift) {
|
|
166
|
-
cleanup();
|
|
167
|
-
resolve(0);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
if (key?.name === "escape") {
|
|
171
|
-
state = clearFilter(state);
|
|
172
|
-
render(state);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (key?.name === "up") {
|
|
176
|
-
state = moveCursorUp(state);
|
|
177
|
-
render(state);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (key?.name === "down") {
|
|
181
|
-
state = moveCursorDown(state);
|
|
182
|
-
render(state);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (key?.name === "space" || str === " ") {
|
|
186
|
-
state = toggleSelection(state);
|
|
187
|
-
render(state);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
if (key?.name === "return" || key?.name === "enter") {
|
|
191
|
-
cleanup();
|
|
192
|
-
void installSelected(state, repositoryPath, force).then((exitCode) => resolve(exitCode));
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (key?.name === "backspace") {
|
|
196
|
-
state = applyFilter(state, state.filter.slice(0, -1));
|
|
197
|
-
render(state);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (str !== undefined &&
|
|
201
|
-
str.length === 1 &&
|
|
202
|
-
str >= " " &&
|
|
203
|
-
str <= "~" &&
|
|
204
|
-
!key?.ctrl &&
|
|
205
|
-
!key?.meta) {
|
|
206
|
-
state = applyFilter(state, state.filter + str);
|
|
207
|
-
render(state);
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
function cleanup() {
|
|
211
|
-
process.stdin.setRawMode(false);
|
|
212
|
-
process.stdin.pause();
|
|
213
|
-
process.stdout.write(`${ANSI.showCursor}${ANSI.reset}`);
|
|
214
|
-
process.stdin.removeListener("keypress", onKeypress);
|
|
215
|
-
}
|
|
216
|
-
process.stdin.on("keypress", onKeypress);
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
async function installSelected(state, repositoryPath, force) {
|
|
220
|
-
const items = getItemsToInstall(state, force);
|
|
221
|
-
const newRoutes = items.filter((entry) => entry.kind === "route");
|
|
222
|
-
const templates = items.filter((entry) => entry.kind === "template");
|
|
223
|
-
const routeEntries = state.allItems.filter((entry) => entry.kind === "route");
|
|
224
|
-
const checkedRoutes = routeEntries
|
|
225
|
-
.filter((entry) => state.selected.has(entry.name))
|
|
226
|
-
.map((entry) => entry.name)
|
|
227
|
-
.filter((name) => routeNames.includes(name));
|
|
228
|
-
const installedRouteNames = routeEntries
|
|
229
|
-
.filter((entry) => entry.installed)
|
|
230
|
-
.map((entry) => entry.name)
|
|
231
|
-
.filter((name) => routeNames.includes(name));
|
|
232
|
-
const removedRoutes = installedRouteNames.filter((name) => !checkedRoutes.includes(name));
|
|
233
|
-
const allConflicts = [];
|
|
234
|
-
const allInstalled = [];
|
|
235
|
-
const allRemoved = [];
|
|
236
|
-
const inspection = await inspectRepository(repositoryPath);
|
|
237
|
-
if (checkedRoutes.length > 0 && (newRoutes.length > 0 || removedRoutes.length > 0 || force)) {
|
|
238
|
-
const result = await installCatalog(repositoryPath, { force, selectedRoutes: checkedRoutes, inspection });
|
|
239
|
-
allConflicts.push(...result.conflicts);
|
|
240
|
-
allInstalled.push(...result.installed);
|
|
241
|
-
if (result.conflicts.length === 0 || force) {
|
|
242
|
-
allRemoved.push(...await removeRouteFiles(repositoryPath, removedRoutes));
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
else if (checkedRoutes.length === 0 && removedRoutes.length > 0) {
|
|
246
|
-
const result = await installCatalog(repositoryPath, { force, selectedRoutes: [], inspection });
|
|
247
|
-
allConflicts.push(...result.conflicts);
|
|
248
|
-
allInstalled.push(...result.installed);
|
|
249
|
-
if (result.conflicts.length === 0 || force) {
|
|
250
|
-
allRemoved.push(...await removeRouteFiles(repositoryPath, removedRoutes));
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
const result = await installMandatoryFiles(repositoryPath, { force });
|
|
255
|
-
allConflicts.push(...result.conflicts);
|
|
256
|
-
allInstalled.push(...result.installed);
|
|
257
|
-
}
|
|
258
|
-
for (const template of templates) {
|
|
259
|
-
if (!isTemplateName(template.name))
|
|
260
|
-
continue;
|
|
261
|
-
const result = await installTemplate(repositoryPath, template.name, { force, inspection });
|
|
262
|
-
allConflicts.push(...result.conflicts);
|
|
263
|
-
allInstalled.push(...result.installed);
|
|
264
|
-
}
|
|
265
|
-
if (allConflicts.length > 0 && !force) {
|
|
266
|
-
console.error(`Conflicts found. Re-run with --force to overwrite:\n${allConflicts.join("\n")}`);
|
|
267
|
-
return 1;
|
|
268
|
-
}
|
|
269
|
-
if (allInstalled.length > 0 || allRemoved.length > 0) {
|
|
270
|
-
if (allInstalled.length > 0) {
|
|
271
|
-
console.log(`Installed ${allInstalled.length} item(s):`);
|
|
272
|
-
for (const file of allInstalled) {
|
|
273
|
-
console.log(` ${file}`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (allRemoved.length > 0) {
|
|
277
|
-
console.log(`Removed ${allRemoved.length} item(s):`);
|
|
278
|
-
for (const file of allRemoved) {
|
|
279
|
-
console.log(` ${file}`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
console.log("All selected items are already installed.");
|
|
285
|
-
}
|
|
286
|
-
return 0;
|
|
287
|
-
}
|
package/dist/tui.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|