diff --git a/.snyk b/.snyk index 1e83d06a5..11e987e10 100644 --- a/.snyk +++ b/.snyk @@ -47,11 +47,6 @@ ignore: reason: 'Transitive dependency in @azure/functions; upgrade path has type errors' expires: '2026-07-28T00:00:00.000Z' created: '2026-01-15T11:04:00.000Z' - 'SNYK-JS-YAUZL-15467445': - - '* > yauzl@<3.2.1': - reason: 'Transitive dependency in @mongodb-memory-server; not exploitable in current usage.' - expires: '2026-03-26T00:00:00.000Z' - created: '2026-03-12T12:35:00.000Z' 'SNYK-JS-UUID-16133035': - '* > uuid@8.3.2': reason: 'Transitive dependency in Docusaurus and Azurite dev-only stacks; no compatible fix path available yet from upstream.' @@ -71,11 +66,6 @@ ignore: reason: 'Mongoose 8.22.1 has TypeScript constraint errors in type definitions (types/inferrawdoctype.d.ts, types/inferschematype.d.ts) that break compilation. Patch attempts failed due to external library type incompatibilities. Risk is Low: requires control of query field names and values.' expires: '2026-11-07T00:00:00.000Z' created: '2026-05-07T09:00:00.000Z' - 'SNYK-JS-BABELPLUGINTRANSFORMMODULESSYSTEMJS-16624576': - - '* > @babel/plugin-transform-modules-systemjs@7.28.5': - reason: 'Transitive dependency in Docusaurus; not exploitable in current usage.' - expires: '2026-06-28T00:00:00.000Z' - created: '2026-05-11T10:00:00.000Z' 'SNYK-JS-AI-16734889': - '@docusaurus/preset-classic@3.10.1 > * > ai@5.0.105': reason: 'Transitive dependency in Docusaurus docsearch; Snyk reports no fixed upgrade or patch available.' @@ -101,11 +91,6 @@ ignore: reason: 'Transitive dependency in Docusaurus CSS optimization/build tooling; Snyk reports no fixed upgrade or patch available. Not exploitable at runtime because docs CSS is repository-controlled and processed at build time.' expires: '2026-09-18T00:00:00.000Z' created: '2026-06-08T00:00:00.000Z' - 'SNYK-JS-SHELLQUOTE-17457810': - - '* > shell-quote@1.8.4': - reason: 'Transitive dependency in @docusaurus/core via webpack-dev-server > launch-editor; fixed in shell-quote@1.9.0 but no direct upgrade path available. Dev-time only; not exploitable in current usage.' - expires: '2026-12-31T00:00:00.000Z' - created: '2026-06-25T00:00:00.000Z' 'SNYK-JS-IMAGESIZE-17295814': - '* > image-size': reason: 'Transitive dependency in @docusaurus/mdx-loader and dev build tooling; no upgrade or patch available from upstream. Not exploitable in current usage (build-time image dimension parsing only).' diff --git a/build-pipeline/scripts/verify.ts b/build-pipeline/scripts/verify.ts new file mode 100644 index 000000000..5e4775f6e --- /dev/null +++ b/build-pipeline/scripts/verify.ts @@ -0,0 +1,38 @@ +#!/usr/bin/env node +/// + +import { architectureTests, coverageMerge, e2eTests, knipCheck, pnpmAudit, pnpmScript, snykCodeScan, snykDependencyScan, sonarPullRequestAnalysis, sonarQualityGate, verificationSequence } from '@cellix/local-dev/silent-runners'; + +const snykOrgArgs = ['--org=cellixjs', '--remote-repo-url=https://github.com/CellixJs/cellixjs']; +const snykDependencyArgs = [...snykOrgArgs, '--all-projects', '--policy-path=.snyk', '--exclude=dist,build,.turbo,coverage,.agents-work,.agents,.claude,.github,requirements.txt']; + +const cellixVerify = verificationSequence + .addStep(pnpmScript('format:check')) + .addStep(architectureTests()) + .addStep(coverageMerge()) + .addStep(e2eTests()) + .addStep(knipCheck()) + .addStep(pnpmAudit({ auditLevel: 'high', dependencyType: 'prod', name: 'audit:prod' })) + .addStep(pnpmAudit({ auditLevel: 'critical', dependencyType: 'dev', name: 'audit:dev' })) + .addStep( + snykDependencyScan({ + args: snykDependencyArgs, + }), + ) + .addStep( + snykCodeScan({ + args: snykOrgArgs, + }), + ) + .addStep(sonarPullRequestAnalysis()) + .addStep(sonarQualityGate()); + +function runVerifyCommand(): void { + const result = cellixVerify.run(); + if (result.status === 0) { + process.stdout.write('verify passed\n'); + } + process.exitCode = result.status; +} + +runVerifyCommand(); diff --git a/knip.json b/knip.json index 04aeba72b..d3323d010 100644 --- a/knip.json +++ b/knip.json @@ -1,6 +1,10 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", "workspaces": { + ".": { + "entry": ["build-pipeline/scripts/verify.ts"], + "project": ["build-pipeline/scripts/**/*.ts"] + }, "apps/api": { "entry": ["src/index.ts", "start-*.ts", "sync-local-settings.ts", "rolldown.config.ts"], "project": ["src/**/*.ts", "*.ts"], diff --git a/package.json b/package.json index c4fb15a67..50a02aa1f 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "sonar:pr": "export PR_NUMBER=$(node build-pipeline/scripts/get-pr-number.cjs) && sonar-scanner -Dsonar.pullrequest.key=$PR_NUMBER -Dsonar.pullrequest.branch=$(git branch --show-current) -Dsonar.pullrequest.base=main", "sonar:pr-windows": "for /f %i in ('node build-pipeline/scripts/get-pr-number.cjs') do set PR_NUMBER=%i && sonar-scanner -Dsonar.pullrequest.key=%PR_NUMBER% -Dsonar.pullrequest.branch=%BRANCH_NAME% -Dsonar.pullrequest.base=main", "check-sonar": "node build-pipeline/scripts/check-sonar-quality-gate.cjs", - "verify": "pnpm run format:check && pnpm run test:arch && pnpm run test:coverage:merge && pnpm run test:e2e:worktree && pnpm run knip && pnpm run audit && pnpm run snyk", + "verify": "node --conditions=source build-pipeline/scripts/verify.ts", "knip": "knip", "snyk": "pnpm run snyk:test && pnpm run snyk:code", "snyk:report": "pnpm run snyk:monitor && pnpm run snyk:code:report", @@ -73,6 +73,7 @@ "@ant-design/cli": "^6.3.5", "@biomejs/biome": "2.4.10", "@cellix/graphql-codegen": "workspace:*", + "@cellix/local-dev": "workspace:*", "@graphql-codegen/cli": "^5.0.7", "@graphql-codegen/introspection": "^4.0.3", "@graphql-codegen/typed-document-node": "^5.1.2", diff --git a/packages/cellix/local-dev/README.md b/packages/cellix/local-dev/README.md index 06d0d6ebe..dc38f2054 100644 --- a/packages/cellix/local-dev/README.md +++ b/packages/cellix/local-dev/README.md @@ -1,8 +1,8 @@ # @cellix/local-dev -Generic local-development helpers for Cellix app wrappers. +Generic local-development and verification helpers for Cellix app wrappers. -This package is intentionally policy-free. It owns reusable mechanics such as worktree port math, URL helpers, JSON and dotenv utilities, process exit forwarding, and generic dev runners. App-specific env keys, hostnames, auth routes, and `local.settings.json` mutations belong in app-owned wrapper scripts or internal repo helpers, not here. +This package is intentionally policy-free. It owns reusable mechanics such as worktree port math, URL helpers, JSON and dotenv utilities, process exit forwarding, generic dev runners, and silent verification-command runners. App-specific env keys, hostnames, auth routes, scanner orgs, and `local.settings.json` mutations belong in app-owned wrapper scripts or internal repo helpers, not here. ## Install @@ -30,6 +30,7 @@ In this monorepo, app packages consume the workspace package directly: - Azure Functions - Node-backed processes - Azurite +- Silent verification command runners and portable tool-wrapper builders ## Recommended consumption pattern @@ -109,6 +110,74 @@ values, including complete URL values nested in objects or arrays. It leaves URL embedded in descriptive text unchanged. Use `convertSettingsForWorktree` when the application needs explicit key-level conversion policy. +## Silent runners + +`runSilentCommand` captures stdout and stderr while a command is running. If the +command succeeds, nothing is printed. If it fails, whatever the command wrote to +stdout and stderr is replayed before the failing status is returned. + +```js +import { runSilentCommand } from '@cellix/local-dev/silent-runners'; + +const result = runSilentCommand({ + command: 'snyk', + args: ['test', '--all-projects'], +}); + +process.exitCode = result.status; +``` + +`result.status` is always a number, so it can be assigned straight to +`process.exitCode`. A command terminated by a signal has no exit status of its +own and is mapped to the shell convention of `128 + signalNumber` (SIGINT +becomes 130, SIGKILL becomes 137); its replayed header names the signal rather +than an exit code, and `result.signal` carries the signal itself. + +Use `runSilentCommandSequence` when a wrapper needs to run several commands in +order. Steps are silent by default; mark a step with `output: 'inherit'` only +when its live output is part of the intended consumer experience. + +```js +import { runSilentCommandSequence } from '@cellix/local-dev/silent-runners'; + +const result = runSilentCommandSequence({ + steps: [ + { name: 'format:check', command: 'pnpm', args: ['run', 'format:check'] }, + { name: 'test:e2e', command: 'pnpm', args: ['run', 'test:e2e'] }, + ], +}); + +process.exitCode = result.status; +``` + +For reusable verification workflows, use the fluent sequence builder: + +```js +import { pnpmScript, verificationSequence } from '@cellix/local-dev/silent-runners'; + +const verify = verificationSequence + .addStep(pnpmScript('format:check')) + .addStep(pnpmScript('test')); + +const result = verify.run(); +process.exitCode = result.status; +``` + +Prefer the named tool wrappers when a command has a known CLI shape: + +```js +import { knipCheck, pnpmAudit, runSilentCommandSequence, snykCodeScan, snykDependencyScan } from '@cellix/local-dev/silent-runners'; + +const result = runSilentCommandSequence({ + steps: [ + knipCheck(), + pnpmAudit({ auditLevel: 'high', dependencyType: 'prod' }), + snykDependencyScan({ args: ['--all-projects', '--org=my-org'] }), + snykCodeScan({ args: ['--org=my-org'] }), + ], +}); +``` + ## Public API All exports are available from `@cellix/local-dev`. Folder-level subpaths are @@ -117,6 +186,7 @@ also published for consumers that want narrower imports: - `@cellix/local-dev/files` - `@cellix/local-dev/process` - `@cellix/local-dev/runners` +- `@cellix/local-dev/silent-runners` - `@cellix/local-dev/urls` - `@cellix/local-dev/vite` - `@cellix/local-dev/workspace` @@ -152,15 +222,27 @@ also published for consumers that want narrower imports: - `getMongoPort` - `getAzuritePorts` - `buildAzuriteConnectionString` -- `runViteDev` -- `runDocusaurusDev` -- `runAzureFunctionsDev` -- `runNodeDev` -- `runAzuriteDev` -- `runTsxDev` deprecated compatibility alias +- `runSilentCommand` +- `runSilentCommandSequence` +- `architectureTests` +- `coverageMerge` +- `e2eTests` +- `knipCheck` +- `livePnpmScript` +- `pnpmAudit` +- `pnpmScript` +- `snykCodeScan` +- `snykDependencyScan` +- `snykIacScan` +- `sonarPullRequestAnalysis` +- `sonarQualityGate` +- `VerificationSequence` +- `verificationSequence` ## Notes - The package derives workspace roots from the caller's current working directory, but it does not infer app layouts or env-variable names. - Worktree names are sanitized before they are inserted into `.localhost` hostnames so branch-style names such as `jason/my-feature` become DNS-safe labels such as `jason-my-feature`. Suffixing is idempotent for hostnames that already contain the sanitized worktree label. +- Silent tool wrappers encode reusable CLI shape; scripts still own project-specific arguments such as org names, paths, and CI policy. +- Captured verification output defaults to 64 MiB and can be adjusted with `maxBuffer` for a command, sequence, or individual sequence step. - If a helper only exists to support one app's local policy, it should usually live with that app instead of being exported here. diff --git a/packages/cellix/local-dev/manifest.md b/packages/cellix/local-dev/manifest.md index 5e75d69ea..62e307a11 100644 --- a/packages/cellix/local-dev/manifest.md +++ b/packages/cellix/local-dev/manifest.md @@ -2,16 +2,17 @@ ## Purpose -Provide generic local-development primitives for Cellix app packages so app-owned wrapper scripts can supply project-specific env keys, hostnames, and local-settings policy without hardcoding those details into the shared package. +Provide generic local-development and verification primitives for Cellix app packages so app-owned wrapper scripts can supply project-specific env keys, hostnames, local-settings policy, and scanner policy without hardcoding those details into the shared package. ## Scope -This package owns generic worktree-aware port math, URL helpers, dotenv and JSON file utilities, child-process lifecycle handling, worktree settings transforms, and generic dev-runner orchestration for Vite, Docusaurus, Azure Functions, Node-backed processes, and Azurite. +This package owns generic worktree-aware port math, URL helpers, dotenv and JSON file utilities, child-process lifecycle handling, worktree settings transforms, generic dev-runner orchestration for Vite, Docusaurus, Azure Functions, Node-backed processes, and Azurite, plus silent verification runners and portable tool-wrapper builders. ## Non-goals - Production runtime behavior - App-specific env-variable names, hostnames, auth-provider paths, or local-settings keys +- Project-specific scanner policy such as Snyk org names, repository URLs, token handling, report publishing, or CI thresholds - Discovering repo-specific app folders from inside the package - Replacing app-owned wrapper scripts that express local project policy @@ -23,6 +24,7 @@ Published entrypoints: - `@cellix/local-dev/files` - `@cellix/local-dev/process` - `@cellix/local-dev/runners` +- `@cellix/local-dev/silent-runners` - `@cellix/local-dev/urls` - `@cellix/local-dev/vite` - `@cellix/local-dev/workspace` @@ -39,8 +41,8 @@ Root entrypoint exports: - `getWorktreePortOffset(worktreeName?)`, `getMongoPort(worktreeName?)`, `getAzuritePorts(worktreeName?)`, `buildAzuriteConnectionString(options)` - `ViteDevRunner`, `DocusaurusDevRunner`, `AzureFunctionsDevRunner`, `NodeDevRunner`, `AzuriteDevRunner` - `WorktreeSettings`, `AzureFunctionsLocalSettings`, `resolveAzureFunctionsLocalSettingsValues`, `convertSettingsForWorktree`, `WorktreeConversionPlan`, `WorktreeMode` -- `runViteDev(options?)`, `runDocusaurusDev(options?)`, `runAzureFunctionsDev(options?)`, `runNodeDev(options?)`, `runAzuriteDev(options)` -- `runTsxDev(options?)` remains as a deprecated compatibility alias for `runNodeDev(options?)` +- `runSilentCommand(options)`, `runSilentCommandSequence(options)`, `VerificationSequence`, `verificationSequence` +- Tool-wrapper builders such as `knipCheck()`, `pnpmAudit(options)`, `snykDependencyScan(options)`, `snykCodeScan(options)`, and `sonarPullRequestAnalysis(options)` ## Core concepts @@ -51,26 +53,33 @@ Root entrypoint exports: - Azure Functions dev runners may prepare `local.settings.json` before startup because the Functions host reads settings from its script root rather than the process environment alone. - Consumers that need the same Azure Functions values outside `func start` can resolve them without writing a file; regular mode remains unscoped unless a worktree name is explicitly available. - Hostname suffixing must sanitize raw worktree names before inserting them into `.localhost` domains, and repeated suffixing with the same worktree label must leave hostnames unchanged. +- Silent verification runners capture external command output, emit nothing on success, and replay whatever the command wrote to stdout/stderr on failure. +- Silent command sequences run ordered verification steps, defaulting each step to silent output while allowing explicit passthrough where live output is intentionally part of the command contract. +- Tool wrappers encode portable CLI shapes; root or app scripts own project-specific arguments. +- Commands are spawned without shell interpolation. ## Package boundaries - Do not encode OCOM app names, auth paths, env-variable names, or `local.settings.json` schemas in this package. +- Do not encode project-specific Snyk, Edgescan, or scanner policy here. - Keep repo-specific hostname and env mapping logic in app-owned scripts or internal repo helpers outside this package. - Avoid widening the public surface with one-off helpers that only exist to support a single app branch. ## Dependencies / relationships -- Downstream consumers in this monorepo: `@apps/api`, `@apps/docs`, `@apps/ui-community`, `@apps/ui-staff`, `@apps/server-oauth2-mock`, `@apps/server-mongodb-memory-mock` +- Downstream consumers in this monorepo: `@apps/api`, `@apps/docs`, `@apps/ui-community`, `@apps/ui-staff`, `@apps/server-oauth2-mock`, `@apps/server-mongodb-memory-mock`, and the root `verify` script - Consumed from app-owned wrapper scripts and from tests through the TypeScript API ## Testing strategy - Prefer public-entrypoint tests for dotenv parsing, URL helpers, worktree port derivation, runner object spawning, Vite arg building, Azurite connection-string building, worktree settings transforms, and generic JSON syncing. +- Public-entrypoint tests should prove silent-runner success output is suppressed, failure output is replayed, ordered sequences stop on failure, and the reusable verification sequence stays immutable. - Avoid tests that prove repo-specific wrapper policy through this package's public contract. ## Documentation obligations - Keep `README.md` focused on the generic helper surface and how app-owned wrappers compose it. +- Document silent verification runners as generic command wrappers, not as project-specific CI policy. - Keep TSDoc aligned on public exports that define package behavior. - Update this manifest when public exports or scope boundaries change. diff --git a/packages/cellix/local-dev/package.json b/packages/cellix/local-dev/package.json index b16991873..7ea383825 100644 --- a/packages/cellix/local-dev/package.json +++ b/packages/cellix/local-dev/package.json @@ -1,7 +1,7 @@ { "name": "@cellix/local-dev", "version": "1.0.0", - "description": "Generic local-development helpers for Cellix app wrappers", + "description": "Generic local-development and verification helpers for Cellix app wrappers", "type": "module", "files": [ "dist", @@ -26,10 +26,18 @@ "types": "./dist/runners/index.d.ts", "default": "./dist/runners/index.js" }, + "./silent-runners": { + "types": "./dist/silent-runners/index.d.ts", + "default": "./dist/silent-runners/index.js" + }, "./urls": { "types": "./dist/urls/index.d.ts", "default": "./dist/urls/index.js" }, + "./vite": { + "types": "./dist/vite/index.d.ts", + "default": "./dist/vite/index.js" + }, "./workspace": { "types": "./dist/workspace/index.d.ts", "default": "./dist/workspace/index.js" diff --git a/packages/cellix/local-dev/src/files/index.ts b/packages/cellix/local-dev/src/files/index.ts index 0eca37078..6a3484aee 100644 --- a/packages/cellix/local-dev/src/files/index.ts +++ b/packages/cellix/local-dev/src/files/index.ts @@ -1,2 +1,2 @@ export { type DotEnvValues, readDotEnv } from './dotenv.ts'; -export { readJsonFile, type SyncJsonFileOptions, syncJsonFile } from './json.ts'; +export { readJsonFile, type SyncJsonFileOptions, syncJsonFile, writeJsonFile } from './json.ts'; diff --git a/packages/cellix/local-dev/src/index.test.ts b/packages/cellix/local-dev/src/index.test.ts index 48d60d4b9..7b83969a2 100644 --- a/packages/cellix/local-dev/src/index.test.ts +++ b/packages/cellix/local-dev/src/index.test.ts @@ -1,3 +1,4 @@ +import type { SpawnSyncOptionsWithStringEncoding, SpawnSyncReturns } from 'node:child_process'; import { EventEmitter } from 'node:events'; import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; @@ -25,6 +26,22 @@ import { ViteDevRunner, WorktreeSettings, } from '@cellix/local-dev'; +import { + architectureTests, + coverageMerge, + e2eTests, + knipCheck, + pnpmAudit, + pnpmScript, + runSilentCommand, + runSilentCommandSequence, + type SilentRunnerSpawnSync, + snykCodeScan, + snykDependencyScan, + sonarPullRequestAnalysis, + sonarQualityGate, + verificationSequence, +} from '@cellix/local-dev/silent-runners'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { buildViteArgs } from './vite/index.ts'; @@ -53,6 +70,53 @@ function expectNoDirectProcessExit(): ReturnType { }) as never); } +function createVerifySpawn(options: { failStep?: string } = {}) { + const calls: Array<{ args: string[]; command: string; stdio: SpawnSyncOptionsWithStringEncoding['stdio'] }> = []; + const spawn = (command: string, args: string[], spawnOptions: SpawnSyncOptionsWithStringEncoding): SpawnSyncReturns => { + const stepName = getStepName(command, args); + calls.push({ + args, + command, + stdio: spawnOptions.stdio, + }); + + return { + output: [`${stepName} stdout`, '', ''], + pid: 123, + signal: null, + status: options.failStep === stepName ? 9 : 0, + stderr: `${stepName} stderr\n`, + stdout: `${stepName} stdout\n`, + }; + }; + + return { calls, spawn }; +} + +function getStepName(command: string, args: string[]): string { + if (command === 'pnpm' && args[0] === 'run' && args[1]) { + return args[1]; + } + + if (command === 'pnpm' && args[0] === 'audit') { + return args.includes('--prod') ? 'audit:prod' : 'audit:dev'; + } + + if (command === 'pnpm' && args[0] === 'exec' && args[1] === 'knip') { + return 'knip'; + } + + if (command === 'pnpm' && args[0] === 'exec' && args[1] === 'snyk' && args[2] === 'test') { + return 'snyk:test'; + } + + if (command === 'pnpm' && args[0] === 'exec' && args[1] === 'snyk' && args[2] === 'code' && args[3] === 'test') { + return 'snyk:code'; + } + + return command; +} + describe('@cellix/local-dev', () => { afterEach(() => { vi.restoreAllMocks(); @@ -518,3 +582,332 @@ describe('@cellix/local-dev', () => { }); }); }); + +describe('@cellix/local-dev/silent-runners', () => { + it('keeps successful local verification commands silent', () => { + const output = { + stderr: '', + stdout: '', + }; + const spawn: SilentRunnerSpawnSync = (command, args, options) => { + expect(command).toBe('snyk'); + expect(args).toEqual(['test']); + expect(options.maxBuffer).toBe(64 * 1024 * 1024); + expect(options.stdio).toBe('pipe'); + return { + output: ['tool output', '', ''], + pid: 123, + signal: null, + status: 0, + stderr: 'hidden success stderr', + stdout: 'hidden success stdout', + }; + }; + + const result = runSilentCommand({ + args: ['test'], + command: 'snyk', + spawn, + streams: { + stderr: { + write: (chunk: string) => { + output.stderr += chunk; + return true; + }, + }, + stdout: { + write: (chunk: string) => { + output.stdout += chunk; + return true; + }, + }, + }, + }); + + expect(result).toMatchObject({ + status: 0, + stderr: 'hidden success stderr', + stdout: 'hidden success stdout', + }); + expect(output).toEqual({ + stderr: '', + stdout: '', + }); + }); + + it('replays local verification command output when a command fails', () => { + const output = { + stderr: '', + stdout: '', + }; + const spawn: SilentRunnerSpawnSync = () => ({ + output: ['tool output', '', ''], + pid: 123, + signal: null, + status: 2, + stderr: 'failure stderr', + stdout: 'failure stdout', + }); + + const result = runSilentCommand({ + args: ['code', 'test'], + command: 'snyk', + spawn, + streams: { + stderr: { + write: (chunk: string) => { + output.stderr += chunk; + return true; + }, + }, + stdout: { + write: (chunk: string) => { + output.stdout += chunk; + return true; + }, + }, + }, + }); + + expect(result.status).toBe(2); + expect(output).toEqual({ + stderr: '\nCommand failed (exit 2): snyk code test\n\nfailure stderr', + stdout: 'failure stdout', + }); + }); + + it('reports the signal instead of an exit code when a command is terminated by a signal', () => { + const output = { + stderr: '', + stdout: '', + }; + const spawn: SilentRunnerSpawnSync = () => ({ + output: ['tool output', '', ''], + pid: 123, + signal: 'SIGINT', + status: null, + stderr: 'partial stderr', + stdout: 'partial stdout', + }); + + const result = runSilentCommand({ + args: ['install'], + command: 'pnpm', + spawn, + streams: { + stderr: { + write: (chunk: string) => { + output.stderr += chunk; + return true; + }, + }, + stdout: { + write: (chunk: string) => { + output.stdout += chunk; + return true; + }, + }, + }, + }); + + expect(result).toMatchObject({ + signal: 'SIGINT', + status: 130, + }); + expect(output.stderr).toContain('Command failed (signal SIGINT): pnpm install'); + expect(output.stderr).not.toContain('exit'); + }); + + it('keeps a signalled command failing when its status is assigned to process.exitCode', () => { + const spawn: SilentRunnerSpawnSync = () => ({ + output: ['', '', ''], + pid: 123, + signal: 'SIGKILL', + status: null, + stderr: '', + stdout: '', + }); + + const result = runSilentCommand({ + command: 'pnpm', + spawn, + streams: { stderr: { write: () => true }, stdout: { write: () => true } }, + }); + process.exitCode = result.status; + + expect(result.status).toBe(137); + expect(process.exitCode).toBe(137); + }); + + it('falls back to a failing status when a command cannot be spawned at all', () => { + const spawn: SilentRunnerSpawnSync = () => ({ + error: new Error('spawn missing-tool ENOENT'), + output: ['', '', ''], + pid: 123, + signal: null, + status: null, + stderr: '', + stdout: '', + }); + + const output = { stderr: '', stdout: '' }; + const result = runSilentCommand({ + command: 'missing-tool', + spawn, + streams: { + stderr: { + write: (chunk: string) => { + output.stderr += chunk; + return true; + }, + }, + stdout: { write: () => true }, + }, + }); + + expect(result.status).toBe(1); + expect(output.stderr).toContain('Command failed (exit 1): missing-tool'); + expect(output.stderr).toContain('spawn missing-tool ENOENT'); + }); + + it('allows callers to configure the captured output buffer', () => { + const observedMaxBuffers: Array = []; + const spawn: SilentRunnerSpawnSync = (_command, _args, options) => { + observedMaxBuffers.push(options.maxBuffer); + return { + output: ['', '', ''], + pid: 123, + signal: null, + status: 0, + stderr: '', + stdout: '', + }; + }; + + runSilentCommand({ + command: 'large-output-tool', + maxBuffer: 128 * 1024 * 1024, + spawn, + }); + runSilentCommandSequence({ + maxBuffer: 96 * 1024 * 1024, + spawn, + steps: [ + { command: 'first-tool', name: 'first-tool' }, + { command: 'second-tool', maxBuffer: 160 * 1024 * 1024, name: 'second-tool' }, + ], + }); + + expect(observedMaxBuffers).toEqual([128 * 1024 * 1024, 96 * 1024 * 1024, 160 * 1024 * 1024]); + }); + + it('keeps the successful verify sequence fully silent', () => { + const { calls, spawn } = createVerifySpawn(); + const output = { stderr: '', stdout: '' }; + const steps = [ + pnpmScript('format:check'), + architectureTests(), + coverageMerge(), + e2eTests(), + knipCheck(), + pnpmAudit({ auditLevel: 'high', dependencyType: 'prod', name: 'audit:prod' }), + pnpmAudit({ auditLevel: 'critical', dependencyType: 'dev', name: 'audit:dev' }), + snykDependencyScan({ args: ['--all-projects'] }), + snykCodeScan(), + sonarPullRequestAnalysis(), + sonarQualityGate(), + ]; + + const result = runSilentCommandSequence({ + spawn, + steps, + streams: { + stderr: { + write: (chunk: string) => { + output.stderr += chunk; + return true; + }, + }, + stdout: { + write: (chunk: string) => { + output.stdout += chunk; + return true; + }, + }, + }, + }); + + expect(result.status).toBe(0); + expect(output).toEqual({ stderr: '', stdout: '' }); + expect(calls.map((call) => getStepName(call.command, call.args))).toEqual(steps.map((step) => step.name)); + expect(calls).toEqual( + steps.map((step) => ({ + args: step.args ?? [], + command: step.command, + stdio: 'pipe', + })), + ); + }); + + it('replays output from the failing silent verify step and stops before later work', () => { + const { calls, spawn } = createVerifySpawn({ failStep: 'snyk:code' }); + const output = { stderr: '', stdout: '' }; + const steps = [ + pnpmScript('format:check'), + architectureTests(), + coverageMerge(), + e2eTests(), + knipCheck(), + pnpmAudit({ auditLevel: 'high', dependencyType: 'prod', name: 'audit:prod' }), + pnpmAudit({ auditLevel: 'critical', dependencyType: 'dev', name: 'audit:dev' }), + snykDependencyScan({ args: ['--all-projects'] }), + snykCodeScan(), + sonarPullRequestAnalysis(), + sonarQualityGate(), + ]; + + const result = runSilentCommandSequence({ + spawn, + steps, + streams: { + stderr: { + write: (chunk: string) => { + output.stderr += chunk; + return true; + }, + }, + stdout: { + write: (chunk: string) => { + output.stdout += chunk; + return true; + }, + }, + }, + }); + + expect(result.status).toBe(9); + expect(result.step.name).toBe('snyk:code'); + expect(output).toEqual({ + stderr: '\nCommand failed (exit 9): pnpm exec snyk code test\n\nsnyk:code stderr\n', + stdout: 'snyk:code stdout\n', + }); + expect(calls.map((call) => getStepName(call.command, call.args))).toEqual(['format:check', 'test:arch', 'test:coverage:merge', 'test:e2e', 'knip', 'audit:prod', 'audit:dev', 'snyk:test', 'snyk:code']); + }); + + it('builds and runs reusable verification sequences with a fluent addStep API', () => { + const { calls, spawn } = createVerifySpawn(); + const sequence = verificationSequence.addStep(pnpmScript('format:check')).addStep(architectureTests()); + + const result = sequence.run({ spawn }); + + expect(result.status).toBe(0); + expect(calls.map((call) => getStepName(call.command, call.args))).toEqual(['format:check', 'test:arch']); + }); + + it('keeps the shared verification sequence immutable', () => { + const configuredSequence = verificationSequence.addStep(pnpmScript('format:check')); + + expect(() => verificationSequence.run()).toThrow('runSilentCommandSequence requires at least one step'); + expect(configuredSequence).not.toBe(verificationSequence); + }); +}); diff --git a/packages/cellix/local-dev/src/index.ts b/packages/cellix/local-dev/src/index.ts index 7dbfeb397..32c1a96f0 100644 --- a/packages/cellix/local-dev/src/index.ts +++ b/packages/cellix/local-dev/src/index.ts @@ -1,6 +1,6 @@ export { isE2E } from './env/index.ts'; export { type DotEnvValues, readDotEnv } from './files/dotenv.ts'; -export { readJsonFile, type SyncJsonFileOptions, syncJsonFile } from './files/json.ts'; +export { readJsonFile, type SyncJsonFileOptions, syncJsonFile, writeJsonFile } from './files/json.ts'; export { forwardChildExit, isGracefulInterruptExit, setProcessExitCode } from './process/index.ts'; export { type AzureFunctionsDevOptions, @@ -20,6 +20,37 @@ export { type ViteDevOptions, ViteDevRunner, } from './runners/index.ts'; +export { + architectureTests, + type CommandOutputMode, + type CommandSequenceStep, + coverageMerge, + e2eTests, + knipCheck, + livePnpmScript, + type PnpmAuditOptions, + type PnpmScriptOptions, + pnpmAudit, + pnpmScript, + runSilentCommand, + runSilentCommandSequence, + type SilentCommandOptions, + type SilentCommandResult, + type SilentCommandSequenceOptions, + type SilentCommandSequenceResult, + type SilentRunnerSpawnSync, + type SilentRunnerStreams, + type SnykScanOptions, + type SonarScriptOptions, + snykCodeScan, + snykDependencyScan, + snykIacScan, + sonarPullRequestAnalysis, + sonarQualityGate, + VerificationSequence, + type VerificationSequenceOptions, + verificationSequence, +} from './silent-runners/index.ts'; export { applyWorktreeSuffix, buildPortlessUrl, diff --git a/packages/cellix/local-dev/src/silent-runners/index.ts b/packages/cellix/local-dev/src/silent-runners/index.ts new file mode 100644 index 000000000..ed6d4bdbc --- /dev/null +++ b/packages/cellix/local-dev/src/silent-runners/index.ts @@ -0,0 +1,342 @@ +import { type SpawnSyncOptionsWithStringEncoding, type SpawnSyncReturns, spawnSync } from 'node:child_process'; +import { constants } from 'node:os'; + +const DEFAULT_MAX_BUFFER_BYTES = 64 * 1024 * 1024; + +export { knipCheck } from './knip.ts'; +export { livePnpmScript, type PnpmScriptOptions, pnpmScript } from './pnpm.ts'; +export { type PnpmAuditOptions, pnpmAudit } from './pnpm-audit.ts'; +export { type SnykScanOptions, snykCodeScan, snykDependencyScan, snykIacScan } from './snyk.ts'; +export { type SonarScriptOptions, sonarPullRequestAnalysis, sonarQualityGate } from './sonar.ts'; +export { architectureTests, coverageMerge, e2eTests } from './test-runners.ts'; + +/** + * Spawn function used by silent command runners. + * + * Consumers usually rely on the default `child_process.spawnSync`; tests and + * advanced wrappers can inject this to observe command execution without + * running the external tool. + */ +export type SilentRunnerSpawnSync = (command: string, args: string[], options: SpawnSyncOptionsWithStringEncoding) => SpawnSyncReturns; + +/** + * Writable stream surface used when replaying captured output after a command + * fails. + */ +export interface SilentRunnerStreams { + /** Stream that receives captured stdout when the command fails. */ + stdout?: Pick; + /** Stream that receives captured stderr when the command fails. */ + stderr?: Pick; +} + +/** + * Options for running an external command silently until failure. + */ +export interface SilentCommandOptions { + /** Executable name. */ + command: string; + /** Command-line arguments passed without shell interpolation. */ + args?: string[]; + /** Working directory for the command. Defaults to the current process cwd. */ + cwd?: string; + /** Environment to pass to the command. Defaults to the current process env. */ + env?: NodeJS.ProcessEnv; + /** Maximum captured output in bytes. Defaults to 64 MiB. */ + maxBuffer?: number; + /** Process spawner used by tests and advanced consumers. Defaults to `child_process.spawnSync`. */ + spawn?: SilentRunnerSpawnSync; + /** Streams used when replaying captured failure output. Defaults to `process.stdout` and `process.stderr`. */ + streams?: SilentRunnerStreams; +} + +/** + * How a command in a verification sequence should handle output. + */ +export type CommandOutputMode = 'silent' | 'inherit'; + +/** + * One command in a verification sequence. + */ +export interface CommandSequenceStep { + /** Human-readable step name for tests and orchestration logs. */ + name: string; + /** Executable name. */ + command: string; + /** Command-line arguments passed without shell interpolation. */ + args?: string[]; + /** Output policy for this step. Defaults to `silent`. */ + output?: CommandOutputMode; + /** Maximum captured output in bytes for this step. */ + maxBuffer?: number; +} + +/** + * Options for running a verification sequence. + */ +export interface SilentCommandSequenceOptions { + /** Steps to run in order until one fails. */ + steps: CommandSequenceStep[]; + /** Working directory for each command. Defaults to the current process cwd. */ + cwd?: string; + /** Environment to pass to each command. Defaults to the current process env. */ + env?: NodeJS.ProcessEnv; + /** Default maximum captured output in bytes for silent steps. Defaults to 64 MiB. */ + maxBuffer?: number; + /** Process spawner used by tests and advanced consumers. Defaults to `child_process.spawnSync`. */ + spawn?: SilentRunnerSpawnSync; + /** Streams used when replaying captured failure output. Defaults to `process.stdout` and `process.stderr`. */ + streams?: SilentRunnerStreams; +} + +interface CommandSequenceContext { + cwd: string | undefined; + env: NodeJS.ProcessEnv | undefined; + maxBuffer: number | undefined; + spawn: SilentRunnerSpawnSync | undefined; + streams: SilentRunnerStreams | undefined; +} + +/** + * Result returned by a silent command runner. + */ +export interface SilentCommandResult { + /** + * Exit status reported by the child process, always a number so it can be + * assigned straight to `process.exitCode`. + * + * A child terminated by a signal reports no status of its own, so it is + * mapped to the shell convention of `128 + signalNumber` (SIGINT becomes 130, + * SIGKILL becomes 137). A spawn that failed outright reports `1`. Read + * `signal` to tell a signalled command apart from one that exited normally. + */ + status: number; + /** Signal reported by the child process, when it was terminated by a signal. */ + signal: NodeJS.Signals | null; + /** Captured stdout. This is replayed only when the command fails. */ + stdout: string; + /** Captured stderr. This is replayed only when the command fails. */ + stderr: string; + /** Spawn error, such as an executable that could not be found. */ + error?: Error; +} + +/** + * Result returned by a command sequence. + */ +export interface SilentCommandSequenceResult extends SilentCommandResult { + /** Step that produced the returned status. */ + step: CommandSequenceStep; +} + +export type VerificationSequenceOptions = Omit; + +/** + * Runs a command with stdout and stderr captured instead of inherited. + * + * Successful commands produce no terminal output. Failed commands replay their + * captured stdout and stderr so local verification stays quiet when healthy and + * still shows whatever the command wrote when something breaks. + * + * @param options - Command, arguments, process context, and optional test seams. + * @returns The child-process result with captured output and normalized status. + * + * @example + * ```ts + * import { runSilentCommand } from '@cellix/local-dev/silent-runners'; + * + * const result = runSilentCommand({ + * command: 'snyk', + * args: ['test', '--all-projects'], + * }); + * + * process.exitCode = result.status; + * ``` + */ +export function runSilentCommand(options: SilentCommandOptions): SilentCommandResult { + const { + args = [], + command, + cwd, + env, + maxBuffer = DEFAULT_MAX_BUFFER_BYTES, + spawn = spawnCommandSync, + streams = { + stderr: process.stderr, + stdout: process.stdout, + }, + } = options; + const result = spawn(command, args, { + cwd, + encoding: 'utf8', + env, + maxBuffer, + stdio: 'pipe', + }); + const normalizedResult = toSilentCommandResult(result); + + if (normalizedResult.status !== 0) { + replayFailureOutput(normalizedResult, streams, command, args); + } + + return normalizedResult; +} + +/** + * Runs commands in order, stopping at the first failure. + * + * Steps default to silent output. Use `output: 'inherit'` only when live output + * is part of the command's intended consumer experience. + * + * @param options - Sequence steps and optional process context. + * @returns The final successful step result or the first failing step result. + */ +export function runSilentCommandSequence(options: SilentCommandSequenceOptions): SilentCommandSequenceResult { + const { cwd, env, maxBuffer, spawn, steps, streams } = options; + + let lastResult: SilentCommandSequenceResult | undefined; + + for (const step of steps) { + const context: CommandSequenceContext = { cwd, env, maxBuffer, spawn, streams }; + const result = step.output === 'inherit' ? runInheritedCommand(buildCommandOptions(step, context)) : runSilentCommand(buildCommandOptions(step, context)); + lastResult = { + ...result, + step, + }; + + if (result.status !== 0) { + return lastResult; + } + } + + if (!lastResult) { + throw new Error('runSilentCommandSequence requires at least one step'); + } + + return lastResult; +} + +/** + * Fluent builder for an ordered sequence of verification commands. + * + * Steps execute in insertion order and stop at the first failure. The sequence + * can be configured once with shared process options and run repeatedly. + * + * @example + * ```ts + * const verify = verificationSequence + * .addStep(pnpmScript('format:check')) + * .addStep(pnpmScript('test')); + * + * const result = verify.run(); + * process.exitCode = result.status; + * ``` + */ +export class VerificationSequence { + readonly #options: VerificationSequenceOptions; + readonly #steps: CommandSequenceStep[]; + + constructor(options: VerificationSequenceOptions = {}, steps: CommandSequenceStep[] = []) { + this.#options = options; + this.#steps = steps; + } + + /** + * Appends a command to the sequence. + * + * @param step - Command descriptor to execute after previously added steps. + * @returns A new sequence containing the appended step. + */ + addStep(step: CommandSequenceStep): VerificationSequence { + return new VerificationSequence(this.#options, [...this.#steps, step]); + } + + /** + * Runs the configured steps in insertion order. + * + * Per-run options override options supplied when the sequence was created. + * + * @param options - Optional process context and test seams for this run. + * @returns The final successful result or the first failing step result. + * @throws When the sequence has no steps. + */ + run(options: VerificationSequenceOptions = {}): SilentCommandSequenceResult { + return runSilentCommandSequence({ + ...this.#options, + ...options, + steps: [...this.#steps], + }); + } +} + +/** + * Empty reusable verification sequence for fluent workflow definitions. + * + * Adding a step returns a new sequence, so this shared object remains empty and + * can safely be reused by multiple consumers. + */ +export const verificationSequence = new VerificationSequence(); + +function runInheritedCommand(options: SilentCommandOptions): SilentCommandResult { + const { args = [], command, cwd, env, spawn = spawnCommandSync } = options; + return toSilentCommandResult( + spawn(command, args, { + cwd, + encoding: 'utf8', + env, + stdio: 'inherit', + }), + ); +} + +function buildCommandOptions(step: CommandSequenceStep, options: CommandSequenceContext): SilentCommandOptions { + const maxBuffer = step.maxBuffer ?? options.maxBuffer; + + return { + ...(step.args ? { args: step.args } : {}), + ...(options.cwd ? { cwd: options.cwd } : {}), + ...(options.env ? { env: options.env } : {}), + ...(maxBuffer !== undefined ? { maxBuffer } : {}), + ...(options.spawn ? { spawn: options.spawn } : {}), + ...(options.streams ? { streams: options.streams } : {}), + command: step.command, + }; +} + +function toSilentCommandResult(result: SpawnSyncReturns): SilentCommandResult { + return { + ...(result.error ? { error: result.error } : {}), + signal: result.signal, + status: result.status ?? signalExitStatus(result.signal), + stderr: result.stderr ?? '', + stdout: result.stdout ?? '', + }; +} + +function signalExitStatus(signal: NodeJS.Signals | null): number { + const signalNumber = signal ? (constants.signals as Partial>)[signal] : undefined; + + return signalNumber === undefined ? 1 : 128 + signalNumber; +} + +function spawnCommandSync(command: string, args: string[], options: SpawnSyncOptionsWithStringEncoding): SpawnSyncReturns { + return spawnSync(command, args, { + ...options, + encoding: 'utf8', + }); +} + +function replayFailureOutput(result: SilentCommandResult, streams: SilentRunnerStreams, command: string, args: string[]): void { + const commandLine = [command, ...args].join(' '); + const reason = result.signal ? `signal ${result.signal}` : `exit ${result.status}`; + streams.stderr?.write(`\nCommand failed (${reason}): ${commandLine}\n\n`); + if (result.stdout) { + streams.stdout?.write(result.stdout); + } + if (result.stderr) { + streams.stderr?.write(result.stderr); + } + if (result.error) { + streams.stderr?.write(`${result.error.message}\n`); + } +} diff --git a/packages/cellix/local-dev/src/silent-runners/knip.ts b/packages/cellix/local-dev/src/silent-runners/knip.ts new file mode 100644 index 000000000..32e96651f --- /dev/null +++ b/packages/cellix/local-dev/src/silent-runners/knip.ts @@ -0,0 +1,12 @@ +import type { CommandSequenceStep } from './index.ts'; + +/** + * Runs Knip unused-code/dependency analysis. + */ +export function knipCheck(): CommandSequenceStep { + return { + args: ['exec', 'knip'], + command: 'pnpm', + name: 'knip', + }; +} diff --git a/packages/cellix/local-dev/src/silent-runners/pnpm-audit.ts b/packages/cellix/local-dev/src/silent-runners/pnpm-audit.ts new file mode 100644 index 000000000..68ed5e50e --- /dev/null +++ b/packages/cellix/local-dev/src/silent-runners/pnpm-audit.ts @@ -0,0 +1,18 @@ +import type { CommandSequenceStep } from './index.ts'; + +export interface PnpmAuditOptions { + auditLevel: 'low' | 'moderate' | 'high' | 'critical'; + dependencyType: 'prod' | 'dev'; + name?: string; +} + +/** + * Runs `pnpm audit` for one dependency class and audit threshold. + */ +export function pnpmAudit(options: PnpmAuditOptions): CommandSequenceStep { + return { + args: ['audit', `--audit-level=${options.auditLevel}`, `--${options.dependencyType}`], + command: 'pnpm', + name: options.name ?? `audit:${options.dependencyType}`, + }; +} diff --git a/packages/cellix/local-dev/src/silent-runners/pnpm.ts b/packages/cellix/local-dev/src/silent-runners/pnpm.ts new file mode 100644 index 000000000..d73c8881f --- /dev/null +++ b/packages/cellix/local-dev/src/silent-runners/pnpm.ts @@ -0,0 +1,31 @@ +import type { CommandOutputMode, CommandSequenceStep } from './index.ts'; + +export interface PnpmScriptOptions { + name?: string; + output?: CommandOutputMode; +} + +/** + * Runs a package-manager script. + * + * Use this for project-owned scripts whose internals should remain delegated to + * the consuming repository. + */ +export function pnpmScript(script: string, options: PnpmScriptOptions = {}): CommandSequenceStep { + return { + args: ['run', script], + command: 'pnpm', + name: options.name ?? script, + ...(options.output ? { output: options.output } : {}), + }; +} + +/** + * Runs a package-manager script with live output. + */ +export function livePnpmScript(script: string, options: Omit = {}): CommandSequenceStep { + return pnpmScript(script, { + ...options, + output: 'inherit', + }); +} diff --git a/packages/cellix/local-dev/src/silent-runners/snyk.ts b/packages/cellix/local-dev/src/silent-runners/snyk.ts new file mode 100644 index 000000000..e19c00805 --- /dev/null +++ b/packages/cellix/local-dev/src/silent-runners/snyk.ts @@ -0,0 +1,39 @@ +import type { CommandSequenceStep } from './index.ts'; + +export interface SnykScanOptions { + args?: string[]; + name?: string; +} + +/** + * Runs Snyk dependency scanning (`snyk test`). + */ +export function snykDependencyScan(options: SnykScanOptions = {}): CommandSequenceStep { + return { + args: ['exec', 'snyk', 'test', ...(options.args ?? [])], + command: 'pnpm', + name: options.name ?? 'snyk:test', + }; +} + +/** + * Runs Snyk code scanning (`snyk code test`). + */ +export function snykCodeScan(options: SnykScanOptions = {}): CommandSequenceStep { + return { + args: ['exec', 'snyk', 'code', 'test', ...(options.args ?? [])], + command: 'pnpm', + name: options.name ?? 'snyk:code', + }; +} + +/** + * Runs Snyk IaC scanning (`snyk iac test`). + */ +export function snykIacScan(targets: string[] = [], options: SnykScanOptions = {}): CommandSequenceStep { + return { + args: ['exec', 'snyk', 'iac', 'test', ...targets, ...(options.args ?? [])], + command: 'pnpm', + name: options.name ?? 'snyk:iac', + }; +} diff --git a/packages/cellix/local-dev/src/silent-runners/sonar.ts b/packages/cellix/local-dev/src/silent-runners/sonar.ts new file mode 100644 index 000000000..ac6cdc64d --- /dev/null +++ b/packages/cellix/local-dev/src/silent-runners/sonar.ts @@ -0,0 +1,27 @@ +import type { CommandSequenceStep } from './index.ts'; +import { pnpmScript } from './pnpm.ts'; + +export interface SonarScriptOptions { + script?: string; +} + +/** + * Runs a repository-owned Sonar pull-request analysis script. + * + * The PR-number and branch-resolution policy is intentionally delegated to the + * consuming repository because those details vary by CI provider. + */ +export function sonarPullRequestAnalysis(options: SonarScriptOptions = {}): CommandSequenceStep { + return pnpmScript(options.script ?? 'sonar:pr', { + name: 'sonar:pr', + }); +} + +/** + * Runs a repository-owned Sonar quality-gate script. + */ +export function sonarQualityGate(options: SonarScriptOptions = {}): CommandSequenceStep { + return pnpmScript(options.script ?? 'check-sonar', { + name: 'check-sonar', + }); +} diff --git a/packages/cellix/local-dev/src/silent-runners/test-runners.ts b/packages/cellix/local-dev/src/silent-runners/test-runners.ts new file mode 100644 index 000000000..0699f2c95 --- /dev/null +++ b/packages/cellix/local-dev/src/silent-runners/test-runners.ts @@ -0,0 +1,23 @@ +import type { CommandSequenceStep } from './index.ts'; +import { pnpmScript } from './pnpm.ts'; + +/** + * Runs the repository's architecture test script silently. + */ +export function architectureTests(script = 'test:arch'): CommandSequenceStep { + return pnpmScript(script); +} + +/** + * Runs the repository's coverage-and-merge script silently. + */ +export function coverageMerge(script = 'test:coverage:merge'): CommandSequenceStep { + return pnpmScript(script); +} + +/** + * Runs the repository's e2e test script silently until failure. + */ +export function e2eTests(script = 'test:e2e'): CommandSequenceStep { + return pnpmScript(script); +} diff --git a/packages/cellix/local-dev/tsconfig.vitest.json b/packages/cellix/local-dev/tsconfig.vitest.json index 0d3bc7fca..b71187e8f 100644 --- a/packages/cellix/local-dev/tsconfig.vitest.json +++ b/packages/cellix/local-dev/tsconfig.vitest.json @@ -2,7 +2,8 @@ "extends": ["./tsconfig.json", "@cellix/config-typescript/vitest"], "compilerOptions": { "paths": { - "@cellix/local-dev": ["./src/index.ts"] + "@cellix/local-dev": ["./src/index.ts"], + "@cellix/local-dev/silent-runners": ["./src/silent-runners/index.ts"] } }, "include": ["src/**/*.ts"] diff --git a/packages/cellix/local-dev/vitest.config.ts b/packages/cellix/local-dev/vitest.config.ts index 2c2d02b84..629142ed9 100644 --- a/packages/cellix/local-dev/vitest.config.ts +++ b/packages/cellix/local-dev/vitest.config.ts @@ -12,6 +12,7 @@ export default mergeConfig(nodeConfig, { }, resolve: { alias: { + '@cellix/local-dev/silent-runners': join(dirname, 'src/silent-runners/index.ts'), '@cellix/local-dev': join(dirname, 'src/index.ts'), }, }, diff --git a/packages/cellix/serenity-framework/src/formatters/agent-formatter.ts b/packages/cellix/serenity-framework/src/formatters/agent-formatter.ts index c06a3b36c..f41105253 100644 --- a/packages/cellix/serenity-framework/src/formatters/agent-formatter.ts +++ b/packages/cellix/serenity-framework/src/formatters/agent-formatter.ts @@ -104,6 +104,10 @@ export default class AgentFormatter extends Formatter { } private onTestRunFinished(testRunFinished: TestRunFinished): void { + if (this.issueCount === 0) { + return; + } + this.log('\n--- (Agent) Results ---\n'); const parts = Object.entries(this.statusCounts).map(([status, count]) => `${status}: ${count}`); @@ -114,6 +118,6 @@ export default class AgentFormatter extends Formatter { this.log(`Duration: ${ms}ms\n`); } - this.log(this.issueCount === 0 ? 'All scenarios passed.\n' : `Issues: ${this.issueCount}\n`); + this.log(`Issues: ${this.issueCount}\n`); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7fb196c84..3b89e16c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,7 +136,8 @@ overrides: diff@4.0.2: 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 - protobufjs@7.5.4: 7.6.3 + protobufjs@7.5.4: 7.6.5 + protobufjs@7.6.3: 7.6.5 serve-handler>minimatch: 3.1.5 serialize-javascript@6.0.2: 7.0.5 serialize-javascript@7.0.4: 7.0.5 @@ -146,6 +147,8 @@ overrides: qs: 6.15.2 uuid@11.1.0: 11.1.1 express@4.22.1: 4.22.2 + body-parser@1.20.5: 1.20.6 + body-parser@2.2.2: 2.3.0 ajv@^6: 6.14.0 lodash: 4.18.1 lodash-es: 4.18.1 @@ -161,13 +164,16 @@ overrides: playwright-core: 1.59.0 playwright: 1.59.0 postcss: 8.5.10 - protobufjs: 7.6.3 + protobufjs: 7.6.5 ip-address: ^10.1.1 fast-uri: ^4.0.1 '@babel/plugin-transform-modules-systemjs': 7.29.4 '@babel/core': ^7.29.6 - js-yaml@4.1.1: 4.2.0 + js-yaml@4.1.1: 4.3.0 + js-yaml@3.14.2: 3.15.0 + js-yaml@4.2.0: 4.3.0 shell-quote@<1.8.4: 1.8.4 + '@opentelemetry/propagator-jaeger@1.30.1': 2.9.0 '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 '@opentelemetry/core@2.7.1': 2.8.0 ws: 8.21.0 @@ -209,6 +215,9 @@ importers: '@cellix/graphql-codegen': specifier: workspace:* version: link:packages/cellix/graphql-codegen + '@cellix/local-dev': + specifier: workspace:* + version: link:packages/cellix/local-dev '@graphql-codegen/cli': specifier: ^5.0.7 version: 5.0.7(@parcel/watcher@2.5.1)(@types/node@22.19.15)(graphql@16.12.0)(supports-color@8.1.1)(typescript@6.0.3) @@ -5308,6 +5317,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.9.0': + resolution: {integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/exporter-logs-otlp-grpc@0.57.2': resolution: {integrity: sha512-eovEy10n3umjKJl2Ey6TLzikPE+W4cUQ4gCwgGP1RqzTGtgDra0WjIqdy29ohiUKfvmbiL3MndZww58xfIvyFw==} engines: {node: '>=14'} @@ -5440,9 +5455,9 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@1.30.1': - resolution: {integrity: sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==} - engines: {node: '>=14'} + '@opentelemetry/propagator-jaeger@2.9.0': + resolution: {integrity: sha512-4mYGty27rYvSM0jtp1ZUOqd3LfVRCYg9H5G9OFzSx5HViYToU21MFhWfco7x1HwXr7ER8yGOiCIHZUwjPksc0Q==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -6175,9 +6190,6 @@ packages: '@protobufjs/inquire@1.1.1': resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} - '@protobufjs/inquire@1.1.2': - resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} - '@protobufjs/path@1.1.2': resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} @@ -7875,12 +7887,12 @@ packages: bn.js@5.2.3: resolution: {integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==} - body-parser@1.20.5: - resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} + body-parser@1.20.6: + resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} bonjour-service@1.3.0: @@ -8379,6 +8391,10 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + continuation-local-storage@3.2.1: resolution: {integrity: sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==} @@ -9746,6 +9762,10 @@ packages: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + icss-utils@5.1.0: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -10200,12 +10220,12 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + js-yaml@3.15.0: + resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==} hasBin: true - js-yaml@4.2.0: - resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true jsbi@4.3.2: @@ -12048,8 +12068,8 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - protobufjs@7.6.3: - resolution: {integrity: sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==} + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} proxy-addr@2.0.7: @@ -13436,9 +13456,9 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} @@ -14445,7 +14465,7 @@ snapshots: '@apollo/utils.withrequired': 3.0.0 '@graphql-tools/schema': 10.0.30(graphql@16.12.0) async-retry: 1.3.3 - body-parser: 2.2.2(supports-color@8.1.1) + body-parser: 2.3.0(supports-color@8.1.1) content-type: 1.0.5 cors: 2.8.5 finalhandler: 2.1.1(supports-color@8.1.1) @@ -16487,7 +16507,7 @@ snapshots: '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 fs-extra: 11.3.2 - js-yaml: 4.2.0 + js-yaml: 4.3.0 lodash: 4.18.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) @@ -16933,7 +16953,7 @@ snapshots: '@docusaurus/utils-common': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) fs-extra: 11.3.2 joi: 17.13.4 - js-yaml: 4.2.0 + js-yaml: 4.3.0 lodash: 4.18.1 tslib: 2.8.1 transitivePeerDependencies: @@ -16958,7 +16978,7 @@ snapshots: globby: 11.1.0 gray-matter: 4.0.3 jiti: 2.6.1 - js-yaml: 4.2.0 + js-yaml: 4.3.0 lodash: 4.18.1 micromatch: 4.0.8 p-queue: 6.6.2 @@ -17534,7 +17554,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 jose: 5.10.0 - js-yaml: 4.2.0 + js-yaml: 4.3.0 lodash: 4.18.1 scuid: 1.1.0 tslib: 2.8.1 @@ -17631,7 +17651,7 @@ snapshots: dependencies: lodash.camelcase: 4.3.0 long: 5.3.2 - protobufjs: 7.6.3 + protobufjs: 7.6.5 yargs: 17.7.2 '@hapi/hoek@9.3.0': {} @@ -17876,6 +17896,11 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/exporter-logs-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.4 @@ -18073,17 +18098,17 @@ snapshots: '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - protobufjs: 7.6.3 + protobufjs: 7.6.5 '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-jaeger@2.9.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': dependencies: @@ -18168,7 +18193,7 @@ snapshots: '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.9.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) semver: 7.7.4 @@ -18646,8 +18671,6 @@ snapshots: '@protobufjs/inquire@1.1.1': {} - '@protobufjs/inquire@1.1.2': {} - '@protobufjs/path@1.1.2': {} '@protobufjs/pool@1.1.0': {} @@ -20625,7 +20648,7 @@ snapshots: bn.js@5.2.3: {} - body-parser@1.20.5: + body-parser@1.20.6: dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -20642,17 +20665,17 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.2.2(supports-color@8.1.1): + body-parser@2.3.0(supports-color@8.1.1): dependencies: bytes: 3.1.2 - content-type: 1.0.5 + content-type: 2.0.0 debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.1 - iconv-lite: 0.7.0 + iconv-lite: 0.7.3 on-finished: 2.4.1 qs: 6.15.2 raw-body: 3.0.2 - type-is: 2.0.1 + type-is: 2.1.0 transitivePeerDependencies: - supports-color @@ -21192,6 +21215,8 @@ snapshots: content-type@1.0.5: {} + content-type@2.0.0: {} + continuation-local-storage@3.2.1: dependencies: async-listener: 0.6.10 @@ -21237,7 +21262,7 @@ snapshots: cosmiconfig@8.3.6(typescript@6.0.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.2.0 + js-yaml: 4.3.0 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -22019,7 +22044,7 @@ snapshots: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.5 + body-parser: 1.20.6 content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 @@ -22526,7 +22551,7 @@ snapshots: gray-matter@4.0.3: dependencies: - js-yaml: 3.14.2 + js-yaml: 3.15.0 kind-of: 6.0.3 section-matter: 1.0.0 strip-bom-string: 1.0.0 @@ -22877,6 +22902,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + icss-utils@5.1.0(postcss@8.5.10): dependencies: postcss: 8.5.10 @@ -23283,12 +23312,12 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.14.2: + js-yaml@3.15.0: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.2.0: + js-yaml@4.3.0: dependencies: argparse: 2.0.1 @@ -23416,7 +23445,7 @@ snapshots: fast-glob: 3.3.3 formatly: 0.3.0 jiti: 2.6.1 - js-yaml: 4.2.0 + js-yaml: 4.3.0 minimist: 1.2.8 oxc-resolver: 11.14.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) picocolors: 1.1.1 @@ -25585,7 +25614,7 @@ snapshots: proto-list@1.2.4: {} - protobufjs@7.6.3: + protobufjs@7.6.5: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 @@ -25593,7 +25622,6 @@ snapshots: '@protobufjs/eventemitter': 1.1.1 '@protobufjs/fetch': 1.1.1 '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.2 '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.1 @@ -27162,9 +27190,9 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 - type-is@2.0.1: + type-is@2.1.0: dependencies: - content-type: 1.0.5 + content-type: 2.0.0 media-typer: 1.1.0 mime-types: 3.0.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b48bb6c08..c081648ae 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -84,7 +84,8 @@ overrides: 'diff@4.0.2': 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 - 'protobufjs@7.5.4': 7.6.3 + 'protobufjs@7.5.4': 7.6.5 + 'protobufjs@7.6.3': 7.6.5 'serve-handler>minimatch': 3.1.5 'serialize-javascript@6.0.2': 7.0.5 'serialize-javascript@7.0.4': 7.0.5 @@ -94,6 +95,8 @@ overrides: qs: 6.15.2 'uuid@11.1.0': 11.1.1 'express@4.22.1': 4.22.2 + 'body-parser@1.20.5': 1.20.6 + 'body-parser@2.2.2': 2.3.0 'ajv@^6': 6.14.0 lodash: 4.18.1 lodash-es: 4.18.1 @@ -109,13 +112,16 @@ overrides: playwright-core: 1.59.0 playwright: 1.59.0 postcss: 8.5.10 - protobufjs: 7.6.3 + protobufjs: 7.6.5 ip-address: ^10.1.1 fast-uri: ^4.0.1 '@babel/plugin-transform-modules-systemjs': 7.29.4 '@babel/core': ^7.29.6 - 'js-yaml@4.1.1': 4.2.0 + 'js-yaml@4.1.1': 4.3.0 + 'js-yaml@3.14.2': 3.15.0 + 'js-yaml@4.2.0': 4.3.0 'shell-quote@<1.8.4': 1.8.4 + '@opentelemetry/propagator-jaeger@1.30.1': 2.9.0 '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 '@opentelemetry/core@2.7.1': 2.8.0 ws: 8.21.0 diff --git a/readme.md b/readme.md index 570439ab0..67310b602 100644 --- a/readme.md +++ b/readme.md @@ -170,6 +170,7 @@ The local mock OpenID Connect provider used for UI development is configured per } ``` + The envVars values are environment variable names (from the UI app's `.env` file) that the server resolves to actual values at startup. Per-developer overrides @@ -232,6 +233,7 @@ CellixJS integrates [Agent Skills](https://agentskills.io) to provide AI coding The managed skill set lives in [`.agents/skills/`](.agents/skills/) and is mirrored for Copilot discovery through symlinks in [`.github/skills/`](.github/skills/). Only skills relevant to normal work in this repo are committed. + **Current managed skills:** - **madr-enforcement**: Enforces ADR-defined architectural standards across the codebase - **turborepo**: Guides monorepo task graph, caching, and pipeline work