@sdods/cli 0.13.2 → 0.14.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/dist/.tsbuildinfo +1 -1
- package/dist/commands/db.d.ts +5 -0
- package/dist/commands/db.js +171 -11
- package/dist/commands/doctor.js +0 -5
- package/dist/commands/runs.d.ts +4 -0
- package/dist/commands/runs.js +49 -0
- package/dist/program.js +2 -0
- package/package.json +8 -8
- package/skills/sdods-start-ui/SKILL.md +2 -2
- package/templates/.env.example +0 -5
package/dist/commands/db.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
/** `.sdods/runs` under the workspace, or SDODS_ARTIFACTS_DIR (absolute, or relative to it). */
|
|
3
|
+
export declare function artifactsRootOf(rootDir: string): string;
|
|
4
|
+
/** `.sdods/backups`, next to the artifacts (SDODS_BACKUPS_DIR overrides). */
|
|
5
|
+
export declare function backupsRootOf(rootDir: string): string;
|
|
6
|
+
export declare function formatBytes(n: number | null | undefined): string;
|
|
2
7
|
export declare function register(program: Command): void;
|
|
3
8
|
//# sourceMappingURL=db.d.ts.map
|
package/dist/commands/db.js
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import { parse as parseYaml } from 'yaml';
|
|
5
5
|
import { WorkspaceFileSchema } from '@sdods/contracts';
|
|
6
6
|
import { SdodsError } from '@sdods/core';
|
|
7
7
|
import { createContext } from '../context.js';
|
|
8
|
-
import { json, ok, out, table, warn } from '../ui.js';
|
|
8
|
+
import { json, ok, out, parseIntFlag, table, warn } from '../ui.js';
|
|
9
9
|
async function loadDb() {
|
|
10
10
|
return import('@sdods/db');
|
|
11
11
|
}
|
|
12
|
+
/** `.sdods/runs` under the workspace, or SDODS_ARTIFACTS_DIR (absolute, or relative to it). */
|
|
13
|
+
export function artifactsRootOf(rootDir) {
|
|
14
|
+
return resolve(rootDir, process.env.SDODS_ARTIFACTS_DIR ?? '.sdods/runs');
|
|
15
|
+
}
|
|
16
|
+
/** `.sdods/backups`, next to the artifacts (SDODS_BACKUPS_DIR overrides). */
|
|
17
|
+
export function backupsRootOf(rootDir) {
|
|
18
|
+
return process.env.SDODS_BACKUPS_DIR
|
|
19
|
+
? resolve(rootDir, process.env.SDODS_BACKUPS_DIR)
|
|
20
|
+
: join(dirname(artifactsRootOf(rootDir)), 'backups');
|
|
21
|
+
}
|
|
22
|
+
export function formatBytes(n) {
|
|
23
|
+
if (n == null)
|
|
24
|
+
return 'unknown';
|
|
25
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
26
|
+
let v = n;
|
|
27
|
+
let i = 0;
|
|
28
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
29
|
+
v /= 1024;
|
|
30
|
+
i++;
|
|
31
|
+
}
|
|
32
|
+
return `${v < 10 && i > 0 ? v.toFixed(1) : Math.round(v)} ${units[i]}`;
|
|
33
|
+
}
|
|
12
34
|
export function register(program) {
|
|
13
35
|
const db = program
|
|
14
36
|
.command('db')
|
|
@@ -55,13 +77,13 @@ export function register(program) {
|
|
|
55
77
|
if (ctx.opts.json)
|
|
56
78
|
return json({
|
|
57
79
|
driver: adb.driver,
|
|
58
|
-
config: { ...adb.config, databaseUrl: redactUrl(adb.config.databaseUrl) },
|
|
80
|
+
config: { ...adb.config, databaseUrl: m.redactUrl(adb.config.databaseUrl) },
|
|
59
81
|
migrations: status,
|
|
60
82
|
});
|
|
61
83
|
out(pc.bold(`driver: ${adb.driver}`) +
|
|
62
84
|
pc.dim(adb.driver === 'sqlite'
|
|
63
85
|
? ` ${adb.config.sqlitePath}`
|
|
64
|
-
: ` ${redactUrl(adb.config.databaseUrl)}`));
|
|
86
|
+
: ` ${m.redactUrl(adb.config.databaseUrl)}`));
|
|
65
87
|
table(status.map((s) => ({
|
|
66
88
|
migration: s.name,
|
|
67
89
|
applied: s.executedAt ? s.executedAt : pc.yellow('pending'),
|
|
@@ -196,13 +218,156 @@ export function register(program) {
|
|
|
196
218
|
keepRuns: opts.keepRuns ? Number(opts.keepRuns) : undefined,
|
|
197
219
|
keepDays: opts.keepDays ? Number(opts.keepDays) : undefined,
|
|
198
220
|
projectSlug: opts.project,
|
|
199
|
-
artifactsRoot:
|
|
221
|
+
artifactsRoot: artifactsRootOf(ctx.rootDir),
|
|
200
222
|
dryRun: Boolean(opts.dryRun),
|
|
223
|
+
actor: { type: 'cli' },
|
|
201
224
|
});
|
|
202
225
|
if (ctx.opts.json)
|
|
203
226
|
return json(res);
|
|
204
227
|
table(res.deleted.map((d) => ({ run: d.id, project: d.projectSlug, created: d.createdAt })));
|
|
205
|
-
|
|
228
|
+
for (const s of res.skipped)
|
|
229
|
+
warn(`kept ${s.id}: ${s.reason === 'live' ? 'still running' : s.reason}`);
|
|
230
|
+
ok(`${opts.dryRun ? 'Would delete' : 'Deleted'} ${res.deleted.length} run(s), ${opts.dryRun ? 'freeing' : 'freed'} ${formatBytes(res.freedBytes)}; kept ${res.kept}`);
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
await adb.close();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
db.command('stats')
|
|
237
|
+
.description('Show which database is in use, how big it is, and what it holds')
|
|
238
|
+
.action(async (_opts, cmd) => {
|
|
239
|
+
const ctx = createContext(cmd);
|
|
240
|
+
const m = await loadDb();
|
|
241
|
+
const adb = m.createDb();
|
|
242
|
+
try {
|
|
243
|
+
await m.migrateToLatest(adb);
|
|
244
|
+
const s = await m.storageStats(adb, { artifactsRoot: artifactsRootOf(ctx.rootDir) });
|
|
245
|
+
const backups = m.listBackups(backupsRootOf(ctx.rootDir));
|
|
246
|
+
if (ctx.opts.json)
|
|
247
|
+
return json({ ...s, backupsDir: backupsRootOf(ctx.rootDir), backups: backups.length });
|
|
248
|
+
out(pc.bold(`driver: ${s.driver}`));
|
|
249
|
+
out(`location: ${s.location}`);
|
|
250
|
+
out(`database: ${formatBytes(s.dbBytes)}`);
|
|
251
|
+
out(`artifacts: ${formatBytes(s.artifactsBytes)} ${pc.dim(s.artifactsDir ?? '')}`);
|
|
252
|
+
out(`backups: ${backups.length} ${pc.dim(backupsRootOf(ctx.rootDir))}`);
|
|
253
|
+
out(`runs: ${s.counts.runs}${s.oldestRunAt ? pc.dim(` ${s.oldestRunAt.slice(0, 10)} → ${s.newestRunAt?.slice(0, 10)}`) : ''}`);
|
|
254
|
+
out(`rows: ${s.counts.scenarios} scenarios, ${s.counts.artifacts} artifacts, ${s.counts.auditLog} audit entries, ${s.counts.users} users`);
|
|
255
|
+
out(`migrations: ${s.migrations.pending ? pc.yellow(`${s.migrations.pending} pending`) : pc.green('up to date')} ${pc.dim(s.migrations.latest ?? '')}`);
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
await adb.close();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
db.command('retention')
|
|
262
|
+
.description('Show or set how much run history to keep (applied daily by `sdods serve` when --auto on)')
|
|
263
|
+
.option('--keep-runs <n>', 'runs to keep per project (0 = no limit)', parseIntFlag('keep-runs'))
|
|
264
|
+
.option('--keep-days <n>', 'delete runs older than n days (0 = no limit)', parseIntFlag('keep-days'))
|
|
265
|
+
.option('--auto <on|off>', 'clean up automatically once a day')
|
|
266
|
+
.action(async (opts, cmd) => {
|
|
267
|
+
const ctx = createContext(cmd);
|
|
268
|
+
const m = await loadDb();
|
|
269
|
+
const adb = m.createDb();
|
|
270
|
+
try {
|
|
271
|
+
await m.migrateToLatest(adb);
|
|
272
|
+
const patch = {};
|
|
273
|
+
if (opts.keepRuns !== undefined)
|
|
274
|
+
patch.keepRuns = opts.keepRuns || null;
|
|
275
|
+
if (opts.keepDays !== undefined)
|
|
276
|
+
patch.keepDays = opts.keepDays || null;
|
|
277
|
+
if (opts.auto !== undefined) {
|
|
278
|
+
if (!['on', 'off'].includes(opts.auto))
|
|
279
|
+
throw new SdodsError('NOT_SUPPORTED', '--auto expects on or off.', { exitCode: 2 });
|
|
280
|
+
patch.auto = opts.auto === 'on';
|
|
281
|
+
}
|
|
282
|
+
const r = Object.keys(patch).length
|
|
283
|
+
? await m.setRetentionSettings(adb.db, patch)
|
|
284
|
+
: await m.getRetentionSettings(adb.db);
|
|
285
|
+
if (Object.keys(patch).length)
|
|
286
|
+
await m.audit(adb.db, {
|
|
287
|
+
actorType: 'cli',
|
|
288
|
+
action: 'retention.update',
|
|
289
|
+
targetType: 'settings',
|
|
290
|
+
targetId: 'retention',
|
|
291
|
+
details: { ...r },
|
|
292
|
+
});
|
|
293
|
+
if (ctx.opts.json)
|
|
294
|
+
return json(r);
|
|
295
|
+
out(`keep runs per project: ${r.keepRuns ?? 'no limit'}`);
|
|
296
|
+
out(`keep days: ${r.keepDays ?? 'no limit'}`);
|
|
297
|
+
out(`auto clean-up: ${r.auto ? 'on (daily)' : 'off'}`);
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
await adb.close();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
db.command('backup')
|
|
304
|
+
.description('Back up the database to .sdods/backups/<timestamp>/ (JSONL + manifest)')
|
|
305
|
+
.option('--out <dir>', 'backups folder (default .sdods/backups)')
|
|
306
|
+
.action(async (opts, cmd) => {
|
|
307
|
+
const ctx = createContext(cmd);
|
|
308
|
+
const m = await loadDb();
|
|
309
|
+
const adb = m.createDb();
|
|
310
|
+
try {
|
|
311
|
+
await m.migrateToLatest(adb);
|
|
312
|
+
const { VERSION } = await import('@sdods/core');
|
|
313
|
+
const b = await m.createBackup(adb, opts.out ?? backupsRootOf(ctx.rootDir), {
|
|
314
|
+
label: 'manual',
|
|
315
|
+
sdodsVersion: VERSION,
|
|
316
|
+
});
|
|
317
|
+
if (ctx.opts.json)
|
|
318
|
+
return json(b);
|
|
319
|
+
ok(`Backed up ${b.rows} row(s), ${formatBytes(b.bytes)} → ${b.dir}`);
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
await adb.close();
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
db.command('backups')
|
|
326
|
+
.description('List backups, newest first')
|
|
327
|
+
.option('--dir <dir>', 'backups folder (default .sdods/backups)')
|
|
328
|
+
.action(async (opts, cmd) => {
|
|
329
|
+
const ctx = createContext(cmd);
|
|
330
|
+
const m = await loadDb();
|
|
331
|
+
const list = m.listBackups(opts.dir ?? backupsRootOf(ctx.rootDir));
|
|
332
|
+
if (ctx.opts.json)
|
|
333
|
+
return json(list);
|
|
334
|
+
if (!list.length)
|
|
335
|
+
return out(pc.dim('No backups yet. Create one with `sdods db backup`.'));
|
|
336
|
+
table(list.map((b) => ({
|
|
337
|
+
id: b.id,
|
|
338
|
+
label: b.label,
|
|
339
|
+
rows: b.rows,
|
|
340
|
+
size: formatBytes(b.bytes),
|
|
341
|
+
dir: b.dir,
|
|
342
|
+
})));
|
|
343
|
+
});
|
|
344
|
+
db.command('restore <dir>')
|
|
345
|
+
.description('Replace the database contents with a backup (a pre-restore backup is taken first)')
|
|
346
|
+
.option('--yes', 'confirm: the current data is replaced')
|
|
347
|
+
.action(async (dir, opts, cmd) => {
|
|
348
|
+
const ctx = createContext(cmd);
|
|
349
|
+
if (!opts.yes)
|
|
350
|
+
throw new SdodsError('NOT_SUPPORTED', 'restore replaces every row in the database; pass --yes to confirm.', { exitCode: 2 });
|
|
351
|
+
const m = await loadDb();
|
|
352
|
+
const adb = m.createDb();
|
|
353
|
+
try {
|
|
354
|
+
await m.migrateToLatest(adb);
|
|
355
|
+
const { VERSION } = await import('@sdods/core');
|
|
356
|
+
const res = await m.restoreBackup(adb, resolve(dir), {
|
|
357
|
+
safetyRoot: backupsRootOf(ctx.rootDir),
|
|
358
|
+
sdodsVersion: VERSION,
|
|
359
|
+
});
|
|
360
|
+
await m.audit(adb.db, {
|
|
361
|
+
actorType: 'cli',
|
|
362
|
+
action: 'backup.restore',
|
|
363
|
+
targetType: 'backup',
|
|
364
|
+
targetId: resolve(dir),
|
|
365
|
+
details: { safety: res.safety?.id ?? null },
|
|
366
|
+
});
|
|
367
|
+
if (ctx.opts.json)
|
|
368
|
+
return json(res);
|
|
369
|
+
table(res.imported.tables.filter((x) => x.rows).map((x) => ({ table: x.table, rows: x.rows })));
|
|
370
|
+
ok(`Restored from ${resolve(dir)}${res.safety ? `. The previous data is in ${res.safety.dir}` : ''}`);
|
|
206
371
|
}
|
|
207
372
|
finally {
|
|
208
373
|
await adb.close();
|
|
@@ -233,9 +398,4 @@ export function register(program) {
|
|
|
233
398
|
}
|
|
234
399
|
});
|
|
235
400
|
}
|
|
236
|
-
function redactUrl(url) {
|
|
237
|
-
if (!url)
|
|
238
|
-
return '';
|
|
239
|
-
return url.replace(/\/\/([^:]+):[^@]+@/, '//$1:***@');
|
|
240
|
-
}
|
|
241
401
|
//# sourceMappingURL=db.js.map
|
package/dist/commands/doctor.js
CHANGED
|
@@ -241,11 +241,6 @@ function tokenMatrix(cliStatus, localLlm) {
|
|
|
241
241
|
requirement: 'optional',
|
|
242
242
|
present: has('SDODS_TOKEN'),
|
|
243
243
|
detail: 'self-issued, free: MCP over HTTP and CI result ingest (`sdods tokens create`)',
|
|
244
|
-
}, {
|
|
245
|
-
name: 'NPM_TOKEN',
|
|
246
|
-
requirement: 'ci-only',
|
|
247
|
-
present: has('NPM_TOKEN'),
|
|
248
|
-
detail: 'GitHub secret for publishing @sdods/* packages',
|
|
249
244
|
});
|
|
250
245
|
return rows;
|
|
251
246
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { SdodsError } from '@sdods/core';
|
|
3
|
+
import { createContext } from '../context.js';
|
|
4
|
+
import { json, ok, out, table, warn } from '../ui.js';
|
|
5
|
+
import { artifactsRootOf, formatBytes } from './db.js';
|
|
6
|
+
/** `sdods runs …`: manage stored run history (`sdods run` starts a run). */
|
|
7
|
+
export function register(program) {
|
|
8
|
+
const runs = program.command('runs').description('Manage stored run history');
|
|
9
|
+
runs
|
|
10
|
+
.command('rm <ids...>')
|
|
11
|
+
.alias('delete')
|
|
12
|
+
.description('Delete runs, their results and artifact folders (running runs are skipped)')
|
|
13
|
+
.option('--dry-run', 'show what would be deleted')
|
|
14
|
+
.option('--yes', 'confirm the delete')
|
|
15
|
+
.action(async (ids, opts, cmd) => {
|
|
16
|
+
const ctx = createContext(cmd);
|
|
17
|
+
if (!opts.dryRun && !opts.yes)
|
|
18
|
+
throw new SdodsError('NOT_SUPPORTED', 'Deleting runs cannot be undone; pass --yes to confirm (or --dry-run to preview).', { exitCode: 2 });
|
|
19
|
+
const m = await import('@sdods/db');
|
|
20
|
+
const adb = m.createDb();
|
|
21
|
+
try {
|
|
22
|
+
await m.migrateToLatest(adb);
|
|
23
|
+
const res = await m.deleteRuns(adb, ids, {
|
|
24
|
+
artifactsRoot: artifactsRootOf(ctx.rootDir),
|
|
25
|
+
actor: { type: 'cli' },
|
|
26
|
+
reason: 'manual',
|
|
27
|
+
dryRun: Boolean(opts.dryRun),
|
|
28
|
+
});
|
|
29
|
+
if (ctx.opts.json)
|
|
30
|
+
return json(res);
|
|
31
|
+
if (res.deleted.length)
|
|
32
|
+
table(res.deleted.map((d) => ({
|
|
33
|
+
run: d.id,
|
|
34
|
+
project: d.projectSlug,
|
|
35
|
+
created: d.createdAt,
|
|
36
|
+
size: formatBytes(d.bytes),
|
|
37
|
+
})));
|
|
38
|
+
for (const s of res.skipped)
|
|
39
|
+
warn(`${s.id}: ${s.reason === 'live' ? 'still running, stop it first' : 'not found'}`);
|
|
40
|
+
if (!res.deleted.length)
|
|
41
|
+
return out(pc.dim('Nothing to delete.'));
|
|
42
|
+
ok(`${opts.dryRun ? 'Would delete' : 'Deleted'} ${res.deleted.length} run(s), ${opts.dryRun ? 'freeing' : 'freed'} ${formatBytes(res.freedBytes)}`);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await adb.close();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=runs.js.map
|
package/dist/program.js
CHANGED
|
@@ -15,6 +15,7 @@ import { register as registerHar } from './commands/har.js';
|
|
|
15
15
|
import { register as registerAuth } from './commands/auth.js';
|
|
16
16
|
import { register as registerData } from './commands/data.js';
|
|
17
17
|
import { register as registerDb } from './commands/db.js';
|
|
18
|
+
import { register as registerRuns } from './commands/runs.js';
|
|
18
19
|
import { register as registerReport } from './commands/report.js';
|
|
19
20
|
import { register as registerHeal } from './commands/heal.js';
|
|
20
21
|
import { register as registerInsights } from './commands/insights.js';
|
|
@@ -75,6 +76,7 @@ export function buildProgram() {
|
|
|
75
76
|
registerAuth(program);
|
|
76
77
|
registerData(program);
|
|
77
78
|
registerDb(program);
|
|
79
|
+
registerRuns(program);
|
|
78
80
|
registerReport(program);
|
|
79
81
|
registerHeal(program);
|
|
80
82
|
registerInsights(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "SDODS command line: run, record, analyze, agents, MCP server, scheduler and the web server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"funding": "https://sdods.com/sponsor/",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"node": ">=22"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@sdods/contracts": "0.
|
|
30
|
-
"@sdods/core": "0.
|
|
29
|
+
"@sdods/contracts": "0.14.1",
|
|
30
|
+
"@sdods/core": "0.14.1",
|
|
31
31
|
"commander": "^15.0.0",
|
|
32
32
|
"execa": "^10.1.0",
|
|
33
33
|
"picocolors": "^1.1.1",
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"zod": "^4.6.5",
|
|
37
37
|
"@playwright/test": "^1.64.0",
|
|
38
38
|
"playwright-core": "^1.64.0",
|
|
39
|
-
"@sdods/integrations": "0.
|
|
40
|
-
"@sdods/mcp": "0.
|
|
41
|
-
"@sdods/agents": "0.
|
|
42
|
-
"@sdods/db": "0.
|
|
39
|
+
"@sdods/integrations": "0.14.1",
|
|
40
|
+
"@sdods/mcp": "0.14.1",
|
|
41
|
+
"@sdods/agents": "0.14.1",
|
|
42
|
+
"@sdods/db": "0.14.1",
|
|
43
43
|
"csv-parse": "^7.0.3",
|
|
44
|
-
"@sdods/server": "0.
|
|
44
|
+
"@sdods/server": "0.14.1"
|
|
45
45
|
},
|
|
46
46
|
"homepage": "https://sdods.com",
|
|
47
47
|
"bugs": {
|
|
@@ -295,8 +295,8 @@ sdods browsers install --with-deps # refresh engines afterwards (Linux needs ro
|
|
|
295
295
|
sdods doctor # confirm
|
|
296
296
|
```
|
|
297
297
|
|
|
298
|
-
CI does this on a schedule: `.github/workflows/dependencies.yml`
|
|
299
|
-
|
|
298
|
+
CI does this on a schedule: `.github/workflows/dependencies.yml` posts a freshness report to Yarlis
|
|
299
|
+
Dev (Google Chat, through Yarlis Relay) on the 15th, and on the 1st of the month upgrades `@sdods/*` and Playwright, runs the suite,
|
|
300
300
|
and opens a PR only if it passes.
|
|
301
301
|
|
|
302
302
|
After any upgrade, restart `serve` — a running server keeps the old bundle in memory.
|
package/templates/.env.example
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
# mandatory only when the feature next to it is used
|
|
6
6
|
# one-of agents need ANY ONE of the four options in the Agents block
|
|
7
7
|
# optional turns on an integration
|
|
8
|
-
# ci-only GitHub Actions secrets, never needed locally
|
|
9
8
|
|
|
10
9
|
# ── Database (mandatory: none; DATABASE_URL only when DB_DRIVER=postgres) ─────
|
|
11
10
|
DB_DRIVER=sqlite
|
|
@@ -57,7 +56,3 @@ SESSION_SECRET=change-me-32-chars-minimum-please # openssl rand -hex 32
|
|
|
57
56
|
# ── SDODS's own token (optional, free): MCP over HTTP and CI ingest ─────────
|
|
58
57
|
# SDODS_SERVER_URL=http://127.0.0.1:4444
|
|
59
58
|
# SDODS_TOKEN=amx_... # sdods tokens create --user <name> --scopes runs:ingest,runs:read
|
|
60
|
-
|
|
61
|
-
# ── CI-only GitHub secrets (never needed locally) ────────────────────────────
|
|
62
|
-
# FIREBASE_SERVICE_ACCOUNT_AUTOMAX_DOCS docs deploy (Firebase Hosting)
|
|
63
|
-
# NPM_TOKEN publishing @sdods/* packages
|