From 2a6fb19ba425a06f89e14621b1b7934c9b175bf6 Mon Sep 17 00:00:00 2001 From: ARYAN <57013028+Aryan1718@users.noreply.github.com> Date: Tue, 26 May 2026 03:16:53 -0700 Subject: [PATCH] fix(scripts): make package artifacts pnpm launch work on Windows Fix Windows package artifact script invocation under pnpm. --- scripts/package-artifacts.mjs | 299 ++++++++++++++++++----------- scripts/package-artifacts.test.mjs | 52 +++-- 2 files changed, 226 insertions(+), 125 deletions(-) diff --git a/scripts/package-artifacts.mjs b/scripts/package-artifacts.mjs index 627850b4..e1ff8c6c 100644 --- a/scripts/package-artifacts.mjs +++ b/scripts/package-artifacts.mjs @@ -28,6 +28,20 @@ export const NPM_ARTIFACT_PACKAGES = [{ name: PUBLIC_NPM_PACKAGE_NAME, packageRo export const CLI_PYTHON_ASSET_MANIFEST = 'manifest.json'; +function pnpmCommand(args) { + if (process.platform === 'win32') { + return { + command: 'cmd.exe', + args: ['/d', '/s', '/c', 'pnpm', ...args], + }; + } + + return { + command: 'pnpm', + args, + }; +} + function scriptRootDir() { return resolve(dirname(fileURLToPath(import.meta.url)), '..'); } @@ -70,8 +84,7 @@ export function packageArtifactLayout(rootDir = scriptRootDir(), version = publi export function buildArtifactCommands(layout) { return [ { - command: 'pnpm', - args: ['--filter', PUBLIC_NPM_PACKAGE_NAME, 'run', 'build'], + ...pnpmCommand(['--filter', PUBLIC_NPM_PACKAGE_NAME, 'run', 'build']), cwd: layout.rootDir, }, { @@ -80,8 +93,7 @@ export function buildArtifactCommands(layout) { cwd: layout.rootDir, }, { - command: 'pnpm', - args: ['pack', '--out', layout.cliTarball], + ...pnpmCommand(['pack', '--out', layout.cliTarball]), cwd: join(layout.rootDir, 'packages', 'cli'), }, ]; @@ -460,11 +472,19 @@ import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { DatabaseSync } from 'node:sqlite'; +import { setTimeout as delay } from 'node:timers/promises'; import { promisify } from 'node:util'; const execFileAsync = promisify(execFile); const require = createRequire(import.meta.url); +function pnpmCommand(args) { + if (process.platform === 'win32') { + return { command: 'cmd.exe', args: ['/d', '/s', '/c', 'pnpm', ...args] }; + } + return { command: 'pnpm', args }; +} + async function run(command, args, options = {}) { process.stdout.write('$ ' + command + ' ' + args.join(' ') + '\\n'); try { @@ -551,6 +571,21 @@ function requireIncludes(values, expected, label) { assert.ok(values.includes(expected), label + ' did not include ' + expected + ': ' + values.join(', ')); } +async function rmWithRetry(path) { + for (let attempt = 0; ; attempt += 1) { + try { + await rm(path, { recursive: true, force: true }); + return; + } catch (error) { + const code = typeof error?.code === 'string' ? error.code : ''; + if (attempt >= 4 || !['EBUSY', 'ENOTEMPTY', 'EPERM'].includes(code)) { + throw error; + } + await delay(500); + } + } +} + async function writeSqliteWarehouse(projectDir) { const database = new DatabaseSync(join(projectDir, 'warehouse.db')); try { @@ -575,50 +610,58 @@ let daemonStarted = false; try { const projectDir = join(root, 'project'); - const version = await run('pnpm', ['exec', 'ktx', '--version']); + const version = await run(...Object.values(pnpmCommand(['exec', 'ktx', '--version']))); requireSuccess('ktx public package version', version); requireOutput('ktx public package version', version, await installedPackageVersionPattern()); const runtimeStatusBefore = parseJsonResultWithExitCode( 'ktx admin runtime status missing', - await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'status', '--json']), + await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'status', '--json']))), 1, ); assert.equal(runtimeStatusBefore.kind, 'missing'); assert.equal(runtimeStatusBefore.layout.runtimeRoot, process.env.KTX_RUNTIME_ROOT); process.stdout.write('ktx managed runtime starts missing in isolated root\\n'); - const init = await run('pnpm', [ - 'exec', - 'ktx', - 'setup', - '--project-dir', - projectDir, - '--no-input', - '--yes', - '--skip-llm', - '--skip-embeddings', - '--skip-databases', - '--skip-sources', - '--skip-agents', - ]); + const init = await run( + ...Object.values( + pnpmCommand([ + 'exec', + 'ktx', + 'setup', + '--project-dir', + projectDir, + '--no-input', + '--yes', + '--skip-llm', + '--skip-embeddings', + '--skip-databases', + '--skip-sources', + '--skip-agents', + ]), + ), + ); requireSuccess('ktx setup', init); const emptyProjectDir = join(root, 'empty-project'); - const emptyInit = await run('pnpm', [ - 'exec', - 'ktx', - 'setup', - '--project-dir', - emptyProjectDir, - '--no-input', - '--yes', - '--skip-llm', - '--skip-embeddings', - '--skip-databases', - '--skip-sources', - '--skip-agents', - ]); + const emptyInit = await run( + ...Object.values( + pnpmCommand([ + 'exec', + 'ktx', + 'setup', + '--project-dir', + emptyProjectDir, + '--no-input', + '--yes', + '--skip-llm', + '--skip-embeddings', + '--skip-databases', + '--skip-sources', + '--skip-agents', + ]), + ), + ); requireSuccess('ktx setup empty project', emptyInit); await writeFile( join(projectDir, 'ktx.yaml'), @@ -658,17 +701,21 @@ try { 'utf-8', ); - const wikiSearch = await run('pnpm', [ - 'exec', - 'ktx', - 'wiki', - 'revenue', - '--json', - '--limit', - '5', - '--project-dir', - projectDir, - ]); + const wikiSearch = await run( + ...Object.values( + pnpmCommand([ + 'exec', + 'ktx', + 'wiki', + 'revenue', + '--json', + '--limit', + '5', + '--project-dir', + projectDir, + ]), + ), + ); const wikiSearchJson = parseJsonResult('ktx wiki search', wikiSearch); assert.equal(wikiSearchJson.kind, 'list'); assert.equal(wikiSearchJson.data.items.length, 1); @@ -700,17 +747,21 @@ try { await mkdir(join(projectDir, 'semantic-layer', 'warehouse'), { recursive: true }); await writeFile(join(projectDir, 'semantic-layer', 'warehouse', 'orders.yaml'), slYaml, 'utf-8'); - const slSearch = await run('pnpm', [ - 'exec', - 'ktx', - 'sl', - 'orders', - '--json', - '--connection-id', - 'warehouse', - '--project-dir', - projectDir, - ]); + const slSearch = await run( + ...Object.values( + pnpmCommand([ + 'exec', + 'ktx', + 'sl', + 'orders', + '--json', + '--connection-id', + 'warehouse', + '--project-dir', + projectDir, + ]), + ), + ); const slSearchJson = parseJsonResult('ktx sl search', slSearch); assert.equal(slSearchJson.kind, 'list'); assert.equal(slSearchJson.data.items.length, 1); @@ -720,17 +771,25 @@ try { requireIncludes(slSearchJson.data.items[0].matchReasons, 'lexical', 'sl search match reasons'); process.stdout.write('ktx sl search hybrid metadata verified\\n'); - const slQuery = await run('pnpm', ['exec', 'ktx', 'sl', 'query', - '--connection-id', - 'warehouse', - '--measure', - 'orders.order_count', - '--format', - 'json', - '--yes', - '--project-dir', - projectDir, - ]); + const slQuery = await run( + ...Object.values( + pnpmCommand([ + 'exec', + 'ktx', + 'sl', + 'query', + '--connection-id', + 'warehouse', + '--measure', + 'orders.order_count', + '--format', + 'json', + '--yes', + '--project-dir', + projectDir, + ]), + ), + ); requireSuccessWithStderr( 'ktx sl query first managed runtime install', slQuery, @@ -741,27 +800,35 @@ try { const runtimeStatusAfter = parseJsonResult( 'ktx admin runtime status ready', - await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'status', '--json']), + await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'status', '--json']))), ); assert.equal(runtimeStatusAfter.kind, 'ready'); assert.deepEqual(runtimeStatusAfter.manifest.features, ['core']); assert.equal(runtimeStatusAfter.layout.runtimeRoot, process.env.KTX_RUNTIME_ROOT); process.stdout.write('ktx managed runtime lazy install verified\\n'); - const sqliteSlQuery = await run('pnpm', ['exec', 'ktx', 'sl', 'query', - '--connection-id', - 'warehouse', - '--measure', - 'orders.order_count', - '--format', - 'json', - '--execute', - '--max-rows', - '100', - '--yes', - '--project-dir', - projectDir, - ]); + const sqliteSlQuery = await run( + ...Object.values( + pnpmCommand([ + 'exec', + 'ktx', + 'sl', + 'query', + '--connection-id', + 'warehouse', + '--measure', + 'orders.order_count', + '--format', + 'json', + '--execute', + '--max-rows', + '100', + '--yes', + '--project-dir', + projectDir, + ]), + ), + ); requireSuccess('ktx sl query sqlite execute', sqliteSlQuery); requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"dialect": "sqlite"/); requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"mode": "executed"/); @@ -769,36 +836,35 @@ try { requireOutput('ktx sl query sqlite execute', sqliteSlQuery, /"rows": \\[\\s*\\[\\s*3\\s*\\]\\s*\\]/); process.stdout.write('ktx sl query sqlite execute verified\\n'); - const runtimeDoctor = await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'status']); + const runtimeDoctor = await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'status']))); requireSuccess('ktx admin runtime status', runtimeDoctor); requireOutput('ktx admin runtime status', runtimeDoctor, /KTX Python runtime/); requireOutput('ktx admin runtime status', runtimeDoctor, /status: ready/); process.stdout.write('ktx admin runtime status verified\\n'); - const runtimeStart = await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'start']); + const runtimeStart = await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'start']))); requireSuccess('ktx admin runtime start', runtimeStart); daemonStarted = true; requireOutput('ktx admin runtime start', runtimeStart, /Started KTX daemon/); requireOutput('ktx admin runtime start', runtimeStart, /url: http:\\/\\/127\\.0\\.0\\.1:\\d+/); requireOutput('ktx admin runtime start', runtimeStart, /features: core/); - const runtimeStartReuse = await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'start']); + const runtimeStartReuse = await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'start']))); requireSuccess('ktx admin runtime start reuse', runtimeStartReuse); requireOutput('ktx admin runtime start reuse', runtimeStartReuse, /Using existing KTX daemon/); requireOutput('ktx admin runtime start reuse', runtimeStartReuse, /features: core/); - const runtimeStop = await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'stop']); + const runtimeStop = await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'stop']))); requireSuccess('ktx admin runtime stop', runtimeStop); daemonStarted = false; requireOutput('ktx admin runtime stop', runtimeStop, /Stopped KTX daemon/); process.stdout.write('ktx admin runtime daemon lifecycle verified\\n'); - const structuralScan = await run('pnpm', ['exec', 'ktx', 'ingest', 'warehouse', - '--project-dir', - projectDir, - '--fast', - '--no-input', - ]); + const structuralScan = await run( + ...Object.values( + pnpmCommand(['exec', 'ktx', 'ingest', 'warehouse', '--project-dir', projectDir, '--fast', '--no-input']), + ), + ); requireSuccessWithProjectStderr('ktx ingest fast', structuralScan, projectDir); requireOutput('ktx ingest fast', structuralScan, /Ingest finished/); requireOutput('ktx ingest fast', structuralScan, /Database schema/); @@ -806,12 +872,11 @@ try { await access(join(projectDir, 'semantic-layer', 'warehouse', '_schema', 'public.yaml')); process.stdout.write('ktx ingest fast verified\\n'); - const enrichedScan = await run('pnpm', ['exec', 'ktx', 'ingest', 'warehouse', - '--project-dir', - projectDir, - '--deep', - '--no-input', - ]); + const enrichedScan = await run( + ...Object.values( + pnpmCommand(['exec', 'ktx', 'ingest', 'warehouse', '--project-dir', projectDir, '--deep', '--no-input']), + ), + ); requireExitCodeWithProjectStderr('ktx ingest deep readiness guard', enrichedScan, projectDir, 1); requireOutput('ktx ingest deep readiness guard', enrichedScan, /Ingest finished with partial failures/); requireOutput('ktx ingest deep readiness guard', enrichedScan, /requires deep ingest readiness/); @@ -821,14 +886,15 @@ try { process.stdout.write('ktx ingest state verified\\n'); } finally { if (daemonStarted) { - await run('pnpm', ['exec', 'ktx', 'admin', 'runtime', 'stop']); + await run(...Object.values(pnpmCommand(['exec', 'ktx', 'admin', 'runtime', 'stop']))); + await delay(500); } if (previousRuntimeRoot === undefined) { delete process.env.KTX_RUNTIME_ROOT; } else { process.env.KTX_RUNTIME_ROOT = previousRuntimeRoot; } - await rm(root, { recursive: true, force: true }); + await rmWithRetry(root); } `; } @@ -844,6 +910,13 @@ import { promisify } from 'node:util'; const execFileAsync = promisify(execFile); +function pnpmCommand(args) { + if (process.platform === 'win32') { + return { command: 'cmd.exe', args: ['/d', '/s', '/c', 'pnpm', ...args] }; + } + return { command: 'pnpm', args }; +} + async function run(command, args, options = {}) { process.stdout.write('$ ' + command + ' ' + args.join(' ') + '\\n'); try { @@ -880,17 +953,17 @@ try { const packageJson = JSON.parse(await readFile(join(process.cwd(), 'package.json'), 'utf8')); assert.deepEqual(Object.keys(packageJson.dependencies), ['@kaelio/ktx']); - const help = await run('pnpm', ['exec', 'ktx', '--help']); + const help = await run(...Object.values(pnpmCommand(['exec', 'ktx', '--help']))); requireSuccess('ktx --help', help); requireStdout('ktx --help', help, /Usage: ktx/); requireStdout('ktx --help', help, /setup/); - const setupHelp = await run('pnpm', ['exec', 'ktx', 'setup', '--help']); + const setupHelp = await run(...Object.values(pnpmCommand(['exec', 'ktx', 'setup', '--help']))); requireSuccess('ktx setup --help', setupHelp); requireStdout('ktx setup --help', setupHelp, /Usage: ktx setup/); requireStdout('ktx setup --help', setupHelp, /--no-input/); - const doctor = await run('pnpm', ['exec', 'ktx', 'status', '--verbose', '--no-input']); + const doctor = await run(...Object.values(pnpmCommand(['exec', 'ktx', 'status', '--verbose', '--no-input']))); assert.ok([0, 1].includes(doctor.code), 'ktx status setup exit code must be 0 or 1'); requireStdout('ktx status setup', doctor, /KTX status/); requireStdout('ktx status setup', doctor, /No project here yet\\./); @@ -949,10 +1022,19 @@ async function verifyNpmArtifacts(layout, tmpRoot) { await writeFile(join(projectDir, 'verify-installed-cli.mjs'), npmRuntimeSmokeSource()); await writeFile(join(projectDir, 'verify-installed-cli-commands.mjs'), npmCliSmokeSource()); - await runCommand('pnpm', ['install'], { cwd: projectDir }); - await runCommand('pnpm', ['rebuild', 'better-sqlite3'], { cwd: projectDir }); + { + const pnpmInstall = pnpmCommand(['install']); + await runCommand(pnpmInstall.command, pnpmInstall.args, { cwd: projectDir }); + } + { + const pnpmRebuild = pnpmCommand(['rebuild', 'better-sqlite3']); + await runCommand(pnpmRebuild.command, pnpmRebuild.args, { cwd: projectDir }); + } await runCommand('node', ['verify-npm.mjs'], { cwd: projectDir }); - await runCommand('pnpm', ['exec', 'ktx', '--version'], { cwd: projectDir }); + { + const pnpmExecVersion = pnpmCommand(['exec', 'ktx', '--version']); + await runCommand(pnpmExecVersion.command, pnpmExecVersion.args, { cwd: projectDir }); + } await runCommand('node', ['verify-installed-cli.mjs'], { cwd: projectDir }); await runCommand('node', ['verify-installed-cli-commands.mjs'], { cwd: projectDir }); } @@ -968,7 +1050,10 @@ async function verifyNpmCliArtifacts(layout, tmpRoot) { await writeFile(join(projectDir, 'pnpm-workspace.yaml'), npmSmokePnpmWorkspaceYaml()); await writeFile(join(projectDir, 'verify-installed-cli-commands.mjs'), npmCliSmokeSource()); - await runCommand('pnpm', ['install'], { cwd: projectDir }); + { + const pnpmInstall = pnpmCommand(['install']); + await runCommand(pnpmInstall.command, pnpmInstall.args, { cwd: projectDir }); + } await runCommand('node', ['verify-installed-cli-commands.mjs'], { cwd: projectDir }); } diff --git a/scripts/package-artifacts.test.mjs b/scripts/package-artifacts.test.mjs index 7ea9339b..a1d2489d 100644 --- a/scripts/package-artifacts.test.mjs +++ b/scripts/package-artifacts.test.mjs @@ -97,12 +97,12 @@ describe('packageArtifactLayout', () => { it('uses stable artifact paths under ktx/dist/artifacts', () => { const layout = packageArtifactLayout('/repo/ktx', PUBLIC_NPM_PACKAGE_VERSION); - assert.equal(layout.artifactDir, '/repo/ktx/dist/artifacts'); - assert.equal(layout.npmDir, '/repo/ktx/dist/artifacts/npm'); - assert.equal(layout.pythonDir, '/repo/ktx/dist/artifacts/python'); + assert.equal(layout.artifactDir, join('/repo/ktx', 'dist', 'artifacts')); + assert.equal(layout.npmDir, join('/repo/ktx', 'dist', 'artifacts', 'npm')); + assert.equal(layout.pythonDir, join('/repo/ktx', 'dist', 'artifacts', 'python')); assert.equal( layout.cliTarball, - `/repo/ktx/dist/artifacts/npm/kaelio-ktx-${PUBLIC_NPM_PACKAGE_VERSION}.tgz`, + join('/repo/ktx', 'dist', 'artifacts', 'npm', `kaelio-ktx-${PUBLIC_NPM_PACKAGE_VERSION}.tgz`), ); assert.deepEqual(Object.keys(layout.npmTarballs), ['@kaelio/ktx']); }); @@ -112,17 +112,21 @@ describe('buildArtifactCommands', () => { it('builds the CLI package, then the runtime wheel, then packs the npm tarball directly', () => { const layout = packageArtifactLayout('/repo/ktx', PUBLIC_NPM_PACKAGE_VERSION); const commands = buildArtifactCommands(layout); + const expectedBuildCommand = + process.platform === 'win32' + ? ['cmd.exe', ['/d', '/s', '/c', 'pnpm', '--filter', '@kaelio/ktx', 'run', 'build'], layout.rootDir] + : ['pnpm', ['--filter', '@kaelio/ktx', 'run', 'build'], layout.rootDir]; + const expectedPackCommand = + process.platform === 'win32' + ? ['cmd.exe', ['/d', '/s', '/c', 'pnpm', 'pack', '--out', layout.cliTarball], join('/repo/ktx', 'packages', 'cli')] + : ['pnpm', ['pack', '--out', layout.cliTarball], join('/repo/ktx', 'packages', 'cli')]; assert.deepEqual( commands.map((command) => [command.command, command.args, command.cwd]), [ - ['pnpm', ['--filter', '@kaelio/ktx', 'run', 'build'], '/repo/ktx'], - [process.execPath, ['scripts/build-python-runtime-wheel.mjs'], '/repo/ktx'], - [ - 'pnpm', - ['pack', '--out', `/repo/ktx/dist/artifacts/npm/kaelio-ktx-${PUBLIC_NPM_PACKAGE_VERSION}.tgz`], - '/repo/ktx/packages/cli', - ], + expectedBuildCommand, + [process.execPath, ['scripts/build-python-runtime-wheel.mjs'], layout.rootDir], + expectedPackCommand, ], ); }); @@ -476,18 +480,27 @@ describe('verification snippets', () => { it('runs installed CLI commands through the public package runtime', () => { const source = npmRuntimeSmokeSource(); + assert.match(source, /function pnpmCommand\(args\)/); + assert.match(source, /process\.platform === 'win32'/); + assert.match(source, /command: 'cmd\.exe'/); + assert.match(source, /args: \['\/d', '\/s', '\/c', 'pnpm', \.\.\.args\]/); + assert.match(source, /import \{ setTimeout as delay \} from 'node:timers\/promises';/); + assert.match(source, /async function rmWithRetry\(path\)/); + assert.match(source, /await delay\(500\)/); + assert.match(source, /await rmWithRetry\(root\)/); assert.match(source, /ktx public package version/); assert.match(source, /installedPackageVersionPattern/); assert.doesNotMatch(source, /@kaelio\\\/ktx 0\\\.1\\\.0/); - assert.match(source, /'ktx', 'sl', 'query'/); + assert.match(source, /pnpmCommand\(\[\s*'exec',\s*'ktx',\s*'sl',\s*'query'/); assert.doesNotMatch(source, /@ktx\/context/); assert.doesNotMatch(source, /@modelcontextprotocol/); assert.doesNotMatch(source, /startSemanticDaemon/); - assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'setup'/); + assert.doesNotMatch(source, /run\('pnpm',/); + assert.match(source, /pnpmCommand\(\[\s*'exec',\s*'ktx',\s*'setup'/); assert.match(source, /wiki', 'global', 'revenue\.md'/); - assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'wiki',\s*'revenue'/); + assert.match(source, /pnpmCommand\(\[\s*'exec',\s*'ktx',\s*'wiki',\s*'revenue'/); assert.match(source, /semantic-layer', 'warehouse', 'orders\.yaml'/); - assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'sl',\s*'orders'/); + assert.match(source, /pnpmCommand\(\[\s*'exec',\s*'ktx',\s*'sl',\s*'orders'/); assert.match(source, /orders\.order_count/); assert.match(source, /node:sqlite/); assert.match(source, /driver: sqlite/); @@ -516,7 +529,7 @@ describe('verification snippets', () => { assert.match(source, /ktx admin runtime stop/); assert.doesNotMatch(source, /ktx admin runtime prune/); assert.doesNotMatch(source, /staleRuntimeDir/); - assert.match(source, /run\('pnpm', \[\s*'exec',\s*'ktx',\s*'ingest',\s*'warehouse'/); + assert.match(source, /pnpmCommand\(\['exec', 'ktx', 'ingest', 'warehouse'/); assert.match(source, /'--deep'/); assert.doesNotMatch(source, /'--enrich'/); assert.match(source, /ktx ingest fast verified/); @@ -534,8 +547,11 @@ describe('verification snippets', () => { it('exercises supported public package CLI commands', () => { const source = npmCliSmokeSource(); - assert.match(source, /pnpm', \['exec', 'ktx', '--help'\]/); - assert.match(source, /pnpm', \['exec', 'ktx', 'setup', '--help'\]/); + assert.match(source, /function pnpmCommand\(args\)/); + assert.match(source, /process\.platform === 'win32'/); + assert.doesNotMatch(source, /run\('pnpm',/); + assert.match(source, /pnpmCommand\(\['exec', 'ktx', '--help'\]\)/); + assert.match(source, /pnpmCommand\(\['exec', 'ktx', 'setup', '--help'\]\)/); assert.match(source, /Usage: ktx setup/); assert.doesNotMatch(source, new RegExp(["'demo'", "'--mode'", "'deterministic'"].join(', '))); assert.match(source, /'status', '--verbose', '--no-input'/);