mindvest-atlas 0.6.1 → 0.6.2
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/package.json +1 -1
- package/src/commands/remote-control.js +12 -2
- package/src/config.js +1 -1
- package/src/remote-control.js +50 -0
package/package.json
CHANGED
|
@@ -15,8 +15,8 @@ import { requestJson } from '../api/client.js';
|
|
|
15
15
|
import { runStream } from '../util/stream-loop.js';
|
|
16
16
|
import { color, eprintln } from '../util/ui.js';
|
|
17
17
|
import {
|
|
18
|
-
DEFAULT_TIMEOUT_MS, computerName, jobEnv, listJobFiles, listRuns, machineId,
|
|
19
|
-
readJobFile, staleness, summarize, tail, workspace, workspaceRoot,
|
|
18
|
+
DEFAULT_TIMEOUT_MS, claimRun, computerName, jobEnv, listJobFiles, listRuns, machineId,
|
|
19
|
+
osLabel, readJobFile, releaseRun, staleness, summarize, tail, workspace, workspaceRoot,
|
|
20
20
|
} from '../remote-control.js';
|
|
21
21
|
import fs from 'node:fs';
|
|
22
22
|
import path from 'node:path';
|
|
@@ -121,6 +121,13 @@ async function runJob(job, root, timeoutMs) {
|
|
|
121
121
|
return report(job, 'not_built', started, Date.now());
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// ONE RUN PER PROJECT. A schedule firing again mid-run would race two copies
|
|
125
|
+
// of the same script over one folder, one .env and one broker account.
|
|
126
|
+
if (!claimRun(dir, name)) {
|
|
127
|
+
eprintln(color.yellow(`· ${name} — already running, left alone`));
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
124
131
|
eprintln(color.dim(`▶ ${name}`));
|
|
125
132
|
const env = {
|
|
126
133
|
...process.env,
|
|
@@ -137,17 +144,20 @@ async function runJob(job, root, timeoutMs) {
|
|
|
137
144
|
eprintln(color.red(`✗ ${name} — timed out`));
|
|
138
145
|
writeLog(dir, job, 'failed', started, finished,
|
|
139
146
|
`Stopped after ${Math.round(timeoutMs / 1000)}s. The script did not finish.\n${tail(r.out) || ''}`);
|
|
147
|
+
releaseRun(dir);
|
|
140
148
|
return report(job, 'failed', started, finished);
|
|
141
149
|
}
|
|
142
150
|
if (r.error) {
|
|
143
151
|
eprintln(color.red(`✗ ${name} — ${r.error}`));
|
|
144
152
|
writeLog(dir, job, 'failed', started, finished, `${r.error}\n${tail(r.out) || ''}`);
|
|
153
|
+
releaseRun(dir);
|
|
145
154
|
return report(job, 'failed', started, finished);
|
|
146
155
|
}
|
|
147
156
|
const ok = r.code === 0;
|
|
148
157
|
eprintln((ok ? color.green('✓ ') : color.red('✗ ')) + name
|
|
149
158
|
+ color.dim(` (${((finished - started) / 1000).toFixed(1)}s)`));
|
|
150
159
|
writeLog(dir, job, ok ? 'completed' : 'failed', started, finished, tail(r.out));
|
|
160
|
+
releaseRun(dir);
|
|
151
161
|
return report(job, ok ? 'completed' : 'failed', started, finished);
|
|
152
162
|
}
|
|
153
163
|
|
package/src/config.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
|
|
5
|
-
export const VERSION = '0.6.
|
|
5
|
+
export const VERSION = '0.6.2';
|
|
6
6
|
export const DEFAULT_BASE_URL = 'https://atlasmcp.finmanagerai.com';
|
|
7
7
|
export const DEFAULT_SCOPE = 'atlas broker';
|
|
8
8
|
export const CLIENT_NAME = 'Atlas CLI';
|
package/src/remote-control.js
CHANGED
|
@@ -307,6 +307,43 @@ export function readJobFile(job, rel, root) {
|
|
|
307
307
|
* the filename (when, and how it ended) and the log's own second line (how
|
|
308
308
|
* long), which is why the writer stamps both.
|
|
309
309
|
*/
|
|
310
|
+
export const RUNNING_MARKER = '.running';
|
|
311
|
+
|
|
312
|
+
export const runningMarker = (dir) => path.join(dir, 'runs', RUNNING_MARKER);
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Take a project's single run slot, or say who already has it.
|
|
316
|
+
*
|
|
317
|
+
* ONE RUN PER PROJECT. A job that fires again while its last run is still going
|
|
318
|
+
* would have two copies of the same script racing over the same folder, the
|
|
319
|
+
* same .env and the same broker — so the second one does not start. The marker
|
|
320
|
+
* is a file rather than memory so it survives the runner restarting mid-job and
|
|
321
|
+
* is visible to anyone looking at the folder.
|
|
322
|
+
*/
|
|
323
|
+
export function claimRun(dir, name) {
|
|
324
|
+
const m = runningMarker(dir);
|
|
325
|
+
try {
|
|
326
|
+
fs.mkdirSync(path.dirname(m), { recursive: true });
|
|
327
|
+
// Exclusive create: two runners on one folder cannot both win.
|
|
328
|
+
fs.writeFileSync(m, `${Date.now() / 1000}\n${name || ''}\n`, { flag: 'wx' });
|
|
329
|
+
return true;
|
|
330
|
+
} catch (err) {
|
|
331
|
+
if (err.code !== 'EEXIST') return true; // cannot write a marker: better to run than never
|
|
332
|
+
let started = 0;
|
|
333
|
+
try { started = parseFloat(fs.readFileSync(m, 'utf8').split('\n')[0]) || 0; } catch { started = 0; }
|
|
334
|
+
// A marker left behind by a crash must not wedge a project forever.
|
|
335
|
+
if (started && Date.now() / 1000 - started > 24 * 3600) {
|
|
336
|
+
releaseRun(dir);
|
|
337
|
+
return claimRun(dir, name);
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function releaseRun(dir) {
|
|
344
|
+
try { fs.unlinkSync(runningMarker(dir)); } catch { /* already gone */ }
|
|
345
|
+
}
|
|
346
|
+
|
|
310
347
|
export function listRuns(root, limit = 50) {
|
|
311
348
|
const base = path.join(workspaceRoot(root), SCRIPTS_DIR);
|
|
312
349
|
const runs = [];
|
|
@@ -315,6 +352,19 @@ export function listRuns(root, limit = 50) {
|
|
|
315
352
|
jobs = fs.readdirSync(base, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
316
353
|
} catch { return runs; }
|
|
317
354
|
for (const job of jobs) {
|
|
355
|
+
// A project running right now has no log yet — it is the most important row
|
|
356
|
+
// on the page and would otherwise be invisible until it finished.
|
|
357
|
+
const m = runningMarker(path.join(base, job));
|
|
358
|
+
if (fs.existsSync(m)) {
|
|
359
|
+
let started = Date.now() / 1000;
|
|
360
|
+
let nm = job;
|
|
361
|
+
try {
|
|
362
|
+
const [a, b] = fs.readFileSync(m, 'utf8').split('\n');
|
|
363
|
+
started = parseFloat(a) || started;
|
|
364
|
+
if (b) nm = b;
|
|
365
|
+
} catch { /* mid-write */ }
|
|
366
|
+
runs.push({ job, name: nm, status: 'running', started_at: started, duration_s: null, log: '' });
|
|
367
|
+
}
|
|
318
368
|
let files = [];
|
|
319
369
|
try {
|
|
320
370
|
files = fs.readdirSync(path.join(base, job, 'runs')).filter((f) => f.endsWith('.log')).sort().reverse();
|