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