@brainervirus/workit-core 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +180 -0
- package/commands/wk-changelog.md +2 -0
- package/commands/wk-commit.md +2 -0
- package/commands/wk-docs-refresh.md +2 -0
- package/commands/wk-handoff.md +2 -0
- package/commands/wk-implement.md +2 -0
- package/commands/wk-init.md +2 -0
- package/commands/wk-issue-update.md +2 -0
- package/commands/wk-meetings.md +2 -0
- package/commands/wk-pr.md +2 -0
- package/commands/wk-release-notes.md +2 -0
- package/commands/wk-status.md +2 -0
- package/commands/wk-verify.md +2 -0
- package/package.json +43 -0
- package/scripts/_shared/common.sh +158 -0
- package/scripts/changelog-context.sh +42 -0
- package/scripts/docs-refresh-context.sh +40 -0
- package/scripts/fixtures/sample-plan.md +15 -0
- package/scripts/fixtures/sample-sdd/progress.md +2 -0
- package/scripts/init/apply.sh +5 -0
- package/scripts/init/status.sh +5 -0
- package/scripts/init/toolkit-status.sh +5 -0
- package/scripts/install-cursor-plugin.sh +83 -0
- package/scripts/install-opencode-plugin.sh +76 -0
- package/scripts/pr-create.sh +5 -0
- package/scripts/pr-ready-context.sh +88 -0
- package/scripts/present/ascii-wireframe.sh +5 -0
- package/scripts/present/flow-diagram.sh +5 -0
- package/scripts/release-notes-context.sh +40 -0
- package/scripts/rewrite-workspace-deps.ts +17 -0
- package/scripts/run-cursor-mcp.sh +10 -0
- package/scripts/sync-runtime.sh +96 -0
- package/scripts/update-superpowers.sh +56 -0
- package/scripts/vcs/config.sh +5 -0
- package/scripts/vcs/merged-style.sh +5 -0
- package/scripts/vcs/token-create-urls.sh +5 -0
- package/scripts/vcs/verify-token.sh +5 -0
- package/scripts/verify-project.sh +140 -0
- package/scripts/youtrack/api.sh +5 -0
- package/scripts/youtrack/config.sh +5 -0
- package/scripts/youtrack/greeting.sh +5 -0
- package/scripts/youtrack/parse-duration.sh +5 -0
- package/scripts/youtrack/token-create-url.sh +5 -0
- package/scripts/youtrack/verify-token.sh +5 -0
- package/scripts/youtrack/work-date-ms.sh +5 -0
- package/skills/wk-changelog/SKILL.md +15 -0
- package/skills/wk-commit/SKILL.md +16 -0
- package/skills/wk-docs-refresh/SKILL.md +15 -0
- package/skills/wk-handoff/SKILL.md +17 -0
- package/skills/wk-implement/SKILL.md +41 -0
- package/skills/wk-init/SKILL.md +31 -0
- package/skills/wk-issue-update/SKILL.md +27 -0
- package/skills/wk-issue-update/references/youtrack-update-style.md +81 -0
- package/skills/wk-meetings/SKILL.md +17 -0
- package/skills/wk-pr/SKILL.md +27 -0
- package/skills/wk-release-notes/SKILL.md +15 -0
- package/skills/wk-status/SKILL.md +16 -0
- package/skills/wk-verify/SKILL.md +16 -0
- package/src/core/branch.ts +246 -0
- package/src/core/changelog.ts +312 -0
- package/src/core/config-guard.ts +26 -0
- package/src/core/config.ts +73 -0
- package/src/core/detector.ts +207 -0
- package/src/core/doc-render.ts +14 -0
- package/src/core/docs-repo.ts +196 -0
- package/src/core/docs-validate.ts +255 -0
- package/src/core/flow-state.ts +225 -0
- package/src/core/git.ts +56 -0
- package/src/core/gitignore.ts +43 -0
- package/src/core/handoff-context.ts +115 -0
- package/src/core/hygiene.ts +77 -0
- package/src/core/init.ts +443 -0
- package/src/core/parse-sections.ts +24 -0
- package/src/core/plan-tasks.ts +33 -0
- package/src/core/ports/init-apply.ts +15 -0
- package/src/core/ports/init-status.ts +4 -0
- package/src/core/ports/init-toolkit-status.ts +4 -0
- package/src/core/ports/pr-create.ts +22 -0
- package/src/core/ports/present-ascii.ts +10 -0
- package/src/core/ports/present-flow.ts +10 -0
- package/src/core/ports/vcs-config.ts +14 -0
- package/src/core/ports/vcs-merged-style.ts +5 -0
- package/src/core/ports/vcs-token-create-urls.ts +4 -0
- package/src/core/ports/vcs-verify-token.ts +4 -0
- package/src/core/ports/youtrack-api.ts +17 -0
- package/src/core/ports/youtrack-config.ts +23 -0
- package/src/core/ports/youtrack-greeting.ts +10 -0
- package/src/core/ports/youtrack-parse-duration.ts +14 -0
- package/src/core/ports/youtrack-token-create-url.ts +4 -0
- package/src/core/ports/youtrack-verify-token.ts +12 -0
- package/src/core/ports/youtrack-work-date-ms.ts +10 -0
- package/src/core/pr-create.ts +212 -0
- package/src/core/present.ts +100 -0
- package/src/core/reminder.ts +82 -0
- package/src/core/repo-tool.ts +9 -0
- package/src/core/rules.ts +122 -0
- package/src/core/scripts.ts +42 -0
- package/src/core/sdd.ts +188 -0
- package/src/core/templates.ts +37 -0
- package/src/core/vcs-config.ts +252 -0
- package/src/core/verify-parse.ts +27 -0
- package/src/core/workspaces.ts +81 -0
- package/src/core/youtrack.ts +488 -0
- package/src/core.ts +62 -0
- package/src/state.ts +22 -0
- package/src/tools/docs-repo.ts +42 -0
- package/src/tools/flow.ts +88 -0
- package/src/tools/handoff.ts +160 -0
- package/src/tools/index.ts +22 -0
- package/src/tools/present.ts +45 -0
- package/src/tools/repo.ts +357 -0
- package/src/tools/rules.ts +30 -0
- package/src/tools/sdd.ts +189 -0
- package/src/tools/templates.ts +27 -0
- package/src/tools/youtrack.ts +356 -0
- package/templates/execution-contract.md +56 -0
- package/templates/greeting.md +1 -0
- package/templates/headers.md +3 -0
- package/templates/hygiene/.editorconfig +8 -0
- package/templates/hygiene/.gitattributes +3 -0
- package/templates/hygiene/CHANGELOG.md +14 -0
- package/templates/hygiene/CONTRIBUTING.md +3 -0
- package/templates/hygiene/LICENSE +21 -0
- package/templates/hygiene/README.md +3 -0
- package/templates/issue-update.md +6 -0
- package/templates/plan-template.md +25 -0
- package/templates/spec-template.md +51 -0
- package/templates/superpowers-doc-contract.md +69 -0
- package/vendor/superpowers/skills/brainstorming/SKILL.md +159 -0
- package/vendor/superpowers/skills/brainstorming/scripts/frame-template.html +213 -0
- package/vendor/superpowers/skills/brainstorming/scripts/helper.js +167 -0
- package/vendor/superpowers/skills/brainstorming/scripts/server.cjs +723 -0
- package/vendor/superpowers/skills/brainstorming/scripts/start-server.sh +209 -0
- package/vendor/superpowers/skills/brainstorming/scripts/stop-server.sh +120 -0
- package/vendor/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/vendor/superpowers/skills/brainstorming/visual-companion.md +291 -0
- package/vendor/superpowers/skills/dispatching-parallel-agents/SKILL.md +185 -0
- package/vendor/superpowers/skills/executing-plans/SKILL.md +70 -0
- package/vendor/superpowers/skills/finishing-a-development-branch/SKILL.md +241 -0
- package/vendor/superpowers/skills/receiving-code-review/SKILL.md +213 -0
- package/vendor/superpowers/skills/requesting-code-review/SKILL.md +103 -0
- package/vendor/superpowers/skills/requesting-code-review/code-reviewer.md +172 -0
- package/vendor/superpowers/skills/subagent-driven-development/SKILL.md +418 -0
- package/vendor/superpowers/skills/subagent-driven-development/implementer-prompt.md +139 -0
- package/vendor/superpowers/skills/subagent-driven-development/scripts/review-package +44 -0
- package/vendor/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +22 -0
- package/vendor/superpowers/skills/subagent-driven-development/scripts/task-brief +40 -0
- package/vendor/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +188 -0
- package/vendor/superpowers/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/vendor/superpowers/skills/systematic-debugging/SKILL.md +296 -0
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/vendor/superpowers/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/vendor/superpowers/skills/systematic-debugging/find-polluter.sh +63 -0
- package/vendor/superpowers/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/vendor/superpowers/skills/systematic-debugging/test-academic.md +14 -0
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/vendor/superpowers/skills/test-driven-development/SKILL.md +371 -0
- package/vendor/superpowers/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/vendor/superpowers/skills/using-git-worktrees/SKILL.md +202 -0
- package/vendor/superpowers/skills/using-superpowers/SKILL.md +62 -0
- package/vendor/superpowers/skills/using-superpowers/references/antigravity-tools.md +23 -0
- package/vendor/superpowers/skills/using-superpowers/references/codex-tools.md +39 -0
- package/vendor/superpowers/skills/using-superpowers/references/pi-tools.md +16 -0
- package/vendor/superpowers/skills/verification-before-completion/SKILL.md +139 -0
- package/vendor/superpowers/skills/writing-plans/SKILL.md +174 -0
- package/vendor/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/vendor/superpowers/skills/writing-skills/SKILL.md +689 -0
- package/vendor/superpowers/skills/writing-skills/anthropic-best-practices.md +1150 -0
- package/vendor/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/vendor/superpowers/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/vendor/superpowers/skills/writing-skills/persuasion-principles.md +187 -0
- package/vendor/superpowers/skills/writing-skills/render-graphs.js +168 -0
- package/vendor/superpowers/skills/writing-skills/testing-skills-with-subagents.md +384 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { fail, ok } from "../core";
|
|
3
|
+
import {
|
|
4
|
+
readFlowState,
|
|
5
|
+
transitionSpec,
|
|
6
|
+
transitionPlan,
|
|
7
|
+
recordMenuChoice,
|
|
8
|
+
slugFromPath,
|
|
9
|
+
} from "../core/flow-state";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
const output = (value: unknown) => JSON.stringify(value, null, 2);
|
|
13
|
+
|
|
14
|
+
const flowPathFor = (slug: string) =>
|
|
15
|
+
path.posix.join("docs", slug, "sdd", "flow.json");
|
|
16
|
+
|
|
17
|
+
export function createFlowTools() {
|
|
18
|
+
return {
|
|
19
|
+
workflow_flow_status: tool({
|
|
20
|
+
description: "Read the spec/plan approval flow state for a workflow",
|
|
21
|
+
args: {
|
|
22
|
+
plan_path: tool.schema.string().optional(),
|
|
23
|
+
spec_path: tool.schema.string().optional(),
|
|
24
|
+
},
|
|
25
|
+
execute: async ({ plan_path, spec_path }, context) => {
|
|
26
|
+
try {
|
|
27
|
+
const slug = slugFromPath(plan_path ?? spec_path ?? "");
|
|
28
|
+
if (!slug) return output(fail("plan_path or spec_path required"));
|
|
29
|
+
const state = readFlowState(context.directory, slug);
|
|
30
|
+
return output(ok({
|
|
31
|
+
slug,
|
|
32
|
+
spec: state.spec,
|
|
33
|
+
plan: state.plan,
|
|
34
|
+
menu: state.menu,
|
|
35
|
+
flow_path: flowPathFor(slug),
|
|
36
|
+
}));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return output(fail(error instanceof Error ? error.message : "flow status failed"));
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
workflow_spec_approve: tool({
|
|
43
|
+
description: "Advance spec status: first call self_reviewed, second call approved (after user approval)",
|
|
44
|
+
args: {
|
|
45
|
+
confirmed: tool.schema.boolean(),
|
|
46
|
+
spec_path: tool.schema.string(),
|
|
47
|
+
},
|
|
48
|
+
execute: async ({ confirmed, spec_path }, context) => {
|
|
49
|
+
const slug = slugFromPath(spec_path);
|
|
50
|
+
const result = transitionSpec(context.directory, slug, spec_path, confirmed);
|
|
51
|
+
return output(
|
|
52
|
+
result.ok
|
|
53
|
+
? ok({ spec: spec_path, status: readFlowState(context.directory, slug).spec.status })
|
|
54
|
+
: fail(result.error),
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
workflow_plan_approve: tool({
|
|
59
|
+
description: "Advance plan status: first call self_reviewed, second call approved. Requires approved spec.",
|
|
60
|
+
args: {
|
|
61
|
+
confirmed: tool.schema.boolean(),
|
|
62
|
+
plan_path: tool.schema.string(),
|
|
63
|
+
},
|
|
64
|
+
execute: async ({ confirmed, plan_path }, context) => {
|
|
65
|
+
const slug = slugFromPath(plan_path);
|
|
66
|
+
const result = transitionPlan(context.directory, slug, plan_path, confirmed);
|
|
67
|
+
return output(
|
|
68
|
+
result.ok
|
|
69
|
+
? ok({ plan: plan_path, status: readFlowState(context.directory, slug).plan.status })
|
|
70
|
+
: fail(result.error),
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
workflow_plan_menu: tool({
|
|
75
|
+
description: "Record the answered post-plan choice menu (called after native question)",
|
|
76
|
+
args: {
|
|
77
|
+
confirmed: tool.schema.boolean(),
|
|
78
|
+
plan_path: tool.schema.string(),
|
|
79
|
+
choice: tool.schema.enum(["subagent-driven", "inline", "handoff", "review-spec", "review-plan"]),
|
|
80
|
+
},
|
|
81
|
+
execute: async ({ confirmed, plan_path, choice }, context) => {
|
|
82
|
+
const slug = slugFromPath(plan_path);
|
|
83
|
+
const result = recordMenuChoice(context.directory, slug, plan_path, choice, confirmed);
|
|
84
|
+
return output(result.ok ? ok({ menu: { presented: true, chosen: choice } }) : fail(result.error));
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { tool, type PluginInput } from "@opencode-ai/plugin";
|
|
4
|
+
import { fail, ok, type Result } from "../core";
|
|
5
|
+
import { assertFlowGates } from "../core/flow-state";
|
|
6
|
+
import { resolveWorkflowPaths, buildHandoffContract } from "../core/handoff-context";
|
|
7
|
+
import { WorkflowStateStore } from "../state";
|
|
8
|
+
|
|
9
|
+
type ApiResponse<T> = { data?: T; error?: unknown };
|
|
10
|
+
type ApiResult<T> = Promise<ApiResponse<T>>;
|
|
11
|
+
type ApiVoidResult = Promise<ApiResponse<void> | void>;
|
|
12
|
+
type ApiUnknownResult = Promise<ApiResponse<unknown> | void>;
|
|
13
|
+
|
|
14
|
+
export type HandoffClient = {
|
|
15
|
+
session: {
|
|
16
|
+
create(input: {
|
|
17
|
+
body: { title: string };
|
|
18
|
+
query: { directory: string };
|
|
19
|
+
}): ApiResult<{ id: string }>;
|
|
20
|
+
promptAsync(input: {
|
|
21
|
+
path: { id: string };
|
|
22
|
+
query: { directory: string };
|
|
23
|
+
body: { parts: [{ type: "text"; text: string }] };
|
|
24
|
+
}): ApiVoidResult;
|
|
25
|
+
};
|
|
26
|
+
tui: {
|
|
27
|
+
selectSession(input: {
|
|
28
|
+
body: { sessionID: string };
|
|
29
|
+
query: { directory: string };
|
|
30
|
+
}): ApiUnknownResult;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const adaptPluginHandoffClient = (client: PluginInput["client"]): HandoffClient => ({
|
|
35
|
+
session: {
|
|
36
|
+
create: (input) => client.session.create(input),
|
|
37
|
+
promptAsync: (input) => client.session.promptAsync(input),
|
|
38
|
+
},
|
|
39
|
+
tui: {
|
|
40
|
+
selectSession: ({ body, query }) => client.tui.publish({
|
|
41
|
+
body: { type: "tui.session.select", properties: { sessionID: body.sessionID } } as never,
|
|
42
|
+
query,
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export type HandoffRequest = {
|
|
48
|
+
directory: string;
|
|
49
|
+
title: string;
|
|
50
|
+
prompt: string;
|
|
51
|
+
stay: boolean;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type HandoffData = {
|
|
55
|
+
sessionID?: string;
|
|
56
|
+
seeded?: boolean;
|
|
57
|
+
selected?: boolean;
|
|
58
|
+
stage?: "create" | "seed" | "select";
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const message = (error: unknown) => error instanceof Error
|
|
62
|
+
? error.message
|
|
63
|
+
: typeof error === "object" && error !== null && "message" in error
|
|
64
|
+
? String(error.message)
|
|
65
|
+
: String(error);
|
|
66
|
+
const apiError = (response: ApiResponse<unknown> | void) => response?.error;
|
|
67
|
+
|
|
68
|
+
export async function handoffSession(
|
|
69
|
+
client: HandoffClient,
|
|
70
|
+
request: HandoffRequest,
|
|
71
|
+
): Promise<Result<HandoffData>> {
|
|
72
|
+
let sessionID: string | undefined;
|
|
73
|
+
try {
|
|
74
|
+
const created = await client.session.create({
|
|
75
|
+
body: { title: request.title },
|
|
76
|
+
query: { directory: request.directory },
|
|
77
|
+
});
|
|
78
|
+
if (apiError(created)) throw apiError(created);
|
|
79
|
+
sessionID = created.data?.id;
|
|
80
|
+
if (!sessionID) return fail("session create returned no ID", { stage: "create" });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return fail(message(error), { stage: "create" });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const seeded = await client.session.promptAsync({
|
|
87
|
+
path: { id: sessionID },
|
|
88
|
+
query: { directory: request.directory },
|
|
89
|
+
body: { parts: [{ type: "text", text: request.prompt }] },
|
|
90
|
+
});
|
|
91
|
+
if (apiError(seeded)) throw apiError(seeded);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return fail(message(error), { sessionID, seeded: false, selected: false, stage: "seed" });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (request.stay) return ok({ sessionID, seeded: true, selected: false });
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const selected = await client.tui.selectSession({
|
|
100
|
+
body: { sessionID },
|
|
101
|
+
query: { directory: request.directory },
|
|
102
|
+
});
|
|
103
|
+
if (apiError(selected)) throw apiError(selected);
|
|
104
|
+
if (selected?.data !== true) throw new Error("session selection unavailable");
|
|
105
|
+
return ok({ sessionID, seeded: true, selected: true });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return fail(message(error), { sessionID, seeded: true, selected: false, stage: "select" });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const output = (value: unknown) => JSON.stringify(value, null, 2);
|
|
112
|
+
|
|
113
|
+
export type HandoffContextResult =
|
|
114
|
+
| { prompt: string; spec: string; plan: string; sdd: string }
|
|
115
|
+
| { error: string };
|
|
116
|
+
|
|
117
|
+
export const buildHandoffPrompt = (root: string, message: string): HandoffContextResult => {
|
|
118
|
+
const resolved = resolveWorkflowPaths(root, message);
|
|
119
|
+
if ("error" in resolved) return { error: resolved.error };
|
|
120
|
+
const templatePath = path.resolve(
|
|
121
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
122
|
+
"../../templates/execution-contract.md",
|
|
123
|
+
);
|
|
124
|
+
const contract = buildHandoffContract({ root, spec: resolved.spec, plan: resolved.plan, templatePath });
|
|
125
|
+
if ("error" in contract) return { error: contract.error };
|
|
126
|
+
const sdd = `docs/${path.basename(path.dirname(resolved.plan))}/sdd`;
|
|
127
|
+
return { prompt: contract.prompt, spec: resolved.spec, plan: resolved.plan, sdd };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
export function createHandoffTools(
|
|
133
|
+
client: HandoffClient,
|
|
134
|
+
state: WorkflowStateStore,
|
|
135
|
+
) {
|
|
136
|
+
return {
|
|
137
|
+
workflow_handoff_session: tool({
|
|
138
|
+
description: "Create, seed, and select a continuation session; --stay in the message skips selection",
|
|
139
|
+
args: { message: tool.schema.string() },
|
|
140
|
+
execute: async ({ message: userMessage }, context) => {
|
|
141
|
+
const built = buildHandoffPrompt(context.directory, userMessage);
|
|
142
|
+
if ("error" in built) return output(fail(built.error));
|
|
143
|
+
const active = built;
|
|
144
|
+
try {
|
|
145
|
+
const gate = assertFlowGates(context.directory, active.plan);
|
|
146
|
+
if (!gate.ok) return output(fail(gate.error));
|
|
147
|
+
state.set(context.sessionID, { spec: active.spec, plan: active.plan, sdd: active.sdd });
|
|
148
|
+
return output(await handoffSession(client, {
|
|
149
|
+
directory: context.directory,
|
|
150
|
+
title: `Continue ${path.basename(path.dirname(active.plan))}`,
|
|
151
|
+
prompt: active.prompt,
|
|
152
|
+
stay: /(?:^|\s)--stay(?:\s|$)/.test(userMessage),
|
|
153
|
+
}));
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return output(fail(message(error)));
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createRepoTools } from "./repo";
|
|
2
|
+
import { WorkflowStateStore } from "../state";
|
|
3
|
+
import { createSddTools } from "./sdd";
|
|
4
|
+
import { createHandoffTools, type HandoffClient } from "./handoff";
|
|
5
|
+
import { createYouTrackTools } from "./youtrack";
|
|
6
|
+
import { createPresentTools } from "./present";
|
|
7
|
+
import { createFlowTools } from "./flow";
|
|
8
|
+
import { createDocsRepoTools } from "./docs-repo";
|
|
9
|
+
import { createTemplateTools } from "./templates";
|
|
10
|
+
import { createRuleTools } from "./rules";
|
|
11
|
+
|
|
12
|
+
export const createTools = (client: HandoffClient, state: WorkflowStateStore) => ({
|
|
13
|
+
...createRepoTools(),
|
|
14
|
+
...createSddTools(state),
|
|
15
|
+
...createHandoffTools(client, state),
|
|
16
|
+
...createYouTrackTools(),
|
|
17
|
+
...createPresentTools(),
|
|
18
|
+
...createFlowTools(),
|
|
19
|
+
...createDocsRepoTools(),
|
|
20
|
+
...createTemplateTools(),
|
|
21
|
+
...createRuleTools(),
|
|
22
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { fail, ok } from "../core";
|
|
3
|
+
import { asciiWireframe, flowDiagram } from "../core/present";
|
|
4
|
+
|
|
5
|
+
const output = (value: unknown) => JSON.stringify(value, null, 2);
|
|
6
|
+
|
|
7
|
+
export function createPresentTools() {
|
|
8
|
+
return {
|
|
9
|
+
workflow_present_ascii: tool({
|
|
10
|
+
description: "Render deterministic ASCII UI wireframe from JSON spec",
|
|
11
|
+
args: {
|
|
12
|
+
title: tool.schema.string().optional(),
|
|
13
|
+
width: tool.schema.number().optional(),
|
|
14
|
+
rows: tool.schema.array(tool.schema.record(tool.schema.string(), tool.schema.any())),
|
|
15
|
+
},
|
|
16
|
+
execute: async (spec) => {
|
|
17
|
+
const result = asciiWireframe(spec);
|
|
18
|
+
if ("error" in result) return output(fail(result.error));
|
|
19
|
+
return output(ok(result.data));
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
workflow_present_flow: tool({
|
|
23
|
+
description: "Render mermaid flowchart from JSON nodes/edges",
|
|
24
|
+
args: {
|
|
25
|
+
title: tool.schema.string().optional(),
|
|
26
|
+
direction: tool.schema.enum(["TD", "LR", "BT", "RL"]).optional(),
|
|
27
|
+
nodes: tool.schema.array(tool.schema.object({
|
|
28
|
+
id: tool.schema.string(),
|
|
29
|
+
label: tool.schema.string(),
|
|
30
|
+
shape: tool.schema.string().optional(),
|
|
31
|
+
})),
|
|
32
|
+
edges: tool.schema.array(tool.schema.object({
|
|
33
|
+
from: tool.schema.string(),
|
|
34
|
+
to: tool.schema.string(),
|
|
35
|
+
label: tool.schema.string().optional(),
|
|
36
|
+
})),
|
|
37
|
+
},
|
|
38
|
+
execute: async (spec) => {
|
|
39
|
+
const result = flowDiagram(spec);
|
|
40
|
+
if ("error" in result) return output(fail(result.error));
|
|
41
|
+
return output(ok(result.data));
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { tool, type ToolContext } from "@opencode-ai/plugin";
|
|
5
|
+
import { fail, gitRevisionParts, ok, resolveInside, run } from "../core";
|
|
6
|
+
import { changelogApply } from "../core/changelog";
|
|
7
|
+
import { gitContext } from "../core/git";
|
|
8
|
+
import { parseKeyValueLines, parseSections } from "../core/parse-sections";
|
|
9
|
+
import { parseVerifyOutput } from "../core/verify-parse";
|
|
10
|
+
import { branchSetup } from "../core/branch";
|
|
11
|
+
import { configDir, readConfig, writeConfig, type BranchPreset, type ToolkitConfig } from "../core/config";
|
|
12
|
+
import { ensureProjectGitignore } from "../core/gitignore";
|
|
13
|
+
import { ensureHygieneFiles } from "../core/hygiene";
|
|
14
|
+
|
|
15
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
16
|
+
const scripts = path.join(packageRoot, "scripts");
|
|
17
|
+
type RunResult = ReturnType<typeof run>;
|
|
18
|
+
|
|
19
|
+
export type RepoRuntime = {
|
|
20
|
+
runScript(root: string, script: string, args: string[], env?: Record<string, string>): RunResult;
|
|
21
|
+
git(root: string, args: string[]): RunResult;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const defaultRuntime: RepoRuntime = {
|
|
25
|
+
runScript: (root, script, args, env) => run(root, "bash", [path.join(scripts, script), ...args], env),
|
|
26
|
+
git: (root, args) => run(root, "git", args),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const output = (value: unknown) => JSON.stringify(value, null, 2);
|
|
30
|
+
const diagnostics = ({ stdout, stderr, exitCode }: RunResult) => ({ stdout, stderr, exitCode });
|
|
31
|
+
const requireConfirmed = (confirmed: boolean) => confirmed === true ? null : output(fail("confirmed: true required"));
|
|
32
|
+
import { resolveBranchPolicy } from "../core/config";
|
|
33
|
+
const branchPolicy = () => resolveBranchPolicy(readConfig());
|
|
34
|
+
|
|
35
|
+
function scriptResult<T extends object>(result: RunResult, parse: (stdout: string) => T) {
|
|
36
|
+
if (result.exitCode !== 0) {
|
|
37
|
+
return fail(result.stderr.trim() || result.stdout.trim() || "workflow script failed", diagnostics(result));
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return ok({ ...parse(result.stdout), exitCode: 0, ...(result.stderr ? { stderr: result.stderr } : {}) });
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return fail(error instanceof Error ? error.message : "workflow output parse failed", diagnostics(result));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const json = (stdout: string) => JSON.parse(stdout.trim()) as Record<string, unknown>;
|
|
47
|
+
const legacyScriptResult = (result: RunResult) => {
|
|
48
|
+
let parsed: Record<string, unknown> | null = null;
|
|
49
|
+
try { parsed = json(result.stdout); } catch { /* handled below */ }
|
|
50
|
+
if (result.exitCode !== 0 || parsed?.error || parsed?.ok === false) return fail(
|
|
51
|
+
parsed?.error
|
|
52
|
+
? String(parsed.error)
|
|
53
|
+
: parsed?.ok === false
|
|
54
|
+
? "legacy operation reported failure"
|
|
55
|
+
: result.stderr.trim() || result.stdout.trim() || "workflow script failed",
|
|
56
|
+
diagnostics(result),
|
|
57
|
+
);
|
|
58
|
+
if (!parsed) return fail("workflow output parse failed", diagnostics(result));
|
|
59
|
+
const { ok: _legacyOk, ...data } = parsed;
|
|
60
|
+
return ok({ ...data, exitCode: 0, ...(result.stderr ? { stderr: result.stderr } : {}) });
|
|
61
|
+
};
|
|
62
|
+
const optionalJson = (value: string | undefined) => {
|
|
63
|
+
if (!value?.trim()) return null;
|
|
64
|
+
try { return JSON.parse(value); } catch { return null; }
|
|
65
|
+
};
|
|
66
|
+
const sections = (stdout: string) => parseSections(stdout) as Record<string, string>;
|
|
67
|
+
export const normalizeLegacyResult = (value: Record<string, unknown>) => {
|
|
68
|
+
if (value.error) return fail(String(value.error));
|
|
69
|
+
if (value.ok === false) return fail("legacy operation reported failure");
|
|
70
|
+
const { ok: _legacyOk, ...data } = value;
|
|
71
|
+
return ok(data);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const parsePr = (stdout: string) => {
|
|
75
|
+
const part = sections(stdout);
|
|
76
|
+
const repo = parseKeyValueLines(part.Repository ?? "", [
|
|
77
|
+
"branch", "range", "base_ref", "merge_base", "diff_range", "range_mode", "git_sync",
|
|
78
|
+
]);
|
|
79
|
+
return {
|
|
80
|
+
...repo,
|
|
81
|
+
commits: part.Commits ?? "",
|
|
82
|
+
diff_stat: part["Diff Stat"] ?? "",
|
|
83
|
+
files: part["Changed Files"] ?? "",
|
|
84
|
+
pr_template: part["PR Template"] ?? "",
|
|
85
|
+
vcs_config: optionalJson(part["VCS Config"]),
|
|
86
|
+
merged_pr_style: optionalJson(part["Merged PR Style"]),
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const parseChangelog = (stdout: string) => {
|
|
91
|
+
const part = sections(stdout);
|
|
92
|
+
const repo = parseKeyValueLines(part.Repository ?? "", ["branch", "range"]);
|
|
93
|
+
return {
|
|
94
|
+
...repo,
|
|
95
|
+
changelog_excerpt: part["Existing CHANGELOG.md"] ?? "",
|
|
96
|
+
rules: part["Keep a Changelog Rules"] ?? "",
|
|
97
|
+
commits: part.Commits ?? "",
|
|
98
|
+
diff_stat: part["Diff Stat"] ?? "",
|
|
99
|
+
files: part["Changed Files"] ?? "",
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const parseRelease = (stdout: string) => {
|
|
104
|
+
const part = sections(stdout);
|
|
105
|
+
const repo = parseKeyValueLines(part.Repository ?? "", ["requested", "range"]);
|
|
106
|
+
return {
|
|
107
|
+
...repo,
|
|
108
|
+
tags: part.Tags ?? "",
|
|
109
|
+
commits: part.Commits ?? "",
|
|
110
|
+
diff_stat: part["Diff Stat"] ?? "",
|
|
111
|
+
files: part["Changed Files"] ?? "",
|
|
112
|
+
release_files: part["Existing Release Files"] ?? "",
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const parseDocs = (stdout: string) => {
|
|
117
|
+
const part = sections(stdout);
|
|
118
|
+
const repo = parseKeyValueLines(part.Repository ?? "", ["branch", "range"]);
|
|
119
|
+
return {
|
|
120
|
+
...repo,
|
|
121
|
+
changed_files: part["Changed Files"] ?? "",
|
|
122
|
+
readme_preview: part["README Preview"] ?? "",
|
|
123
|
+
package_scripts: part["Package Scripts"] ?? "",
|
|
124
|
+
files: part["Documentation Files"] ?? "",
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export function createRepoTools(runtime: RepoRuntime = defaultRuntime) {
|
|
129
|
+
const validateRange = (root: string, value: string) => {
|
|
130
|
+
for (const revision of gitRevisionParts(value)) {
|
|
131
|
+
const resolved = runtime.git(root, ["rev-parse", "--verify", "--quiet", "--end-of-options", `${revision}^{commit}`]);
|
|
132
|
+
if (resolved.exitCode !== 0) throw new Error(`invalid Git revision or range: ${value}`);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const contextWithRange = (
|
|
136
|
+
root: string, script: string, value: string | undefined,
|
|
137
|
+
parse: (stdout: string) => Record<string, unknown>,
|
|
138
|
+
) => {
|
|
139
|
+
try { if (value) validateRange(root, value); }
|
|
140
|
+
catch (error) { return output(fail(error instanceof Error ? error.message : "invalid Git revision or range")); }
|
|
141
|
+
return output(scriptResult(runtime.runScript(root, script, value ? [value] : []), parse));
|
|
142
|
+
};
|
|
143
|
+
const invoke = (script: string, parse: (stdout: string) => Record<string, unknown>, args: string[] = []) =>
|
|
144
|
+
async (_input: unknown, context: ToolContext) => output(scriptResult(runtime.runScript(context.directory, script, args), parse));
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
workflow_toolkit_init_status: tool({
|
|
148
|
+
description: "Inspect toolkit initialization", args: {}, execute: invoke("init/status.sh", json),
|
|
149
|
+
}),
|
|
150
|
+
workflow_toolkit_status: tool({
|
|
151
|
+
description: "Inspect toolkit and repository state", args: {}, execute: invoke("init/toolkit-status.sh", json),
|
|
152
|
+
}),
|
|
153
|
+
workflow_git_context: tool({
|
|
154
|
+
description: "Read Git branch and change context",
|
|
155
|
+
args: { paths: tool.schema.array(tool.schema.string()).optional() },
|
|
156
|
+
execute: async ({ paths }, context) => output(ok(gitContext(context.directory, paths ?? []))),
|
|
157
|
+
}),
|
|
158
|
+
workflow_verify: tool({
|
|
159
|
+
description: "Discover and run repository verification",
|
|
160
|
+
args: { dry_run: tool.schema.boolean().optional() },
|
|
161
|
+
execute: async ({ dry_run }, context) => output(scriptResult(
|
|
162
|
+
runtime.runScript(context.directory, "verify-project.sh", dry_run ? ["--dry-run"] : []), parseVerifyOutput,
|
|
163
|
+
)),
|
|
164
|
+
}),
|
|
165
|
+
workflow_pr_context: tool({
|
|
166
|
+
description: "Gather branch-exclusive PR context",
|
|
167
|
+
args: { range: tool.schema.string().optional() },
|
|
168
|
+
execute: async ({ range }, context) => contextWithRange(context.directory, "pr-ready-context.sh", range, parsePr),
|
|
169
|
+
}),
|
|
170
|
+
workflow_changelog_context: tool({
|
|
171
|
+
description: "Gather changelog context",
|
|
172
|
+
args: { range: tool.schema.string().optional() },
|
|
173
|
+
execute: async ({ range }, context) => contextWithRange(context.directory, "changelog-context.sh", range, parseChangelog),
|
|
174
|
+
}),
|
|
175
|
+
workflow_release_notes_context: tool({
|
|
176
|
+
description: "Gather release notes for an explicit range",
|
|
177
|
+
args: { range_or_tag: tool.schema.string() },
|
|
178
|
+
execute: async ({ range_or_tag }, context) => !range_or_tag.trim()
|
|
179
|
+
? output(fail("release tag or range required"))
|
|
180
|
+
: contextWithRange(context.directory, "release-notes-context.sh", range_or_tag, parseRelease),
|
|
181
|
+
}),
|
|
182
|
+
workflow_docs_context: tool({
|
|
183
|
+
description: "Gather documentation refresh context",
|
|
184
|
+
args: { range: tool.schema.string().optional() },
|
|
185
|
+
execute: async ({ range }, context) => output(scriptResult(
|
|
186
|
+
runtime.runScript(context.directory, "docs-refresh-context.sh", range ? [range] : []), parseDocs,
|
|
187
|
+
)),
|
|
188
|
+
}),
|
|
189
|
+
workflow_changelog_apply: tool({
|
|
190
|
+
description: "Apply confirmed Keep a Changelog entries to Unreleased",
|
|
191
|
+
args: {
|
|
192
|
+
confirmed: tool.schema.boolean(),
|
|
193
|
+
entries: tool.schema.union([
|
|
194
|
+
tool.schema.record(tool.schema.string(), tool.schema.array(tool.schema.string())),
|
|
195
|
+
tool.schema.array(tool.schema.object({ category: tool.schema.string(), text: tool.schema.string() })),
|
|
196
|
+
]).optional(),
|
|
197
|
+
path: tool.schema.string().optional(),
|
|
198
|
+
normalize_only: tool.schema.boolean().optional(),
|
|
199
|
+
},
|
|
200
|
+
execute: async ({ confirmed, entries, path: changelogPath, normalize_only }, context) => {
|
|
201
|
+
const rejected = requireConfirmed(confirmed);
|
|
202
|
+
if (rejected) return rejected;
|
|
203
|
+
try {
|
|
204
|
+
changelogPath = resolveInside(context.directory, changelogPath ?? "CHANGELOG.md");
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return output(fail(error instanceof Error ? error.message : "invalid changelog path"));
|
|
207
|
+
}
|
|
208
|
+
return output(normalizeLegacyResult(changelogApply({
|
|
209
|
+
entries, path: changelogPath, normalize_only, workspace_root: realpathSync(context.directory),
|
|
210
|
+
}) as Record<string, unknown>));
|
|
211
|
+
},
|
|
212
|
+
}),
|
|
213
|
+
workflow_branch_setup: tool({
|
|
214
|
+
description: "Apply a confirmed in-place feature or bugfix branch setup",
|
|
215
|
+
args: {
|
|
216
|
+
confirmed: tool.schema.boolean(),
|
|
217
|
+
action: tool.schema.enum(["setup", "reapply_stash"]).optional(),
|
|
218
|
+
sdd_dir: tool.schema.string().optional(),
|
|
219
|
+
target_branch: tool.schema.string().optional(),
|
|
220
|
+
stash: tool.schema.enum(["yes", "no"]).optional(),
|
|
221
|
+
},
|
|
222
|
+
execute: async ({ confirmed, action, sdd_dir, target_branch, stash }, context) => {
|
|
223
|
+
const rejected = requireConfirmed(confirmed);
|
|
224
|
+
if (rejected) return rejected;
|
|
225
|
+
let resolvedSdd = sdd_dir ?? "docs";
|
|
226
|
+
try {
|
|
227
|
+
resolvedSdd = resolveInside(context.directory, resolvedSdd);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return output(fail(error instanceof Error ? error.message : "invalid SDD path"));
|
|
230
|
+
}
|
|
231
|
+
const result = branchSetup({
|
|
232
|
+
action, sdd_dir: resolvedSdd, target_branch, stash, workspace_root: context.directory,
|
|
233
|
+
});
|
|
234
|
+
return output(legacyScriptResult({
|
|
235
|
+
stdout: JSON.stringify(result),
|
|
236
|
+
stderr: "",
|
|
237
|
+
exitCode: result.error ? 1 : 0,
|
|
238
|
+
cwd: context.directory,
|
|
239
|
+
}));
|
|
240
|
+
},
|
|
241
|
+
}),
|
|
242
|
+
workflow_commit: tool({
|
|
243
|
+
description: "Commit the current index on a feature or bugfix branch without staging files",
|
|
244
|
+
args: { confirmed: tool.schema.boolean(), message: tool.schema.string() },
|
|
245
|
+
execute: async ({ confirmed, message }, context) => {
|
|
246
|
+
const rejected = requireConfirmed(confirmed);
|
|
247
|
+
if (rejected) return rejected;
|
|
248
|
+
const branch = runtime.git(context.directory, ["branch", "--show-current"]);
|
|
249
|
+
if (branch.exitCode !== 0) return output(fail(
|
|
250
|
+
branch.stderr.trim() || branch.stdout.trim() || "unable to read current branch", diagnostics(branch),
|
|
251
|
+
));
|
|
252
|
+
const name = branch.stdout.trim();
|
|
253
|
+
const pol = branchPolicy();
|
|
254
|
+
if (pol.protected.has(name.toLowerCase())) return output(fail(`cannot commit on protected branch ${name}`));
|
|
255
|
+
if (!pol.allowed.some((r) => r.test(name)) || name.endsWith("/")) return output(fail(`commit requires an allowed branch (current: ${name})`));
|
|
256
|
+
return output(scriptResult(runtime.git(context.directory, ["commit", "-m", message]),
|
|
257
|
+
(stdout) => ({ stdout: stdout.trim() })));
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
workflow_pr_create: tool({
|
|
261
|
+
description: "Create a confirmed pull or merge request",
|
|
262
|
+
args: {
|
|
263
|
+
confirmed: tool.schema.boolean(),
|
|
264
|
+
title: tool.schema.string(),
|
|
265
|
+
body: tool.schema.string().optional(),
|
|
266
|
+
draft: tool.schema.boolean().optional(),
|
|
267
|
+
target_branch: tool.schema.string().optional(),
|
|
268
|
+
},
|
|
269
|
+
execute: async ({ confirmed, title, body, draft, target_branch }, context) => {
|
|
270
|
+
const rejected = requireConfirmed(confirmed);
|
|
271
|
+
if (rejected) return rejected;
|
|
272
|
+
const branch = runtime.git(context.directory, ["branch", "--show-current"]);
|
|
273
|
+
if (branch.exitCode !== 0) return output(fail(
|
|
274
|
+
branch.stderr.trim() || branch.stdout.trim() || "unable to read current branch", diagnostics(branch),
|
|
275
|
+
));
|
|
276
|
+
const name = branch.stdout.trim();
|
|
277
|
+
const pol = branchPolicy();
|
|
278
|
+
if (!pol.allowed.some((r) => r.test(name)) || name.endsWith("/")) {
|
|
279
|
+
return output(fail(`PR creation requires an allowed branch (current: ${name})`));
|
|
280
|
+
}
|
|
281
|
+
return output(legacyScriptResult(runtime.runScript(context.directory, "pr-create.sh", [], {
|
|
282
|
+
WF_PR_TITLE: title,
|
|
283
|
+
WF_PR_BODY: body ?? "",
|
|
284
|
+
WF_PR_CONFIRMED: "true",
|
|
285
|
+
WF_PR_DRAFT: draft ? "true" : "false",
|
|
286
|
+
WF_PR_TARGET: target_branch ?? "",
|
|
287
|
+
})));
|
|
288
|
+
},
|
|
289
|
+
}),
|
|
290
|
+
workflow_toolkit_init_apply: tool({
|
|
291
|
+
description: "Apply a confirmed toolkit initialization action",
|
|
292
|
+
args: {
|
|
293
|
+
confirmed: tool.schema.boolean(),
|
|
294
|
+
action: tool.schema.enum([
|
|
295
|
+
"youtrack_scaffold", "youtrack_json", "youtrack_token_placeholder", "vcs_scaffold", "config", "gitignore", "hygiene",
|
|
296
|
+
]),
|
|
297
|
+
base_url: tool.schema.string().optional(),
|
|
298
|
+
default_mention: tool.schema.string().optional(),
|
|
299
|
+
meeting_issue: tool.schema.string().optional(),
|
|
300
|
+
vcs_provider: tool.schema.enum(["gitlab", "github"]).optional(),
|
|
301
|
+
vcs_target_branch: tool.schema.string().optional(),
|
|
302
|
+
locale: tool.schema.string().optional(),
|
|
303
|
+
locale_options: tool.schema.array(tool.schema.string()).optional(),
|
|
304
|
+
timezone: tool.schema.string().optional(),
|
|
305
|
+
branch_policy_preset: tool.schema.enum(["gitflow", "github-flow", "trunk-based", "custom"]).optional(),
|
|
306
|
+
branch_policy_allowed: tool.schema.array(tool.schema.string()).optional(),
|
|
307
|
+
branch_policy_protected: tool.schema.array(tool.schema.string()).optional(),
|
|
308
|
+
include_open_source: tool.schema.boolean().optional(),
|
|
309
|
+
},
|
|
310
|
+
execute: async ({
|
|
311
|
+
confirmed, action, base_url, default_mention, meeting_issue, vcs_provider, vcs_target_branch,
|
|
312
|
+
locale, locale_options, timezone, branch_policy_preset, branch_policy_allowed, branch_policy_protected,
|
|
313
|
+
include_open_source,
|
|
314
|
+
}, context) => {
|
|
315
|
+
const rejected = requireConfirmed(confirmed);
|
|
316
|
+
if (rejected) return rejected;
|
|
317
|
+
if (action === "hygiene") {
|
|
318
|
+
const result = ensureHygieneFiles(context.directory, { confirmed, includeOpenSource: include_open_source });
|
|
319
|
+
return output(result.ok ? ok(result) : fail(result.error));
|
|
320
|
+
}
|
|
321
|
+
if (action === "gitignore") {
|
|
322
|
+
const result = ensureProjectGitignore(context.directory, confirmed);
|
|
323
|
+
return output(result.ok ? ok(result) : fail(result.error));
|
|
324
|
+
}
|
|
325
|
+
if (action === "config") {
|
|
326
|
+
const LOCALE_RE = /^[a-z]{2,3}(-[A-Z]{2})?$/;
|
|
327
|
+
if (locale !== undefined && !LOCALE_RE.test(locale)) {
|
|
328
|
+
return output(fail(`invalid locale: ${JSON.stringify(locale)} — expected BCP-47 like en or es-CL`));
|
|
329
|
+
}
|
|
330
|
+
const current = readConfig();
|
|
331
|
+
const next: ToolkitConfig = {
|
|
332
|
+
locale: locale ?? current.locale,
|
|
333
|
+
localeOptions: locale_options ?? current.localeOptions,
|
|
334
|
+
timezone: timezone ?? current.timezone,
|
|
335
|
+
branchPolicy: {
|
|
336
|
+
preset: (branch_policy_preset as BranchPreset) ?? current.branchPolicy.preset,
|
|
337
|
+
allowed: branch_policy_allowed ?? current.branchPolicy.allowed,
|
|
338
|
+
protected: branch_policy_protected ?? current.branchPolicy.protected,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
writeConfig(next);
|
|
342
|
+
return output(ok({ action: "config", path: path.join(configDir(), "config.json"), ...next }));
|
|
343
|
+
}
|
|
344
|
+
const env = Object.fromEntries(Object.entries({
|
|
345
|
+
WORKFLOW_YT_BASE_URL: base_url,
|
|
346
|
+
WORKFLOW_YT_MENTION: default_mention,
|
|
347
|
+
WORKFLOW_YT_MEETING_ISSUE: meeting_issue,
|
|
348
|
+
WORKFLOW_VCS_PROVIDER: vcs_provider,
|
|
349
|
+
WORKFLOW_VCS_TARGET_BRANCH: vcs_target_branch,
|
|
350
|
+
}).filter((entry): entry is [string, string] => entry[1] !== undefined));
|
|
351
|
+
return output(legacyScriptResult(runtime.runScript(
|
|
352
|
+
context.directory, "init/apply.sh", [action, "true"], env,
|
|
353
|
+
)));
|
|
354
|
+
},
|
|
355
|
+
}),
|
|
356
|
+
};
|
|
357
|
+
}
|