@zq-silk/yui 0.0.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.
Files changed (95) hide show
  1. package/ARCHITECTURE.md +141 -0
  2. package/LICENSE +21 -0
  3. package/README.md +211 -0
  4. package/dist/agent/adapterCatalog.js +10 -0
  5. package/dist/agent/agent.js +89 -0
  6. package/dist/agent/agentRegistry.js +10 -0
  7. package/dist/agent/argumentPolicy.js +80 -0
  8. package/dist/brief/taskBrief.js +37 -0
  9. package/dist/cli/commandCatalog.js +647 -0
  10. package/dist/cli/completion.js +111 -0
  11. package/dist/cli/completionWizard.js +143 -0
  12. package/dist/cli/dynamicCompletion.js +48 -0
  13. package/dist/cli/helpRenderer.js +32 -0
  14. package/dist/cli/interactionCandidates.js +139 -0
  15. package/dist/cli/interactionPolicy.js +389 -0
  16. package/dist/cli/interactiveSelection.js +185 -0
  17. package/dist/cli/invocationRouter.js +51 -0
  18. package/dist/cli/roleOptionCatalog.js +67 -0
  19. package/dist/cli/roleWizard.js +546 -0
  20. package/dist/cli/selectionPorts.js +1 -0
  21. package/dist/cli/updateCommand.js +22 -0
  22. package/dist/cli.js +402 -0
  23. package/dist/commands/agentCommands.js +196 -0
  24. package/dist/commands/globalRoleCommands.js +367 -0
  25. package/dist/commands/jobCommands.js +100 -0
  26. package/dist/commands/operatorCommands.js +38 -0
  27. package/dist/commands/repositoryCommands.js +86 -0
  28. package/dist/commands/roleConfiguration.js +201 -0
  29. package/dist/commands/taskCommands.js +1344 -0
  30. package/dist/commands/taskContextCommand.js +215 -0
  31. package/dist/commands/taskInputCommands.js +423 -0
  32. package/dist/commands/taskRoleRuntimeStatus.js +152 -0
  33. package/dist/completion/completionInstaller.js +168 -0
  34. package/dist/completion/completionPort.js +1 -0
  35. package/dist/completion/completionState.js +137 -0
  36. package/dist/completion/completionWizard.js +125 -0
  37. package/dist/completion/fileCompletionManager.js +51 -0
  38. package/dist/config/yuiConfig.js +17 -0
  39. package/dist/context/dispatchContext.js +74 -0
  40. package/dist/controller/clientRuntime.js +215 -0
  41. package/dist/controller/controller.js +158 -0
  42. package/dist/controller/controllerMain.js +37 -0
  43. package/dist/controller/fileSchedulerStoreAdapter.js +322 -0
  44. package/dist/controller/runtime.js +31 -0
  45. package/dist/controller/sessionNotify.js +136 -0
  46. package/dist/core/controllerClient.js +127 -0
  47. package/dist/core/controllerServer.js +269 -0
  48. package/dist/core/protocol.js +169 -0
  49. package/dist/decision/decision.js +42 -0
  50. package/dist/doctor/doctor.js +229 -0
  51. package/dist/errors/cliError.js +38 -0
  52. package/dist/event/taskEvent.js +44 -0
  53. package/dist/executor/agentAdapter.js +338 -0
  54. package/dist/executor/agentExecutor.js +144 -0
  55. package/dist/executor/executorRegistry.js +101 -0
  56. package/dist/executor/fileRoleLaunchPlanner.js +156 -0
  57. package/dist/executor/launchPlan.js +16 -0
  58. package/dist/input/inputRequest.js +326 -0
  59. package/dist/message/message.js +69 -0
  60. package/dist/milestone/milestone.js +27 -0
  61. package/dist/operator/operatorContext.js +66 -0
  62. package/dist/output/rolePresentation.js +82 -0
  63. package/dist/output/table.js +77 -0
  64. package/dist/output/terminal.js +198 -0
  65. package/dist/repository/gitWorkspace.js +210 -0
  66. package/dist/repository/repository.js +55 -0
  67. package/dist/repository/taskWorkspacePreparer.js +256 -0
  68. package/dist/role/role.js +246 -0
  69. package/dist/role/systemRoles.js +20 -0
  70. package/dist/run/agentRun.js +102 -0
  71. package/dist/scheduler/activeRoleRunDelivery.js +94 -0
  72. package/dist/scheduler/archivedTaskRuntime.js +12 -0
  73. package/dist/scheduler/leaderFailure.js +18 -0
  74. package/dist/scheduler/leaderWakeupProcessor.js +143 -0
  75. package/dist/scheduler/operatorInputNotificationProcessor.js +85 -0
  76. package/dist/scheduler/operatorNotification.js +17 -0
  77. package/dist/scheduler/pendingWakeup.js +33 -0
  78. package/dist/scheduler/ports.js +1 -0
  79. package/dist/scheduler/roleRunLiveness.js +41 -0
  80. package/dist/scheduler/wakeupQueue.js +13 -0
  81. package/dist/setup/setupCommand.js +317 -0
  82. package/dist/storage/durableFile.js +38 -0
  83. package/dist/storage/storageSchema.js +259 -0
  84. package/dist/storage/taskStore.js +1032 -0
  85. package/dist/task/task.js +216 -0
  86. package/dist/tmux/commandExecutor.js +69 -0
  87. package/dist/tmux/terminalHandoff.js +17 -0
  88. package/dist/tmux/tmuxManager.js +408 -0
  89. package/dist/workItem/workItem.js +45 -0
  90. package/dist/worktree/roleWorkspace.js +62 -0
  91. package/i18n/README.zh-CN.md +205 -0
  92. package/package.json +47 -0
  93. package/skills/yui-leader/SKILL.md +72 -0
  94. package/skills/yui-operator/SKILL.md +57 -0
  95. package/skills/yui-worker/SKILL.md +31 -0
