mindvest-atlas 0.6.2 → 0.7.0
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 +17 -1
- package/src/config.js +1 -1
- package/src/remote-control.js +90 -1
package/package.json
CHANGED
|
@@ -15,8 +15,9 @@ 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, claimRun, computerName, jobEnv, listJobFiles, listRuns, machineId,
|
|
18
|
+
DEFAULT_TIMEOUT_MS, claimRun, computerName, isPaused, jobEnv, listJobFiles, listRuns, machineId,
|
|
19
19
|
osLabel, readJobFile, releaseRun, staleness, summarize, tail, workspace, workspaceRoot,
|
|
20
|
+
setPaused, writeHelpers,
|
|
20
21
|
} from '../remote-control.js';
|
|
21
22
|
import fs from 'node:fs';
|
|
22
23
|
import path from 'node:path';
|
|
@@ -97,6 +98,10 @@ async function answer(requestId, payload) {
|
|
|
97
98
|
async function handleAsk(ask, root) {
|
|
98
99
|
if (!ask?.request_id) return;
|
|
99
100
|
if (ask.op === 'list') return answer(ask.request_id, { ok: true, jobs: listJobFiles(root) });
|
|
101
|
+
if (ask.op === 'pause') {
|
|
102
|
+
const { dir } = workspace(ask.job || '', root);
|
|
103
|
+
return answer(ask.request_id, { ok: true, paused: setPaused(dir, Boolean(ask.paused)) });
|
|
104
|
+
}
|
|
100
105
|
if (ask.op === 'runs') return answer(ask.request_id, { ok: true, runs: listRuns(root) });
|
|
101
106
|
if (ask.op === 'read') return answer(ask.request_id, readJobFile(ask.job || '', ask.path || '', root));
|
|
102
107
|
return answer(ask.request_id, { ok: false, error: 'Unknown request.' });
|
|
@@ -113,6 +118,13 @@ async function runJob(job, root, timeoutMs) {
|
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
const { dir, entry, argv } = workspace(job.automation_id, root);
|
|
121
|
+
// Paused is the owner saying "not now". A scheduled run that fires anyway
|
|
122
|
+
// would make the switch meaningless; a MANUAL run is the owner asking
|
|
123
|
+
// directly, so it goes through.
|
|
124
|
+
if (isPaused(dir) && job.source !== 'manual') {
|
|
125
|
+
eprintln(color.dim(`· ${name} — paused`));
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
116
128
|
if (!fs.existsSync(entry)) {
|
|
117
129
|
// NOT a failure. The schedule fired and this computer took the job; there
|
|
118
130
|
// was simply nothing to carry out yet. Its owner never sees the code, so
|
|
@@ -128,6 +140,10 @@ async function runJob(job, root, timeoutMs) {
|
|
|
128
140
|
return undefined;
|
|
129
141
|
}
|
|
130
142
|
|
|
143
|
+
// Refreshed every run, so a job always has the supported way to reach a
|
|
144
|
+
// secret and never a reason to open .env itself.
|
|
145
|
+
writeHelpers(dir);
|
|
146
|
+
|
|
131
147
|
eprintln(color.dim(`▶ ${name}`));
|
|
132
148
|
const env = {
|
|
133
149
|
...process.env,
|
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.
|
|
5
|
+
export const VERSION = '0.7.0';
|
|
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
|
@@ -168,6 +168,70 @@ export function readEnvFile(file) {
|
|
|
168
168
|
* Shared first so a key is typed once; the job's own last so a job that wants
|
|
169
169
|
* its own value gets it. Neither is required.
|
|
170
170
|
*/
|
|
171
|
+
/**
|
|
172
|
+
* Written into every job folder before it runs, in both languages.
|
|
173
|
+
*
|
|
174
|
+
* The job imports this instead of opening .env — so the script the model writes
|
|
175
|
+
* has no reason to touch the file, and a model debugging that script has nothing
|
|
176
|
+
* to learn by trying. Regenerated on every run, so editing or deleting it
|
|
177
|
+
* changes nothing.
|
|
178
|
+
*/
|
|
179
|
+
export function writeHelpers(dir) {
|
|
180
|
+
const py = `"""Secrets for this job. Written by Atlas — edits are overwritten.
|
|
181
|
+
|
|
182
|
+
Values come from the environment Atlas set up before starting this script. Do
|
|
183
|
+
NOT open .env: this module is the supported way to reach a secret, and the file
|
|
184
|
+
itself is not something a script or a model needs to read.
|
|
185
|
+
"""
|
|
186
|
+
import os
|
|
187
|
+
import sys
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def secret(name, default=None):
|
|
191
|
+
"""The value of one secret, for USE — never for printing or logging."""
|
|
192
|
+
return os.environ.get(name, default)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def has_secret(name):
|
|
196
|
+
"""Whether a secret has been filled in. True/False only, never the value."""
|
|
197
|
+
return bool(os.environ.get(name))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def require(*names):
|
|
201
|
+
"""Stop cleanly when a secret has not been filled in yet."""
|
|
202
|
+
missing = [n for n in names if not os.environ.get(n)]
|
|
203
|
+
if missing:
|
|
204
|
+
print("Missing: %s. Add the value in Atlas Remote Control, then run again."
|
|
205
|
+
% ", ".join(missing))
|
|
206
|
+
sys.exit(2)
|
|
207
|
+
`;
|
|
208
|
+
const js = `// Secrets for this job. Written by Atlas — edits are overwritten.
|
|
209
|
+
//
|
|
210
|
+
// Values come from the environment Atlas set up before starting this script. Do
|
|
211
|
+
// NOT open .env: this module is the supported way to reach a secret, and the
|
|
212
|
+
// file itself is not something a script or a model needs to read.
|
|
213
|
+
|
|
214
|
+
/** The value of one secret, for USE — never for printing or logging. */
|
|
215
|
+
export const secret = (name, fallback) => process.env[name] ?? fallback;
|
|
216
|
+
|
|
217
|
+
/** Whether a secret has been filled in. True/false only, never the value. */
|
|
218
|
+
export const hasSecret = (name) => Boolean(process.env[name]);
|
|
219
|
+
|
|
220
|
+
/** Stop cleanly when a secret has not been filled in yet. */
|
|
221
|
+
export function require_(...names) {
|
|
222
|
+
const missing = names.filter((n) => !process.env[n]);
|
|
223
|
+
if (missing.length) {
|
|
224
|
+
console.log(\`Missing: \${missing.join(", ")}. Add the value in Atlas Remote Control, then run again.\`);
|
|
225
|
+
process.exit(2);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
229
|
+
try {
|
|
230
|
+
fs.writeFileSync(path.join(dir, 'atlas_env.py'), py);
|
|
231
|
+
fs.writeFileSync(path.join(dir, 'atlas_env.js'), js);
|
|
232
|
+
} catch { /* a helper is a courtesy, never a failure */ }
|
|
233
|
+
}
|
|
234
|
+
|
|
171
235
|
export function jobEnv(dir, root) {
|
|
172
236
|
return { ...readEnvFile(path.join(workspaceRoot(root), '.env')), ...readEnvFile(path.join(dir, '.env')) };
|
|
173
237
|
}
|
|
@@ -267,7 +331,7 @@ export function listJobFiles(root) {
|
|
|
267
331
|
}
|
|
268
332
|
};
|
|
269
333
|
walk(path.join(base, job), '');
|
|
270
|
-
return { job, files };
|
|
334
|
+
return { job, files, paused: isPaused(path.join(base, job)) };
|
|
271
335
|
});
|
|
272
336
|
}
|
|
273
337
|
|
|
@@ -308,6 +372,31 @@ export function readJobFile(job, rel, root) {
|
|
|
308
372
|
* long), which is why the writer stamps both.
|
|
309
373
|
*/
|
|
310
374
|
export const RUNNING_MARKER = '.running';
|
|
375
|
+
/**
|
|
376
|
+
* Marks a project the owner has switched off.
|
|
377
|
+
*
|
|
378
|
+
* Local, like everything else about a project: pausing is a fact about this
|
|
379
|
+
* computer's copy, and a file is the only form of it that survives the runner
|
|
380
|
+
* restarting and is visible in the folder to anyone wondering why nothing has
|
|
381
|
+
* happened.
|
|
382
|
+
*/
|
|
383
|
+
export const PAUSED_MARKER = '.paused';
|
|
384
|
+
|
|
385
|
+
export const isPaused = (dir) => fs.existsSync(path.join(dir, PAUSED_MARKER));
|
|
386
|
+
|
|
387
|
+
/** Switch one project off or back on. Returns the state it ended in. */
|
|
388
|
+
export function setPaused(dir, paused) {
|
|
389
|
+
const m = path.join(dir, PAUSED_MARKER);
|
|
390
|
+
try {
|
|
391
|
+
if (paused) {
|
|
392
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
393
|
+
fs.writeFileSync(m, `${Date.now() / 1000}\n`);
|
|
394
|
+
} else {
|
|
395
|
+
fs.rmSync(m, { force: true });
|
|
396
|
+
}
|
|
397
|
+
} catch { /* best-effort */ }
|
|
398
|
+
return isPaused(dir);
|
|
399
|
+
}
|
|
311
400
|
|
|
312
401
|
export const runningMarker = (dir) => path.join(dir, 'runs', RUNNING_MARKER);
|
|
313
402
|
|