@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,37 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "./theme.js";
|
|
5
|
+
const TOAST_TIMEOUT_MS = 3500;
|
|
6
|
+
const TOAST_MAX = 4;
|
|
7
|
+
function toastColor(kind) {
|
|
8
|
+
switch (kind) {
|
|
9
|
+
case "success": return theme.semantic.success;
|
|
10
|
+
case "error": return theme.semantic.danger;
|
|
11
|
+
case "info": return theme.accent.loop;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function toastIcon(kind) {
|
|
15
|
+
switch (kind) {
|
|
16
|
+
case "success": return "\u2713";
|
|
17
|
+
case "error": return "\u2717";
|
|
18
|
+
case "info": return "\u2139";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function useToasts() {
|
|
22
|
+
const [toasts, setToasts] = useState([]);
|
|
23
|
+
const nextId = useState(() => ({ value: 0 }))[0];
|
|
24
|
+
const push = useCallback((kind, message) => {
|
|
25
|
+
const id = ++nextId.value;
|
|
26
|
+
setToasts((prev) => [...prev, { id, kind, message }].slice(-TOAST_MAX));
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
29
|
+
}, TOAST_TIMEOUT_MS);
|
|
30
|
+
}, [nextId]);
|
|
31
|
+
return { toasts, push };
|
|
32
|
+
}
|
|
33
|
+
export function ToastStack(props) {
|
|
34
|
+
if (props.toasts.length === 0)
|
|
35
|
+
return null;
|
|
36
|
+
return (_jsx(Box, { position: "absolute", bottom: 0, right: 0, flexDirection: "column", alignItems: "flex-end", children: props.toasts.map((toast) => (_jsxs(Box, { backgroundColor: theme.bg.elevated, paddingLeft: 1, paddingRight: 1, children: [_jsxs(Text, { color: toastColor(toast.kind), bold: true, children: [toastIcon(toast.kind), " "] }), _jsx(Text, { color: theme.text.primary, children: toast.message })] }, toast.id))) }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { t } from "../i18n/index.js";
|
|
2
|
+
export function unescapeCommand(str) {
|
|
3
|
+
if (typeof str !== "string")
|
|
4
|
+
return String(str ?? "");
|
|
5
|
+
return str
|
|
6
|
+
.replace(/\\\\/g, "\x00")
|
|
7
|
+
.replace(/\\"/g, '"')
|
|
8
|
+
.replace(/\x00/g, "\\");
|
|
9
|
+
}
|
|
10
|
+
export function quoteArg(arg) {
|
|
11
|
+
const s = typeof arg === "string" ? arg : String(arg ?? "");
|
|
12
|
+
return /[\s"]/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
|
|
13
|
+
}
|
|
14
|
+
export function commandLine(command, args) {
|
|
15
|
+
const safeArgs = Array.isArray(args) ? args : [];
|
|
16
|
+
return [unescapeCommand(command), ...safeArgs.map((a) => quoteArg(unescapeCommand(a)))].join(" ").trim();
|
|
17
|
+
}
|
|
18
|
+
export function formatCmd(command, args, max = 24) {
|
|
19
|
+
const full = commandLine(command, args);
|
|
20
|
+
return full.length > max ? full.slice(0, max - 3) + "..." : full;
|
|
21
|
+
}
|
|
22
|
+
export function describeLoop(loop) {
|
|
23
|
+
return loop.description?.trim() || commandLine(loop.command, loop.commandArgs);
|
|
24
|
+
}
|
|
25
|
+
export function truncate(text, max) {
|
|
26
|
+
return text.length > max ? text.slice(0, max - 3) + "..." : text;
|
|
27
|
+
}
|
|
28
|
+
export function timeAgo(iso) {
|
|
29
|
+
if (!iso)
|
|
30
|
+
return t("format.dash");
|
|
31
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
32
|
+
const secs = Math.floor(diff / 1000);
|
|
33
|
+
if (secs < 5)
|
|
34
|
+
return t("format.justNow");
|
|
35
|
+
if (secs < 60)
|
|
36
|
+
return t("format.secsAgo", { secs });
|
|
37
|
+
const mins = Math.floor(secs / 60);
|
|
38
|
+
if (mins < 60)
|
|
39
|
+
return t("format.minsAgo", { mins });
|
|
40
|
+
const hrs = Math.floor(mins / 60);
|
|
41
|
+
if (hrs < 24)
|
|
42
|
+
return t("format.hrsAgo", { hrs });
|
|
43
|
+
return t("format.daysAgo", { days: Math.floor(hrs / 24) });
|
|
44
|
+
}
|
|
45
|
+
export function timeUntil(iso) {
|
|
46
|
+
if (!iso)
|
|
47
|
+
return t("format.dash");
|
|
48
|
+
const diff = Math.max(0, new Date(iso).getTime() - Date.now());
|
|
49
|
+
const secs = Math.floor(diff / 1000);
|
|
50
|
+
if (secs < 5)
|
|
51
|
+
return t("format.justNow");
|
|
52
|
+
if (secs < 60)
|
|
53
|
+
return t("format.secsAhead", { secs });
|
|
54
|
+
const mins = Math.floor(secs / 60);
|
|
55
|
+
if (mins < 60)
|
|
56
|
+
return t("format.minsAhead", { mins });
|
|
57
|
+
const hrs = Math.floor(mins / 60);
|
|
58
|
+
if (hrs < 24)
|
|
59
|
+
return t("format.hrsAhead", { hrs });
|
|
60
|
+
return t("format.daysAhead", { days: Math.floor(hrs / 24) });
|
|
61
|
+
}
|
|
62
|
+
const STATUS_COLORS = {
|
|
63
|
+
running: "#4ade80",
|
|
64
|
+
waiting: "#6b7280",
|
|
65
|
+
paused: "#facc15",
|
|
66
|
+
idle: "#fb923c",
|
|
67
|
+
stopped: "#f87171",
|
|
68
|
+
};
|
|
69
|
+
export function statusColor(status) {
|
|
70
|
+
return STATUS_COLORS[status] ?? "#ffffff";
|
|
71
|
+
}
|
|
72
|
+
export function timingLabel(loop) {
|
|
73
|
+
if (loop.interval === 0)
|
|
74
|
+
return t("format.durationManual");
|
|
75
|
+
if (loop.status === "paused")
|
|
76
|
+
return t("format.timingPaused");
|
|
77
|
+
if (loop.status === "idle")
|
|
78
|
+
return t("format.timingIdle");
|
|
79
|
+
if (loop.nextRunAt)
|
|
80
|
+
return t("format.timingNext", { timeAgo: timeUntil(loop.nextRunAt) });
|
|
81
|
+
if (loop.lastRunAt)
|
|
82
|
+
return t("format.timingLast", { timeAgo: timeAgo(loop.lastRunAt) });
|
|
83
|
+
return t("format.timingNew");
|
|
84
|
+
}
|
|
85
|
+
export function statusLabel(status) {
|
|
86
|
+
return status === "waiting" ? "waiting" : status;
|
|
87
|
+
}
|
|
88
|
+
export function formatFileSize(bytes) {
|
|
89
|
+
if (bytes < 1024)
|
|
90
|
+
return `${bytes} B`;
|
|
91
|
+
if (bytes < 1024 * 1024)
|
|
92
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
93
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
94
|
+
}
|
|
95
|
+
export function formatRunDuration(ms) {
|
|
96
|
+
if (ms < 1000)
|
|
97
|
+
return `${ms}ms`;
|
|
98
|
+
if (ms < 60000)
|
|
99
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
100
|
+
return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
|
|
101
|
+
}
|
|
102
|
+
export function formatRunTime(iso) {
|
|
103
|
+
const d = new Date(iso);
|
|
104
|
+
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
|
|
105
|
+
}
|
|
106
|
+
export function formatDate(iso) {
|
|
107
|
+
const d = new Date(iso);
|
|
108
|
+
const date = d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
109
|
+
const time = d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
|
|
110
|
+
return `${date} ${time}`;
|
|
111
|
+
}
|
|
112
|
+
export function sinceLabel(loop) {
|
|
113
|
+
const ts = loop.sessionStartedAt ?? loop.createdAt;
|
|
114
|
+
if (!ts)
|
|
115
|
+
return t("format.dash");
|
|
116
|
+
return formatDate(ts);
|
|
117
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
export function useHoverState() {
|
|
3
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
4
|
+
return {
|
|
5
|
+
isHovered,
|
|
6
|
+
hoverProps: {
|
|
7
|
+
onMouseOver: () => setIsHovered(true),
|
|
8
|
+
onMouseOut: () => setIsHovered(false),
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { defaultFilters, applyLoopFilters, cycleSortMode, cycleStatusFilter } from "../../entities/loops/filters.js";
|
|
2
|
+
export { defaultProjectFilters, applyProjectFilters, cycleProjectSortMode, cycleProjectHasLoopsFilter, cycleProjectIsSystemFilter } from "../../entities/projects/filters.js";
|
|
3
|
+
export function resolveInputOwner(state) {
|
|
4
|
+
if (state.modalOpen)
|
|
5
|
+
return "modal";
|
|
6
|
+
if (state.commandBarHasText || state.commandBarDropdownOpen)
|
|
7
|
+
return "commandBar";
|
|
8
|
+
return "panel";
|
|
9
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export const darkTheme = {
|
|
2
|
+
bg: {
|
|
3
|
+
base: "#0a0e14",
|
|
4
|
+
surface: "#111827",
|
|
5
|
+
elevated: "#1e293b",
|
|
6
|
+
hover: "#1e3a5f",
|
|
7
|
+
active: "#1e3a8a", // loop selection (blue)
|
|
8
|
+
activeTask: "#3b1f6e", // task selection (purple)
|
|
9
|
+
activeProject: "#0f3d2e", // project selection (green)
|
|
10
|
+
input: "#0f172a",
|
|
11
|
+
},
|
|
12
|
+
text: {
|
|
13
|
+
primary: "#e5e7eb",
|
|
14
|
+
secondary: "#9ca3af",
|
|
15
|
+
muted: "#6b7280",
|
|
16
|
+
inverse: "#ffffff",
|
|
17
|
+
},
|
|
18
|
+
accent: {
|
|
19
|
+
brand: "#fbbf24",
|
|
20
|
+
loop: "#38bdf8",
|
|
21
|
+
task: "#a78bfa",
|
|
22
|
+
project: "#34d399",
|
|
23
|
+
},
|
|
24
|
+
semantic: {
|
|
25
|
+
success: "#4ade80",
|
|
26
|
+
warning: "#facc15",
|
|
27
|
+
danger: "#f87171",
|
|
28
|
+
idle: "#fb923c",
|
|
29
|
+
},
|
|
30
|
+
border: {
|
|
31
|
+
default: "#1e293b",
|
|
32
|
+
dim: "#374151",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
export const lightTheme = {
|
|
36
|
+
bg: {
|
|
37
|
+
base: "#f8fafc",
|
|
38
|
+
surface: "#f1f5f9",
|
|
39
|
+
elevated: "#e2e8f0",
|
|
40
|
+
hover: "#cbd5e1",
|
|
41
|
+
active: "#93c5fd",
|
|
42
|
+
input: "#f1f5f9",
|
|
43
|
+
},
|
|
44
|
+
text: {
|
|
45
|
+
primary: "#1e293b",
|
|
46
|
+
secondary: "#475569",
|
|
47
|
+
muted: "#94a3b8",
|
|
48
|
+
inverse: "#0f172a",
|
|
49
|
+
},
|
|
50
|
+
accent: {
|
|
51
|
+
brand: "#d97706",
|
|
52
|
+
loop: "#0284c7",
|
|
53
|
+
task: "#7c3aed",
|
|
54
|
+
project: "#059669",
|
|
55
|
+
},
|
|
56
|
+
semantic: {
|
|
57
|
+
success: "#16a34a",
|
|
58
|
+
warning: "#ca8a04",
|
|
59
|
+
danger: "#dc2626",
|
|
60
|
+
idle: "#ea580c",
|
|
61
|
+
},
|
|
62
|
+
border: {
|
|
63
|
+
default: "#cbd5e1",
|
|
64
|
+
dim: "#94a3b8",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
export const space = {
|
|
68
|
+
0: 0,
|
|
69
|
+
1: 1,
|
|
70
|
+
2: 2,
|
|
71
|
+
3: 3,
|
|
72
|
+
4: 4,
|
|
73
|
+
};
|
|
74
|
+
export const ENTITY_COLORS = {
|
|
75
|
+
loop: "#38bdf8",
|
|
76
|
+
task: "#a78bfa",
|
|
77
|
+
project: "#34d399",
|
|
78
|
+
};
|
|
79
|
+
export const STATUS_COLORS = {
|
|
80
|
+
running: "#4ade80",
|
|
81
|
+
waiting: "#6b7280",
|
|
82
|
+
paused: "#facc15",
|
|
83
|
+
idle: "#fb923c",
|
|
84
|
+
stopped: "#f87171",
|
|
85
|
+
};
|
|
86
|
+
export function statusColor(status) {
|
|
87
|
+
return STATUS_COLORS[status] ?? "#9ca3af";
|
|
88
|
+
}
|
|
89
|
+
export function tabAccentColor(tab) {
|
|
90
|
+
const map = {
|
|
91
|
+
loops: darkTheme.accent.loop,
|
|
92
|
+
tasks: darkTheme.accent.task,
|
|
93
|
+
projects: darkTheme.accent.project,
|
|
94
|
+
};
|
|
95
|
+
return map[tab] ?? darkTheme.accent.brand;
|
|
96
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LOG_LINE_CHARS_MAX } from "../config/constants.js";
|
|
2
|
+
const TRUNCATION_MARKER = " …[line truncated]";
|
|
3
|
+
export function clampLine(line, maxChars = LOG_LINE_CHARS_MAX) {
|
|
4
|
+
return line.length > maxChars ? line.slice(0, maxChars) + TRUNCATION_MARKER : line;
|
|
5
|
+
}
|
|
6
|
+
export function clampLines(lines, maxCount, maxChars = LOG_LINE_CHARS_MAX) {
|
|
7
|
+
const tail = lines.length > maxCount ? lines.slice(lines.length - maxCount) : lines;
|
|
8
|
+
return tail.map((l) => clampLine(l, maxChars));
|
|
9
|
+
}
|
|
10
|
+
export function appendClamped(prev, line, maxCount) {
|
|
11
|
+
const next = [...prev, clampLine(line)];
|
|
12
|
+
return next.length > maxCount ? next.slice(next.length - maxCount) : next;
|
|
13
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PASTE_MAX_CHARS } from "../config/constants.js";
|
|
2
|
+
import { CODE_EDITOR_WRAP_LENGTH } from "../config/constants.js";
|
|
3
|
+
export function sanitizePaste(raw) {
|
|
4
|
+
return raw
|
|
5
|
+
.replace(/\x1b\[20[01]~/g, "")
|
|
6
|
+
.replace(/[\r\n]+/g, " ")
|
|
7
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
|
8
|
+
.slice(0, PASTE_MAX_CHARS);
|
|
9
|
+
}
|
|
10
|
+
export function sanitizeMultilinePaste(raw) {
|
|
11
|
+
return raw
|
|
12
|
+
.replace(/\x1b\[20[01]~/g, "")
|
|
13
|
+
.replace(/\r\n/g, "\n")
|
|
14
|
+
.replace(/\r/g, "\n")
|
|
15
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
|
16
|
+
.slice(0, PASTE_MAX_CHARS);
|
|
17
|
+
}
|
|
18
|
+
export function wrapCommand(text, maxLen = CODE_EDITOR_WRAP_LENGTH) {
|
|
19
|
+
const words = text.split(" ");
|
|
20
|
+
const lines = [];
|
|
21
|
+
let current = "";
|
|
22
|
+
for (const word of words) {
|
|
23
|
+
if (current.length === 0) {
|
|
24
|
+
current = word;
|
|
25
|
+
}
|
|
26
|
+
else if (current.length + 1 + word.length <= maxLen) {
|
|
27
|
+
current += " " + word;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
lines.push(current);
|
|
31
|
+
current = word;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (current)
|
|
35
|
+
lines.push(current);
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const OPERATORS = new Set(["|", "&&", "||", ";", ">", ">>", "<"]);
|
|
2
|
+
/**
|
|
3
|
+
* Scan a command line into tokens, preserving whitespace runs as
|
|
4
|
+
* "whitespace" tokens. Used by highlightSegments for rendering.
|
|
5
|
+
*/
|
|
6
|
+
function scanTokens(line) {
|
|
7
|
+
if (!line)
|
|
8
|
+
return [];
|
|
9
|
+
const tokens = [];
|
|
10
|
+
let i = 0;
|
|
11
|
+
const len = line.length;
|
|
12
|
+
while (i < len) {
|
|
13
|
+
// Whitespace run preserve it as a token
|
|
14
|
+
if (line[i] === " " || line[i] === "\t") {
|
|
15
|
+
let j = i;
|
|
16
|
+
while (j < len && (line[j] === " " || line[j] === "\t"))
|
|
17
|
+
j++;
|
|
18
|
+
tokens.push({ type: "whitespace", value: line.slice(i, j) });
|
|
19
|
+
i = j;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
// Try to match multi-char operators first (&&, ||, >>)
|
|
23
|
+
let opMatch;
|
|
24
|
+
for (const op of OPERATORS) {
|
|
25
|
+
if (op.length > 1 && line.slice(i, i + op.length) === op) {
|
|
26
|
+
opMatch = op;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (opMatch) {
|
|
31
|
+
tokens.push({ type: "operator", value: opMatch });
|
|
32
|
+
i += opMatch.length;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Try to match single-char operators
|
|
36
|
+
if (OPERATORS.has(line[i])) {
|
|
37
|
+
tokens.push({ type: "operator", value: line[i] });
|
|
38
|
+
i++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
// Quoted string
|
|
42
|
+
if (line[i] === '"' || line[i] === "'") {
|
|
43
|
+
const quote = line[i];
|
|
44
|
+
let j = i + 1;
|
|
45
|
+
while (j < len && line[j] !== quote) {
|
|
46
|
+
// Handle escaped quote
|
|
47
|
+
if (line[j] === "\\" && j + 1 < len) {
|
|
48
|
+
j += 2;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
j++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Include closing quote if found
|
|
55
|
+
if (j < len && line[j] === quote)
|
|
56
|
+
j++;
|
|
57
|
+
tokens.push({ type: "string", value: line.slice(i, j) });
|
|
58
|
+
i = j;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// Unquoted word consume until whitespace or operator
|
|
62
|
+
let j = i;
|
|
63
|
+
while (j < len) {
|
|
64
|
+
if (line[j] === " " || line[j] === "\t")
|
|
65
|
+
break;
|
|
66
|
+
// Check for multi-char operator boundary
|
|
67
|
+
let isOpBoundary = false;
|
|
68
|
+
for (const op of OPERATORS) {
|
|
69
|
+
if (op.length > 1 && line.slice(j, j + op.length) === op) {
|
|
70
|
+
isOpBoundary = true;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (isOpBoundary)
|
|
75
|
+
break;
|
|
76
|
+
// Check for single-char operator boundary
|
|
77
|
+
if (OPERATORS.has(line[j])) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
// Check for quoted string start
|
|
81
|
+
if (line[j] === '"' || line[j] === "'")
|
|
82
|
+
break;
|
|
83
|
+
j++;
|
|
84
|
+
}
|
|
85
|
+
const raw = line.slice(i, j);
|
|
86
|
+
tokens.push(classifyWord(raw));
|
|
87
|
+
i = j;
|
|
88
|
+
}
|
|
89
|
+
return tokens;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Tokenize a command line for syntax highlighting purposes.
|
|
93
|
+
* This is a cosmetic tokenizer the real parsing for execution
|
|
94
|
+
* lives in src/loop-config.ts parseCommandLine.
|
|
95
|
+
*
|
|
96
|
+
* Whitespace is not returned (use highlightSegments for rendering
|
|
97
|
+
* that needs to preserve spacing).
|
|
98
|
+
*/
|
|
99
|
+
export function tokenizeCommand(line) {
|
|
100
|
+
return scanTokens(line).filter((tok) => tok.type !== "whitespace");
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Produce colored segments for a command line, preserving whitespace
|
|
104
|
+
* exactly so token-by-token rendering does not collapse spaces.
|
|
105
|
+
*/
|
|
106
|
+
export function highlightSegments(line, colors, whitespaceColor) {
|
|
107
|
+
return scanTokens(line).map((tok) => ({
|
|
108
|
+
value: tok.value,
|
|
109
|
+
color: tok.type === "whitespace" ? whitespaceColor : colors[tok.type],
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
function classifyWord(value) {
|
|
113
|
+
// Flag: --something (long flag)
|
|
114
|
+
if (value.startsWith("--") && value.length > 2) {
|
|
115
|
+
return { type: "flag", value };
|
|
116
|
+
}
|
|
117
|
+
// Flag: -f (single letter flag, not negative number like -1)
|
|
118
|
+
if (value.startsWith("-") &&
|
|
119
|
+
value.length === 2 &&
|
|
120
|
+
/[a-zA-Z]/.test(value[1])) {
|
|
121
|
+
return { type: "flag", value };
|
|
122
|
+
}
|
|
123
|
+
// Flag: -abc (combined short flags like -xyz)
|
|
124
|
+
if (value.startsWith("-") && value.length > 1 && /^[a-zA-Z]+$/.test(value.slice(1))) {
|
|
125
|
+
return { type: "flag", value };
|
|
126
|
+
}
|
|
127
|
+
return { type: "word", value };
|
|
128
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { parseDuration } from "../../duration.js";
|
|
3
|
+
import { parseMaxRuns } from "../../loop-config.js";
|
|
4
|
+
import { t } from "../i18n/index.js";
|
|
5
|
+
const validators = {
|
|
6
|
+
name(value) {
|
|
7
|
+
if (!value.trim())
|
|
8
|
+
return t("errors.commandEmpty");
|
|
9
|
+
return null;
|
|
10
|
+
},
|
|
11
|
+
interval(value) {
|
|
12
|
+
if (!value.trim())
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
parseDuration(value);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
return err instanceof Error ? err.message : t("wizard.validationError");
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
command(value, options) {
|
|
23
|
+
if (options?.taskMode === "existing")
|
|
24
|
+
return null;
|
|
25
|
+
if (!value.trim())
|
|
26
|
+
return t("errors.commandEmpty");
|
|
27
|
+
return null;
|
|
28
|
+
},
|
|
29
|
+
description(value) {
|
|
30
|
+
if (!value.trim())
|
|
31
|
+
return null;
|
|
32
|
+
return null;
|
|
33
|
+
},
|
|
34
|
+
maxRuns(value) {
|
|
35
|
+
if (!value.trim())
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
parseMaxRuns(value);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
return err instanceof Error ? err.message : t("wizard.validationError");
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
cwd(value) {
|
|
46
|
+
if (!value.trim())
|
|
47
|
+
return null;
|
|
48
|
+
if (!fs.existsSync(value))
|
|
49
|
+
return t("board.cwdMissing", { cwd: value });
|
|
50
|
+
return null;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
export function validateField(key, value, options) {
|
|
54
|
+
const fn = validators[key];
|
|
55
|
+
return fn ? fn(value, options) : null;
|
|
56
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_TASK_MAX_RUNS = 5;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* capture.ts — Capture CLI output as evidence assets.
|
|
3
|
+
*
|
|
4
|
+
* For a CLI app, evidence is text-based (stdout/stderr captured to .txt files),
|
|
5
|
+
* not browser screenshots. Each capture checkpoint writes a numbered .txt file.
|
|
6
|
+
*/
|
|
7
|
+
import { writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
/** Max bytes per capture file (1 MB) */
|
|
10
|
+
const MAX_CAPTURE_BYTES = 1_024_000;
|
|
11
|
+
/**
|
|
12
|
+
* Write capture files and return asset descriptors.
|
|
13
|
+
* Files go to openspec/changes/<changeId>/evidence/NN-label.txt
|
|
14
|
+
*/
|
|
15
|
+
export function captureOutput(changeId, checkpoints) {
|
|
16
|
+
const evidenceDir = join("openspec", "changes", changeId, "evidence");
|
|
17
|
+
if (!existsSync(evidenceDir)) {
|
|
18
|
+
mkdirSync(evidenceDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
return checkpoints.map((cp, i) => {
|
|
21
|
+
const num = String(i + 1).padStart(2, "0");
|
|
22
|
+
const filename = `${num}-${cp.label.replace(/\s+/g, "-").toLowerCase()}.txt`;
|
|
23
|
+
const filepath = join(evidenceDir, filename);
|
|
24
|
+
const content = [
|
|
25
|
+
`# ${cp.label}`,
|
|
26
|
+
`# Exit code: ${cp.exitCode}`,
|
|
27
|
+
"",
|
|
28
|
+
"## stdout",
|
|
29
|
+
truncate(cp.stdout, MAX_CAPTURE_BYTES),
|
|
30
|
+
"",
|
|
31
|
+
"## stderr",
|
|
32
|
+
truncate(cp.stderr, MAX_CAPTURE_BYTES),
|
|
33
|
+
].join("\n");
|
|
34
|
+
writeFileSync(filepath, content, "utf-8");
|
|
35
|
+
const bytes = statSync(filepath).size;
|
|
36
|
+
return {
|
|
37
|
+
type: "text-capture",
|
|
38
|
+
path: filepath,
|
|
39
|
+
caption: cp.label,
|
|
40
|
+
bytes,
|
|
41
|
+
format: "txt",
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function truncate(text, maxBytes) {
|
|
46
|
+
const buf = Buffer.from(text, "utf-8");
|
|
47
|
+
if (buf.length <= maxBytes)
|
|
48
|
+
return text;
|
|
49
|
+
return (buf.subarray(0, maxBytes).toString("utf-8") +
|
|
50
|
+
`\n... [truncated, ${buf.length} bytes total]`);
|
|
51
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli.ts — Visual evidence capture CLI.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* pnpm visual-evidence --change <id>
|
|
6
|
+
* pnpm visual-evidence --input <path.json>
|
|
7
|
+
*
|
|
8
|
+
* Exit codes: 0 = passed/skipped, 1 = failed, 2 = blocked, 3 = invalid input
|
|
9
|
+
*
|
|
10
|
+
* Capture never commits or pushes.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
13
|
+
import { runEvidence } from "./run.js";
|
|
14
|
+
function parseArgs(args) {
|
|
15
|
+
const result = {};
|
|
16
|
+
for (let i = 0; i < args.length; i++) {
|
|
17
|
+
if (args[i] === "--change" && args[i + 1]) {
|
|
18
|
+
result.changeId = args[++i];
|
|
19
|
+
}
|
|
20
|
+
else if (args[i] === "--input" && args[i + 1]) {
|
|
21
|
+
result.inputPath = args[++i];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
async function main() {
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
const parsed = parseArgs(args);
|
|
29
|
+
if (!parsed.changeId && !parsed.inputPath) {
|
|
30
|
+
console.error("Usage: visual-evidence --change <id> | --input <path.json>");
|
|
31
|
+
return 3;
|
|
32
|
+
}
|
|
33
|
+
let changeId;
|
|
34
|
+
if (parsed.inputPath) {
|
|
35
|
+
if (!existsSync(parsed.inputPath)) {
|
|
36
|
+
console.error(`Input file not found: ${parsed.inputPath}`);
|
|
37
|
+
return 3;
|
|
38
|
+
}
|
|
39
|
+
const input = JSON.parse(readFileSync(parsed.inputPath, "utf-8"));
|
|
40
|
+
if (!input.changeId) {
|
|
41
|
+
console.error("Input JSON missing 'changeId'");
|
|
42
|
+
return 3;
|
|
43
|
+
}
|
|
44
|
+
changeId = input.changeId;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
changeId = parsed.changeId;
|
|
48
|
+
}
|
|
49
|
+
console.log(`🔍 Running evidence capture for change: ${changeId}`);
|
|
50
|
+
const result = await runEvidence(changeId);
|
|
51
|
+
const emoji = {
|
|
52
|
+
passed: "✅",
|
|
53
|
+
skipped: "⏭️",
|
|
54
|
+
failed: "❌",
|
|
55
|
+
blocked: "🚫",
|
|
56
|
+
};
|
|
57
|
+
console.log(`${emoji[result.status] ?? ""} Status: ${result.status}`);
|
|
58
|
+
if (result.reason) {
|
|
59
|
+
console.log(` Reason: ${result.reason}`);
|
|
60
|
+
}
|
|
61
|
+
if (result.failedStep) {
|
|
62
|
+
console.log(` Failed step: ${result.failedStep}`);
|
|
63
|
+
}
|
|
64
|
+
if (result.manifestPath) {
|
|
65
|
+
console.log(` Manifest: ${result.manifestPath}`);
|
|
66
|
+
}
|
|
67
|
+
// Exit codes
|
|
68
|
+
switch (result.status) {
|
|
69
|
+
case "passed":
|
|
70
|
+
case "skipped":
|
|
71
|
+
return 0;
|
|
72
|
+
case "failed":
|
|
73
|
+
return 1;
|
|
74
|
+
case "blocked":
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
main()
|
|
79
|
+
.then((code) => {
|
|
80
|
+
process.exit(code);
|
|
81
|
+
})
|
|
82
|
+
.catch((err) => {
|
|
83
|
+
console.error("Unexpected error:", err);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
});
|