@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.
- package/LICENSE +21 -0
- package/README.md +534 -0
- package/dist/app/App.js +132 -0
- package/dist/app/BoardLayout.js +5 -0
- package/dist/app/index.js +48 -0
- package/dist/app/providers/index.js +1 -0
- package/dist/app/router/index.js +16 -0
- package/dist/app/types.js +1 -0
- package/dist/cli/import-validator.js +152 -0
- package/dist/cli/import-writer.js +81 -0
- package/dist/cli.js +573 -0
- package/dist/client/cli-format.js +7 -0
- package/dist/client/commands.js +162 -0
- package/dist/client/ipc.js +96 -0
- package/dist/client/project-commands.js +164 -0
- package/dist/core/command/command-runner.js +300 -0
- package/dist/core/command/process-tree.js +73 -0
- package/dist/core/command/resolve-cwd.js +8 -0
- package/dist/core/command/stdout-capture-transform.js +73 -0
- package/dist/core/context/context-parser.js +65 -0
- package/dist/core/context/log-context.js +3 -0
- package/dist/core/context/template.js +20 -0
- package/dist/core/context/validate-context.js +19 -0
- package/dist/core/foreground/index.js +81 -0
- package/dist/core/logging/bounded-log-reader.js +368 -0
- package/dist/core/logging/log-follower.js +26 -0
- package/dist/core/logging/log-parser.js +29 -0
- package/dist/core/logging/log-rotator.js +34 -0
- package/dist/core/logging/rotating-log-stream.js +137 -0
- package/dist/core/loop/chain-executor.js +171 -0
- package/dist/core/loop/delay-utils.js +65 -0
- package/dist/core/loop/loop-controller.js +240 -0
- package/dist/core/loop/loop-runner.js +194 -0
- package/dist/core/loop/run-executor.js +144 -0
- package/dist/core/loop/types.js +1 -0
- package/dist/core/scheduling/index.js +16 -0
- package/dist/daemon/daemon-log.js +11 -0
- package/dist/daemon/diagnostics.js +85 -0
- package/dist/daemon/http/helpers.js +81 -0
- package/dist/daemon/http/openapi.js +119 -0
- package/dist/daemon/http/route-loops.js +311 -0
- package/dist/daemon/http/route-misc.js +60 -0
- package/dist/daemon/http/route-projects.js +55 -0
- package/dist/daemon/http/route-tasks.js +141 -0
- package/dist/daemon/http/routes.js +19 -0
- package/dist/daemon/http/server.js +82 -0
- package/dist/daemon/http/sse.js +36 -0
- package/dist/daemon/index.js +189 -0
- package/dist/daemon/ipc/send.js +5 -0
- package/dist/daemon/managers/loop-entry.js +27 -0
- package/dist/daemon/managers/loop-manager.js +346 -0
- package/dist/daemon/managers/loop-options.js +17 -0
- package/dist/daemon/managers/loop-serialization.js +53 -0
- package/dist/daemon/managers/project-manager.js +170 -0
- package/dist/daemon/managers/task-manager.js +94 -0
- package/dist/daemon/mcp/index.js +2 -0
- package/dist/daemon/mcp/openapi-sync.js +50 -0
- package/dist/daemon/mcp/server.js +167 -0
- package/dist/daemon/mcp/tools.js +382 -0
- package/dist/daemon/recipe/deferred-reload.js +32 -0
- package/dist/daemon/recipe/diagram-reader.js +26 -0
- package/dist/daemon/recipe/file-writer.js +52 -0
- package/dist/daemon/recipe/id-remapper.js +46 -0
- package/dist/daemon/recipe/runtime-state.js +23 -0
- package/dist/daemon/recipe/scanner.js +165 -0
- package/dist/daemon/recipe/task-store.js +33 -0
- package/dist/daemon/recipe/validator.js +35 -0
- package/dist/daemon/server/handlers/diagnostics-handlers.js +7 -0
- package/dist/daemon/server/handlers/index.js +101 -0
- package/dist/daemon/server/handlers/log-handlers.js +129 -0
- package/dist/daemon/server/handlers/loop-handlers.js +64 -0
- package/dist/daemon/server/handlers/project-handlers.js +42 -0
- package/dist/daemon/server/handlers/settings-handlers.js +8 -0
- package/dist/daemon/server/handlers/task-handlers.js +20 -0
- package/dist/daemon/server/handlers/telemetry-handlers.js +5 -0
- package/dist/daemon/server/index.js +117 -0
- package/dist/daemon/settings-manager.js +73 -0
- package/dist/daemon/spawner/index.js +204 -0
- package/dist/daemon/state/index.js +209 -0
- package/dist/daemon/telemetry/agent-integrations/agent-integration.js +1 -0
- package/dist/daemon/telemetry/agent-integrations/claude-code-integration.js +55 -0
- package/dist/daemon/telemetry/agent-integrations/detect-agent-integration.js +20 -0
- package/dist/daemon/telemetry/agent-integrations/index.js +3 -0
- package/dist/daemon/telemetry/agent-integrations/opencode-integration.js +72 -0
- package/dist/daemon/telemetry/index.js +6 -0
- package/dist/daemon/telemetry/noop-telemetry-adapter.js +53 -0
- package/dist/daemon/telemetry/open-telemetry-adapter.js +464 -0
- package/dist/daemon/telemetry/telemetry-manager.js +147 -0
- package/dist/daemon/telemetry/telemetry-redaction.js +67 -0
- package/dist/daemon/telemetry/telemetry-types.js +45 -0
- package/dist/daemon/telemetry/telemetry.js +1 -0
- package/dist/daemon/watcher/index.js +250 -0
- package/dist/duration.js +24 -0
- package/dist/entities/loops/filters.js +109 -0
- package/dist/entities/loops/index.js +1 -0
- package/dist/entities/projects/filters.js +69 -0
- package/dist/entities/projects/index.js +1 -0
- package/dist/entities/tasks/filters.js +41 -0
- package/dist/entities/tasks/index.js +1 -0
- package/dist/entry.js +16 -0
- package/dist/esm-loader.js +39 -0
- package/dist/features/chain-editor/ChainEditor.js +116 -0
- package/dist/features/chain-editor/mermaidToAscii.js +375 -0
- package/dist/features/chain-editor/renderChainDiagram.js +106 -0
- package/dist/features/code-editor/CodeEditorModal.js +84 -0
- package/dist/features/code-editor/CodeEditorPreview.js +14 -0
- package/dist/features/code-editor/useEditorKeyboard.js +150 -0
- package/dist/features/code-editor/useModalDimensions.js +9 -0
- package/dist/features/commands/commands.js +132 -0
- package/dist/features/commands/useCommandHandlers.js +280 -0
- package/dist/features/commands/useContextualActions.js +78 -0
- package/dist/features/commands/useGlobalShortcuts.js +154 -0
- package/dist/features/forms/FormRouter.js +46 -0
- package/dist/features/overlays/ContextHelpModal.js +8 -0
- package/dist/features/overlays/DiagramModal.js +45 -0
- package/dist/features/overlays/ExportModal.js +51 -0
- package/dist/features/overlays/HelpGuideModal.js +23 -0
- package/dist/features/overlays/HelpModal.js +59 -0
- package/dist/features/overlays/OverlayStack.js +10 -0
- package/dist/features/overlays/ProjectsModal.js +42 -0
- package/dist/features/overlays/TaskPickerModal.js +62 -0
- package/dist/features/overlays/WelcomeScreen.js +74 -0
- package/dist/features/overlays/useOverlayStack.js +55 -0
- package/dist/features/state/useAppState.js +151 -0
- package/dist/logger.js +25 -0
- package/dist/loop-config.js +152 -0
- package/dist/shared/clipboard.js +111 -0
- package/dist/shared/config/constants.js +104 -0
- package/dist/shared/config/paths.js +67 -0
- package/dist/shared/container/index.js +24 -0
- package/dist/shared/fs-utils.js +44 -0
- package/dist/shared/hooks/useBreakpoint.js +11 -0
- package/dist/shared/hooks/useDaemonSettings.js +41 -0
- package/dist/shared/hooks/useInject.js +6 -0
- package/dist/shared/hooks/useLogStream.js +37 -0
- package/dist/shared/hooks/useLoopFormValidation.js +131 -0
- package/dist/shared/hooks/useLoopPolling.js +30 -0
- package/dist/shared/hooks/useMouseScroll.js +56 -0
- package/dist/shared/hooks/useUndoRedo.js +77 -0
- package/dist/shared/i18n/en.json +709 -0
- package/dist/shared/i18n/index.js +11 -0
- package/dist/shared/providers/InversifyProvider.js +6 -0
- package/dist/shared/services/export-service.js +43 -0
- package/dist/shared/services/log-service.js +28 -0
- package/dist/shared/services/loop-service.js +78 -0
- package/dist/shared/services/project-service.js +39 -0
- package/dist/shared/services/settings-service.js +102 -0
- package/dist/shared/services/task-service.js +41 -0
- package/dist/shared/services/types.js +8 -0
- package/dist/shared/sleep.js +17 -0
- package/dist/shared/tail.js +4 -0
- package/dist/shared/ui/Button.js +18 -0
- package/dist/shared/ui/DebugPanel.js +9 -0
- package/dist/shared/ui/FocusableButton.js +18 -0
- package/dist/shared/ui/FocusableInput.js +72 -0
- package/dist/shared/ui/FocusableList.js +48 -0
- package/dist/shared/ui/Modal.js +13 -0
- package/dist/shared/ui/SelectModal.js +78 -0
- package/dist/shared/ui/Toast.js +37 -0
- package/dist/shared/ui/format.js +117 -0
- package/dist/shared/ui/hooks/useHoverState.js +11 -0
- package/dist/shared/ui/state.js +9 -0
- package/dist/shared/ui/theme.js +96 -0
- package/dist/shared/utils/log-lines.js +13 -0
- package/dist/shared/utils/paste.js +37 -0
- package/dist/shared/utils/syntax.js +128 -0
- package/dist/shared/utils/validation.js +56 -0
- package/dist/types.js +1 -0
- package/dist/visual-evidence/capture.js +51 -0
- package/dist/visual-evidence/cli.js +85 -0
- package/dist/visual-evidence/evidence-required.js +71 -0
- package/dist/visual-evidence/index.js +10 -0
- package/dist/visual-evidence/launch.js +61 -0
- package/dist/visual-evidence/manifest.js +32 -0
- package/dist/visual-evidence/openspec-resolver.js +45 -0
- package/dist/visual-evidence/publish.js +126 -0
- package/dist/visual-evidence/run.js +121 -0
- package/dist/visual-evidence/scenario-registry.js +132 -0
- package/dist/widgets/command-input/CommandDropdown.js +69 -0
- package/dist/widgets/command-input/CommandInput.js +133 -0
- package/dist/widgets/command-input/HintBar.js +9 -0
- package/dist/widgets/command-input/InputModes.js +72 -0
- package/dist/widgets/commands-browser/CommandsBrowserModal.js +85 -0
- package/dist/widgets/header/Header.js +54 -0
- package/dist/widgets/header/TabBar.js +20 -0
- package/dist/widgets/left-panel/LeftPanel.js +34 -0
- package/dist/widgets/left-panel/Navigator.js +107 -0
- package/dist/widgets/left-panel/ProjectsPage.js +76 -0
- package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
- package/dist/widgets/left-panel/TaskBrowser.js +132 -0
- package/dist/widgets/log-modal/LogModal.js +211 -0
- package/dist/widgets/loop-form/CreateForm.js +82 -0
- package/dist/widgets/loop-form/TextField.js +9 -0
- package/dist/widgets/loop-form/WizardForm.js +199 -0
- package/dist/widgets/loop-form/useCreateSteps.js +135 -0
- package/dist/widgets/loop-form/useHandleComplete.js +94 -0
- package/dist/widgets/project-form/ProjectForm.js +83 -0
- package/dist/widgets/right-panel/Inspector.js +41 -0
- package/dist/widgets/right-panel/RightPanel.js +38 -0
- package/dist/widgets/right-panel/RunHistory.js +187 -0
- package/dist/widgets/task-form/TaskForm.js +272 -0
- package/package.json +119 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { parseDuration } from "./duration.js";
|
|
2
|
+
import { t } from "./shared/i18n/index.js";
|
|
3
|
+
export function parseMaxRuns(value) {
|
|
4
|
+
if (value === null || value === undefined || value === "") {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const parsed = typeof value === "number" ? value : parseInt(value, 10);
|
|
8
|
+
if (Number.isNaN(parsed) || parsed < 1) {
|
|
9
|
+
throw new Error(t("errors.maxRunsInvalid"));
|
|
10
|
+
}
|
|
11
|
+
return parsed;
|
|
12
|
+
}
|
|
13
|
+
export function parseCommandLine(input) {
|
|
14
|
+
const tokens = [];
|
|
15
|
+
let current = "";
|
|
16
|
+
let quote = null;
|
|
17
|
+
let hasToken = false;
|
|
18
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
19
|
+
const char = input[i];
|
|
20
|
+
if (quote) {
|
|
21
|
+
if (char === quote) {
|
|
22
|
+
quote = null;
|
|
23
|
+
}
|
|
24
|
+
else if (char === "\\" && quote === '"') {
|
|
25
|
+
if (i + 1 < input.length) {
|
|
26
|
+
i += 1;
|
|
27
|
+
current += input[i];
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
current += char;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
current += char;
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (char === '"' || char === "'") {
|
|
39
|
+
quote = char;
|
|
40
|
+
hasToken = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (char === " " || char === "\t") {
|
|
44
|
+
if (hasToken) {
|
|
45
|
+
tokens.push(current);
|
|
46
|
+
current = "";
|
|
47
|
+
hasToken = false;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
current += char;
|
|
52
|
+
hasToken = true;
|
|
53
|
+
}
|
|
54
|
+
if (quote) {
|
|
55
|
+
throw new Error(t("errors.unbalancedQuote"));
|
|
56
|
+
}
|
|
57
|
+
if (hasToken) {
|
|
58
|
+
tokens.push(current);
|
|
59
|
+
}
|
|
60
|
+
return tokens;
|
|
61
|
+
}
|
|
62
|
+
export function joinCommandLines(text) {
|
|
63
|
+
// Quote-aware split: don't split on newlines inside quoted strings
|
|
64
|
+
const segments = [];
|
|
65
|
+
let current = "";
|
|
66
|
+
let quote = null;
|
|
67
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
68
|
+
const char = text[i];
|
|
69
|
+
if (quote) {
|
|
70
|
+
if (char === quote) {
|
|
71
|
+
quote = null;
|
|
72
|
+
}
|
|
73
|
+
else if (char === "\\" && quote === '"' && i + 1 < text.length) {
|
|
74
|
+
current += char + text[i + 1];
|
|
75
|
+
i += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
current += char;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (char === '"' || char === "'") {
|
|
82
|
+
quote = char;
|
|
83
|
+
current += char;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (char === "\n") {
|
|
87
|
+
segments.push(current);
|
|
88
|
+
current = "";
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
current += char;
|
|
92
|
+
}
|
|
93
|
+
segments.push(current);
|
|
94
|
+
// Join segments according to backslash-continuation rules:
|
|
95
|
+
// - Empty/whitespace-only segments are dropped
|
|
96
|
+
// - Segment ending with \: backslash consumed, joins to next with NO added space
|
|
97
|
+
// - Segment NOT ending with \: joins to next with a single space
|
|
98
|
+
// Leading whitespace on each line is trimmed (indentation); trailing whitespace
|
|
99
|
+
// before \ is preserved (it's part of the content e.g. separates tokens).
|
|
100
|
+
let result = "";
|
|
101
|
+
let prevContinued = false; // true if previous segment ended with \
|
|
102
|
+
for (const seg of segments) {
|
|
103
|
+
const trimmed = seg.trim();
|
|
104
|
+
if (trimmed === "")
|
|
105
|
+
continue;
|
|
106
|
+
const endsBackslash = trimmed.endsWith("\\");
|
|
107
|
+
const content = endsBackslash ? trimmed.slice(0, -1) : trimmed;
|
|
108
|
+
if (result === "") {
|
|
109
|
+
result = content;
|
|
110
|
+
}
|
|
111
|
+
else if (prevContinued) {
|
|
112
|
+
result += content;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
result += " " + content;
|
|
116
|
+
}
|
|
117
|
+
prevContinued = endsBackslash;
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
export function buildLoopOptions(intervalHuman, input = {}) {
|
|
122
|
+
const command = input.command ?? "";
|
|
123
|
+
const commandArgs = input.commandArgs ?? [];
|
|
124
|
+
const taskId = input.taskId ?? null;
|
|
125
|
+
if (!taskId && !command.trim()) {
|
|
126
|
+
throw new Error(t("errors.commandEmpty"));
|
|
127
|
+
}
|
|
128
|
+
const description = input.description?.trim() ?? "";
|
|
129
|
+
if (!description) {
|
|
130
|
+
throw new Error(t("errors.descriptionEmpty"));
|
|
131
|
+
}
|
|
132
|
+
const parsedInterval = parseDuration(intervalHuman);
|
|
133
|
+
const isManual = parsedInterval === 0;
|
|
134
|
+
const normalizedIntervalHuman = isManual ? "manual" : intervalHuman;
|
|
135
|
+
return {
|
|
136
|
+
intervalHuman: normalizedIntervalHuman,
|
|
137
|
+
options: {
|
|
138
|
+
interval: parsedInterval,
|
|
139
|
+
taskId,
|
|
140
|
+
command,
|
|
141
|
+
commandArgs,
|
|
142
|
+
cwd: input.cwd ?? "",
|
|
143
|
+
immediate: isManual ? false : (input.now ?? false),
|
|
144
|
+
maxRuns: parseMaxRuns(input.maxRuns),
|
|
145
|
+
verbose: input.verbose ?? false,
|
|
146
|
+
description,
|
|
147
|
+
projectId: input.projectId ?? "default",
|
|
148
|
+
offset: input.offset ?? null,
|
|
149
|
+
context: input.context,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
/**
|
|
4
|
+
* Write text to the system clipboard. Never throws returns false if no
|
|
5
|
+
* clipboard tool is available (e.g. a headless SSH box without xclip/xsel).
|
|
6
|
+
*
|
|
7
|
+
* Linux toolchain tried in order: xclip → xsel → wl-copy (Wayland) →
|
|
8
|
+
* tmux load-buffer (when $TMUX is set) → OSC 52 (works over SSH because
|
|
9
|
+
* the local terminal owns the clipboard).
|
|
10
|
+
*/
|
|
11
|
+
export function copyToClipboard(text) {
|
|
12
|
+
const os = platform();
|
|
13
|
+
if (os === "win32") {
|
|
14
|
+
try {
|
|
15
|
+
execFileSync("clip", { input: text });
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (os === "darwin") {
|
|
23
|
+
try {
|
|
24
|
+
execFileSync("pbcopy", { input: text });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Linux / BSD: try each clipboard tool in order. All calls are guarded
|
|
32
|
+
// a missing binary must never throw, since callers invoke this from
|
|
33
|
+
// synchronous keypress handlers (an uncaught throw here kills the app).
|
|
34
|
+
const tries = [
|
|
35
|
+
() => execFileSync("xclip", ["-selection", "clipboard"], { input: text }),
|
|
36
|
+
() => execFileSync("xsel", ["--clipboard", "--input"], { input: text }),
|
|
37
|
+
() => execFileSync("wl-copy", [], { input: text }),
|
|
38
|
+
];
|
|
39
|
+
for (const tryFn of tries) {
|
|
40
|
+
try {
|
|
41
|
+
tryFn();
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// try next tool
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (process.env.TMUX) {
|
|
49
|
+
try {
|
|
50
|
+
execFileSync("tmux", ["load-buffer", "-"], { input: text });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// fall through to OSC 52
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// OSC 52: ask the connected terminal to copy. Works over SSH because the
|
|
58
|
+
// local terminal owns the clipboard. Most modern terminals support it
|
|
59
|
+
// (iTerm2, WezTerm, Windows Terminal, Kitty, Alacritty).
|
|
60
|
+
try {
|
|
61
|
+
process.stdout.write(`\x1b]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read text from the system clipboard. Returns "" if unavailable.
|
|
70
|
+
*
|
|
71
|
+
* Linux toolchain: xclip → xsel → wl-paste → tmux save-buffer -. OSC 52
|
|
72
|
+
* reply is not solicited synchronously (timings are unreliable across
|
|
73
|
+
* terminals); callers fall back to bracketed-paste detection for input.
|
|
74
|
+
*/
|
|
75
|
+
export function readFromClipboard() {
|
|
76
|
+
const os = platform();
|
|
77
|
+
try {
|
|
78
|
+
if (os === "win32") {
|
|
79
|
+
return execFileSync("powershell", ["-NoProfile", "-Command", "Get-Clipboard"], { encoding: "utf-8" }).replace(/\r?\n$/, "");
|
|
80
|
+
}
|
|
81
|
+
if (os === "darwin") {
|
|
82
|
+
return execFileSync("pbpaste", { encoding: "utf-8" });
|
|
83
|
+
}
|
|
84
|
+
// Linux / BSD
|
|
85
|
+
const tries = [
|
|
86
|
+
() => execFileSync("xclip", ["-selection", "clipboard", "-o"], { encoding: "utf-8" }),
|
|
87
|
+
() => execFileSync("xsel", ["--clipboard", "--output"], { encoding: "utf-8" }),
|
|
88
|
+
() => execFileSync("wl-paste", [], { encoding: "utf-8" }),
|
|
89
|
+
];
|
|
90
|
+
for (const tryFn of tries) {
|
|
91
|
+
try {
|
|
92
|
+
return tryFn();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// try next tool
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (process.env.TMUX) {
|
|
99
|
+
try {
|
|
100
|
+
return execFileSync("tmux", ["save-buffer", "-"], { encoding: "utf-8" });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// ignore
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return "";
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export const POLL_MS = 1000;
|
|
2
|
+
export const TOAST_TIMEOUT_MS = 3500;
|
|
3
|
+
export const TOAST_MAX = 4;
|
|
4
|
+
export const LOG_LINES_MAX = 500;
|
|
5
|
+
export const MAX_LOG_BYTES = 1024 * 1024;
|
|
6
|
+
export const MAX_CONTEXT_STDOUT_BYTES = 1024 * 1024;
|
|
7
|
+
/** Max bytes for stdout attached to a span attribute (16KB) */
|
|
8
|
+
export const MAX_SPAN_OUTPUT_BYTES = 16 * 1024;
|
|
9
|
+
export const MAX_LOG_GENERATIONS = 3;
|
|
10
|
+
export const MAX_INMEMORY_RUN_HISTORY = 100;
|
|
11
|
+
export const MAX_STREAM_INITIAL_BYTES = 256 * 1024;
|
|
12
|
+
export const USER_TIMING_SWEEP_MS = 10_000;
|
|
13
|
+
export const LOG_MODAL_LINES_MAX = 5000;
|
|
14
|
+
export const LOG_LINE_CHARS_MAX = 10_000;
|
|
15
|
+
export const FOLLOW_REATTACH_DELAY_MS = 100;
|
|
16
|
+
export const FOLLOW_REATTACH_MAX_ATTEMPTS = 20;
|
|
17
|
+
export const FOLLOW_READ_BATCH_BYTES = 1024 * 1024;
|
|
18
|
+
export const SLEEP_CHUNK_MS = 200;
|
|
19
|
+
export const DAEMON_SPAWN_TIMEOUT_MS = 5000;
|
|
20
|
+
export const DAEMON_POLL_MS = 100;
|
|
21
|
+
export const IPC_TIMEOUT_MS = 10000;
|
|
22
|
+
export const LOG_TAIL_DEFAULT = 50;
|
|
23
|
+
/** Terminal width >= this shows the wide (side-by-side) two-panel layout. */
|
|
24
|
+
export const BOARD_BREAKPOINT_WIDE = 110;
|
|
25
|
+
/** Terminal width >= this and < WIDE shows the compact (stacked) single-panel layout. */
|
|
26
|
+
export const BOARD_BREAKPOINT_COMPACT = 70;
|
|
27
|
+
/** Legacy alias kept for backward compat with any external consumers. */
|
|
28
|
+
export const BOARD_BREAKPOINT_WIDTH = BOARD_BREAKPOINT_WIDE;
|
|
29
|
+
export const HEADER_COMPACT_WIDTH = 60;
|
|
30
|
+
export const SEARCH_SELECT_HEIGHT = 6;
|
|
31
|
+
// DECSET 2004: terminal wraps pasted text in ESC[200~ ... ESC[201~ so the
|
|
32
|
+
// app can insert it wholesale instead of processing each char as a keypress.
|
|
33
|
+
export const BRACKETED_PASTE_ENABLE = "\x1b[?2004h";
|
|
34
|
+
export const BRACKETED_PASTE_DISABLE = "\x1b[?2004l";
|
|
35
|
+
export const PASTE_MAX_CHARS = 4096;
|
|
36
|
+
// DECSET 1000: enable mouse button event tracking (scroll wheel + clicks).
|
|
37
|
+
// DECSET 1006: enable SGR extended mouse mode (structured escape sequences
|
|
38
|
+
// that are unambiguous and easy to parse, e.g. [<64;col;rowM for scroll up).
|
|
39
|
+
export const MOUSE_TRACKING_ENABLE = "\x1b[?1000h\x1b[?1006h";
|
|
40
|
+
export const MOUSE_TRACKING_DISABLE = "\x1b[?1006l\x1b[?1000l";
|
|
41
|
+
export const HOVER_BG = "#1e3a5f";
|
|
42
|
+
export const PROJECT_COLORS = {
|
|
43
|
+
white: "#ffffff",
|
|
44
|
+
cyan: "#06b6d4",
|
|
45
|
+
green: "#4ade80",
|
|
46
|
+
yellow: "#facc15",
|
|
47
|
+
orange: "#fb923c",
|
|
48
|
+
pink: "#f472b6",
|
|
49
|
+
};
|
|
50
|
+
export const PROJECT_COLOR_KEYS = Object.keys(PROJECT_COLORS);
|
|
51
|
+
// Entity theme colors - used for header action buttons and view accents
|
|
52
|
+
export const ENTITY_COLORS = {
|
|
53
|
+
loop: "#38bdf8", // cyan/blue
|
|
54
|
+
task: "#a78bfa", // purple
|
|
55
|
+
project: "#34d399", // green
|
|
56
|
+
};
|
|
57
|
+
export const WIZARD_LOOP_REQUIRED_STEPS = 3;
|
|
58
|
+
export const WIZARD_LOOP_TOTAL_STEPS = 7;
|
|
59
|
+
export const WIZARD_TASK_REQUIRED_STEPS = 2;
|
|
60
|
+
export const WIZARD_TASK_TOTAL_STEPS = 4;
|
|
61
|
+
export const WIZARD_PROJECT_REQUIRED_STEPS = 1;
|
|
62
|
+
export const WIZARD_PROJECT_TOTAL_STEPS = 2;
|
|
63
|
+
export const COMMAND_TIER_ACTION = "action";
|
|
64
|
+
export const COMMAND_TIER_CONFIRM = "confirm";
|
|
65
|
+
export const COMMAND_TIER_GLOBAL = "global";
|
|
66
|
+
export const COMMAND_CATEGORY_GLOBAL = "global";
|
|
67
|
+
export const COMMAND_CATEGORY_FILTERS = "filters";
|
|
68
|
+
export const COMMAND_CATEGORY_LOOP = "loop";
|
|
69
|
+
export const COMMAND_CATEGORY_TASK = "task";
|
|
70
|
+
export const COMMAND_CATEGORY_PROJECT = "project";
|
|
71
|
+
export const CONFIRM_YES = "yes";
|
|
72
|
+
export const CONFIRM_CANCEL = "cancel";
|
|
73
|
+
export const PANEL_LEFT = "left";
|
|
74
|
+
export const PANEL_RIGHT = "right";
|
|
75
|
+
export const COMMAND_TEMPLATES = [
|
|
76
|
+
{ label: "npm run", command: "npm", args: "run" },
|
|
77
|
+
{ label: "npm test", command: "npm", args: "test" },
|
|
78
|
+
{ label: "pnpm build", command: "pnpm", args: "build" },
|
|
79
|
+
{ label: "pnpm test", command: "pnpm", args: "test" },
|
|
80
|
+
{ label: "yarn build", command: "yarn", args: "build" },
|
|
81
|
+
{ label: "dotnet build", command: "dotnet", args: "build" },
|
|
82
|
+
{ label: "dotnet test", command: "dotnet", args: "test" },
|
|
83
|
+
{ label: "docker compose up", command: "docker", args: "compose up" },
|
|
84
|
+
{ label: "make", command: "make", args: "" },
|
|
85
|
+
{ label: "shell script", command: "bash", args: "./script.sh" },
|
|
86
|
+
];
|
|
87
|
+
export const COMMAND_INPUT_HEIGHT = 6;
|
|
88
|
+
export const COMMAND_INPUT_DROPDOWN_MAX_VISIBLE = 6;
|
|
89
|
+
export const HTTP_API_PORT = 8845;
|
|
90
|
+
export const HTTP_API_HOST = "127.0.0.1";
|
|
91
|
+
export const EXPORT_MAX_PREVIEW_LINES = 200;
|
|
92
|
+
export const CTRL_SHORTCUT_EDIT = "Ctrl+E";
|
|
93
|
+
export const CTRL_SHORTCUT_DELETE = "Ctrl+D";
|
|
94
|
+
export const CODE_EDITOR_MAX_VISIBLE = 2;
|
|
95
|
+
export const CODE_EDITOR_MODAL_HEIGHT = 40;
|
|
96
|
+
export const CODE_EDITOR_MODAL_WIDTH = 120;
|
|
97
|
+
export const CODE_EDITOR_WRAP_LENGTH = 80;
|
|
98
|
+
export const CODE_EDITOR_UNDO_LIMIT = 50;
|
|
99
|
+
export const CODE_EDITOR_SYNTAX_COLORS = {
|
|
100
|
+
flag: "#38bdf8",
|
|
101
|
+
string: "#4ade80",
|
|
102
|
+
operator: "#facc15",
|
|
103
|
+
word: "#e5e7eb",
|
|
104
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
export function getDataDir() {
|
|
5
|
+
const override = process.env.LOOP_CLI_HOME;
|
|
6
|
+
const base = override && override.trim() ? override : os.homedir();
|
|
7
|
+
return path.join(base, ".loop-cli");
|
|
8
|
+
}
|
|
9
|
+
export function getLoopsDir() {
|
|
10
|
+
return path.join(getDataDir(), "loops");
|
|
11
|
+
}
|
|
12
|
+
export function getTasksDir() {
|
|
13
|
+
return path.join(getDataDir(), "tasks");
|
|
14
|
+
}
|
|
15
|
+
export function getLogsDir() {
|
|
16
|
+
return path.join(getDataDir(), "logs");
|
|
17
|
+
}
|
|
18
|
+
export function loopFile(id) {
|
|
19
|
+
return path.join(getLoopsDir(), `${id}.json`);
|
|
20
|
+
}
|
|
21
|
+
export function taskFile(id) {
|
|
22
|
+
return path.join(getTasksDir(), `${id}.json`);
|
|
23
|
+
}
|
|
24
|
+
export function loopsJson() {
|
|
25
|
+
return path.join(getDataDir(), "loops.json");
|
|
26
|
+
}
|
|
27
|
+
export function tasksJson() {
|
|
28
|
+
return path.join(getDataDir(), "tasks.json");
|
|
29
|
+
}
|
|
30
|
+
export function recipeRuntimeJson() {
|
|
31
|
+
return path.join(getDataDir(), "recipe-runtime.json");
|
|
32
|
+
}
|
|
33
|
+
export function projectsJson() {
|
|
34
|
+
return path.join(getDataDir(), "projects.json");
|
|
35
|
+
}
|
|
36
|
+
export function settingsJson() {
|
|
37
|
+
return path.join(getDataDir(), "settings.json");
|
|
38
|
+
}
|
|
39
|
+
export function logFile(id) {
|
|
40
|
+
return path.join(getLogsDir(), `${id}.log`);
|
|
41
|
+
}
|
|
42
|
+
export function getPidFile() {
|
|
43
|
+
return path.join(getDataDir(), "daemon.pid");
|
|
44
|
+
}
|
|
45
|
+
export function getSignatureFile() {
|
|
46
|
+
return path.join(getDataDir(), "daemon.sig");
|
|
47
|
+
}
|
|
48
|
+
export function getDaemonStartLockFile() {
|
|
49
|
+
return path.join(getDataDir(), "daemon.start.lock");
|
|
50
|
+
}
|
|
51
|
+
export function getDaemonLogFile() {
|
|
52
|
+
return path.join(getDataDir(), "daemon.log");
|
|
53
|
+
}
|
|
54
|
+
export function getBoardLogFile() {
|
|
55
|
+
return path.join(getDataDir(), "board.log");
|
|
56
|
+
}
|
|
57
|
+
export function getSocketPath() {
|
|
58
|
+
const suffix = crypto
|
|
59
|
+
.createHash("sha1")
|
|
60
|
+
.update(getDataDir())
|
|
61
|
+
.digest("hex")
|
|
62
|
+
.slice(0, 12);
|
|
63
|
+
if (process.platform === "win32") {
|
|
64
|
+
return `\\\\.\\pipe\\loop-cli-${os.userInfo().username}-${suffix}`;
|
|
65
|
+
}
|
|
66
|
+
return path.join(getDataDir(), `daemon-${suffix}.sock`);
|
|
67
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Container } from "inversify";
|
|
2
|
+
import { TYPES } from "../services/types.js";
|
|
3
|
+
import { IpcLoopService } from "../services/loop-service.js";
|
|
4
|
+
import { IpcTaskService } from "../services/task-service.js";
|
|
5
|
+
import { IpcProjectService } from "../services/project-service.js";
|
|
6
|
+
import { IpcLogService } from "../services/log-service.js";
|
|
7
|
+
import { IpcExportService } from "../services/export-service.js";
|
|
8
|
+
import { IpcSettingsService } from "../services/settings-service.js";
|
|
9
|
+
export function createContainer() {
|
|
10
|
+
const container = new Container();
|
|
11
|
+
container.bind(TYPES.LoopService).to(IpcLoopService);
|
|
12
|
+
container.bind(TYPES.TaskService).to(IpcTaskService);
|
|
13
|
+
container.bind(TYPES.ProjectService).to(IpcProjectService);
|
|
14
|
+
container.bind(TYPES.LogService).to(IpcLogService);
|
|
15
|
+
container.bind(TYPES.SettingsService).to(IpcSettingsService);
|
|
16
|
+
container.bind(TYPES.ExportService).toDynamicValue(() => {
|
|
17
|
+
const loopService = container.get(TYPES.LoopService);
|
|
18
|
+
const taskService = container.get(TYPES.TaskService);
|
|
19
|
+
const projectService = container.get(TYPES.ProjectService);
|
|
20
|
+
return new IpcExportService(loopService, taskService, projectService);
|
|
21
|
+
});
|
|
22
|
+
return container;
|
|
23
|
+
}
|
|
24
|
+
export const container = createContainer();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
// On Windows, renaming over a file that another handle has open (e.g. the
|
|
3
|
+
// daemon's own fs.watch on loops.json) can transiently fail with EPERM/EBUSY.
|
|
4
|
+
// Retry the rename a few times before falling back to a direct (non-atomic)
|
|
5
|
+
// write so a momentary lock can't take the daemon down.
|
|
6
|
+
const RENAME_RETRIES = 5;
|
|
7
|
+
const RENAME_BACKOFF_MS = 15;
|
|
8
|
+
function sleepSync(ms) {
|
|
9
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
10
|
+
}
|
|
11
|
+
export function removeIfExists(filePath) {
|
|
12
|
+
if (fs.existsSync(filePath)) {
|
|
13
|
+
fs.unlinkSync(filePath);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function writeFileAtomic(filePath, data) {
|
|
17
|
+
const tmp = `${filePath}.${process.pid}.tmp`;
|
|
18
|
+
fs.writeFileSync(tmp, data);
|
|
19
|
+
let lastErr;
|
|
20
|
+
for (let attempt = 0; attempt < RENAME_RETRIES; attempt++) {
|
|
21
|
+
try {
|
|
22
|
+
fs.renameSync(tmp, filePath);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
lastErr = err;
|
|
27
|
+
const code = err.code;
|
|
28
|
+
if (code !== "EPERM" && code !== "EBUSY" && code !== "EACCES")
|
|
29
|
+
break;
|
|
30
|
+
sleepSync(RENAME_BACKOFF_MS * (attempt + 1));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Fallback: the destination stayed locked. Write in place (non-atomic) so
|
|
34
|
+
// state is still persisted; a partial write is recoverable since corrupt
|
|
35
|
+
// JSON is skipped on load, whereas a crash-looping daemon is not.
|
|
36
|
+
try {
|
|
37
|
+
fs.writeFileSync(filePath, data);
|
|
38
|
+
fs.rmSync(tmp, { force: true });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
fs.rmSync(tmp, { force: true });
|
|
42
|
+
throw lastErr;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useStdout } from "ink";
|
|
2
|
+
import { BOARD_BREAKPOINT_COMPACT, BOARD_BREAKPOINT_WIDE } from "../config/constants.js";
|
|
3
|
+
export function useBreakpoint() {
|
|
4
|
+
const { stdout } = useStdout();
|
|
5
|
+
const width = stdout?.columns ?? 80;
|
|
6
|
+
if (width >= BOARD_BREAKPOINT_WIDE)
|
|
7
|
+
return "wide";
|
|
8
|
+
if (width >= BOARD_BREAKPOINT_COMPACT)
|
|
9
|
+
return "compact";
|
|
10
|
+
return "minimal";
|
|
11
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useInject } from "./useInject.js";
|
|
3
|
+
import { TYPES } from "../services/types.js";
|
|
4
|
+
import { POLL_MS } from "../config/constants.js";
|
|
5
|
+
export function useDaemonSettings() {
|
|
6
|
+
const settingsService = useInject(TYPES.SettingsService);
|
|
7
|
+
const [state, setState] = useState({
|
|
8
|
+
httpApiEnabled: false,
|
|
9
|
+
mcpApiEnabled: false,
|
|
10
|
+
telemetryEnabled: false,
|
|
11
|
+
reachable: false,
|
|
12
|
+
});
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
let cancelled = false;
|
|
15
|
+
const poll = async () => {
|
|
16
|
+
try {
|
|
17
|
+
const settings = await settingsService.getSettings();
|
|
18
|
+
if (!cancelled) {
|
|
19
|
+
setState({
|
|
20
|
+
httpApiEnabled: settings.httpApiEnabled,
|
|
21
|
+
mcpApiEnabled: settings.mcpApiEnabled,
|
|
22
|
+
telemetryEnabled: settings.telemetryEnabled,
|
|
23
|
+
reachable: true,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
if (!cancelled) {
|
|
29
|
+
setState((prev) => ({ ...prev, reachable: false }));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
void poll();
|
|
34
|
+
const timer = setInterval(() => void poll(), POLL_MS);
|
|
35
|
+
return () => {
|
|
36
|
+
cancelled = true;
|
|
37
|
+
clearInterval(timer);
|
|
38
|
+
};
|
|
39
|
+
}, [settingsService]);
|
|
40
|
+
return state;
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useInject } from "./useInject.js";
|
|
3
|
+
import { TYPES } from "../services/types.js";
|
|
4
|
+
import { t } from "../i18n/index.js";
|
|
5
|
+
import { LOG_LINES_MAX } from "../config/constants.js";
|
|
6
|
+
import { appendClamped } from "../utils/log-lines.js";
|
|
7
|
+
export function useLogStream(selectedId, view, onError) {
|
|
8
|
+
const injectedLogService = useInject(TYPES.LogService);
|
|
9
|
+
const logServiceRef = useRef(injectedLogService);
|
|
10
|
+
const logService = logServiceRef.current;
|
|
11
|
+
const [logLines, setLogLines] = useState([]);
|
|
12
|
+
const logSocket = useRef(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
logSocket.current?.destroy();
|
|
15
|
+
logSocket.current = null;
|
|
16
|
+
setLogLines([]);
|
|
17
|
+
if (!selectedId || view !== "board") {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
setLogLines([t("board.logWaiting")]);
|
|
21
|
+
const socket = logService.streamLogs(selectedId, (line) => setLogLines((prev) => {
|
|
22
|
+
const next = prev[0] === t("board.logWaiting") ? [] : prev;
|
|
23
|
+
return appendClamped(next, line, LOG_LINES_MAX);
|
|
24
|
+
}), onError);
|
|
25
|
+
logSocket.current = socket;
|
|
26
|
+
return () => {
|
|
27
|
+
socket.destroy();
|
|
28
|
+
if (logSocket.current === socket) {
|
|
29
|
+
logSocket.current = null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}, [selectedId, view, logService]);
|
|
33
|
+
return {
|
|
34
|
+
logLines,
|
|
35
|
+
destroy: () => logSocket.current?.destroy(),
|
|
36
|
+
};
|
|
37
|
+
}
|