@plainconceptsplatform/loop-task 2.6.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 (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +534 -0
  3. package/dist/app/App.js +132 -0
  4. package/dist/app/BoardLayout.js +5 -0
  5. package/dist/app/index.js +48 -0
  6. package/dist/app/providers/index.js +1 -0
  7. package/dist/app/router/index.js +16 -0
  8. package/dist/app/types.js +1 -0
  9. package/dist/cli/import-validator.js +152 -0
  10. package/dist/cli/import-writer.js +81 -0
  11. package/dist/cli.js +573 -0
  12. package/dist/client/cli-format.js +7 -0
  13. package/dist/client/commands.js +162 -0
  14. package/dist/client/ipc.js +96 -0
  15. package/dist/client/project-commands.js +164 -0
  16. package/dist/core/command/command-runner.js +300 -0
  17. package/dist/core/command/process-tree.js +73 -0
  18. package/dist/core/command/resolve-cwd.js +8 -0
  19. package/dist/core/command/stdout-capture-transform.js +73 -0
  20. package/dist/core/context/context-parser.js +65 -0
  21. package/dist/core/context/log-context.js +3 -0
  22. package/dist/core/context/template.js +20 -0
  23. package/dist/core/context/validate-context.js +19 -0
  24. package/dist/core/foreground/index.js +81 -0
  25. package/dist/core/logging/bounded-log-reader.js +368 -0
  26. package/dist/core/logging/log-follower.js +26 -0
  27. package/dist/core/logging/log-parser.js +29 -0
  28. package/dist/core/logging/log-rotator.js +34 -0
  29. package/dist/core/logging/rotating-log-stream.js +137 -0
  30. package/dist/core/loop/chain-executor.js +171 -0
  31. package/dist/core/loop/delay-utils.js +65 -0
  32. package/dist/core/loop/loop-controller.js +240 -0
  33. package/dist/core/loop/loop-runner.js +194 -0
  34. package/dist/core/loop/run-executor.js +144 -0
  35. package/dist/core/loop/types.js +1 -0
  36. package/dist/core/scheduling/index.js +16 -0
  37. package/dist/daemon/daemon-log.js +11 -0
  38. package/dist/daemon/diagnostics.js +85 -0
  39. package/dist/daemon/http/helpers.js +81 -0
  40. package/dist/daemon/http/openapi.js +119 -0
  41. package/dist/daemon/http/route-loops.js +311 -0
  42. package/dist/daemon/http/route-misc.js +60 -0
  43. package/dist/daemon/http/route-projects.js +55 -0
  44. package/dist/daemon/http/route-tasks.js +141 -0
  45. package/dist/daemon/http/routes.js +19 -0
  46. package/dist/daemon/http/server.js +82 -0
  47. package/dist/daemon/http/sse.js +36 -0
  48. package/dist/daemon/index.js +189 -0
  49. package/dist/daemon/ipc/send.js +5 -0
  50. package/dist/daemon/managers/loop-entry.js +27 -0
  51. package/dist/daemon/managers/loop-manager.js +346 -0
  52. package/dist/daemon/managers/loop-options.js +17 -0
  53. package/dist/daemon/managers/loop-serialization.js +53 -0
  54. package/dist/daemon/managers/project-manager.js +170 -0
  55. package/dist/daemon/managers/task-manager.js +94 -0
  56. package/dist/daemon/mcp/index.js +2 -0
  57. package/dist/daemon/mcp/openapi-sync.js +50 -0
  58. package/dist/daemon/mcp/server.js +167 -0
  59. package/dist/daemon/mcp/tools.js +382 -0
  60. package/dist/daemon/recipe/deferred-reload.js +32 -0
  61. package/dist/daemon/recipe/diagram-reader.js +26 -0
  62. package/dist/daemon/recipe/file-writer.js +52 -0
  63. package/dist/daemon/recipe/id-remapper.js +46 -0
  64. package/dist/daemon/recipe/runtime-state.js +23 -0
  65. package/dist/daemon/recipe/scanner.js +165 -0
  66. package/dist/daemon/recipe/task-store.js +33 -0
  67. package/dist/daemon/recipe/validator.js +35 -0
  68. package/dist/daemon/server/handlers/diagnostics-handlers.js +7 -0
  69. package/dist/daemon/server/handlers/index.js +101 -0
  70. package/dist/daemon/server/handlers/log-handlers.js +129 -0
  71. package/dist/daemon/server/handlers/loop-handlers.js +64 -0
  72. package/dist/daemon/server/handlers/project-handlers.js +42 -0
  73. package/dist/daemon/server/handlers/settings-handlers.js +8 -0
  74. package/dist/daemon/server/handlers/task-handlers.js +20 -0
  75. package/dist/daemon/server/handlers/telemetry-handlers.js +5 -0
  76. package/dist/daemon/server/index.js +117 -0
  77. package/dist/daemon/settings-manager.js +73 -0
  78. package/dist/daemon/spawner/index.js +204 -0
  79. package/dist/daemon/state/index.js +209 -0
  80. package/dist/daemon/telemetry/agent-integrations/agent-integration.js +1 -0
  81. package/dist/daemon/telemetry/agent-integrations/claude-code-integration.js +55 -0
  82. package/dist/daemon/telemetry/agent-integrations/detect-agent-integration.js +20 -0
  83. package/dist/daemon/telemetry/agent-integrations/index.js +3 -0
  84. package/dist/daemon/telemetry/agent-integrations/opencode-integration.js +72 -0
  85. package/dist/daemon/telemetry/index.js +6 -0
  86. package/dist/daemon/telemetry/noop-telemetry-adapter.js +53 -0
  87. package/dist/daemon/telemetry/open-telemetry-adapter.js +464 -0
  88. package/dist/daemon/telemetry/telemetry-manager.js +147 -0
  89. package/dist/daemon/telemetry/telemetry-redaction.js +67 -0
  90. package/dist/daemon/telemetry/telemetry-types.js +45 -0
  91. package/dist/daemon/telemetry/telemetry.js +1 -0
  92. package/dist/daemon/watcher/index.js +250 -0
  93. package/dist/duration.js +24 -0
  94. package/dist/entities/loops/filters.js +109 -0
  95. package/dist/entities/loops/index.js +1 -0
  96. package/dist/entities/projects/filters.js +69 -0
  97. package/dist/entities/projects/index.js +1 -0
  98. package/dist/entities/tasks/filters.js +41 -0
  99. package/dist/entities/tasks/index.js +1 -0
  100. package/dist/entry.js +16 -0
  101. package/dist/esm-loader.js +39 -0
  102. package/dist/features/chain-editor/ChainEditor.js +116 -0
  103. package/dist/features/chain-editor/mermaidToAscii.js +375 -0
  104. package/dist/features/chain-editor/renderChainDiagram.js +106 -0
  105. package/dist/features/code-editor/CodeEditorModal.js +84 -0
  106. package/dist/features/code-editor/CodeEditorPreview.js +14 -0
  107. package/dist/features/code-editor/useEditorKeyboard.js +150 -0
  108. package/dist/features/code-editor/useModalDimensions.js +9 -0
  109. package/dist/features/commands/commands.js +132 -0
  110. package/dist/features/commands/useCommandHandlers.js +280 -0
  111. package/dist/features/commands/useContextualActions.js +78 -0
  112. package/dist/features/commands/useGlobalShortcuts.js +154 -0
  113. package/dist/features/forms/FormRouter.js +46 -0
  114. package/dist/features/overlays/ContextHelpModal.js +8 -0
  115. package/dist/features/overlays/DiagramModal.js +45 -0
  116. package/dist/features/overlays/ExportModal.js +51 -0
  117. package/dist/features/overlays/HelpGuideModal.js +23 -0
  118. package/dist/features/overlays/HelpModal.js +59 -0
  119. package/dist/features/overlays/OverlayStack.js +10 -0
  120. package/dist/features/overlays/ProjectsModal.js +42 -0
  121. package/dist/features/overlays/TaskPickerModal.js +62 -0
  122. package/dist/features/overlays/WelcomeScreen.js +74 -0
  123. package/dist/features/overlays/useOverlayStack.js +55 -0
  124. package/dist/features/state/useAppState.js +151 -0
  125. package/dist/logger.js +25 -0
  126. package/dist/loop-config.js +152 -0
  127. package/dist/shared/clipboard.js +111 -0
  128. package/dist/shared/config/constants.js +104 -0
  129. package/dist/shared/config/paths.js +67 -0
  130. package/dist/shared/container/index.js +24 -0
  131. package/dist/shared/fs-utils.js +44 -0
  132. package/dist/shared/hooks/useBreakpoint.js +11 -0
  133. package/dist/shared/hooks/useDaemonSettings.js +41 -0
  134. package/dist/shared/hooks/useInject.js +6 -0
  135. package/dist/shared/hooks/useLogStream.js +37 -0
  136. package/dist/shared/hooks/useLoopFormValidation.js +131 -0
  137. package/dist/shared/hooks/useLoopPolling.js +30 -0
  138. package/dist/shared/hooks/useMouseScroll.js +56 -0
  139. package/dist/shared/hooks/useUndoRedo.js +77 -0
  140. package/dist/shared/i18n/en.json +709 -0
  141. package/dist/shared/i18n/index.js +11 -0
  142. package/dist/shared/providers/InversifyProvider.js +6 -0
  143. package/dist/shared/services/export-service.js +43 -0
  144. package/dist/shared/services/log-service.js +28 -0
  145. package/dist/shared/services/loop-service.js +78 -0
  146. package/dist/shared/services/project-service.js +39 -0
  147. package/dist/shared/services/settings-service.js +102 -0
  148. package/dist/shared/services/task-service.js +41 -0
  149. package/dist/shared/services/types.js +8 -0
  150. package/dist/shared/sleep.js +17 -0
  151. package/dist/shared/tail.js +4 -0
  152. package/dist/shared/ui/Button.js +18 -0
  153. package/dist/shared/ui/DebugPanel.js +9 -0
  154. package/dist/shared/ui/FocusableButton.js +18 -0
  155. package/dist/shared/ui/FocusableInput.js +72 -0
  156. package/dist/shared/ui/FocusableList.js +48 -0
  157. package/dist/shared/ui/Modal.js +13 -0
  158. package/dist/shared/ui/SelectModal.js +78 -0
  159. package/dist/shared/ui/Toast.js +37 -0
  160. package/dist/shared/ui/format.js +117 -0
  161. package/dist/shared/ui/hooks/useHoverState.js +11 -0
  162. package/dist/shared/ui/state.js +9 -0
  163. package/dist/shared/ui/theme.js +96 -0
  164. package/dist/shared/utils/log-lines.js +13 -0
  165. package/dist/shared/utils/paste.js +37 -0
  166. package/dist/shared/utils/syntax.js +128 -0
  167. package/dist/shared/utils/validation.js +56 -0
  168. package/dist/types.js +1 -0
  169. package/dist/visual-evidence/capture.js +51 -0
  170. package/dist/visual-evidence/cli.js +85 -0
  171. package/dist/visual-evidence/evidence-required.js +71 -0
  172. package/dist/visual-evidence/index.js +10 -0
  173. package/dist/visual-evidence/launch.js +61 -0
  174. package/dist/visual-evidence/manifest.js +32 -0
  175. package/dist/visual-evidence/openspec-resolver.js +45 -0
  176. package/dist/visual-evidence/publish.js +126 -0
  177. package/dist/visual-evidence/run.js +121 -0
  178. package/dist/visual-evidence/scenario-registry.js +132 -0
  179. package/dist/widgets/command-input/CommandDropdown.js +69 -0
  180. package/dist/widgets/command-input/CommandInput.js +133 -0
  181. package/dist/widgets/command-input/HintBar.js +9 -0
  182. package/dist/widgets/command-input/InputModes.js +72 -0
  183. package/dist/widgets/commands-browser/CommandsBrowserModal.js +85 -0
  184. package/dist/widgets/header/Header.js +54 -0
  185. package/dist/widgets/header/TabBar.js +20 -0
  186. package/dist/widgets/left-panel/LeftPanel.js +34 -0
  187. package/dist/widgets/left-panel/Navigator.js +107 -0
  188. package/dist/widgets/left-panel/ProjectsPage.js +76 -0
  189. package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
  190. package/dist/widgets/left-panel/TaskBrowser.js +132 -0
  191. package/dist/widgets/log-modal/LogModal.js +211 -0
  192. package/dist/widgets/loop-form/CreateForm.js +82 -0
  193. package/dist/widgets/loop-form/TextField.js +9 -0
  194. package/dist/widgets/loop-form/WizardForm.js +199 -0
  195. package/dist/widgets/loop-form/useCreateSteps.js +135 -0
  196. package/dist/widgets/loop-form/useHandleComplete.js +94 -0
  197. package/dist/widgets/project-form/ProjectForm.js +83 -0
  198. package/dist/widgets/right-panel/Inspector.js +41 -0
  199. package/dist/widgets/right-panel/RightPanel.js +38 -0
  200. package/dist/widgets/right-panel/RunHistory.js +187 -0
  201. package/dist/widgets/task-form/TaskForm.js +272 -0
  202. package/package.json +119 -0
@@ -0,0 +1,162 @@
1
+ import { formatDuration } from "../duration.js";
2
+ import { t } from "../shared/i18n/index.js";
3
+ import { sendRequest, streamRequest } from "./ipc.js";
4
+ import { formatCmd, pad } from "./cli-format.js";
5
+ export async function createBackgroundLoop(options, intervalHuman) {
6
+ const response = await sendRequest({
7
+ type: "start",
8
+ payload: { ...options, intervalHuman },
9
+ });
10
+ if (response.type !== "ok") {
11
+ throw new Error(response.message);
12
+ }
13
+ return response.data.id;
14
+ }
15
+ export async function startLoop(options, intervalHuman) {
16
+ const id = await createBackgroundLoop(options, intervalHuman).catch((error) => {
17
+ const message = error instanceof Error ? error.message : String(error);
18
+ console.error(t("cli.error", { message }));
19
+ process.exit(1);
20
+ });
21
+ console.log(t("cli.startedTitle"));
22
+ console.log(t("cli.startedId", { id }));
23
+ console.log(t("cli.startedCommand", { command: options.command }));
24
+ console.log(t("cli.startedInterval", { interval: intervalHuman }));
25
+ console.log(t("cli.startedStatus"));
26
+ if (options.interval === 0) {
27
+ console.log(" Note: manual loop use 'trigger' to run on demand");
28
+ }
29
+ console.log();
30
+ console.log(t("cli.startedHint"));
31
+ process.exit(0);
32
+ }
33
+ export async function listLoops() {
34
+ const response = await sendRequest({ type: "list" });
35
+ if (response.type !== "ok") {
36
+ console.error(t("cli.error", { message: response.message }));
37
+ process.exit(1);
38
+ }
39
+ const loops = response.data;
40
+ if (loops.length === 0) {
41
+ console.log(t("cli.noLoops"));
42
+ return;
43
+ }
44
+ const idW = Math.max(4, ...loops.map((l) => l.id.length)) + 2;
45
+ const cmdW = Math.max(7, ...loops.map((l) => formatCmd(l.command, l.commandArgs).length)) + 2;
46
+ const intW = 10;
47
+ const statusW = 10;
48
+ const runsW = 6;
49
+ const header = pad(t("cli.headerId"), idW) +
50
+ pad(t("cli.headerCommand"), cmdW) +
51
+ pad(t("cli.headerInterval"), intW) +
52
+ pad(t("cli.headerStatus"), statusW) +
53
+ pad(t("cli.headerRuns"), runsW) +
54
+ t("cli.headerLastExit");
55
+ console.log(header);
56
+ console.log("-".repeat(header.length));
57
+ for (const loop of loops) {
58
+ const exit = loop.lastExitCode !== null ? String(loop.lastExitCode) : "-";
59
+ console.log(pad(loop.id, idW) +
60
+ pad(formatCmd(loop.command, loop.commandArgs), cmdW) +
61
+ pad(loop.intervalHuman, intW) +
62
+ pad(loop.status, statusW) +
63
+ pad(String(loop.runCount), runsW) +
64
+ exit);
65
+ }
66
+ process.exit(0);
67
+ }
68
+ export async function showStatus(id) {
69
+ const response = await sendRequest({ type: "status", payload: { id } });
70
+ if (response.type !== "ok") {
71
+ console.error(t("cli.error", { message: response.message }));
72
+ process.exit(1);
73
+ }
74
+ const loop = response.data;
75
+ const cmd = `${loop.command} ${loop.commandArgs.join(" ")}`.trim();
76
+ const maxRuns = loop.maxRuns !== null ? String(loop.maxRuns) : t("cli.unlimited");
77
+ console.log(t("cli.statusTitle", { id: loop.id }));
78
+ console.log(t("cli.statusCommand", { command: cmd }));
79
+ const intervalDisplay = loop.interval === 0 ? "manual" : formatDuration(loop.interval);
80
+ console.log(t("cli.statusInterval", { interval: loop.intervalHuman, duration: intervalDisplay }));
81
+ console.log(t("cli.statusStatus", { status: loop.status }));
82
+ console.log(t("cli.statusRuns", { runs: loop.runCount, maxRuns }));
83
+ if ((loop.silentChainCount ?? 0) > 0) {
84
+ console.log(t("cli.statusSilentChains", { count: loop.silentChainCount.toLocaleString() }));
85
+ }
86
+ console.log(t("cli.statusCreated", { created: loop.createdAt }));
87
+ if (loop.lastRunAt) {
88
+ const exitInfo = loop.lastExitCode !== null ? t("cli.exitInfo", { code: loop.lastExitCode }) : "";
89
+ const durInfo = loop.lastDuration !== null ? formatDuration(loop.lastDuration) : "";
90
+ console.log(t("cli.statusLastRun", { lastRun: loop.lastRunAt, exit: exitInfo, duration: durInfo }).trim());
91
+ }
92
+ if (loop.nextRunAt) {
93
+ console.log(t("cli.statusNextRun", { nextRun: loop.nextRunAt }));
94
+ }
95
+ process.exit(0);
96
+ }
97
+ export async function pauseLoop(id) {
98
+ const response = await sendRequest({ type: "pause", payload: { id } });
99
+ if (response.type !== "ok") {
100
+ console.error(t("cli.error", { message: response.message }));
101
+ process.exit(1);
102
+ }
103
+ console.log(t("cli.paused", { id }));
104
+ process.exit(0);
105
+ }
106
+ export async function resumeLoop(id) {
107
+ const response = await sendRequest({ type: "resume", payload: { id } });
108
+ if (response.type !== "ok") {
109
+ console.error(t("cli.error", { message: response.message }));
110
+ process.exit(1);
111
+ }
112
+ console.log(t("cli.resumed", { id }));
113
+ process.exit(0);
114
+ }
115
+ export async function deleteLoop(id) {
116
+ const response = await sendRequest({ type: "delete", payload: { id } });
117
+ if (response.type !== "ok") {
118
+ console.error(t("cli.error", { message: response.message }));
119
+ process.exit(1);
120
+ }
121
+ console.log(t("cli.deleted", { id }));
122
+ process.exit(0);
123
+ }
124
+ export async function viewLogs(id, follow, tail) {
125
+ if (!follow) {
126
+ const response = await sendRequest({
127
+ type: "logs",
128
+ payload: { id, follow: false, tail },
129
+ });
130
+ if (response.type !== "ok") {
131
+ console.error(t("cli.error", { message: response.message }));
132
+ process.exit(1);
133
+ }
134
+ const content = response.data;
135
+ if (content) {
136
+ process.stdout.write(content);
137
+ if (!content.endsWith("\n"))
138
+ process.stdout.write("\n");
139
+ }
140
+ process.exit(0);
141
+ }
142
+ const socket = streamRequest({ type: "logs", payload: { id, follow: true, tail } }, (line) => console.log(line), () => process.exit(0), (err) => {
143
+ console.error(t("cli.error", { message: err.message }));
144
+ process.exit(1);
145
+ });
146
+ process.on("SIGINT", () => {
147
+ socket.destroy();
148
+ process.exit(0);
149
+ });
150
+ }
151
+ export async function attachLoop(id) {
152
+ console.log(t("cli.attaching", { id }));
153
+ const socket = streamRequest({ type: "attach", payload: { id } }, (line) => console.log(line), () => process.exit(0), (err) => {
154
+ console.error(t("cli.error", { message: err.message }));
155
+ process.exit(1);
156
+ });
157
+ process.on("SIGINT", () => {
158
+ console.log(t("cli.detached"));
159
+ socket.destroy();
160
+ process.exit(0);
161
+ });
162
+ }
@@ -0,0 +1,96 @@
1
+ import net from "node:net";
2
+ import { ensureDaemon, getSocket } from "../daemon/spawner/index.js";
3
+ import { t } from "../shared/i18n/index.js";
4
+ import { IPC_TIMEOUT_MS } from "../shared/config/constants.js";
5
+ export async function sendRequest(request) {
6
+ ensureDaemon();
7
+ const socketPath = getSocket();
8
+ return new Promise((resolve, reject) => {
9
+ let resolved = false;
10
+ const socket = net.createConnection(socketPath, () => {
11
+ socket.write(JSON.stringify(request) + "\n");
12
+ });
13
+ let buffer = "";
14
+ socket.on("data", (chunk) => {
15
+ buffer += chunk.toString();
16
+ const lines = buffer.split("\n");
17
+ buffer = lines.pop() ?? "";
18
+ for (const line of lines) {
19
+ if (line.trim() && !resolved) {
20
+ try {
21
+ const response = JSON.parse(line);
22
+ resolved = true;
23
+ socket.destroy();
24
+ resolve(response);
25
+ }
26
+ catch {
27
+ resolved = true;
28
+ socket.destroy();
29
+ reject(new Error(t("errors.invalidResponse")));
30
+ }
31
+ }
32
+ }
33
+ });
34
+ socket.on("close", () => {
35
+ if (!resolved) {
36
+ resolved = true;
37
+ reject(new Error(t("errors.connectionClosed")));
38
+ }
39
+ });
40
+ socket.on("error", (err) => {
41
+ if (!resolved) {
42
+ resolved = true;
43
+ reject(new Error(t("errors.cannotConnect", { message: err.message })));
44
+ }
45
+ });
46
+ socket.setTimeout(IPC_TIMEOUT_MS, () => {
47
+ if (!resolved) {
48
+ resolved = true;
49
+ socket.destroy();
50
+ reject(new Error(t("errors.requestTimedOut")));
51
+ }
52
+ });
53
+ });
54
+ }
55
+ export function streamRequest(request, onLine, onEnd, onError) {
56
+ ensureDaemon();
57
+ const socketPath = getSocket();
58
+ const socket = net.createConnection(socketPath, () => {
59
+ socket.write(JSON.stringify(request) + "\n");
60
+ });
61
+ let buffer = "";
62
+ socket.on("data", (chunk) => {
63
+ buffer += chunk.toString();
64
+ const lines = buffer.split("\n");
65
+ buffer = lines.pop() ?? "";
66
+ for (const line of lines) {
67
+ if (line.trim()) {
68
+ try {
69
+ const response = JSON.parse(line);
70
+ if (response.type === "data") {
71
+ onLine(response.line);
72
+ }
73
+ else if (response.type === "error") {
74
+ onError(new Error(response.message));
75
+ socket.destroy();
76
+ }
77
+ else if (response.type === "end") {
78
+ onEnd();
79
+ socket.destroy();
80
+ }
81
+ }
82
+ catch {
83
+ onError(new Error(t("errors.invalidResponse")));
84
+ socket.destroy();
85
+ }
86
+ }
87
+ }
88
+ });
89
+ socket.on("error", (err) => {
90
+ onError(new Error(t("errors.cannotConnect", { message: err.message })));
91
+ });
92
+ socket.on("close", () => {
93
+ onEnd();
94
+ });
95
+ return socket;
96
+ }
@@ -0,0 +1,164 @@
1
+ import { PROJECT_COLORS } from "../shared/config/constants.js";
2
+ import { t } from "../shared/i18n/index.js";
3
+ import { sendRequest } from "./ipc.js";
4
+ import { pad } from "./cli-format.js";
5
+ async function fetchProjects() {
6
+ const response = await sendRequest({ type: "project-list" });
7
+ if (response.type !== "ok") {
8
+ throw new Error(response.message);
9
+ }
10
+ return response.data;
11
+ }
12
+ export function resolveColor(input) {
13
+ if (PROJECT_COLORS[input])
14
+ return PROJECT_COLORS[input];
15
+ if (/^#[0-9a-fA-F]{6}$/.test(input))
16
+ return input;
17
+ throw new Error(t("cli.projectInvalidColor", {
18
+ color: input,
19
+ valid: Object.keys(PROJECT_COLORS).join(", "),
20
+ }));
21
+ }
22
+ export async function resolveProjectId(idOrName) {
23
+ const projects = await fetchProjects();
24
+ const byId = projects.find((p) => p.id === idOrName);
25
+ if (byId)
26
+ return byId.id;
27
+ const matches = projects.filter((p) => p.name.toLowerCase() === idOrName.toLowerCase());
28
+ if (matches.length === 1)
29
+ return matches[0].id;
30
+ if (matches.length === 0)
31
+ throw new Error(t("cli.projectNotFound", { name: idOrName }));
32
+ throw new Error(t("cli.projectAmbiguous", { name: idOrName }));
33
+ }
34
+ export async function listProjectsCli() {
35
+ try {
36
+ const projects = await fetchProjects();
37
+ if (projects.length === 0) {
38
+ console.log(t("cli.noProjects"));
39
+ process.exit(0);
40
+ }
41
+ const loopResponse = await sendRequest({ type: "list" });
42
+ const loops = loopResponse.type === "ok" ? loopResponse.data : [];
43
+ const counts = new Map();
44
+ for (const loop of loops) {
45
+ counts.set(loop.projectId, (counts.get(loop.projectId) ?? 0) + 1);
46
+ }
47
+ const nameW = Math.max(t("cli.projectHeaderName").length, ...projects.map((p) => p.name.length)) + 2;
48
+ const idW = Math.max(t("cli.projectHeaderId").length, ...projects.map((p) => p.id.length)) + 2;
49
+ const colorW = Math.max(t("cli.projectHeaderColor").length, ...projects.map((p) => p.color.length)) + 2;
50
+ const loopsW = t("cli.projectHeaderLoops").length + 2;
51
+ const header = pad(t("cli.projectHeaderName"), nameW) +
52
+ pad(t("cli.projectHeaderId"), idW) +
53
+ pad(t("cli.projectHeaderColor"), colorW) +
54
+ pad(t("cli.projectHeaderLoops"), loopsW) +
55
+ t("cli.projectHeaderSystem");
56
+ console.log(header);
57
+ console.log("-".repeat(header.length));
58
+ for (const project of projects) {
59
+ console.log(pad(project.name, nameW) +
60
+ pad(project.id, idW) +
61
+ pad(project.color, colorW) +
62
+ pad(String(counts.get(project.id) ?? 0), loopsW) +
63
+ (project.isSystem ? "yes" : ""));
64
+ }
65
+ process.exit(0);
66
+ }
67
+ catch (error) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ console.error(t("cli.error", { message }));
70
+ process.exit(1);
71
+ }
72
+ }
73
+ export async function createProjectCli(name, colorInput, directory, githubSource) {
74
+ try {
75
+ if (githubSource !== undefined && githubSource !== "") {
76
+ if (!/^[a-zA-Z0-9_.\-]+\/[a-zA-Z0-9_.\-]+$/.test(githubSource)) {
77
+ throw new Error(`Invalid github-source format: "${githubSource}". Expected owner/repo (e.g. CKGrafico/loop-task)`);
78
+ }
79
+ }
80
+ const color = colorInput ? resolveColor(colorInput) : PROJECT_COLORS.cyan;
81
+ const response = await sendRequest({
82
+ type: "project-create",
83
+ payload: { name, color, directory, githubSource },
84
+ });
85
+ if (response.type !== "ok") {
86
+ throw new Error(response.message);
87
+ }
88
+ const id = response.data.id;
89
+ console.log(t("cli.projectCreated", { name, id }));
90
+ process.exit(0);
91
+ }
92
+ catch (error) {
93
+ const message = error instanceof Error ? error.message : String(error);
94
+ console.error(t("cli.error", { message }));
95
+ process.exit(1);
96
+ }
97
+ }
98
+ export async function renameProjectCli(idOrName, newName) {
99
+ try {
100
+ const id = await resolveProjectId(idOrName);
101
+ const response = await sendRequest({
102
+ type: "project-update",
103
+ payload: { id, name: newName },
104
+ });
105
+ if (response.type !== "ok") {
106
+ throw new Error(response.message);
107
+ }
108
+ console.log(t("cli.projectRenamed", { name: newName }));
109
+ process.exit(0);
110
+ }
111
+ catch (error) {
112
+ const message = error instanceof Error ? error.message : String(error);
113
+ console.error(t("cli.error", { message }));
114
+ process.exit(1);
115
+ }
116
+ }
117
+ export async function setProjectColorCli(idOrName, colorInput) {
118
+ try {
119
+ const projects = await fetchProjects();
120
+ const byId = projects.find((p) => p.id === idOrName);
121
+ const matches = byId
122
+ ? [byId]
123
+ : projects.filter((p) => p.name.toLowerCase() === idOrName.toLowerCase());
124
+ if (matches.length === 0)
125
+ throw new Error(t("cli.projectNotFound", { name: idOrName }));
126
+ if (matches.length > 1)
127
+ throw new Error(t("cli.projectAmbiguous", { name: idOrName }));
128
+ const project = matches[0];
129
+ const color = resolveColor(colorInput);
130
+ const response = await sendRequest({
131
+ type: "project-update",
132
+ payload: { id: project.id, name: project.name, color },
133
+ });
134
+ if (response.type !== "ok") {
135
+ throw new Error(response.message);
136
+ }
137
+ console.log(t("cli.projectColorSet", { name: project.name, color }));
138
+ process.exit(0);
139
+ }
140
+ catch (error) {
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ console.error(t("cli.error", { message }));
143
+ process.exit(1);
144
+ }
145
+ }
146
+ export async function deleteProjectCli(idOrName) {
147
+ try {
148
+ const id = await resolveProjectId(idOrName);
149
+ const response = await sendRequest({
150
+ type: "project-delete",
151
+ payload: { id },
152
+ });
153
+ if (response.type !== "ok") {
154
+ throw new Error(response.message);
155
+ }
156
+ console.log(t("cli.projectDeleted", { name: idOrName }));
157
+ process.exit(0);
158
+ }
159
+ catch (error) {
160
+ const message = error instanceof Error ? error.message : String(error);
161
+ console.error(t("cli.error", { message }));
162
+ process.exit(1);
163
+ }
164
+ }