@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.
Files changed (68) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +956 -0
  3. package/assets/settings.schema.json +52 -0
  4. package/package.json +68 -0
  5. package/src/cli.ts +197 -0
  6. package/src/commands/cli/chat/client.ts +18 -0
  7. package/src/commands/cli/chat/index.ts +247 -0
  8. package/src/commands/cli/chat.ts +8 -0
  9. package/src/commands/cli/config.ts +55 -0
  10. package/src/commands/cli/init/index.ts +452 -0
  11. package/src/commands/cli/init/onboarding.ts +45 -0
  12. package/src/commands/cli/init/scm.ts +190 -0
  13. package/src/commands/cli/init.ts +8 -0
  14. package/src/commands/cli/update.ts +46 -0
  15. package/src/commands/cli/workflow.ts +164 -0
  16. package/src/lib/merge.ts +65 -0
  17. package/src/lib/path-root-guard.ts +38 -0
  18. package/src/lib/spawn.ts +467 -0
  19. package/src/scripts/bump-version.ts +94 -0
  20. package/src/scripts/constants-base.ts +14 -0
  21. package/src/scripts/constants.ts +34 -0
  22. package/src/sdk/components/color-utils.ts +20 -0
  23. package/src/sdk/components/connectors.test.ts +661 -0
  24. package/src/sdk/components/connectors.ts +156 -0
  25. package/src/sdk/components/edge.tsx +11 -0
  26. package/src/sdk/components/error-boundary.tsx +38 -0
  27. package/src/sdk/components/graph-theme.ts +36 -0
  28. package/src/sdk/components/header.tsx +60 -0
  29. package/src/sdk/components/layout.test.ts +924 -0
  30. package/src/sdk/components/layout.ts +186 -0
  31. package/src/sdk/components/node-card.tsx +68 -0
  32. package/src/sdk/components/orchestrator-panel-contexts.ts +26 -0
  33. package/src/sdk/components/orchestrator-panel-store.test.ts +561 -0
  34. package/src/sdk/components/orchestrator-panel-store.ts +118 -0
  35. package/src/sdk/components/orchestrator-panel-types.ts +21 -0
  36. package/src/sdk/components/orchestrator-panel.tsx +143 -0
  37. package/src/sdk/components/session-graph-panel.tsx +364 -0
  38. package/src/sdk/components/status-helpers.ts +32 -0
  39. package/src/sdk/components/statusline.tsx +63 -0
  40. package/src/sdk/define-workflow.ts +98 -0
  41. package/src/sdk/errors.ts +39 -0
  42. package/src/sdk/index.ts +38 -0
  43. package/src/sdk/providers/claude.ts +316 -0
  44. package/src/sdk/providers/copilot.ts +43 -0
  45. package/src/sdk/providers/opencode.ts +43 -0
  46. package/src/sdk/runtime/discovery.ts +172 -0
  47. package/src/sdk/runtime/executor.test.ts +415 -0
  48. package/src/sdk/runtime/executor.ts +695 -0
  49. package/src/sdk/runtime/loader.ts +372 -0
  50. package/src/sdk/runtime/panel.tsx +9 -0
  51. package/src/sdk/runtime/theme.ts +76 -0
  52. package/src/sdk/runtime/tmux.ts +542 -0
  53. package/src/sdk/types.ts +114 -0
  54. package/src/sdk/workflows.ts +85 -0
  55. package/src/services/config/atomic-config.ts +124 -0
  56. package/src/services/config/atomic-global-config.ts +361 -0
  57. package/src/services/config/config-path.ts +19 -0
  58. package/src/services/config/definitions.ts +176 -0
  59. package/src/services/config/index.ts +7 -0
  60. package/src/services/config/settings-schema.ts +2 -0
  61. package/src/services/config/settings.ts +149 -0
  62. package/src/services/system/copy.ts +381 -0
  63. package/src/services/system/detect.ts +161 -0
  64. package/src/services/system/download.ts +325 -0
  65. package/src/services/system/file-lock.ts +289 -0
  66. package/src/services/system/skills.ts +67 -0
  67. package/src/theme/colors.ts +25 -0
  68. 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
+ }