@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,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Locking Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides a simple file-based locking mechanism to prevent concurrent
|
|
5
|
+
* writes to shared files like progress.txt, feature-list.json, etc.
|
|
6
|
+
*
|
|
7
|
+
* Uses lock files (.lock suffix) with process info to track ownership.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, writeFileSync, readFileSync, unlinkSync, readdirSync } from "node:fs";
|
|
11
|
+
import { join, dirname } from "path";
|
|
12
|
+
import { ensureDirSync } from "@/services/system/copy.ts";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// TYPES
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Lock file content structure.
|
|
20
|
+
*/
|
|
21
|
+
interface LockInfo {
|
|
22
|
+
/** Process ID that holds the lock */
|
|
23
|
+
pid: number;
|
|
24
|
+
/** Session ID (if available) */
|
|
25
|
+
sessionId?: string;
|
|
26
|
+
/** Timestamp when lock was acquired */
|
|
27
|
+
acquiredAt: number;
|
|
28
|
+
/** Hostname where the lock was acquired */
|
|
29
|
+
hostname?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Result of a lock acquisition attempt.
|
|
34
|
+
*/
|
|
35
|
+
export interface LockResult {
|
|
36
|
+
/** Whether the lock was acquired */
|
|
37
|
+
acquired: boolean;
|
|
38
|
+
/** Lock file path */
|
|
39
|
+
lockPath: string;
|
|
40
|
+
/** Error message if lock wasn't acquired */
|
|
41
|
+
error?: string;
|
|
42
|
+
/** Info about the process holding the lock (if not acquired) */
|
|
43
|
+
holder?: LockInfo;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// CONSTANTS
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
/** Lock file suffix */
|
|
51
|
+
const LOCK_SUFFIX = ".lock";
|
|
52
|
+
|
|
53
|
+
/** Default lock timeout in milliseconds (30 seconds) */
|
|
54
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 30000;
|
|
55
|
+
|
|
56
|
+
/** Retry interval for lock acquisition */
|
|
57
|
+
const LOCK_RETRY_INTERVAL_MS = 100;
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// LOCK FUNCTIONS
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the lock file path for a given file.
|
|
65
|
+
*/
|
|
66
|
+
export function getLockPath(filePath: string): string {
|
|
67
|
+
return `${filePath}${LOCK_SUFFIX}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Try to acquire a lock on a file.
|
|
72
|
+
*
|
|
73
|
+
* @param filePath - Path to the file to lock
|
|
74
|
+
* @param sessionId - Optional session ID for tracking
|
|
75
|
+
* @returns Lock result
|
|
76
|
+
*/
|
|
77
|
+
export function tryAcquireLock(filePath: string, sessionId?: string): LockResult {
|
|
78
|
+
const lockPath = getLockPath(filePath);
|
|
79
|
+
|
|
80
|
+
// Check if lock file exists
|
|
81
|
+
if (existsSync(lockPath)) {
|
|
82
|
+
try {
|
|
83
|
+
const content = readFileSync(lockPath, "utf-8");
|
|
84
|
+
const holder = JSON.parse(content) as LockInfo;
|
|
85
|
+
|
|
86
|
+
// Check if the lock holder process is still alive
|
|
87
|
+
if (isProcessAlive(holder.pid)) {
|
|
88
|
+
return {
|
|
89
|
+
acquired: false,
|
|
90
|
+
lockPath,
|
|
91
|
+
error: `File is locked by process ${holder.pid}`,
|
|
92
|
+
holder,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Lock holder process is dead, remove stale lock
|
|
97
|
+
unlinkSync(lockPath);
|
|
98
|
+
} catch {
|
|
99
|
+
// Invalid lock file, remove it
|
|
100
|
+
try {
|
|
101
|
+
unlinkSync(lockPath);
|
|
102
|
+
} catch {
|
|
103
|
+
// Ignore removal errors
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Try to create lock file
|
|
109
|
+
const lockInfo: LockInfo = {
|
|
110
|
+
pid: process.pid,
|
|
111
|
+
sessionId,
|
|
112
|
+
acquiredAt: Date.now(),
|
|
113
|
+
hostname: process.env.HOSTNAME,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// Ensure directory exists
|
|
118
|
+
const dir = dirname(lockPath);
|
|
119
|
+
if (!existsSync(dir)) {
|
|
120
|
+
ensureDirSync(dir);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Write lock file with exclusive flag to prevent race conditions
|
|
124
|
+
writeFileSync(lockPath, JSON.stringify(lockInfo, null, 2), { flag: "wx" });
|
|
125
|
+
return { acquired: true, lockPath };
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Another process might have created the lock
|
|
128
|
+
return {
|
|
129
|
+
acquired: false,
|
|
130
|
+
lockPath,
|
|
131
|
+
error: `Failed to acquire lock: ${error instanceof Error ? error.message : String(error)}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Acquire a lock with retry and timeout.
|
|
138
|
+
*
|
|
139
|
+
* @param filePath - Path to the file to lock
|
|
140
|
+
* @param options - Lock options
|
|
141
|
+
* @returns Lock result
|
|
142
|
+
*/
|
|
143
|
+
export async function acquireLock(
|
|
144
|
+
filePath: string,
|
|
145
|
+
options: {
|
|
146
|
+
sessionId?: string;
|
|
147
|
+
timeoutMs?: number;
|
|
148
|
+
} = {}
|
|
149
|
+
): Promise<LockResult> {
|
|
150
|
+
const { sessionId, timeoutMs = DEFAULT_LOCK_TIMEOUT_MS } = options;
|
|
151
|
+
const startTime = Date.now();
|
|
152
|
+
|
|
153
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
154
|
+
const result = tryAcquireLock(filePath, sessionId);
|
|
155
|
+
if (result.acquired) {
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Wait before retry
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Timeout
|
|
164
|
+
return tryAcquireLock(filePath, sessionId);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Release a lock on a file.
|
|
169
|
+
*
|
|
170
|
+
* @param filePath - Path to the file to unlock
|
|
171
|
+
* @param options - Release options
|
|
172
|
+
* @returns True if lock was released
|
|
173
|
+
*/
|
|
174
|
+
export function releaseLock(
|
|
175
|
+
filePath: string,
|
|
176
|
+
options: { force?: boolean } = {}
|
|
177
|
+
): boolean {
|
|
178
|
+
const lockPath = getLockPath(filePath);
|
|
179
|
+
|
|
180
|
+
if (!existsSync(lockPath)) {
|
|
181
|
+
return true; // Already unlocked
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Verify we own the lock (unless force)
|
|
185
|
+
if (!options.force) {
|
|
186
|
+
try {
|
|
187
|
+
const content = readFileSync(lockPath, "utf-8");
|
|
188
|
+
const holder = JSON.parse(content) as LockInfo;
|
|
189
|
+
if (holder.pid !== process.pid) {
|
|
190
|
+
return false; // We don't own this lock
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
// Invalid lock file, safe to remove
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
unlinkSync(lockPath);
|
|
199
|
+
return true;
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Execute a function while holding a lock on a file.
|
|
207
|
+
*
|
|
208
|
+
* @param filePath - Path to the file to lock
|
|
209
|
+
* @param fn - Function to execute while holding the lock
|
|
210
|
+
* @param options - Lock options
|
|
211
|
+
* @returns Result of the function
|
|
212
|
+
*/
|
|
213
|
+
export async function withLock<T>(
|
|
214
|
+
filePath: string,
|
|
215
|
+
fn: () => T | Promise<T>,
|
|
216
|
+
options: {
|
|
217
|
+
sessionId?: string;
|
|
218
|
+
timeoutMs?: number;
|
|
219
|
+
} = {}
|
|
220
|
+
): Promise<T> {
|
|
221
|
+
const lockResult = await acquireLock(filePath, options);
|
|
222
|
+
|
|
223
|
+
if (!lockResult.acquired) {
|
|
224
|
+
throw new Error(lockResult.error ?? `Failed to acquire lock for ${filePath}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
return await fn();
|
|
229
|
+
} finally {
|
|
230
|
+
releaseLock(filePath);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// HELPER FUNCTIONS
|
|
236
|
+
// ============================================================================
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Check if a process is still alive.
|
|
240
|
+
*/
|
|
241
|
+
function isProcessAlive(pid: number): boolean {
|
|
242
|
+
try {
|
|
243
|
+
// Sending signal 0 doesn't kill the process, just checks if it exists
|
|
244
|
+
process.kill(pid, 0);
|
|
245
|
+
return true;
|
|
246
|
+
} catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Clean up stale locks for a directory.
|
|
253
|
+
* Removes lock files whose holder processes are no longer alive.
|
|
254
|
+
*
|
|
255
|
+
* @param directory - Directory to clean up
|
|
256
|
+
* @returns Number of stale locks removed
|
|
257
|
+
*/
|
|
258
|
+
export function cleanupStaleLocks(directory: string): number {
|
|
259
|
+
let removed = 0;
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const files = readdirSync(directory) as string[];
|
|
263
|
+
for (const file of files) {
|
|
264
|
+
if (file.endsWith(LOCK_SUFFIX)) {
|
|
265
|
+
const lockPath = join(directory, file);
|
|
266
|
+
try {
|
|
267
|
+
const content = readFileSync(lockPath, "utf-8");
|
|
268
|
+
const holder = JSON.parse(content) as LockInfo;
|
|
269
|
+
if (!isProcessAlive(holder.pid)) {
|
|
270
|
+
unlinkSync(lockPath);
|
|
271
|
+
removed++;
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
// Invalid lock file, remove it
|
|
275
|
+
try {
|
|
276
|
+
unlinkSync(lockPath);
|
|
277
|
+
removed++;
|
|
278
|
+
} catch {
|
|
279
|
+
// Ignore
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// Ignore directory read errors
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return removed;
|
|
289
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global skills installation.
|
|
3
|
+
*
|
|
4
|
+
* Installs bundled agent skills globally via `npx skills`, then removes
|
|
5
|
+
* source-control skill variants so `atomic init` can install them
|
|
6
|
+
* locally per-project based on the user's selected SCM + active agent.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const SKILLS_REPO = "https://github.com/flora131/atomic.git";
|
|
10
|
+
const SKILLS_AGENTS = ["claude-code", "opencode", "github-copilot"] as const;
|
|
11
|
+
const SCM_SKILLS_TO_REMOVE_GLOBALLY = [
|
|
12
|
+
"gh-commit",
|
|
13
|
+
"gh-create-pr",
|
|
14
|
+
"sl-commit",
|
|
15
|
+
"sl-submit-diff",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
async function runNpxSkills(args: string[]): Promise<boolean> {
|
|
19
|
+
const npxPath = Bun.which("npx");
|
|
20
|
+
if (!npxPath) {
|
|
21
|
+
console.warn("npx not found on PATH — skipping skills install");
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const proc = Bun.spawn([npxPath, "--yes", "skills", ...args], {
|
|
26
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
27
|
+
});
|
|
28
|
+
const exitCode = await proc.exited;
|
|
29
|
+
return exitCode === 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function installGlobalSkills(): Promise<void> {
|
|
33
|
+
const agentFlags = SKILLS_AGENTS.flatMap((agent) => ["-a", agent]);
|
|
34
|
+
|
|
35
|
+
console.log("Installing bundled skills globally...");
|
|
36
|
+
const addOk = await runNpxSkills([
|
|
37
|
+
"add",
|
|
38
|
+
SKILLS_REPO,
|
|
39
|
+
"--skill",
|
|
40
|
+
"*",
|
|
41
|
+
"-g",
|
|
42
|
+
...agentFlags,
|
|
43
|
+
"-y",
|
|
44
|
+
]);
|
|
45
|
+
if (!addOk) {
|
|
46
|
+
console.warn("Warning: 'npx skills add' exited non-zero (non-fatal)");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const removeSkillFlags = SCM_SKILLS_TO_REMOVE_GLOBALLY.flatMap((skill) => [
|
|
51
|
+
"--skill",
|
|
52
|
+
skill,
|
|
53
|
+
]);
|
|
54
|
+
console.log(
|
|
55
|
+
"Removing source-control skill variants globally (added per-project by `atomic init`)...",
|
|
56
|
+
);
|
|
57
|
+
const removeOk = await runNpxSkills([
|
|
58
|
+
"remove",
|
|
59
|
+
...removeSkillFlags,
|
|
60
|
+
"-g",
|
|
61
|
+
...agentFlags,
|
|
62
|
+
"-y",
|
|
63
|
+
]);
|
|
64
|
+
if (!removeOk) {
|
|
65
|
+
console.warn("Warning: 'npx skills remove' exited non-zero (non-fatal)");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { supportsColor } from "@/services/system/detect.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ANSI color and formatting codes for CLI output
|
|
5
|
+
* Respects the NO_COLOR environment variable
|
|
6
|
+
*/
|
|
7
|
+
const ANSI_CODES = {
|
|
8
|
+
bold: "\x1b[1m",
|
|
9
|
+
dim: "\x1b[2m",
|
|
10
|
+
reset: "\x1b[0m",
|
|
11
|
+
red: "\x1b[31m",
|
|
12
|
+
green: "\x1b[32m",
|
|
13
|
+
yellow: "\x1b[33m",
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
const NO_COLORS = {
|
|
17
|
+
bold: "",
|
|
18
|
+
dim: "",
|
|
19
|
+
reset: "",
|
|
20
|
+
red: "",
|
|
21
|
+
green: "",
|
|
22
|
+
yellow: "",
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export const COLORS = supportsColor() ? ANSI_CODES : NO_COLORS;
|