@@ -0,0 +1,111 @@
1
+ import { ROOT_COMMAND, listPublicCommandPaths, orderedImmediateTokens, visibleChildren } from "./commandCatalog.js";
2
+ export function renderCompletion(shell, identity = "yui") {
3
+ const entries = collectEntries(ROOT_COMMAND);
4
+ switch (shell) {
5
+ case "bash": return renderBash(entries, identity);
6
+ case "zsh": return renderZsh(entries, identity);
7
+ case "fish": return renderFish(entries, identity);
8
+ default: throw new Error("Completion shell must be one of bash, zsh, fish.");
9
+ }
10
+ }
11
+ function collectEntries(root) {
12
+ const entries = [];
13
+ const visit = (node) => {
14
+ if (!node.hidden && !node.commandPathArguments) {
15
+ entries.push({
16
+ path: node.path.slice(1).join(" "),
17
+ immediate: orderedImmediateTokens(node),
18
+ options: node.options
19
+ });
20
+ }
21
+ visibleChildren(node).forEach(visit);
22
+ };
23
+ visit(root);
24
+ return entries;
25
+ }
26
+ function manifest(prefix) {
27
+ return listPublicCommandPaths().map((path) => `${prefix} ${path}`).join("\n");
28
+ }
29
+ function renderBash(entries, identity) {
30
+ const functionName = identity === "yui" ? "_yui" : "_yui_dev";
31
+ return `${manifest("# yui command:")}
32
+ ${functionName}() {
33
+ local current path dynamic_candidate
34
+ local -a candidates dynamic_candidates
35
+ current="\${COMP_WORDS[COMP_CWORD]}"
36
+ path=""
37
+ if (( COMP_CWORD > 1 )); then
38
+ path="\${COMP_WORDS[*]:1:COMP_CWORD-1}"
39
+ fi
40
+ while IFS= read -r dynamic_candidate; do
41
+ [[ -n "$dynamic_candidate" ]] && dynamic_candidates+=("$dynamic_candidate")
42
+ done < <(command ${identity} completion candidates "$current" -- "\${COMP_WORDS[@]:1:COMP_CWORD-1}" 2>/dev/null)
43
+ if (( \${#dynamic_candidates[@]} > 0 )); then
44
+ candidates=("\${dynamic_candidates[@]}")
45
+ else
46
+ case "$path" in
47
+ ${entries.map((entry) => ` ${bashPattern(entry.path)}) candidates=(${[...entry.immediate, ...entry.options].map(shellQuote).join(" ")});;`).join("\n")}
48
+ *) candidates=();;
49
+ esac
50
+ fi
51
+ COMPREPLY=( $(compgen -W "\${candidates[*]}" -- "$current") )
52
+ }
53
+ complete -F ${functionName} ${identity}
54
+ `;
55
+ }
56
+ function renderZsh(entries, identity) {
57
+ return `#compdef ${identity}
58
+ ${manifest("# yui command:")}
59
+ local current path dynamic_output
60
+ local -a candidates
61
+ current="$words[CURRENT]"
62
+ path=""
63
+ if (( CURRENT > 2 )); then
64
+ path="\${(j: :)words[2,CURRENT-1]}"
65
+ fi
66
+ dynamic_output="$(command ${identity} completion candidates "$current" -- "\${(@)words[2,CURRENT-1]}" 2>/dev/null)"
67
+ if [[ -n "$dynamic_output" ]]; then
68
+ candidates=("\${(@f)dynamic_output}")
69
+ else
70
+ case "$path" in
71
+ ${entries.map((entry) => ` ${zshPattern(entry.path)}) candidates=(${[...entry.immediate, ...entry.options].map(shellQuote).join(" ")});;`).join("\n")}
72
+ *) candidates=();;
73
+ esac
74
+ fi
75
+ (( \${#candidates[@]} > 0 )) && compadd -- "$candidates[@]"
76
+ `;
77
+ }
78
+ function renderFish(entries, identity) {
79
+ const lines = [
80
+ manifest("# yui command:"),
81
+ `function __${identity.replaceAll("-", "_")}_dynamic`,
82
+ ` ${identity} completion candidates (commandline -ct) -- (commandline -opc | string split ' ' | tail -n +2) 2>/dev/null`,
83
+ "end",
84
+ `complete -c ${identity} -f -a '(__${identity.replaceAll("-", "_")}_dynamic)'`
85
+ ];
86
+ for (const entry of entries) {
87
+ const candidates = [...entry.immediate, ...entry.options].join(" ");
88
+ if (candidates.length === 0)
89
+ continue;
90
+ const condition = entry.path.length === 0
91
+ ? "__fish_use_subcommand"
92
+ : `__fish_seen_subcommand_from ${entry.path.split(" ").map(fishQuote).join(" ")}`;
93
+ lines.push(`complete -c ${identity} -n '${condition}' -a '${escapeSingle(candidates)}'`);
94
+ }
95
+ return `${lines.join("\n")}\n`;
96
+ }
97
+ function bashPattern(path) {
98
+ return path.length === 0 ? '""' : shellQuote(path);
99
+ }
100
+ function zshPattern(path) {
101
+ return path.length === 0 ? '""' : shellQuote(path);
102
+ }
103
+ function shellQuote(value) {
104
+ return `'${escapeSingle(value)}'`;
105
+ }
106
+ function fishQuote(value) {
107
+ return `'${escapeSingle(value)}'`;
108
+ }
109
+ function escapeSingle(value) {
110
+ return value.replaceAll("'", "'\\''");
111
+ }
@@ -0,0 +1,143 @@
1
+ import { resolve } from "node:path";
2
+ import { COMPLETION_SHELLS, activationBlock, shellQuote, } from "../completion/completionState.js";
3
+ import { renderTable } from "../output/table.js";
4
+ export class CompletionWizardError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "CompletionWizardError";
8
+ }
9
+ }
10
+ export async function runCompletionWizard(port, io, options = {}) {
11
+ if (!io.interactive || io.json) {
12
+ throw new CompletionWizardError("Completion configuration requires an interactive terminal.");
13
+ }
14
+ const overview = await port.inspect();
15
+ const state = options.shell === undefined
16
+ ? await chooseShell(overview, io, options.defaultSelection ?? "current-shell")
17
+ : overview.states.find((candidate) => candidate.shell === options.shell);
18
+ if (state === undefined) {
19
+ return "Completion setup skipped.\n";
20
+ }
21
+ const decision = normalize(await io.question(`Selected: ${label(state.shell)} (${state.action})\n`
22
+ + `Script: ${state.installation.scriptPath}\n`
23
+ + `Startup: ${state.installation.activationPath}`
24
+ + `${activationIsAutomatic(state.shell, state.installation, state.suggested)
25
+ ? " (automatic)"
26
+ : " (managed startup block)"}\n`
27
+ + "Use these paths? [Y/n/customize]: "));
28
+ if (decision === "n" || decision === "no") {
29
+ return `Completion ${state.shell} setup skipped.\n`;
30
+ }
31
+ let installation = state.installation;
32
+ let customized = false;
33
+ if (decision === "customize") {
34
+ installation = await customizeInstallation(state.installation, io);
35
+ customized = true;
36
+ }
37
+ else if (decision !== "" && decision !== "y" && decision !== "yes") {
38
+ throw new CompletionWizardError("Choose Y, n, or customize.");
39
+ }
40
+ const automatic = activationIsAutomatic(state.shell, installation, state.suggested);
41
+ let activate = false;
42
+ let activationReady = automatic;
43
+ if (!automatic) {
44
+ const activationAnswer = normalize(await io.question(`${activationBlock(state.shell, installation, overview.identity)}\n`
45
+ + `Update ${installation.activationPath} with this managed Yui block? [Y/n]: `));
46
+ if (activationAnswer !== ""
47
+ && activationAnswer !== "y"
48
+ && activationAnswer !== "yes"
49
+ && activationAnswer !== "n"
50
+ && activationAnswer !== "no") {
51
+ throw new CompletionWizardError("Choose Y or n.");
52
+ }
53
+ activate = activationAnswer === ""
54
+ || activationAnswer === "y"
55
+ || activationAnswer === "yes";
56
+ activationReady = activate || (!customized && state.activationCurrent);
57
+ }
58
+ await port.install({ shell: state.shell, installation, activate });
59
+ if (!activationReady) {
60
+ return `Completion ${state.shell} script installed; startup activation still required.\n`;
61
+ }
62
+ const result = `Completion ${state.shell} ${pastTense(state.action)}.\n`;
63
+ return automatic
64
+ ? result
65
+ : `${result}${activationGuidance(state.shell, installation, state.suggested, overview)}`;
66
+ }
67
+ async function chooseShell(overview, io, defaultSelection) {
68
+ io.write(`${renderCompletionStateTable(overview.states, io.width)}\n\n`);
69
+ const defaultHint = defaultSelection === "skip"
70
+ ? " [skip]"
71
+ : overview.currentShell === undefined ? "" : ` [${overview.currentShell}]`;
72
+ const skipHint = defaultSelection === "skip" ? "" : " (or skip)";
73
+ const answer = normalize(await io.question(`Choose shell by number or name${defaultHint}${skipHint}: `));
74
+ if (answer === "skip" || (answer === "" && defaultSelection === "skip")) {
75
+ return undefined;
76
+ }
77
+ const shell = parseShell(answer, overview.currentShell);
78
+ const state = overview.states.find((candidate) => candidate.shell === shell);
79
+ if (state === undefined)
80
+ throw new CompletionWizardError("Completion shell is unavailable.");
81
+ return state;
82
+ }
83
+ function renderCompletionStateTable(states, width) {
84
+ return renderTable("Completion installation", [
85
+ { header: "#", minWidth: 1, maxWidth: 3 },
86
+ { header: "Shell", minWidth: 4, maxWidth: 6 },
87
+ { header: "Status", minWidth: 12, maxWidth: 13 },
88
+ { header: "Action", minWidth: 7, maxWidth: 7 },
89
+ { header: "Current", minWidth: 7, maxWidth: 7 },
90
+ { header: "Script", minWidth: 8, maxWidth: 88 }
91
+ ], states.map((state, index) => [
92
+ String(index + 1),
93
+ label(state.shell),
94
+ state.status,
95
+ state.action,
96
+ state.current ? "yes" : "",
97
+ state.installation.scriptPath
98
+ ]), width);
99
+ }
100
+ async function customizeInstallation(suggested, io) {
101
+ const scriptAnswer = (await io.question(`Completion script path [${suggested.scriptPath}]: `))?.trim() ?? "";
102
+ const activationAnswer = (await io.question(`Startup file path [${suggested.activationPath}]: `))?.trim() ?? "";
103
+ return Object.freeze({
104
+ scriptPath: resolve(scriptAnswer || suggested.scriptPath),
105
+ activationPath: resolve(activationAnswer || suggested.activationPath)
106
+ });
107
+ }
108
+ function parseShell(value, current) {
109
+ if (value.length === 0 && current !== undefined)
110
+ return current;
111
+ if (/^[1-3]$/.test(value)) {
112
+ return COMPLETION_SHELLS[Number.parseInt(value, 10) - 1];
113
+ }
114
+ if (COMPLETION_SHELLS.includes(value)) {
115
+ return value;
116
+ }
117
+ throw new CompletionWizardError("Choose Bash, Zsh, or Fish by number or name.");
118
+ }
119
+ function activationGuidance(shell, installation, suggested, overview) {
120
+ const unchanged = "The current shell is unchanged.\n";
121
+ if (installation.activationPath !== suggested.activationPath) {
122
+ return `${unchanged}Load the custom startup file from ${shell}: source ${shellQuote(installation.activationPath)}\n`;
123
+ }
124
+ return overview.currentShell === shell
125
+ ? `${unchanged}Restart the current shell to activate completion: exec ${shell}\n`
126
+ : `${unchanged}Switch this terminal to ${shell} to activate completion: exec ${shell}\n`;
127
+ }
128
+ function pastTense(action) {
129
+ switch (action) {
130
+ case "Install": return "installed";
131
+ case "Refresh": return "refreshed";
132
+ case "Repair": return "repaired";
133
+ }
134
+ }
135
+ function label(shell) {
136
+ return `${shell[0]?.toUpperCase() ?? ""}${shell.slice(1)}`;
137
+ }
138
+ function normalize(value) {
139
+ return value?.trim().toLowerCase() ?? "";
140
+ }
141
+ function activationIsAutomatic(shell, installation, suggested) {
142
+ return shell === "fish" && installation.scriptPath === suggested.scriptPath;
143
+ }
@@ -0,0 +1,48 @@
1
+ import { findChild, ROOT_COMMAND, visibleChildren } from "./commandCatalog.js";
2
+ import { getSelectionCandidates } from "./interactionCandidates.js";
3
+ import { findInteractionPolicy } from "./interactionPolicy.js";
4
+ export async function resolveCompletionCandidates(input) {
5
+ const words = input.words[0] === "yui" || input.words[0] === "yui-dev"
6
+ ? input.words.slice(1)
7
+ : [...input.words];
8
+ const { node, consumed } = resolveCommand(words);
9
+ if (consumed === words.length && node.children.length > 0) {
10
+ return prefix(visibleChildren(node).map((child) => child.name), input.current);
11
+ }
12
+ const previous = words.at(-1);
13
+ if (previous !== undefined && Object.hasOwn(node.optionValues, previous)) {
14
+ return prefix(node.optionValues[previous] ?? [], input.current);
15
+ }
16
+ if (input.current.startsWith("-")) {
17
+ return prefix(node.options, input.current);
18
+ }
19
+ const argumentPosition = words.length;
20
+ const policy = findInteractionPolicy(node);
21
+ const selector = policy?.selectors.find((candidate) => selectorApplies(candidate, previous, argumentPosition));
22
+ if (selector !== undefined) {
23
+ const set = await getSelectionCandidates(selector, input.ports, words);
24
+ return prefix(set?.candidates.map((candidate) => candidate.value) ?? [], input.current);
25
+ }
26
+ const relative = argumentPosition - (node.path.length - 1);
27
+ return prefix(node.argumentValues[relative] ?? [], input.current);
28
+ }
29
+ function selectorApplies(selector, previous, argumentPosition) {
30
+ if (selector.option !== undefined)
31
+ return selector.option === previous;
32
+ return selector.argumentIndex === argumentPosition;
33
+ }
34
+ function resolveCommand(words) {
35
+ let node = ROOT_COMMAND;
36
+ let consumed = 0;
37
+ while (consumed < words.length && node.kind !== "leaf") {
38
+ const child = findChild(node, words[consumed] ?? "");
39
+ if (child === undefined)
40
+ break;
41
+ node = child;
42
+ consumed += 1;
43
+ }
44
+ return { node, consumed };
45
+ }
46
+ function prefix(values, current) {
47
+ return [...new Set(values)].filter((value) => value.startsWith(current));
48
+ }
@@ -0,0 +1,32 @@
1
+ import { visibleCommandSections } from "./commandCatalog.js";
2
+ export function renderCommandHelp(node, version) {
3
+ const title = node.path.length === 1
4
+ ? `Yui ${version}`
5
+ : `Yui ${node.path.slice(1).join(" ")}`;
6
+ const lines = [title, ""];
7
+ if (node.summary.length > 0)
8
+ lines.push(node.summary, "");
9
+ lines.push("Usage:", ...node.usage.map((usage) => ` ${usage}`));
10
+ for (const section of visibleCommandSections(node)) {
11
+ const width = Math.max(...section.entries.map((entry) => entry.name.length));
12
+ lines.push("", `${section.title}:`, ...section.entries.map((entry) => ` ${entry.name.padEnd(width)} ${entry.summary}`));
13
+ }
14
+ if (node.options.length > 0) {
15
+ const width = Math.max(...node.options.map((option) => option.length));
16
+ lines.push("", "Options:", ...node.options.map((option) => ` ${option.padEnd(width)} ${describeOption(option)}`));
17
+ }
18
+ return `${lines.join("\n")}\n`;
19
+ }
20
+ function describeOption(option) {
21
+ const descriptions = {
22
+ "--agent": "Select an Agent.",
23
+ "--base": "Set the base Git reference.",
24
+ "--input": "Set dispatch input.",
25
+ "--json": "Emit machine-readable JSON.",
26
+ "--repository": "Select a repository.",
27
+ "--role": "Select a Task role.",
28
+ "--summary": "Set summary text.",
29
+ "--task": "Select a Task."
30
+ };
31
+ return descriptions[option] ?? `Set ${option.slice(2).replaceAll("-", " ")}.`;
32
+ }
@@ -0,0 +1,139 @@
1
+ import { orderRoleOptions } from "./roleOptionCatalog.js";
2
+ export async function getSelectionCandidates(selector, ports, args) {
3
+ switch (selector.provider) {
4
+ case "tasks":
5
+ return entities("task", "Select task", (await list(ports, "task.list", {})).filter((task) => selector.statuses === undefined
6
+ || selector.statuses.includes(stringField(task, "status") ?? "")), ["id", "title", "status"]);
7
+ case "repositories":
8
+ return entities("repository", "Select repository", await list(ports, "repository.list", {}), ["id", "name", "path"]);
9
+ case "configured-agents": {
10
+ const agents = await list(ports, "agent.list", {});
11
+ const config = await optionalEntity(ports, "config.get", {});
12
+ const defaultValue = config === undefined
13
+ ? undefined
14
+ : stringField(config, "defaultAgent");
15
+ const set = entities("agent", "Select Agent", agents.map((agent) => ({
16
+ ...agent,
17
+ default: stringField(agent, "id") === defaultValue ? "default" : ""
18
+ })), ["id", "adapterId", "command", "default"]);
19
+ return { ...set, defaultValue };
20
+ }
21
+ case "global-roles":
22
+ return entities("global role", "Select global Role", await list(ports, "role.list", {}), ["name", "activeAgentId", "workspace"]);
23
+ case "jobs":
24
+ return entities("job", "Select job", await list(ports, "jobs.list", {}), ["id", "type", "status"]);
25
+ case "input-requests": {
26
+ const taskId = dependencyValue(selector, args);
27
+ return entities("input request", taskId === undefined ? "Select input request" : `Select input request: ${taskId}`, (await list(ports, "task.input.list", {
28
+ ...(taskId === undefined ? {} : { taskId }),
29
+ all: true
30
+ })).filter((request) => selector.statuses === undefined
31
+ || selector.statuses.includes(stringField(request, "status") ?? "")), ["id", "taskId", "status", "question"]);
32
+ }
33
+ case "task-roles": {
34
+ const taskId = dependencyValue(selector, args);
35
+ if (taskId === undefined)
36
+ return null;
37
+ const roles = orderRoleOptions(await list(ports, "task.role.list", { taskId }));
38
+ return entities("task role", `Select Task role: ${taskId}`, roles, ["name", "kind", "agentId"]);
39
+ }
40
+ case "task-decisions": {
41
+ const taskId = dependencyValue(selector, args);
42
+ if (taskId === undefined)
43
+ return null;
44
+ const decisions = (await list(ports, "task.decision.list", { taskId })).filter((decision) => selector.statuses === undefined
45
+ || selector.statuses.includes(stringField(decision, "status") ?? ""));
46
+ return entities("decision", `Select Decision: ${taskId}`, decisions, ["id", "status", "title"]);
47
+ }
48
+ case "task-milestones": {
49
+ const taskId = dependencyValue(selector, args);
50
+ if (taskId === undefined)
51
+ return null;
52
+ return entities("milestone", `Select Milestone: ${taskId}`, await list(ports, "task.milestone.list", { taskId }), ["id", "title", "createdAt"]);
53
+ }
54
+ case "task-events": {
55
+ const taskId = dependencyValue(selector, args);
56
+ if (taskId === undefined)
57
+ return null;
58
+ return entities("event", `Select Event: ${taskId}`, await list(ports, "task.event.list", { taskId }), ["id", "type", "createdAt"]);
59
+ }
60
+ case "work-items":
61
+ return entities("work item", "Select work item", await listAllWorkItems(ports), ["id", "title", "status"]);
62
+ case "runs":
63
+ return entities("run", "Select run", await listAllRuns(ports), ["id", "status", "workItemId"]);
64
+ }
65
+ }
66
+ async function listAllWorkItems(ports) {
67
+ const tasks = await list(ports, "task.list", {});
68
+ const groups = await Promise.all(tasks.flatMap((task) => {
69
+ const taskId = stringField(task, "id");
70
+ return taskId === undefined ? [] : [list(ports, "task.work.list", { taskId })];
71
+ }));
72
+ return groups.flat();
73
+ }
74
+ async function listAllRuns(ports) {
75
+ const workItems = await listAllWorkItems(ports);
76
+ const groups = await Promise.all(workItems.flatMap((item) => {
77
+ const workItemId = stringField(item, "id");
78
+ return workItemId === undefined ? [] : [list(ports, "task.run.list", { workItemId })];
79
+ }));
80
+ return groups.flat();
81
+ }
82
+ async function list(ports, method, params) {
83
+ const value = await ports.call(method, params);
84
+ return Array.isArray(value)
85
+ ? value.filter((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry))
86
+ : [];
87
+ }
88
+ async function optionalEntity(ports, method, params) {
89
+ try {
90
+ const value = await ports.call(method, params);
91
+ return typeof value === "object" && value !== null && !Array.isArray(value)
92
+ ? value
93
+ : undefined;
94
+ }
95
+ catch {
96
+ return undefined;
97
+ }
98
+ }
99
+ function dependencyValue(selector, args) {
100
+ if (selector.dependsOn === undefined)
101
+ return undefined;
102
+ const value = args[selector.dependsOn];
103
+ return value === undefined || value.startsWith("--") ? undefined : value;
104
+ }
105
+ function entities(label, title, input, fields) {
106
+ const valueField = fields[0] ?? "id";
107
+ const candidates = input.flatMap((entity) => {
108
+ const value = stringField(entity, valueField) ?? stringField(entity, "id");
109
+ if (value === undefined)
110
+ return [];
111
+ return [{ value, cells: fields.map((field) => displayField(entity[field])) }];
112
+ });
113
+ return {
114
+ entityLabel: label,
115
+ title,
116
+ columns: fields.map((field) => ({
117
+ header: heading(field),
118
+ minWidth: Math.min(12, Math.max(3, field.length)),
119
+ maxWidth: field === "title" || field === "path" ? 48 : 24
120
+ })),
121
+ candidates,
122
+ emptyMessage: `No ${label}s are available.`,
123
+ overflowHint: `Pass the ${label} explicitly.`
124
+ };
125
+ }
126
+ function stringField(entity, name) {
127
+ const value = entity[name];
128
+ return typeof value === "string" && value.length > 0 ? value : undefined;
129
+ }
130
+ function displayField(value) {
131
+ if (typeof value === "string")
132
+ return value;
133
+ if (typeof value === "number" || typeof value === "boolean")
134
+ return String(value);
135
+ return "";
136
+ }
137
+ function heading(field) {
138
+ return `${field[0]?.toUpperCase() ?? ""}${field.slice(1).replaceAll(/([A-Z])/g, " $1")}`;
139
+ }