opencastle 0.26.1 → 0.27.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/README.md +7 -1
- package/bin/cli.mjs +10 -0
- package/dist/cli/agents.d.ts +3 -0
- package/dist/cli/agents.d.ts.map +1 -0
- package/dist/cli/agents.js +161 -0
- package/dist/cli/agents.js.map +1 -0
- package/dist/cli/baselines.d.ts +3 -0
- package/dist/cli/baselines.d.ts.map +1 -0
- package/dist/cli/baselines.js +128 -0
- package/dist/cli/baselines.js.map +1 -0
- package/dist/cli/convoy/engine.d.ts +68 -2
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +2102 -26
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +1572 -70
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/events.d.ts +4 -1
- package/dist/cli/convoy/events.d.ts.map +1 -1
- package/dist/cli/convoy/events.js +74 -13
- package/dist/cli/convoy/events.js.map +1 -1
- package/dist/cli/convoy/events.test.js +154 -27
- package/dist/cli/convoy/events.test.js.map +1 -1
- package/dist/cli/convoy/expertise.d.ts +16 -0
- package/dist/cli/convoy/expertise.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.js +121 -0
- package/dist/cli/convoy/expertise.js.map +1 -0
- package/dist/cli/convoy/expertise.test.d.ts +2 -0
- package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.test.js +96 -0
- package/dist/cli/convoy/expertise.test.js.map +1 -0
- package/dist/cli/convoy/export.test.js +1 -0
- package/dist/cli/convoy/export.test.js.map +1 -1
- package/dist/cli/convoy/formula.d.ts +19 -0
- package/dist/cli/convoy/formula.d.ts.map +1 -0
- package/dist/cli/convoy/formula.js +142 -0
- package/dist/cli/convoy/formula.js.map +1 -0
- package/dist/cli/convoy/formula.test.d.ts +2 -0
- package/dist/cli/convoy/formula.test.d.ts.map +1 -0
- package/dist/cli/convoy/formula.test.js +342 -0
- package/dist/cli/convoy/formula.test.js.map +1 -0
- package/dist/cli/convoy/gates.d.ts +128 -0
- package/dist/cli/convoy/gates.d.ts.map +1 -0
- package/dist/cli/convoy/gates.js +606 -0
- package/dist/cli/convoy/gates.js.map +1 -0
- package/dist/cli/convoy/gates.test.d.ts +2 -0
- package/dist/cli/convoy/gates.test.d.ts.map +1 -0
- package/dist/cli/convoy/gates.test.js +976 -0
- package/dist/cli/convoy/gates.test.js.map +1 -0
- package/dist/cli/convoy/health.d.ts +11 -0
- package/dist/cli/convoy/health.d.ts.map +1 -1
- package/dist/cli/convoy/health.js +54 -0
- package/dist/cli/convoy/health.js.map +1 -1
- package/dist/cli/convoy/health.test.js +56 -1
- package/dist/cli/convoy/health.test.js.map +1 -1
- package/dist/cli/convoy/issues.d.ts +8 -0
- package/dist/cli/convoy/issues.d.ts.map +1 -0
- package/dist/cli/convoy/issues.js +98 -0
- package/dist/cli/convoy/issues.js.map +1 -0
- package/dist/cli/convoy/issues.test.d.ts +2 -0
- package/dist/cli/convoy/issues.test.d.ts.map +1 -0
- package/dist/cli/convoy/issues.test.js +107 -0
- package/dist/cli/convoy/issues.test.js.map +1 -0
- package/dist/cli/convoy/knowledge.d.ts +5 -0
- package/dist/cli/convoy/knowledge.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.js +116 -0
- package/dist/cli/convoy/knowledge.js.map +1 -0
- package/dist/cli/convoy/knowledge.test.d.ts +2 -0
- package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.test.js +87 -0
- package/dist/cli/convoy/knowledge.test.js.map +1 -0
- package/dist/cli/convoy/lessons.d.ts +17 -0
- package/dist/cli/convoy/lessons.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.js +149 -0
- package/dist/cli/convoy/lessons.js.map +1 -0
- package/dist/cli/convoy/lessons.test.d.ts +2 -0
- package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.test.js +135 -0
- package/dist/cli/convoy/lessons.test.js.map +1 -0
- package/dist/cli/convoy/lock.d.ts +13 -0
- package/dist/cli/convoy/lock.d.ts.map +1 -0
- package/dist/cli/convoy/lock.js +88 -0
- package/dist/cli/convoy/lock.js.map +1 -0
- package/dist/cli/convoy/lock.test.d.ts +2 -0
- package/dist/cli/convoy/lock.test.d.ts.map +1 -0
- package/dist/cli/convoy/lock.test.js +136 -0
- package/dist/cli/convoy/lock.test.js.map +1 -0
- package/dist/cli/convoy/merge.d.ts +4 -0
- package/dist/cli/convoy/merge.d.ts.map +1 -1
- package/dist/cli/convoy/merge.js +18 -1
- package/dist/cli/convoy/merge.js.map +1 -1
- package/dist/cli/convoy/merge.test.js +6 -7
- package/dist/cli/convoy/merge.test.js.map +1 -1
- package/dist/cli/convoy/partition.d.ts +51 -0
- package/dist/cli/convoy/partition.d.ts.map +1 -0
- package/dist/cli/convoy/partition.js +186 -0
- package/dist/cli/convoy/partition.js.map +1 -0
- package/dist/cli/convoy/partition.test.d.ts +2 -0
- package/dist/cli/convoy/partition.test.d.ts.map +1 -0
- package/dist/cli/convoy/partition.test.js +315 -0
- package/dist/cli/convoy/partition.test.js.map +1 -0
- package/dist/cli/convoy/pipeline.test.js +6 -0
- package/dist/cli/convoy/pipeline.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts +47 -5
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +525 -19
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +1345 -12
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/types.d.ts +156 -2
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/destroy.d.ts +3 -0
- package/dist/cli/destroy.d.ts.map +1 -0
- package/dist/cli/destroy.js +69 -0
- package/dist/cli/destroy.js.map +1 -0
- package/dist/cli/destroy.test.d.ts +2 -0
- package/dist/cli/destroy.test.d.ts.map +1 -0
- package/dist/cli/destroy.test.js +116 -0
- package/dist/cli/destroy.test.js.map +1 -0
- package/dist/cli/gitignore.d.ts +9 -0
- package/dist/cli/gitignore.d.ts.map +1 -1
- package/dist/cli/gitignore.js +29 -0
- package/dist/cli/gitignore.js.map +1 -1
- package/dist/cli/plan.d.ts +3 -0
- package/dist/cli/plan.d.ts.map +1 -0
- package/dist/cli/plan.js +288 -0
- package/dist/cli/plan.js.map +1 -0
- package/dist/cli/run/adapters/claude.d.ts +2 -0
- package/dist/cli/run/adapters/claude.d.ts.map +1 -1
- package/dist/cli/run/adapters/claude.js +89 -49
- package/dist/cli/run/adapters/claude.js.map +1 -1
- package/dist/cli/run/adapters/claude.test.d.ts +2 -0
- package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/claude.test.js +205 -0
- package/dist/cli/run/adapters/claude.test.js.map +1 -0
- package/dist/cli/run/adapters/copilot.d.ts +1 -0
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +84 -46
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
- package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/copilot.test.js +195 -0
- package/dist/cli/run/adapters/copilot.test.js.map +1 -0
- package/dist/cli/run/adapters/cursor.d.ts +1 -0
- package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/run/adapters/cursor.js +83 -47
- package/dist/cli/run/adapters/cursor.js.map +1 -1
- package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
- package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/cursor.test.js +129 -0
- package/dist/cli/run/adapters/cursor.test.js.map +1 -0
- package/dist/cli/run/adapters/opencode.d.ts +1 -0
- package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
- package/dist/cli/run/adapters/opencode.js +81 -47
- package/dist/cli/run/adapters/opencode.js.map +1 -1
- package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
- package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/opencode.test.js +119 -0
- package/dist/cli/run/adapters/opencode.test.js.map +1 -0
- package/dist/cli/run/executor.js +1 -1
- package/dist/cli/run/executor.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +245 -4
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +669 -0
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +362 -22
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +85 -2
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/types.js.map +1 -1
- package/dist/cli/watch.d.ts +15 -0
- package/dist/cli/watch.d.ts.map +1 -0
- package/dist/cli/watch.js +279 -0
- package/dist/cli/watch.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/agents.ts +177 -0
- package/src/cli/baselines.ts +143 -0
- package/src/cli/convoy/engine.test.ts +1839 -70
- package/src/cli/convoy/engine.ts +2417 -38
- package/src/cli/convoy/events.test.ts +179 -38
- package/src/cli/convoy/events.ts +88 -16
- package/src/cli/convoy/expertise.test.ts +128 -0
- package/src/cli/convoy/expertise.ts +163 -0
- package/src/cli/convoy/export.test.ts +1 -0
- package/src/cli/convoy/formula.test.ts +405 -0
- package/src/cli/convoy/formula.ts +174 -0
- package/src/cli/convoy/gates.test.ts +1169 -0
- package/src/cli/convoy/gates.ts +774 -0
- package/src/cli/convoy/health.test.ts +64 -2
- package/src/cli/convoy/health.ts +80 -2
- package/src/cli/convoy/issues.test.ts +143 -0
- package/src/cli/convoy/issues.ts +136 -0
- package/src/cli/convoy/knowledge.test.ts +101 -0
- package/src/cli/convoy/knowledge.ts +132 -0
- package/src/cli/convoy/lessons.test.ts +188 -0
- package/src/cli/convoy/lessons.ts +164 -0
- package/src/cli/convoy/lock.test.ts +181 -0
- package/src/cli/convoy/lock.ts +103 -0
- package/src/cli/convoy/merge.test.ts +6 -7
- package/src/cli/convoy/merge.ts +19 -1
- package/src/cli/convoy/partition.test.ts +423 -0
- package/src/cli/convoy/partition.ts +232 -0
- package/src/cli/convoy/pipeline.test.ts +6 -0
- package/src/cli/convoy/store.test.ts +1512 -14
- package/src/cli/convoy/store.ts +676 -30
- package/src/cli/convoy/types.ts +170 -1
- package/src/cli/destroy.test.ts +141 -0
- package/src/cli/destroy.ts +88 -0
- package/src/cli/gitignore.ts +36 -0
- package/src/cli/plan.ts +316 -0
- package/src/cli/run/adapters/claude.test.ts +234 -0
- package/src/cli/run/adapters/claude.ts +45 -5
- package/src/cli/run/adapters/copilot.test.ts +224 -0
- package/src/cli/run/adapters/copilot.ts +34 -4
- package/src/cli/run/adapters/cursor.test.ts +144 -0
- package/src/cli/run/adapters/cursor.ts +33 -2
- package/src/cli/run/adapters/opencode.test.ts +135 -0
- package/src/cli/run/adapters/opencode.ts +30 -2
- package/src/cli/run/executor.ts +1 -1
- package/src/cli/run/schema.test.ts +758 -0
- package/src/cli/run/schema.ts +300 -25
- package/src/cli/run.ts +341 -21
- package/src/cli/types.ts +86 -1
- package/src/cli/watch.ts +298 -0
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { hostname as getHostname } from 'node:os';
|
|
2
|
+
export class EngineAlreadyRunningError extends Error {
|
|
3
|
+
pid;
|
|
4
|
+
hostname;
|
|
5
|
+
constructor(pid, hostname) {
|
|
6
|
+
super(`Another opencastle process (PID ${pid} on ${hostname}) is already running against this database.`);
|
|
7
|
+
this.pid = pid;
|
|
8
|
+
this.hostname = hostname;
|
|
9
|
+
this.name = 'EngineAlreadyRunningError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function checkStaleness(row) {
|
|
13
|
+
const heartbeatAge = Date.now() - new Date(row.last_heartbeat).getTime();
|
|
14
|
+
if (heartbeatAge <= 30_000)
|
|
15
|
+
return false;
|
|
16
|
+
if (row.hostname !== getHostname())
|
|
17
|
+
return true;
|
|
18
|
+
try {
|
|
19
|
+
process.kill(row.pid, 0);
|
|
20
|
+
return false; // PID is alive on this host
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return true; // PID is dead
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function isLockStale(db) {
|
|
27
|
+
const row = db
|
|
28
|
+
.prepare('SELECT pid, hostname, last_heartbeat FROM engine_lock WHERE id = 1')
|
|
29
|
+
.get();
|
|
30
|
+
if (!row)
|
|
31
|
+
return true;
|
|
32
|
+
return checkStaleness(row);
|
|
33
|
+
}
|
|
34
|
+
export function releaseEngineLock(db) {
|
|
35
|
+
db.exec('DELETE FROM engine_lock WHERE id = 1');
|
|
36
|
+
}
|
|
37
|
+
export function acquireEngineLock(db, _dbPath) {
|
|
38
|
+
// BEGIN IMMEDIATE acquires a write lock upfront, preventing concurrent writers
|
|
39
|
+
try {
|
|
40
|
+
db.exec('BEGIN IMMEDIATE');
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const msg = err.message ?? '';
|
|
44
|
+
if (msg.includes('SQLITE_BUSY') || msg.includes('database is locked')) {
|
|
45
|
+
throw new EngineAlreadyRunningError(0, 'unknown');
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
const existing = db
|
|
50
|
+
.prepare('SELECT pid, hostname, last_heartbeat FROM engine_lock WHERE id = 1')
|
|
51
|
+
.get();
|
|
52
|
+
if (existing) {
|
|
53
|
+
const stale = checkStaleness(existing);
|
|
54
|
+
if (!stale) {
|
|
55
|
+
db.exec('ROLLBACK');
|
|
56
|
+
throw new EngineAlreadyRunningError(existing.pid, existing.hostname);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const now = new Date().toISOString();
|
|
60
|
+
db.prepare('INSERT OR REPLACE INTO engine_lock (id, pid, hostname, started_at, last_heartbeat) VALUES (1, ?, ?, ?, ?)').run(process.pid, getHostname(), now, now);
|
|
61
|
+
db.exec('COMMIT');
|
|
62
|
+
let heartbeatInterval;
|
|
63
|
+
function startHeartbeat() {
|
|
64
|
+
heartbeatInterval = setInterval(() => {
|
|
65
|
+
try {
|
|
66
|
+
db.prepare('UPDATE engine_lock SET last_heartbeat = ? WHERE id = 1').run(new Date().toISOString());
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Ignore errors — DB may have been closed
|
|
70
|
+
}
|
|
71
|
+
}, 10_000);
|
|
72
|
+
return heartbeatInterval;
|
|
73
|
+
}
|
|
74
|
+
function release() {
|
|
75
|
+
if (heartbeatInterval !== undefined) {
|
|
76
|
+
clearInterval(heartbeatInterval);
|
|
77
|
+
heartbeatInterval = undefined;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
db.exec('DELETE FROM engine_lock WHERE id = 1');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Ignore errors — DB may have been closed
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { release, startHeartbeat };
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../../src/cli/convoy/lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.test.d.ts","sourceRoot":"","sources":["../../../src/cli/convoy/lock.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { tmpdir, hostname } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
+
import { acquireEngineLock, EngineAlreadyRunningError, isLockStale, } from './lock.js';
|
|
8
|
+
const LOCK_TABLE_SQL = `
|
|
9
|
+
CREATE TABLE IF NOT EXISTS engine_lock (
|
|
10
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
11
|
+
pid INTEGER NOT NULL,
|
|
12
|
+
hostname TEXT NOT NULL,
|
|
13
|
+
started_at TEXT NOT NULL,
|
|
14
|
+
last_heartbeat TEXT NOT NULL
|
|
15
|
+
)
|
|
16
|
+
`;
|
|
17
|
+
let tmpDir;
|
|
18
|
+
let dbPath;
|
|
19
|
+
let db;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'lock-test-')));
|
|
22
|
+
dbPath = join(tmpDir, 'test.db');
|
|
23
|
+
db = new DatabaseSync(dbPath);
|
|
24
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
25
|
+
db.exec(LOCK_TABLE_SQL);
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
try {
|
|
29
|
+
db.close();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// already closed
|
|
33
|
+
}
|
|
34
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
describe('engine lock', () => {
|
|
37
|
+
it('takes over a stale lock when heartbeat is expired and PID is dead', () => {
|
|
38
|
+
const staleTime = new Date(Date.now() - 60_000).toISOString();
|
|
39
|
+
const deadPid = 999999;
|
|
40
|
+
// Verify the PID is actually dead on this machine
|
|
41
|
+
expect(() => process.kill(deadPid, 0)).toThrow();
|
|
42
|
+
db.prepare('INSERT INTO engine_lock (id, pid, hostname, started_at, last_heartbeat) VALUES (1, ?, ?, ?, ?)').run(deadPid, hostname(), staleTime, staleTime);
|
|
43
|
+
const lock = acquireEngineLock(db, dbPath);
|
|
44
|
+
const row = db
|
|
45
|
+
.prepare('SELECT pid FROM engine_lock WHERE id = 1')
|
|
46
|
+
.get();
|
|
47
|
+
expect(row.pid).toBe(process.pid);
|
|
48
|
+
lock.release();
|
|
49
|
+
});
|
|
50
|
+
it('throws EngineAlreadyRunningError when lock is held by a live process', () => {
|
|
51
|
+
const now = new Date().toISOString();
|
|
52
|
+
db.prepare('INSERT INTO engine_lock (id, pid, hostname, started_at, last_heartbeat) VALUES (1, ?, ?, ?, ?)').run(process.pid, hostname(), now, now);
|
|
53
|
+
expect(() => acquireEngineLock(db, dbPath)).toThrow(EngineAlreadyRunningError);
|
|
54
|
+
});
|
|
55
|
+
it('takes over when hostname differs (treated as stale regardless of PID)', () => {
|
|
56
|
+
const staleTime = new Date(Date.now() - 60_000).toISOString();
|
|
57
|
+
db.prepare('INSERT INTO engine_lock (id, pid, hostname, started_at, last_heartbeat) VALUES (1, ?, ?, ?, ?)').run(process.pid, 'other-host.example.com', staleTime, staleTime);
|
|
58
|
+
const lock = acquireEngineLock(db, dbPath);
|
|
59
|
+
const row = db
|
|
60
|
+
.prepare('SELECT hostname FROM engine_lock WHERE id = 1')
|
|
61
|
+
.get();
|
|
62
|
+
expect(row.hostname).toBe(hostname());
|
|
63
|
+
lock.release();
|
|
64
|
+
});
|
|
65
|
+
it('release deletes the lock row', () => {
|
|
66
|
+
const lock = acquireEngineLock(db, dbPath);
|
|
67
|
+
lock.release();
|
|
68
|
+
const row = db.prepare('SELECT * FROM engine_lock WHERE id = 1').get();
|
|
69
|
+
expect(row).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
it('startHeartbeat updates last_heartbeat after 10 seconds', () => {
|
|
72
|
+
vi.useFakeTimers();
|
|
73
|
+
try {
|
|
74
|
+
const lock = acquireEngineLock(db, dbPath);
|
|
75
|
+
const before = db
|
|
76
|
+
.prepare('SELECT last_heartbeat FROM engine_lock WHERE id = 1')
|
|
77
|
+
.get().last_heartbeat;
|
|
78
|
+
lock.startHeartbeat();
|
|
79
|
+
vi.advanceTimersByTime(10_000);
|
|
80
|
+
const after = db
|
|
81
|
+
.prepare('SELECT last_heartbeat FROM engine_lock WHERE id = 1')
|
|
82
|
+
.get().last_heartbeat;
|
|
83
|
+
expect(after).not.toBe(before);
|
|
84
|
+
lock.release();
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
vi.useRealTimers();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
it('throws EngineAlreadyRunningError when SQLITE_BUSY from a concurrent write lock', () => {
|
|
91
|
+
// Hold a BEGIN IMMEDIATE transaction on a second connection so the first
|
|
92
|
+
// connection's BEGIN IMMEDIATE will return SQLITE_BUSY.
|
|
93
|
+
const db2 = new DatabaseSync(dbPath);
|
|
94
|
+
db2.exec('PRAGMA journal_mode = WAL');
|
|
95
|
+
db2.exec(LOCK_TABLE_SQL);
|
|
96
|
+
db2.exec('BEGIN IMMEDIATE');
|
|
97
|
+
try {
|
|
98
|
+
expect(() => acquireEngineLock(db, dbPath)).toThrow(EngineAlreadyRunningError);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
db2.exec('ROLLBACK');
|
|
102
|
+
db2.close();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
it('takes over lock from different hostname when heartbeat expired', () => {
|
|
106
|
+
const staleTime = new Date(Date.now() - 60_000).toISOString();
|
|
107
|
+
db.prepare('INSERT INTO engine_lock (id, pid, hostname, started_at, last_heartbeat) VALUES (1, ?, ?, ?, ?)').run(process.pid, 'ci-runner-42.example.com', staleTime, staleTime);
|
|
108
|
+
const lock = acquireEngineLock(db, dbPath);
|
|
109
|
+
const row = db
|
|
110
|
+
.prepare('SELECT hostname, pid FROM engine_lock WHERE id = 1')
|
|
111
|
+
.get();
|
|
112
|
+
expect(row.hostname).toBe(hostname());
|
|
113
|
+
expect(row.pid).toBe(process.pid);
|
|
114
|
+
lock.release();
|
|
115
|
+
});
|
|
116
|
+
it('does NOT take over lock from different hostname when heartbeat is fresh', () => {
|
|
117
|
+
const freshTime = new Date().toISOString();
|
|
118
|
+
db.prepare('INSERT INTO engine_lock (id, pid, hostname, started_at, last_heartbeat) VALUES (1, ?, ?, ?, ?)').run(12345, 'other-host.example.com', freshTime, freshTime);
|
|
119
|
+
expect(() => acquireEngineLock(db, dbPath)).toThrow(EngineAlreadyRunningError);
|
|
120
|
+
});
|
|
121
|
+
it('isLockStale returns true when no lock exists', () => {
|
|
122
|
+
expect(isLockStale(db)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
it('isLockStale returns false for fresh lock on same host', () => {
|
|
125
|
+
const now = new Date().toISOString();
|
|
126
|
+
db.prepare('INSERT INTO engine_lock (id, pid, hostname, started_at, last_heartbeat) VALUES (1, ?, ?, ?, ?)').run(process.pid, hostname(), now, now);
|
|
127
|
+
expect(isLockStale(db)).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
it('isLockStale returns true for expired lock with dead PID on same host', () => {
|
|
130
|
+
const staleTime = new Date(Date.now() - 60_000).toISOString();
|
|
131
|
+
const deadPid = 999999;
|
|
132
|
+
db.prepare('INSERT INTO engine_lock (id, pid, hostname, started_at, last_heartbeat) VALUES (1, ?, ?, ?, ?)').run(deadPid, hostname(), staleTime, staleTime);
|
|
133
|
+
expect(isLockStale(db)).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=lock.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.test.js","sourceRoot":"","sources":["../../../src/cli/convoy/lock.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,10 @@ export interface MergeResult {
|
|
|
3
3
|
conflicted: boolean;
|
|
4
4
|
message: string;
|
|
5
5
|
}
|
|
6
|
+
export declare class MergeConflictError extends Error {
|
|
7
|
+
readonly conflictingFiles: string[];
|
|
8
|
+
constructor(conflictingFiles: string[], message?: string);
|
|
9
|
+
}
|
|
6
10
|
export interface MergeQueue {
|
|
7
11
|
/**
|
|
8
12
|
* Merge a single worktree's changes back onto the target branch.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../src/cli/convoy/merge.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,OAAO,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;CAChG;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../src/cli/convoy/merge.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,OAAO,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;aAEzB,gBAAgB,EAAE,MAAM,EAAE;gBAA1B,gBAAgB,EAAE,MAAM,EAAE,EAC1C,OAAO,CAAC,EAAE,MAAM;CAKnB;AAED,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;CAChG;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CA2E7D"}
|
package/dist/cli/convoy/merge.js
CHANGED
|
@@ -2,6 +2,14 @@ import { execFile as execFileCb } from 'node:child_process';
|
|
|
2
2
|
import { resolve, join, sep } from 'node:path';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
4
|
const execFile = promisify(execFileCb);
|
|
5
|
+
export class MergeConflictError extends Error {
|
|
6
|
+
conflictingFiles;
|
|
7
|
+
constructor(conflictingFiles, message) {
|
|
8
|
+
super(message ?? `Merge conflict in: ${conflictingFiles.join(', ')}`);
|
|
9
|
+
this.conflictingFiles = conflictingFiles;
|
|
10
|
+
this.name = 'MergeConflictError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
5
13
|
export function createMergeQueue(repoPath) {
|
|
6
14
|
const worktreesDir = resolve(join(repoPath, '.opencastle', 'worktrees'));
|
|
7
15
|
async function merge(worktreePath, worktreeBranch, targetBranch) {
|
|
@@ -51,8 +59,17 @@ export function createMergeQueue(repoPath) {
|
|
|
51
59
|
const isConflict = error.code === 1 &&
|
|
52
60
|
((error.stderr ?? '').includes('CONFLICT') || (error.stdout ?? '').includes('CONFLICT'));
|
|
53
61
|
if (isConflict) {
|
|
62
|
+
// Collect conflicting files before aborting
|
|
63
|
+
let conflictingFiles = [];
|
|
64
|
+
try {
|
|
65
|
+
const { stdout: conflictOut } = await execFile('git', [
|
|
66
|
+
'-C', repoPath, 'diff', '--name-only', '--diff-filter=U',
|
|
67
|
+
]);
|
|
68
|
+
conflictingFiles = conflictOut.split('\n').filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
catch { /* ignore — we still abort */ }
|
|
54
71
|
await execFile('git', ['-C', repoPath, 'merge', '--abort']);
|
|
55
|
-
|
|
72
|
+
throw new MergeConflictError(conflictingFiles);
|
|
56
73
|
}
|
|
57
74
|
throw err;
|
|
58
75
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge.js","sourceRoot":"","sources":["../../../src/cli/convoy/merge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC,MAAM,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"merge.js","sourceRoot":"","sources":["../../../src/cli/convoy/merge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC,MAAM,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,CAAA;AAQtC,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAEzB;IADlB,YACkB,gBAA0B,EAC1C,OAAgB;QAEhB,KAAK,CAAC,OAAO,IAAI,sBAAsB,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAHrD,qBAAgB,GAAhB,gBAAgB,CAAU;QAI1C,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAA;IAClC,CAAC;CACF;AAWD,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC,CAAA;IAExE,KAAK,UAAU,KAAK,CAClB,YAAoB,EACpB,cAAsB,EACtB,YAAoB;QAEpB,MAAM,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;QAC9C,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,YAAY,GAAG,GAAG,CAAC,EAAE,CAAC;YACrD,MAAM,IAAI,KAAK,CAAC,SAAS,YAAY,8CAA8C,CAAC,CAAA;QACtF,CAAC;QAED,qDAAqD;QACrD,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;QAE5D,0EAA0E;QAC1E,yEAAyE;QACzE,kFAAkF;QAClF,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE;YAC/C,IAAI;YACJ,gBAAgB;YAChB,MAAM;YACN,UAAU;YACV,aAAa;SACd,CAAC,CAAA;QACF,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAA;QAE/C,IAAI,cAAc,EAAE,CAAC;YACnB,MAAM,QAAQ,CAAC,KAAK,EAAE;gBACpB,IAAI;gBACJ,gBAAgB;gBAChB,QAAQ;gBACR,IAAI;gBACJ,WAAW,cAAc,YAAY;aACtC,CAAC,CAAA;QACJ,CAAC;QAED,oEAAoE;QACpE,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC,CAAA;QAEjE,IAAI,CAAC;YACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE;gBACvC,IAAI;gBACJ,QAAQ;gBACR,OAAO;gBACP,cAAc;gBACd,WAAW;aACZ,CAAC,CAAA;YACF,IAAI,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC;gBAC1C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAA;YAC7E,CAAC;YACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAA;QAC7E,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,KAAK,GAAG,GAAmE,CAAA;YACjF,MAAM,UAAU,GACd,KAAK,CAAC,IAAI,KAAK,CAAC;gBAChB,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAA;YAC1F,IAAI,UAAU,EAAE,CAAC;gBACf,4CAA4C;gBAC5C,IAAI,gBAAgB,GAAa,EAAE,CAAA;gBACnC,IAAI,CAAC;oBACH,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE;wBACpD,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,iBAAiB;qBACzD,CAAC,CAAA;oBACF,gBAAgB,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;gBAC5D,CAAC;gBAAC,MAAM,CAAC,CAAC,6BAA6B,CAAC,CAAC;gBACzC,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAA;gBAC3D,MAAM,IAAI,kBAAkB,CAAC,gBAAgB,CAAC,CAAA;YAChD,CAAC;YACD,MAAM,GAAG,CAAA;QACX,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,CAAA;AAClB,CAAC"}
|
|
@@ -4,7 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { execFile as execFileCb } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
-
import { createMergeQueue } from './merge.js';
|
|
7
|
+
import { createMergeQueue, MergeConflictError } from './merge.js';
|
|
8
8
|
const execFile = promisify(execFileCb);
|
|
9
9
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
10
10
|
let repoPath;
|
|
@@ -76,17 +76,15 @@ describe('merge - no changes', () => {
|
|
|
76
76
|
});
|
|
77
77
|
// ── merge conflict ────────────────────────────────────────────────────────────
|
|
78
78
|
describe('merge - conflict', () => {
|
|
79
|
-
it('
|
|
79
|
+
it('throws MergeConflictError and aborts when two worktrees edit the same file', async () => {
|
|
80
80
|
const worktree1 = await addWorktree(repoPath, 'worker1', featureBranch);
|
|
81
81
|
const worktree2 = await addWorktree(repoPath, 'worker2', featureBranch);
|
|
82
82
|
writeFileSync(join(worktree1, 'shared.txt'), 'content from worker 1');
|
|
83
83
|
writeFileSync(join(worktree2, 'shared.txt'), 'content from worker 2');
|
|
84
84
|
const first = await queue.merge(worktree1, 'convoy-worker1', featureBranch);
|
|
85
85
|
expect(first).toEqual({ success: true, conflicted: false, message: 'Merged successfully' });
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
expect(second.conflicted).toBe(true);
|
|
89
|
-
expect(second.message).toContain('conflict');
|
|
86
|
+
await expect(queue.merge(worktree2, 'convoy-worker2', featureBranch))
|
|
87
|
+
.rejects.toThrow(MergeConflictError);
|
|
90
88
|
});
|
|
91
89
|
it('leaves the repo in a clean state (no pending merge) after aborting a conflict', async () => {
|
|
92
90
|
const worktree1 = await addWorktree(repoPath, 'worker1', featureBranch);
|
|
@@ -94,7 +92,8 @@ describe('merge - conflict', () => {
|
|
|
94
92
|
writeFileSync(join(worktree1, 'shared.txt'), 'content from worker 1');
|
|
95
93
|
writeFileSync(join(worktree2, 'shared.txt'), 'content from worker 2');
|
|
96
94
|
await queue.merge(worktree1, 'convoy-worker1', featureBranch);
|
|
97
|
-
await queue.merge(worktree2, 'convoy-worker2', featureBranch)
|
|
95
|
+
await expect(queue.merge(worktree2, 'convoy-worker2', featureBranch))
|
|
96
|
+
.rejects.toBeInstanceOf(MergeConflictError);
|
|
98
97
|
// --untracked-files=no excludes the .opencastle/worktrees/ dir from the check;
|
|
99
98
|
// we only want to verify there is no pending merge (no staged/modified tracked files).
|
|
100
99
|
const { stdout } = await execFile('git', ['-C', repoPath, 'status', '--porcelain', '--untracked-files=no']);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge.test.js","sourceRoot":"","sources":["../../../src/cli/convoy/merge.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACrF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,QAAQ,IAAI,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACrC,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"merge.test.js","sourceRoot":"","sources":["../../../src/cli/convoy/merge.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACrF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,QAAQ,IAAI,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACrC,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AACpE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAGjE,MAAM,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,CAAA;AAEtC,iFAAiF;AAEjF,IAAI,QAAgB,CAAA;AACpB,IAAI,aAAqB,CAAA;AACzB,IAAI,KAAiB,CAAA;AAErB,KAAK,UAAU,aAAa;IAC1B,MAAM,IAAI,GAAG,YAAY,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC,CAAA;IACrE,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;IACrC,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,eAAe,CAAC,CAAC,CAAA;IAC5E,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;IAClE,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE,QAAQ,CAAC,CAAA;IAChD,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;IAChD,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAA;IACrE,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAA;IAClE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AACvD,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,IAAY,EAAE,QAAgB,EAAE,MAAc;IACvE,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,WAAW,CAAC,CAAA;IAC3D,SAAS,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5C,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAA;IACjD,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC,CAAA;IACxG,OAAO,YAAY,CAAA;AACrB,CAAC;AAED,UAAU,CAAC,KAAK,IAAI,EAAE;IACpB,MAAM,MAAM,GAAG,MAAM,aAAa,EAAE,CAAA;IACpC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;IAC1B,aAAa,GAAG,MAAM,CAAC,aAAa,CAAA;IACpC,KAAK,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAA;AACpC,CAAC,CAAC,CAAA;AAEF,SAAS,CAAC,GAAG,EAAE;IACb,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AACpD,CAAC,CAAC,CAAA;AAEF,iFAAiF;AAEjF,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,mEAAmE,EAAE,KAAK,IAAI,EAAE;QACjF,MAAM,YAAY,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,CAAA;QAC1E,aAAa,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,EAAE,eAAe,CAAC,CAAA;QAEhE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAA;QAE/E,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAA;IAC9F,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,mEAAmE,EAAE,KAAK,IAAI,EAAE;QACjF,MAAM,YAAY,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,CAAA;QAC1E,aAAa,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,EAAE,eAAe,CAAC,CAAA;QAEhE,MAAM,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAA;QAEhE,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAA;QAC9C,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC7D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uEAAuE,EAAE,KAAK,IAAI,EAAE;QACrF,MAAM,YAAY,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,CAAA;QAC1E,aAAa,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,EAAE,eAAe,CAAC,CAAA;QAEhE,MAAM,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAA;QAEhE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,gBAAgB,CAAC,CAAC,CAAA;QAChG,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,kCAAkC,CAAC,CAAA;IAC9D,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,iFAAiF;AAEjF,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,iFAAiF,EAAE,KAAK,IAAI,EAAE;QAC/F,MAAM,YAAY,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,CAAA;QAE1E,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAA;QAE/E,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAA;IAC9F,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;QACvE,MAAM,YAAY,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,CAAA;QAE1E,MAAM,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAA;QAEhE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,gBAAgB,CAAC,CAAC,CAAA;QAChG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,kCAAkC,CAAC,CAAA;IAClE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,iFAAiF;AAEjF,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,4EAA4E,EAAE,KAAK,IAAI,EAAE;QAC1F,MAAM,SAAS,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,CAAA;QACvE,MAAM,SAAS,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,CAAA;QAEvE,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,uBAAuB,CAAC,CAAA;QACrE,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,uBAAuB,CAAC,CAAA;QAErE,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAA;QAC3E,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAA;QAE3F,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAC;aAClE,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAA;IACxC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+EAA+E,EAAE,KAAK,IAAI,EAAE;QAC7F,MAAM,SAAS,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,CAAA;QACvE,MAAM,SAAS,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,CAAA;QAEvE,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,uBAAuB,CAAC,CAAA;QACrE,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,uBAAuB,CAAC,CAAA;QAErE,MAAM,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAA;QAC7D,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAC;aAClE,OAAO,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAA;QAE7C,+EAA+E;QAC/E,uFAAuF;QACvF,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,sBAAsB,CAAC,CAAC,CAAA;QAC3G,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAChC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,iFAAiF;AAEjF,QAAQ,CAAC,mCAAmC,EAAE,GAAG,EAAE;IACjD,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACpE,MAAM,YAAY,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,CAAA;QAE1E,aAAa,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,EAAE,uBAAuB,CAAC,CAAA;QACxE,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;QACxD,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,sBAAsB,CAAC,CAAC,CAAA;QAEnF,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAA;QAE/E,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAA;IAC9F,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;QACpF,MAAM,YAAY,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,CAAA;QAE1E,aAAa,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,EAAE,uBAAuB,CAAC,CAAA;QACxE,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;QACxD,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,sBAAsB,CAAC,CAAC,CAAA;QAEnF,MAAM,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,gBAAgB,EAAE,aAAa,CAAC,CAAA;QAEhE,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAA;QAC9C,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC7D,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,iFAAiF;AAEjF,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;QAC1E,MAAM,YAAY,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,aAAa,CAAC,CAAA;QAE1E,MAAM,MAAM,CACV,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,oBAAoB,EAAE,aAAa,CAAC,CAC/D,CAAC,OAAO,CAAC,OAAO,EAAE,CAAA;IACrB,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,MAAM,CACV,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,aAAa,EAAE,aAAa,CAAC,CACvD,CAAC,OAAO,CAAC,OAAO,CAAC,yCAAyC,CAAC,CAAA;IAC9D,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Task } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a file path for partition comparison.
|
|
4
|
+
* - Rejects glob patterns (* or ?)
|
|
5
|
+
* - Strips leading ./ and /
|
|
6
|
+
* - Replaces backslashes with forward slashes
|
|
7
|
+
* - Resolves . and .. via path.normalize()
|
|
8
|
+
* - Preserves trailing slash for directories
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizePath(p: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Returns true if path a and path b overlap (exact match or prefix containment).
|
|
13
|
+
* Example: 'src/auth/' overlaps 'src/auth/service.ts' in both directions.
|
|
14
|
+
*/
|
|
15
|
+
export declare function pathsOverlap(a: string, b: string): boolean;
|
|
16
|
+
export interface PartitionConflict {
|
|
17
|
+
phase: number;
|
|
18
|
+
taskA: string;
|
|
19
|
+
taskB: string;
|
|
20
|
+
overlapping: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface PartitionValidationResult {
|
|
23
|
+
valid: boolean;
|
|
24
|
+
conflicts: PartitionConflict[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate that tasks within the same parallel phase do not have overlapping file partitions.
|
|
28
|
+
* Tasks in different phases (sequential) are allowed to share files.
|
|
29
|
+
*/
|
|
30
|
+
export declare function validateFilePartitions(_tasks: Task[], phases: Task[][]): PartitionValidationResult;
|
|
31
|
+
/**
|
|
32
|
+
* Probe whether the filesystem is case-sensitive by creating a mixed-case temp file
|
|
33
|
+
* and checking if the lowercase path resolves to the same inode.
|
|
34
|
+
*
|
|
35
|
+
* Uses realpathSync per LES-003: on macOS, os.tmpdir() returns /var/... which is a
|
|
36
|
+
* symlink to /private/var/... — realpathSync resolves this to the canonical path.
|
|
37
|
+
*
|
|
38
|
+
* Returns true if case-sensitive (git-compatible default), false if case-insensitive.
|
|
39
|
+
*/
|
|
40
|
+
export declare function determineFsCaseSensitivity(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Before task execution: scan each file in the task's files[] partition.
|
|
43
|
+
* If any resolved symlink target escapes the basePath directory, throw symlink_escape.
|
|
44
|
+
*/
|
|
45
|
+
export declare function scanSymlinks(files: string[], basePath: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* After task execution: scan files[] in the worktree for new symlinks that escape the partition.
|
|
48
|
+
* Throws symlink_escape_post_task if any symlink target is outside worktreePath.
|
|
49
|
+
*/
|
|
50
|
+
export declare function scanNewSymlinks(worktreePath: string, files: string[]): void;
|
|
51
|
+
//# sourceMappingURL=partition.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"partition.d.ts","sourceRoot":"","sources":["../../../src/cli/convoy/partition.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAIvC;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAuC/C;AAID;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAM1D;AAID,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,OAAO,CAAA;IACd,SAAS,EAAE,iBAAiB,EAAE,CAAA;CAC/B;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,IAAI,EAAE,EACd,MAAM,EAAE,IAAI,EAAE,EAAE,GACf,yBAAyB,CAsC3B;AAID;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,IAAI,OAAO,CAmBpD;AAID;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CA2BpE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CA6B3E"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { statSync, realpathSync, lstatSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { normalize, join, resolve } from 'node:path';
|
|
4
|
+
// ── Path normalization ────────────────────────────────────────────────────────
|
|
5
|
+
/**
|
|
6
|
+
* Normalize a file path for partition comparison.
|
|
7
|
+
* - Rejects glob patterns (* or ?)
|
|
8
|
+
* - Strips leading ./ and /
|
|
9
|
+
* - Replaces backslashes with forward slashes
|
|
10
|
+
* - Resolves . and .. via path.normalize()
|
|
11
|
+
* - Preserves trailing slash for directories
|
|
12
|
+
*/
|
|
13
|
+
export function normalizePath(p) {
|
|
14
|
+
if (p.includes('*') || p.includes('?')) {
|
|
15
|
+
throw new Error(`Glob patterns are not allowed in file paths: "${p}"`);
|
|
16
|
+
}
|
|
17
|
+
// Record whether the path indicates a directory (trailing slash)
|
|
18
|
+
const hasTrailingSlash = p.endsWith('/') || p.endsWith('\\');
|
|
19
|
+
// Normalize separators to forward slash
|
|
20
|
+
let result = p.replace(/\\/g, '/');
|
|
21
|
+
// Strip trailing slashes before further processing
|
|
22
|
+
result = result.replace(/\/+$/, '');
|
|
23
|
+
// Strip leading './' (may be multiple, e.g. '././')
|
|
24
|
+
result = result.replace(/^(\.\/)+/, '');
|
|
25
|
+
// Strip leading '/'
|
|
26
|
+
result = result.replace(/^\/+/, '');
|
|
27
|
+
// Reject any .. path segment — even those that would not escape the root.
|
|
28
|
+
// All usage of .. is rejected for safety, not just escaping traversals.
|
|
29
|
+
if (/(^|\/)\.\.(\/|$)/.test(result)) {
|
|
30
|
+
throw new Error(`Path traversal detected: "${p}" resolves to a path containing ".." segments`);
|
|
31
|
+
}
|
|
32
|
+
// Resolve '.' and '..' segments
|
|
33
|
+
result = normalize(result).replace(/\\/g, '/');
|
|
34
|
+
// normalize can introduce leading './' (e.g. for '.') — strip it again
|
|
35
|
+
result = result.replace(/^(\.\/)+/, '');
|
|
36
|
+
result = result.replace(/^\/+/, '');
|
|
37
|
+
// Restore trailing slash for directories (but not when result is '.' or empty)
|
|
38
|
+
if (hasTrailingSlash && result !== '.' && result !== '') {
|
|
39
|
+
result += '/';
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
// ── Overlap detection ─────────────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Returns true if path a and path b overlap (exact match or prefix containment).
|
|
46
|
+
* Example: 'src/auth/' overlaps 'src/auth/service.ts' in both directions.
|
|
47
|
+
*/
|
|
48
|
+
export function pathsOverlap(a, b) {
|
|
49
|
+
if (a === b)
|
|
50
|
+
return true;
|
|
51
|
+
// Treat each path as a potential directory prefix
|
|
52
|
+
const aDir = a.endsWith('/') ? a : a + '/';
|
|
53
|
+
const bDir = b.endsWith('/') ? b : b + '/';
|
|
54
|
+
return b.startsWith(aDir) || a.startsWith(bDir);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate that tasks within the same parallel phase do not have overlapping file partitions.
|
|
58
|
+
* Tasks in different phases (sequential) are allowed to share files.
|
|
59
|
+
*/
|
|
60
|
+
export function validateFilePartitions(_tasks, phases) {
|
|
61
|
+
const isCaseSensitive = determineFsCaseSensitivity();
|
|
62
|
+
const conflicts = [];
|
|
63
|
+
for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
|
|
64
|
+
const phaseTasks = phases[phaseIdx];
|
|
65
|
+
for (let i = 0; i < phaseTasks.length; i++) {
|
|
66
|
+
for (let j = i + 1; j < phaseTasks.length; j++) {
|
|
67
|
+
const taskA = phaseTasks[i];
|
|
68
|
+
const taskB = phaseTasks[j];
|
|
69
|
+
// Empty files arrays are not partitioned — skip
|
|
70
|
+
if (!taskA.files.length || !taskB.files.length)
|
|
71
|
+
continue;
|
|
72
|
+
const normalizedA = taskA.files.map(normalizePath);
|
|
73
|
+
const normalizedB = taskB.files.map(normalizePath);
|
|
74
|
+
const overlapping = [];
|
|
75
|
+
for (const fileA of normalizedA) {
|
|
76
|
+
for (const fileB of normalizedB) {
|
|
77
|
+
const directOverlap = pathsOverlap(fileA, fileB);
|
|
78
|
+
// On case-insensitive filesystems, also check lowercased paths
|
|
79
|
+
const ciOverlap = !isCaseSensitive && pathsOverlap(fileA.toLowerCase(), fileB.toLowerCase());
|
|
80
|
+
if ((directOverlap || ciOverlap) && !overlapping.includes(fileA)) {
|
|
81
|
+
overlapping.push(fileA);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (overlapping.length > 0) {
|
|
86
|
+
conflicts.push({ phase: phaseIdx, taskA: taskA.id, taskB: taskB.id, overlapping });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { valid: conflicts.length === 0, conflicts };
|
|
92
|
+
}
|
|
93
|
+
// ── Filesystem case-sensitivity probe ────────────────────────────────────────
|
|
94
|
+
/**
|
|
95
|
+
* Probe whether the filesystem is case-sensitive by creating a mixed-case temp file
|
|
96
|
+
* and checking if the lowercase path resolves to the same inode.
|
|
97
|
+
*
|
|
98
|
+
* Uses realpathSync per LES-003: on macOS, os.tmpdir() returns /var/... which is a
|
|
99
|
+
* symlink to /private/var/... — realpathSync resolves this to the canonical path.
|
|
100
|
+
*
|
|
101
|
+
* Returns true if case-sensitive (git-compatible default), false if case-insensitive.
|
|
102
|
+
*/
|
|
103
|
+
export function determineFsCaseSensitivity() {
|
|
104
|
+
const base = realpathSync(tmpdir());
|
|
105
|
+
const mixedCase = join(base, `OpenCastle_CaseSensitivity_${Date.now()}`);
|
|
106
|
+
const lowerCase = mixedCase.toLowerCase();
|
|
107
|
+
try {
|
|
108
|
+
writeFileSync(mixedCase, '');
|
|
109
|
+
try {
|
|
110
|
+
const statMixed = statSync(mixedCase);
|
|
111
|
+
const statLower = statSync(lowerCase);
|
|
112
|
+
// Same inode → same file → case-insensitive
|
|
113
|
+
return statMixed.ino !== statLower.ino;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// stat(lowerCase) threw → file not found at lowercase path → case-sensitive
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
try {
|
|
122
|
+
rmSync(mixedCase);
|
|
123
|
+
}
|
|
124
|
+
catch { /* ignore cleanup errors */ }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ── Symlink security scan ─────────────────────────────────────────────────────
|
|
128
|
+
/**
|
|
129
|
+
* Before task execution: scan each file in the task's files[] partition.
|
|
130
|
+
* If any resolved symlink target escapes the basePath directory, throw symlink_escape.
|
|
131
|
+
*/
|
|
132
|
+
export function scanSymlinks(files, basePath) {
|
|
133
|
+
const realBase = realpathSync(resolve(basePath));
|
|
134
|
+
for (const file of files) {
|
|
135
|
+
const absPath = join(realBase, normalizePath(file));
|
|
136
|
+
let stat;
|
|
137
|
+
try {
|
|
138
|
+
stat = lstatSync(absPath);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
continue; // file doesn't exist yet — skip
|
|
142
|
+
}
|
|
143
|
+
if (stat.isSymbolicLink()) {
|
|
144
|
+
let realTarget;
|
|
145
|
+
try {
|
|
146
|
+
realTarget = realpathSync(absPath);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
throw new Error(`symlink_escape: symlink at "${file}" could not be resolved`);
|
|
150
|
+
}
|
|
151
|
+
if (!realTarget.startsWith(realBase + '/') && realTarget !== realBase) {
|
|
152
|
+
throw new Error(`symlink_escape: "${file}" is a symlink that resolves outside the partition`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* After task execution: scan files[] in the worktree for new symlinks that escape the partition.
|
|
159
|
+
* Throws symlink_escape_post_task if any symlink target is outside worktreePath.
|
|
160
|
+
*/
|
|
161
|
+
export function scanNewSymlinks(worktreePath, files) {
|
|
162
|
+
const realBase = realpathSync(resolve(worktreePath));
|
|
163
|
+
for (const file of files) {
|
|
164
|
+
const absPath = join(realBase, normalizePath(file));
|
|
165
|
+
let stat;
|
|
166
|
+
try {
|
|
167
|
+
stat = lstatSync(absPath);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (stat.isSymbolicLink()) {
|
|
173
|
+
let realTarget;
|
|
174
|
+
try {
|
|
175
|
+
realTarget = realpathSync(absPath);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
throw new Error(`symlink_escape_post_task: "${file}" is a new symlink that cannot be resolved`);
|
|
179
|
+
}
|
|
180
|
+
if (!realTarget.startsWith(realBase + '/') && realTarget !== realBase) {
|
|
181
|
+
throw new Error(`symlink_escape_post_task: "${file}" is a new symlink that escapes the partition`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=partition.js.map
|