@sdods/cli 0.5.1 → 0.6.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.
@@ -3,7 +3,7 @@ import { isAbsolute, join, relative, resolve as resolvePath } from 'node:path';
3
3
  import { execa } from 'execa';
4
4
  import pc from 'picocolors';
5
5
  import { RecordingModeSchema, newRunId, runFiles, } from '@sdods/contracts';
6
- import { SdodsError, CLI_OVERRIDES_ENV, VERSION, formatFindings, lintProject, listGeneratedProjects, moduleByName, moduleDir, normalizeTagExpr, parseTagExpr, serializeCliOverrides, setupTierOf, } from '@sdods/core';
6
+ import { SdodsError, CLI_OVERRIDES_ENV, VERSION, formatFindings, lintProject, listGeneratedProjects, moduleByName, moduleDir, normalizeTagExpr, parseTagExpr, redactRunTraces, serializeCliOverrides, setupTierOf, } from '@sdods/core';
7
7
  import { analyzeChangeImpact } from '@sdods/mcp';
8
8
  import { createContext } from '../context.js';
9
9
  import { browserStatuses } from './browsers.js';
@@ -378,6 +378,16 @@ export async function runCommand(flags, cmd) {
378
378
  manifest.finishedAt = new Date().toISOString();
379
379
  manifest.exitCode = exitCode;
380
380
  writeManifest();
381
+ // Traces carry the session of the account that ran each test (cookies, Authorization headers,
382
+ // localStorage/IndexedDB tokens). Redact every copy before anything reads the run directory:
383
+ // ingest, integrations, and the CI step that uploads it as an artifact (#101).
384
+ if (cfg.project.evidence.redactTraces !== false) {
385
+ const redaction = redactRunTraces(runDir);
386
+ if (redaction.files.length)
387
+ out(pc.dim(`traces: redacted ${redaction.values} credential value(s) in ${redaction.files.length} file(s)`));
388
+ for (const e of redaction.errors)
389
+ warn(`trace redaction failed for ${relative(ctx.rootDir, e.file)}: ${e.error}`);
390
+ }
381
391
  const summary = readSummary(runDir, runId, exitCode, Date.now() - started);
382
392
  if (summary)
383
393
  writeFileSync(join(runDir, runFiles.summary), JSON.stringify(summary, null, 2));
@@ -1,11 +1,15 @@
1
1
  import { SdodsError } from '@sdods/core';
2
2
  import { MIN_PASSWORD_LENGTH } from '@sdods/contracts/names';
3
3
  import { createContext } from '../context.js';
4
- import { json, ok, table } from '../ui.js';
4
+ import { info, json, ok, table, warn } from '../ui.js';
5
5
  async function openDb() {
6
6
  const db = await import('@sdods/db');
7
7
  return db.openDb();
8
8
  }
9
+ function assertPassword(password) {
10
+ if (String(password).length < MIN_PASSWORD_LENGTH)
11
+ throw new SdodsError('CONFIG_INVALID', `Password must be at least ${MIN_PASSWORD_LENGTH} characters.`, { exitCode: 2 });
12
+ }
9
13
  export function register(program) {
10
14
  const users = program
11
15
  .command('users')
@@ -21,10 +25,7 @@ export function register(program) {
21
25
  .option('--org-owner', 'grant organization owner on orgs without an owner')
22
26
  .action(async (opts, cmd) => {
23
27
  const ctx = createContext(cmd);
24
- if (String(opts.password).length < 8)
25
- throw new SdodsError('CONFIG_INVALID', 'Password must be at least 8 characters.', {
26
- exitCode: 2,
27
- });
28
+ assertPassword(opts.password);
28
29
  const adb = await openDb();
29
30
  try {
30
31
  const db = await import('@sdods/db');
@@ -132,6 +133,73 @@ export function register(program) {
132
133
  await adb.close();
133
134
  }
134
135
  });
136
+ users
137
+ .command('set-password <username>')
138
+ .description('Set a password and sign the user out everywhere; --activate re-enables the account')
139
+ .requiredOption('--password <password>', `new password (min ${MIN_PASSWORD_LENGTH} chars)`)
140
+ .option('--activate', 're-enable a deactivated account')
141
+ .action(async (username, opts, cmd) => {
142
+ const ctx = createContext(cmd);
143
+ assertPassword(opts.password);
144
+ const adb = await openDb();
145
+ try {
146
+ const db = await import('@sdods/db');
147
+ const { hashPassword } = await import('@sdods/server');
148
+ const u = await db.getUserByUsername(adb.db, username);
149
+ if (!u)
150
+ throw new SdodsError('CONFIG_NOT_FOUND', `User ${username} not found.`, {
151
+ exitCode: 2,
152
+ });
153
+ await db.updateUser(adb.db, adb.driver, u.id, {
154
+ passwordHash: await hashPassword(opts.password),
155
+ ...(opts.activate ? { active: true } : {}),
156
+ });
157
+ await db.deleteSessionsForUser(adb.db, u.id);
158
+ await db.audit(adb.db, {
159
+ actorType: 'cli',
160
+ action: 'user.password',
161
+ targetType: 'user',
162
+ targetId: u.id,
163
+ details: { username, activated: Boolean(opts.activate) },
164
+ });
165
+ const active = opts.activate ? true : u.active;
166
+ if (ctx.opts.json)
167
+ return json({ id: u.id, username, active });
168
+ ok(`Password set for ${username}; existing sessions signed out`);
169
+ if (!active)
170
+ warn(`${username} is deactivated; pass --activate to let them sign in`);
171
+ }
172
+ finally {
173
+ await adb.close();
174
+ }
175
+ });
176
+ users
177
+ .command('reset')
178
+ .description('Remove every user, session, API token and membership; projects and runs stay (next serve offers /setup)')
179
+ .option('--yes', 'confirm')
180
+ .action(async (opts, cmd) => {
181
+ const ctx = createContext(cmd);
182
+ if (!opts.yes)
183
+ throw new SdodsError('NOT_SUPPORTED', 'users reset removes every user; pass --yes to confirm.', { exitCode: 2 });
184
+ const adb = await openDb();
185
+ try {
186
+ const db = await import('@sdods/db');
187
+ const removed = await db.deleteAllUsers(adb.db);
188
+ await db.audit(adb.db, {
189
+ actorType: 'cli',
190
+ action: 'users.reset',
191
+ targetType: 'user',
192
+ details: { removed },
193
+ });
194
+ if (ctx.opts.json)
195
+ return json({ removed });
196
+ ok(`Removed ${removed} user${removed === 1 ? '' : 's'}; projects, runs and schedules are kept`);
197
+ info('Restart `sdods serve` for a one-time /setup link, or run: sdods users create --admin --username <name> --password <pw>');
198
+ }
199
+ finally {
200
+ await adb.close();
201
+ }
202
+ });
135
203
  users
136
204
  .command('grant <username>')
137
205
  .description('Grant a membership role: --org <slug> --role owner|admin|member, or --workspace <slug> --role admin|editor|viewer')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdods/cli",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
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
  "author": "SDODS <admin@sdods.com>",
@@ -23,8 +23,8 @@
23
23
  "node": ">=22"
24
24
  },
25
25
  "dependencies": {
26
- "@sdods/contracts": "0.5.1",
27
- "@sdods/core": "0.5.1",
26
+ "@sdods/contracts": "0.6.0",
27
+ "@sdods/core": "0.6.0",
28
28
  "commander": "^15.0.0",
29
29
  "execa": "^10.0.1",
30
30
  "picocolors": "^1.1.1",
@@ -33,12 +33,12 @@
33
33
  "zod": "^4.5.4",
34
34
  "@playwright/test": "^1.63.0",
35
35
  "playwright-core": "^1.63.0",
36
- "@sdods/integrations": "0.5.1",
37
- "@sdods/mcp": "0.5.1",
38
- "@sdods/agents": "0.5.1",
39
- "@sdods/db": "0.5.1",
36
+ "@sdods/integrations": "0.6.0",
37
+ "@sdods/mcp": "0.6.0",
38
+ "@sdods/agents": "0.6.0",
39
+ "@sdods/db": "0.6.0",
40
40
  "csv-parse": "^7.0.2",
41
- "@sdods/server": "0.5.1"
41
+ "@sdods/server": "0.6.0"
42
42
  },
43
43
  "homepage": "https://sdods.com",
44
44
  "bugs": {