@bastani/atomic 0.5.0-1
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 +24 -0
- package/README.md +956 -0
- package/assets/settings.schema.json +52 -0
- package/package.json +68 -0
- package/src/cli.ts +197 -0
- package/src/commands/cli/chat/client.ts +18 -0
- package/src/commands/cli/chat/index.ts +247 -0
- package/src/commands/cli/chat.ts +8 -0
- package/src/commands/cli/config.ts +55 -0
- package/src/commands/cli/init/index.ts +452 -0
- package/src/commands/cli/init/onboarding.ts +45 -0
- package/src/commands/cli/init/scm.ts +190 -0
- package/src/commands/cli/init.ts +8 -0
- package/src/commands/cli/update.ts +46 -0
- package/src/commands/cli/workflow.ts +164 -0
- package/src/lib/merge.ts +65 -0
- package/src/lib/path-root-guard.ts +38 -0
- package/src/lib/spawn.ts +467 -0
- package/src/scripts/bump-version.ts +94 -0
- package/src/scripts/constants-base.ts +14 -0
- package/src/scripts/constants.ts +34 -0
- package/src/sdk/components/color-utils.ts +20 -0
- package/src/sdk/components/connectors.test.ts +661 -0
- package/src/sdk/components/connectors.ts +156 -0
- package/src/sdk/components/edge.tsx +11 -0
- package/src/sdk/components/error-boundary.tsx +38 -0
- package/src/sdk/components/graph-theme.ts +36 -0
- package/src/sdk/components/header.tsx +60 -0
- package/src/sdk/components/layout.test.ts +924 -0
- package/src/sdk/components/layout.ts +186 -0
- package/src/sdk/components/node-card.tsx +68 -0
- package/src/sdk/components/orchestrator-panel-contexts.ts +26 -0
- package/src/sdk/components/orchestrator-panel-store.test.ts +561 -0
- package/src/sdk/components/orchestrator-panel-store.ts +118 -0
- package/src/sdk/components/orchestrator-panel-types.ts +21 -0
- package/src/sdk/components/orchestrator-panel.tsx +143 -0
- package/src/sdk/components/session-graph-panel.tsx +364 -0
- package/src/sdk/components/status-helpers.ts +32 -0
- package/src/sdk/components/statusline.tsx +63 -0
- package/src/sdk/define-workflow.ts +98 -0
- package/src/sdk/errors.ts +39 -0
- package/src/sdk/index.ts +38 -0
- package/src/sdk/providers/claude.ts +316 -0
- package/src/sdk/providers/copilot.ts +43 -0
- package/src/sdk/providers/opencode.ts +43 -0
- package/src/sdk/runtime/discovery.ts +172 -0
- package/src/sdk/runtime/executor.test.ts +415 -0
- package/src/sdk/runtime/executor.ts +695 -0
- package/src/sdk/runtime/loader.ts +372 -0
- package/src/sdk/runtime/panel.tsx +9 -0
- package/src/sdk/runtime/theme.ts +76 -0
- package/src/sdk/runtime/tmux.ts +542 -0
- package/src/sdk/types.ts +114 -0
- package/src/sdk/workflows.ts +85 -0
- package/src/services/config/atomic-config.ts +124 -0
- package/src/services/config/atomic-global-config.ts +361 -0
- package/src/services/config/config-path.ts +19 -0
- package/src/services/config/definitions.ts +176 -0
- package/src/services/config/index.ts +7 -0
- package/src/services/config/settings-schema.ts +2 -0
- package/src/services/config/settings.ts +149 -0
- package/src/services/system/copy.ts +381 -0
- package/src/services/system/detect.ts +161 -0
- package/src/services/system/download.ts +325 -0
- package/src/services/system/file-lock.ts +289 -0
- package/src/services/system/skills.ts +67 -0
- package/src/theme/colors.ts +25 -0
- package/src/version.ts +7 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// ─── Layout ───────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
import type { SessionData, SessionStatus } from "./orchestrator-panel-types.ts";
|
|
4
|
+
|
|
5
|
+
// ─── Layout Constants ─────────────────────────────
|
|
6
|
+
|
|
7
|
+
export const NODE_W = 36;
|
|
8
|
+
export const NODE_H = 4;
|
|
9
|
+
export const H_GAP = 6;
|
|
10
|
+
export const V_GAP = 3;
|
|
11
|
+
export const PAD = 3;
|
|
12
|
+
|
|
13
|
+
// ─── Layout Types ─────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface LayoutNode {
|
|
16
|
+
name: string;
|
|
17
|
+
status: SessionStatus;
|
|
18
|
+
parents: string[];
|
|
19
|
+
error?: string;
|
|
20
|
+
startedAt: number | null;
|
|
21
|
+
endedAt: number | null;
|
|
22
|
+
children: LayoutNode[];
|
|
23
|
+
depth: number;
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LayoutResult {
|
|
29
|
+
roots: LayoutNode[];
|
|
30
|
+
map: Record<string, LayoutNode>;
|
|
31
|
+
rowH: Record<number, number>;
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── Layout Helpers ──────────────────────────────
|
|
37
|
+
|
|
38
|
+
/** Shift a node and all its tree descendants by `dx` pixels horizontally. */
|
|
39
|
+
function shiftSubtree(n: LayoutNode, dx: number): void {
|
|
40
|
+
n.x += dx;
|
|
41
|
+
for (const c of n.children) shiftSubtree(c, dx);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Walk every depth level and push overlapping nodes apart.
|
|
46
|
+
* Merge-node shifts can cause a sub-tree to land on top of nodes that
|
|
47
|
+
* were already placed by the root-tree pass. This resolves those
|
|
48
|
+
* collisions by nudging the rightmost overlapping node (and its
|
|
49
|
+
* descendants) to the right.
|
|
50
|
+
*/
|
|
51
|
+
function resolveOverlaps(map: Record<string, LayoutNode>): void {
|
|
52
|
+
const byDepth: Record<number, LayoutNode[]> = {};
|
|
53
|
+
for (const n of Object.values(map)) {
|
|
54
|
+
(byDepth[n.depth] ??= []).push(n);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const depths = Object.keys(byDepth).map(Number).sort((a, b) => a - b);
|
|
58
|
+
for (const d of depths) {
|
|
59
|
+
const nodes = byDepth[d]!;
|
|
60
|
+
if (nodes.length < 2) continue;
|
|
61
|
+
nodes.sort((a, b) => a.x - b.x);
|
|
62
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
63
|
+
const prev = nodes[i - 1]!;
|
|
64
|
+
const curr = nodes[i]!;
|
|
65
|
+
const minX = prev.x + NODE_W + H_GAP;
|
|
66
|
+
if (curr.x < minX) {
|
|
67
|
+
shiftSubtree(curr, minX - curr.x);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Layout Computation ───────────────────────────
|
|
74
|
+
|
|
75
|
+
export function computeLayout(sessions: SessionData[]): LayoutResult {
|
|
76
|
+
const map: Record<string, LayoutNode> = {};
|
|
77
|
+
const roots: LayoutNode[] = [];
|
|
78
|
+
const mergeNodes: LayoutNode[] = [];
|
|
79
|
+
|
|
80
|
+
for (const s of sessions) {
|
|
81
|
+
map[s.name] = {
|
|
82
|
+
name: s.name,
|
|
83
|
+
status: s.status,
|
|
84
|
+
parents: s.parents,
|
|
85
|
+
error: s.error,
|
|
86
|
+
startedAt: s.startedAt,
|
|
87
|
+
endedAt: s.endedAt,
|
|
88
|
+
children: [],
|
|
89
|
+
depth: 0,
|
|
90
|
+
x: 0,
|
|
91
|
+
y: 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Classify: single-parent → tree child, multi-parent → merge node, no parent → root
|
|
96
|
+
for (const s of sessions) {
|
|
97
|
+
if (s.parents.length > 1) {
|
|
98
|
+
mergeNodes.push(map[s.name]!);
|
|
99
|
+
} else if (s.parents.length === 1 && map[s.parents[0]!]) {
|
|
100
|
+
map[s.parents[0]!]!.children.push(map[s.name]!);
|
|
101
|
+
} else {
|
|
102
|
+
roots.push(map[s.name]!);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function setDepth(n: LayoutNode, d: number) {
|
|
107
|
+
n.depth = d;
|
|
108
|
+
for (const c of n.children) setDepth(c, d + 1);
|
|
109
|
+
}
|
|
110
|
+
for (const r of roots) setDepth(r, 0);
|
|
111
|
+
|
|
112
|
+
// Merge nodes: depth = max(parent depths) + 1, then recurse into children
|
|
113
|
+
for (const m of mergeNodes) {
|
|
114
|
+
const maxParentDepth = Math.max(...m.parents.map((p) => map[p]?.depth ?? 0));
|
|
115
|
+
setDepth(m, maxParentDepth + 1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const rowH: Record<number, number> = {};
|
|
119
|
+
for (const n of Object.values(map)) {
|
|
120
|
+
rowH[n.depth] = Math.max(rowH[n.depth] ?? 0, NODE_H);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function yAt(d: number): number {
|
|
124
|
+
let y = 0;
|
|
125
|
+
for (let i = 0; i < d; i++) y += (rowH[i] ?? NODE_H) + V_GAP;
|
|
126
|
+
return y;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let cursor = 0;
|
|
130
|
+
|
|
131
|
+
function place(n: LayoutNode) {
|
|
132
|
+
if (n.children.length === 0) {
|
|
133
|
+
n.x = cursor;
|
|
134
|
+
n.y = yAt(n.depth);
|
|
135
|
+
cursor += NODE_W + H_GAP;
|
|
136
|
+
} else {
|
|
137
|
+
for (const c of n.children) place(c);
|
|
138
|
+
const first = n.children[0]!;
|
|
139
|
+
const last = n.children[n.children.length - 1]!;
|
|
140
|
+
n.x = Math.round((first.x + last.x) / 2);
|
|
141
|
+
n.y = yAt(n.depth);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let firstRoot = true;
|
|
146
|
+
for (const r of roots) {
|
|
147
|
+
if (!firstRoot) cursor += H_GAP;
|
|
148
|
+
place(r);
|
|
149
|
+
firstRoot = false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Place merge nodes centered under all parents (and their sub-trees)
|
|
153
|
+
for (const m of mergeNodes) {
|
|
154
|
+
const parentCenters = m.parents.map((p) => (map[p]?.x ?? 0) + Math.floor(NODE_W / 2));
|
|
155
|
+
const avgCenter = Math.round(parentCenters.reduce((a, b) => a + b, 0) / parentCenters.length);
|
|
156
|
+
|
|
157
|
+
if (m.children.length > 0) {
|
|
158
|
+
// Place sub-tree first, then shift to center under parents
|
|
159
|
+
place(m);
|
|
160
|
+
const currentCenter = m.x + Math.floor(NODE_W / 2);
|
|
161
|
+
const dx = avgCenter - currentCenter;
|
|
162
|
+
shiftSubtree(m, dx);
|
|
163
|
+
} else {
|
|
164
|
+
m.x = avgCenter - Math.floor(NODE_W / 2);
|
|
165
|
+
m.y = yAt(m.depth);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Resolve horizontal overlaps that merge-node shifts may have introduced.
|
|
170
|
+
// Process depths in ascending order so child shifts cascade correctly.
|
|
171
|
+
resolveOverlaps(map);
|
|
172
|
+
|
|
173
|
+
for (const n of Object.values(map)) {
|
|
174
|
+
n.x += PAD;
|
|
175
|
+
n.y += PAD;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let maxX = 0;
|
|
179
|
+
let maxY = 0;
|
|
180
|
+
for (const n of Object.values(map)) {
|
|
181
|
+
maxX = Math.max(maxX, n.x + NODE_W);
|
|
182
|
+
maxY = Math.max(maxY, n.y + NODE_H);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { roots, map, rowH, width: maxX + PAD, height: maxY + PAD };
|
|
186
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/react */
|
|
2
|
+
|
|
3
|
+
import { lerpColor } from "./color-utils.ts";
|
|
4
|
+
import { useGraphTheme } from "./orchestrator-panel-contexts.ts";
|
|
5
|
+
import { statusColor, fmtDuration } from "./status-helpers.ts";
|
|
6
|
+
import { NODE_W, type LayoutNode } from "./layout.ts";
|
|
7
|
+
|
|
8
|
+
export function NodeCard({
|
|
9
|
+
node,
|
|
10
|
+
focused,
|
|
11
|
+
pulsePhase,
|
|
12
|
+
displayH,
|
|
13
|
+
}: {
|
|
14
|
+
node: LayoutNode;
|
|
15
|
+
focused: boolean;
|
|
16
|
+
pulsePhase: number;
|
|
17
|
+
displayH: number;
|
|
18
|
+
}) {
|
|
19
|
+
const theme = useGraphTheme();
|
|
20
|
+
const sc = statusColor(node.status, theme);
|
|
21
|
+
const isPending = node.status === "pending";
|
|
22
|
+
const isRunning = node.status === "running";
|
|
23
|
+
|
|
24
|
+
// Border: running nodes smoothly pulse, others show status color
|
|
25
|
+
let borderCol: string;
|
|
26
|
+
if (isRunning) {
|
|
27
|
+
const t = (Math.sin((pulsePhase / 32) * Math.PI * 2 - Math.PI / 2) + 1) / 2;
|
|
28
|
+
borderCol = focused
|
|
29
|
+
? lerpColor(theme.warning, "#ffffff", 0.2)
|
|
30
|
+
: lerpColor(theme.border, theme.warning, t);
|
|
31
|
+
} else if (isPending) {
|
|
32
|
+
borderCol = focused ? sc : theme.borderActive;
|
|
33
|
+
} else {
|
|
34
|
+
borderCol = sc;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Background: focused nodes get a subtle status-colored tint
|
|
38
|
+
const bgCol = focused ? lerpColor(theme.background, sc, 0.12) : "transparent";
|
|
39
|
+
|
|
40
|
+
// Duration computed live from start/end timestamps
|
|
41
|
+
const durCol = isPending ? theme.textDim : sc;
|
|
42
|
+
const duration =
|
|
43
|
+
node.startedAt !== null
|
|
44
|
+
? fmtDuration((node.endedAt ?? Date.now()) - node.startedAt)
|
|
45
|
+
: "\u2014";
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<box
|
|
49
|
+
position="absolute"
|
|
50
|
+
left={node.x}
|
|
51
|
+
top={node.y}
|
|
52
|
+
width={NODE_W}
|
|
53
|
+
height={displayH}
|
|
54
|
+
border
|
|
55
|
+
borderStyle="rounded"
|
|
56
|
+
borderColor={borderCol}
|
|
57
|
+
backgroundColor={bgCol}
|
|
58
|
+
flexDirection="column"
|
|
59
|
+
justifyContent="center"
|
|
60
|
+
title={` ${node.name} `}
|
|
61
|
+
titleAlignment="center"
|
|
62
|
+
>
|
|
63
|
+
<box alignItems="center">
|
|
64
|
+
<text fg={durCol}>{duration}</text>
|
|
65
|
+
</box>
|
|
66
|
+
</box>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// ─── React Contexts & Hooks ───────────────────────
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, useEffect } from "react";
|
|
4
|
+
import type { PanelStore } from "./orchestrator-panel-store.ts";
|
|
5
|
+
import type { GraphTheme } from "./graph-theme.ts";
|
|
6
|
+
|
|
7
|
+
export const StoreContext = createContext<PanelStore | null>(null);
|
|
8
|
+
export const ThemeContext = createContext<GraphTheme | null>(null);
|
|
9
|
+
export const TmuxSessionContext = createContext("");
|
|
10
|
+
|
|
11
|
+
export function useStore(): PanelStore {
|
|
12
|
+
const ctx = useContext(StoreContext);
|
|
13
|
+
if (!ctx) throw new Error("useStore must be used within StoreContext.Provider");
|
|
14
|
+
return ctx;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useGraphTheme(): GraphTheme {
|
|
18
|
+
const ctx = useContext(ThemeContext);
|
|
19
|
+
if (!ctx) throw new Error("useGraphTheme must be used within ThemeContext.Provider");
|
|
20
|
+
return ctx;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useStoreSubscription(store: PanelStore): void {
|
|
24
|
+
const [, forceRender] = useState(0);
|
|
25
|
+
useEffect(() => store.subscribe(() => forceRender((c) => c + 1)), [store]);
|
|
26
|
+
}
|