diff --git a/.github/workflows/engine-parity.yml b/.github/workflows/engine-parity.yml index a3e7842..8c527ba 100644 --- a/.github/workflows/engine-parity.yml +++ b/.github/workflows/engine-parity.yml @@ -24,5 +24,8 @@ jobs: - name: Parity check contract run: node scripts/test-check-engine-parity.mjs + - name: Coding-agent layer contract + run: node scripts/test-agent-layer.mjs + - name: Generated devDependencies and overrides match the pinned engine release run: node scripts/check-engine-parity.mjs diff --git a/README.md b/README.md index 723ab2d..29d2f8e 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,18 @@ Choose the themes: Of course, you can combine the options, e.g.: > npx neo-app -w workspace -n MyApp -t all +### Coding agents +Every new workspace is ready for a coding agent such as Claude Code or Codex: +- `AGENTS.md` (and a `CLAUDE.md` that imports it) explains how the engine works and where its source, guides and examples live in the workspace; +- the `neo-app-authoring` and `neural-link` skills are linked into `.agents/skills/` and `.claude/skills/` by the workspace's own `npm install`; +- each app's `neo-config.json` sets `useAiClient: "development"`, so the app connects to the Neural Link while you build it. + +Leave all of that out: +> npx neo-app --no-agent + +Add it to a workspace you already have (it changes nothing else, and keeps an existing `AGENTS.md` or `CLAUDE.md`): +> npx neo-app --agent-only -w workspace + ## Starting a local web-server By default, npx neo-app will start a webpack dev-server right after the build. diff --git a/package.json b/package.json index 75d5795..9fd1c84 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "scripts" : { "neo-app": "node ./tasks/neo-app.mjs", - "test" : "node scripts/test-check-engine-parity.mjs && node scripts/check-engine-parity.mjs" + "test" : "node scripts/test-check-engine-parity.mjs && node scripts/test-agent-layer.mjs && node scripts/check-engine-parity.mjs" }, "bin" : { "neo-app": "./tasks/neo-app.mjs" diff --git a/scripts/check-engine-parity.mjs b/scripts/check-engine-parity.mjs index aca55f6..7c483f6 100644 --- a/scripts/check-engine-parity.mjs +++ b/scripts/check-engine-parity.mjs @@ -38,18 +38,31 @@ export function generatedPackageJson() { return packageJson } +/** + * @summary DevDependencies both sides declare on purpose at different releases, each with the reason. + * + * `neo-agent-skills` serves two audiences. The engine pins the maintainer release it lints itself with, and a + * workspace installs the release whose skills carry the `app` audience. Equal ranges would be the defect here. + * @member {Map} + */ +export const INDEPENDENT = new Map([ + ['neo-agent-skills', 'the engine pins its maintainer release; a workspace installs the app-audience release'] +]); + /** * @summary Compares the scaffold's devDependencies with the engine's. * @param {Object} scaffold The generated devDependencies * @param {Object} engine The engine release's devDependencies - * @returns {{drift: Object[], scaffoldOnly: String[]}} `drift` holds `{name, scaffold, engine}` for each differing range + * @returns {{drift: Object[], independent: String[], scaffoldOnly: String[]}} `drift` holds `{name, scaffold, engine}` + * for each differing range; `independent` lists the declared exceptions both sides carry */ export function findDrift(scaffold, engine) { const names = Object.keys(scaffold); return { - drift : names.filter(name => name in engine && scaffold[name] !== engine[name]) + drift : names.filter(name => name in engine && !INDEPENDENT.has(name) && scaffold[name] !== engine[name]) .map(name => ({name, scaffold: scaffold[name], engine: engine[name]})), + independent : names.filter(name => name in engine && INDEPENDENT.has(name)), scaffoldOnly: names.filter(name => !(name in engine)) } } @@ -92,13 +105,15 @@ if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToP range = packageJson.dependencies['neo.mjs'], // One matching version answers as a string, several as an ascending list version = [].concat(npmView(`neo.mjs@${range}`, 'version')).at(-1), - {drift, scaffoldOnly} = findDrift(packageJson.devDependencies, npmView(`neo.mjs@${version}`, 'devDependencies')), + {drift, independent, scaffoldOnly} = findDrift(packageJson.devDependencies, npmView(`neo.mjs@${version}`, 'devDependencies')), overrideDrift = findOverrideDrift(packageJson.overrides, npmView(`neo.mjs@${version}`, 'overrides')); if (scaffoldOnly.length > 0) { console.log(`check-engine-parity: not compared, no engine counterpart: ${scaffoldOnly.join(', ')}`) } + independent.forEach(name => console.log(`check-engine-parity: not compared, independent by design: ${name} (${INDEPENDENT.get(name)})`)); + if (drift.length > 0) { console.error(`check-engine-parity: ${drift.length} generated devDependencies differ from neo.mjs@${version} ` + `(the newest release \`${range}\` allows):`); diff --git a/scripts/test-agent-layer.mjs b/scripts/test-agent-layer.mjs new file mode 100644 index 0000000..c00611e --- /dev/null +++ b/scripts/test-agent-layer.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node +/** + * @summary Contract checks for the coding-agent layer: what a new workspace gets with and without it, and what + * `--agent-only` adds to an existing one — against real files in a throwaway directory, with no install. + */ + +import assert from 'node:assert/strict'; +// node:fs, not fs-extra: the CI job runs this with no install, and the tasks need only the plain calls. +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import createAgentLayer, {AGENT_SKILLS_RANGE, AI_CLIENT_ENVIRONMENT} from '../tasks/createAgentLayer.mjs'; +import createNeoConfigJson from '../tasks/createNeoConfigJson.mjs'; +import createPackageJson from '../tasks/createPackageJson.mjs'; + +const + root = fs.mkdtempSync(path.join(os.tmpdir(), 'neo-app-agent-')), + read = (...parts) => JSON.parse(fs.readFileSync(path.join(...parts), 'utf-8')), + scaffold = (name, agent) => { + const workspace = path.join(root, name), + appPath = path.join(workspace, 'apps', 'myapp'); + + fs.mkdirSync(appPath, {recursive: true}); + createPackageJson.init('MyApp', workspace, fs, os, path, agent); + createNeoConfigJson.init('MyApp', appPath, fs, ['DragDrop', 'Navigator', 'Stylesheet'], os, path, ['all'], 'no', 'no', agent); + agent && createAgentLayer.writeClaudeMd(workspace, fs, path); + + return workspace + }; + +try { + // ── A new workspace gets the layer by default ──────────────────────────────────────────────── + { + const workspace = scaffold('with', true), + packageJson = read(workspace, 'package.json'); + + assert.equal(packageJson.scripts.prepare, 'neo-agent-skills-materialize --audience app', 'its install links the app skills'); + assert.equal(packageJson.devDependencies['neo-agent-skills'], AGENT_SKILLS_RANGE, 'from the release that declares them'); + assert.deepEqual(Object.keys(packageJson.devDependencies), [...Object.keys(packageJson.devDependencies)].sort(), 'devDependencies stay alphabetical'); + assert.deepEqual(Object.keys(packageJson.scripts), [...Object.keys(packageJson.scripts)].sort(), 'and so do the scripts'); + assert.equal(read(workspace, 'apps', 'myapp', 'neo-config.json').useAiClient, AI_CLIENT_ENVIRONMENT, 'the app connects to the Neural Link in development'); + assert.equal(fs.readFileSync(path.join(workspace, 'CLAUDE.md'), 'utf-8'), '@AGENTS.md\n', 'and CLAUDE.md only imports AGENTS.md'); + } + + // ── --no-agent writes none of it ───────────────────────────────────────────────────────────── + { + const workspace = scaffold('without', false), + packageJson = read(workspace, 'package.json'); + + assert.equal(packageJson.scripts.prepare, undefined, 'no prepare'); + assert.equal(packageJson.devDependencies['neo-agent-skills'], undefined, 'no skills dependency'); + assert.equal(read(workspace, 'apps', 'myapp', 'neo-config.json').useAiClient, undefined, 'no Neural Link client'); + assert.ok(!fs.existsSync(path.join(workspace, 'CLAUDE.md')), 'no CLAUDE.md'); + } + + // ── --agent-only adds the layer to an existing workspace, and only once ────────────────────── + { + const workspace = scaffold('existing', false), + other = path.join(workspace, 'apps', 'other'); + + fs.mkdirSync(other, {recursive: true}); + fs.writeFileSync(path.join(other, 'neo-config.json'), JSON.stringify({useAiClient: false}, null, 4)); + fs.writeFileSync(path.join(workspace, 'README.md'), 'mine\n'); + + const changed = createAgentLayer.addToWorkspace(workspace, fs, os, path); + + assert.deepEqual(changed.sort(), ['CLAUDE.md', path.join('apps', 'myapp', 'neo-config.json'), 'package.json'].sort(), + 'it changes the package, the app without the switch, and adds CLAUDE.md'); + assert.equal(read(other, 'neo-config.json').useAiClient, false, 'an app that already sets useAiClient keeps its choice'); + assert.equal(fs.readFileSync(path.join(workspace, 'README.md'), 'utf-8'), 'mine\n', 'and no other file changes'); + assert.deepEqual(createAgentLayer.addToWorkspace(workspace, fs, os, path), [], 'a second run changes nothing'); + } + + // ── AGENTS.md: an existing file is the workspace's, never regenerated ─────────────────────── + { + const workspace = path.join(root, 'with'); + + fs.writeFileSync(path.join(workspace, 'AGENTS.md'), 'edited\n'); + + const spawned = []; + + assert.equal(createAgentLayer.generateAgentsMd(workspace, fs, path, (...args) => {spawned.push(args); return {status: 0}}, 'npx'), false); + assert.equal(spawned.length, 0, 'the generator is not even started'); + assert.equal(fs.readFileSync(path.join(workspace, 'AGENTS.md'), 'utf-8'), 'edited\n'); + } + + // ── A generator that fails is a loud failure, not a missing file ──────────────────────────── + { + const workspace = path.join(root, 'without'); + + assert.throws(() => createAgentLayer.generateAgentsMd(workspace, fs, path, () => ({status: 1}), 'npx'), /AGENTS\.md generator exited 1/); + } +} finally { + fs.rmSync(root, {force: true, recursive: true}); +} + +console.log('agent-layer: contract arms green.'); diff --git a/scripts/test-check-engine-parity.mjs b/scripts/test-check-engine-parity.mjs index 3dd17fa..bb27f9f 100644 --- a/scripts/test-check-engine-parity.mjs +++ b/scripts/test-check-engine-parity.mjs @@ -37,6 +37,18 @@ import {findDrift, findOverrideDrift, generatedPackageJson} from './check-engine assert.deepEqual(scaffoldOnly, ['chromadb']) } +// ── A declared independent range is listed, never drift — and only the declared one ────────────────── +{ + const {drift, independent} = findDrift( + {marked: '^17.0.4', 'neo-agent-skills': '^0.1.60'}, + {marked: '^18.0.13', 'neo-agent-skills': '0.1.56'} + ); + + assert.deepEqual(independent, ['neo-agent-skills'], 'the skills package serves another audience in the workspace'); + assert.deepEqual(drift.map(({name}) => name), ['marked'], 'while an undeclared difference beside it is still drift'); + assert.ok(generatedPackageJson().devDependencies['neo-agent-skills'], 'and the scaffold really declares it'); +} + // ── Overrides compare in full, both ways: npm reads them only in the root project ────────────────── { const engine = {'gray-matter': {'js-yaml': '^3.15.2'}, 'lodash-es': '^4.18.0'}; diff --git a/tasks/createAgentLayer.mjs b/tasks/createAgentLayer.mjs new file mode 100644 index 0000000..a6cffbd --- /dev/null +++ b/tasks/createAgentLayer.mjs @@ -0,0 +1,134 @@ +/** + * @summary The coding-agent layer of a workspace: what an agent working in it reads first, and the skills it gets. + * + * A workspace `neo-app` creates is usually built by a coding agent, and an agent that knows nothing about the + * engine writes React into it. The layer is four things, each from its single source: + * + * - `AGENTS.md`, emitted by the installed `neo-agent-skills` generator for the `neo-app-workspace` kind, after the + * workspace's own install. It is written once and then belongs to the workspace, so later installs never + * overwrite an edited copy. + * - `CLAUDE.md`, which only imports `AGENTS.md`: Claude Code reads its own file name. + * - the `app` skills, linked into `.agents/skills` and `.claude/skills` by the workspace's `prepare` on every install, + * so they update with the package. + * - `useAiClient` in each app's `neo-config.json`, set to the development environment, so the app connects to the + * Neural Link while it is being built and never in a production build. + */ + +/** + * @summary The release whose skills carry the `app` audience. + * @member {String} + */ +export const AGENT_SKILLS_RANGE = '^0.1.61'; + +/** + * @summary The environment in which a scaffolded app connects to the Neural Link. + * @member {String} + */ +export const AI_CLIENT_ENVIRONMENT = 'development'; + +/** + * @summary The scripts the layer adds to a workspace's `package.json`. + * @member {Object} + */ +export const agentScripts = { + prepare: 'neo-agent-skills-materialize --audience app' +}; + +const + CLAUDE_MD = '@AGENTS.md\n', + // The generator's declared kind for a workspace `neo-app` creates; never one of the engine's own repositories. + AGENTS_MD_ARGS = ['--no-install', 'neo-agent-skills-agents-md', '--repo', 'neo-app-workspace', '--audience', 'app', '--out', 'AGENTS.md']; + +export default { + /** + * @summary Writes `CLAUDE.md` into a workspace that has none. + * @param {String} workspace + * @param {Object} fs + * @param {Object} path + * @returns {Boolean} Whether the file was written + */ + writeClaudeMd(workspace, fs, path) { + const file = path.join(workspace, 'CLAUDE.md'); + + if (fs.existsSync(file)) { + return false + } + + fs.writeFileSync(file, CLAUDE_MD); + return true + }, + + /** + * @summary Emits `AGENTS.md` from the workspace's installed skills package, unless the workspace already has one. + * Run after the workspace's install: the generator is that install's `neo-agent-skills` binary. + * @param {String} workspace + * @param {Object} fs + * @param {Object} path + * @param {Function} spawnSync + * @param {String} npxCmd + * @returns {Boolean} Whether the file was written + */ + generateAgentsMd(workspace, fs, path, spawnSync, npxCmd) { + if (fs.existsSync(path.join(workspace, 'AGENTS.md'))) { + return false + } + + const result = spawnSync(npxCmd, AGENTS_MD_ARGS, {cwd: workspace, encoding: 'utf-8', shell: true, stdio: 'inherit'}); + + if (result.status !== 0) { + throw new Error(`neo-app: the AGENTS.md generator exited ${result.status ?? result.error?.message}; is neo-agent-skills ${AGENT_SKILLS_RANGE} installed?`) + } + + return true + }, + + /** + * @summary Adds the layer to an existing workspace (`--agent-only`): the skills dependency and its `prepare`, and + * `useAiClient` in every app that does not set it. A key the workspace already sets is left as it is. + * @param {String} workspace + * @param {Object} fs + * @param {Object} os + * @param {Object} path + * @returns {String[]} The files it changed, relative to the workspace + */ + addToWorkspace(workspace, fs, os, path) { + const + changed = [], + packageJsonPath = path.join(workspace, 'package.json'), + packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + + packageJson.scripts ??= {}; + packageJson.devDependencies ??= {}; + + if (!packageJson.scripts.prepare && !packageJson.devDependencies['neo-agent-skills']) { + Object.assign(packageJson.scripts, agentScripts); + packageJson.devDependencies['neo-agent-skills'] = AGENT_SKILLS_RANGE; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4) + os.EOL); + changed.push('package.json') + } + + const appsDir = path.join(workspace, 'apps'); + + for (const app of fs.existsSync(appsDir) ? fs.readdirSync(appsDir) : []) { + const configPath = path.join(appsDir, app, 'neo-config.json'); + + if (!fs.existsSync(configPath)) { + continue + } + + const neoConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + + if (!('useAiClient' in neoConfig)) { + neoConfig.useAiClient = AI_CLIENT_ENVIRONMENT; + fs.writeFileSync(configPath, JSON.stringify(neoConfig, null, 4)); + changed.push(path.join('apps', app, 'neo-config.json')) + } + } + + if (this.writeClaudeMd(workspace, fs, path)) { + changed.push('CLAUDE.md') + } + + return changed + } +}; diff --git a/tasks/createGitignore.mjs b/tasks/createGitignore.mjs index a257af7..8165eab 100644 --- a/tasks/createGitignore.mjs +++ b/tasks/createGitignore.mjs @@ -33,6 +33,10 @@ export default { "test/playwright/test-results/", "# Single-file Playwright runs via npx", "test-results/", + "", + "# Coding-agent skills: links into node_modules, refreshed by every install", + ".agents/skills/", + ".claude/skills/", ].join(os.EOL); fs.writeFileSync(path.join(folder, '.gitignore'), gitignoreContent); diff --git a/tasks/createNeoConfigJson.mjs b/tasks/createNeoConfigJson.mjs index 6f832a8..165e275 100644 --- a/tasks/createNeoConfigJson.mjs +++ b/tasks/createNeoConfigJson.mjs @@ -1,5 +1,19 @@ +import {AI_CLIENT_ENVIRONMENT} from './createAgentLayer.mjs'; + export default { - init(appName, folder, fs, mainThreadAddons, os, path, themes, useSharedWorkers, useServiceWorker) { + /** + * @param {String} appName + * @param {String} folder + * @param {Object} fs + * @param {String[]} mainThreadAddons + * @param {Object} os + * @param {Object} path + * @param {String[]} themes + * @param {String} useSharedWorkers + * @param {String} useServiceWorker + * @param {Boolean} [agent=true] Let the app connect to the Neural Link in development, so a coding agent can verify it live + */ + init(appName, folder, fs, mainThreadAddons, os, path, themes, useSharedWorkers, useServiceWorker, agent = true) { let neoConfig = { appPath : `../../apps/${appName.toLowerCase()}/app.mjs`, basePath : '../../', @@ -33,6 +47,10 @@ export default { neoConfig.useServiceWorker = true; } + if (agent) { + neoConfig.useAiClient = AI_CLIENT_ENVIRONMENT; + } + let configs = Object.entries(neoConfig).sort((a, b) => a[0].localeCompare(b[0])); neoConfig = {}; diff --git a/tasks/createPackageJson.mjs b/tasks/createPackageJson.mjs index e933db9..36d2049 100644 --- a/tasks/createPackageJson.mjs +++ b/tasks/createPackageJson.mjs @@ -1,5 +1,15 @@ +import {AGENT_SKILLS_RANGE, agentScripts} from './createAgentLayer.mjs'; + export default { - init(appName, folder, fs, os, path) { + /** + * @param {String} appName + * @param {String} folder + * @param {Object} fs + * @param {Object} os + * @param {Object} path + * @param {Boolean} [agent=true] Add the coding-agent layer: the `app` skills, materialized by the workspace's own install + */ + init(appName, folder, fs, os, path, agent = true) { const packageJson = { name : appName.toLowerCase(), version: '0.1.0', @@ -83,6 +93,15 @@ export default { } }; + if (agent) { + Object.assign(packageJson.scripts, agentScripts); + packageJson.devDependencies['neo-agent-skills'] = AGENT_SKILLS_RANGE; + + // Both blocks are written alphabetically; an added key keeps that order. + packageJson.scripts = Object.fromEntries(Object.entries(packageJson.scripts).sort(([a], [b]) => a.localeCompare(b))); + packageJson.devDependencies = Object.fromEntries(Object.entries(packageJson.devDependencies).sort(([a], [b]) => a.localeCompare(b))) + } + fs.writeFileSync( path.join(folder, 'package.json'), JSON.stringify(packageJson, null, 4) + os.EOL diff --git a/tasks/neo-app.mjs b/tasks/neo-app.mjs index 76938a3..e128670 100755 --- a/tasks/neo-app.mjs +++ b/tasks/neo-app.mjs @@ -4,6 +4,7 @@ import chalk from 'chalk'; import {spawnSync} from 'child_process'; import {Command, Option} from 'commander/esm.mjs'; import copyDocsApp from './copyDocsApp.mjs'; +import createAgentLayer from './createAgentLayer.mjs'; import createApp from './createApp.mjs'; import createGitignore from './createGitignore.mjs'; import createIndexHtml from './createIndexHtml.mjs'; @@ -46,6 +47,8 @@ program .addOption(new Option('-u, --useSharedWorkers ', 'use SharedWorkers').choices(['yes', 'no']).argParser(sanitizeInput)) .addOption(new Option('-v, --useServiceWorker ', 'use ServiceWorker').choices(['yes', 'no']).argParser(sanitizeInput)) .option('-w, --workspace ', 'name of the project root folder', sanitizeInput) + .option('--no-agent', 'skip the coding-agent layer (AGENTS.md, CLAUDE.md, the app skills, the Neural Link in development)') + .option('--agent-only', 'add the coding-agent layer to the existing workspace named by -w, then exit') .allowUnknownOption() .on('--help', () => { console.log('\nIn case you have any issues, please create a ticket here:'); @@ -72,12 +75,34 @@ if (programOpts.info) { showNotFound: true }) .then(console.log); +} else if (programOpts.agentOnly) { + // An existing workspace gets the layer and nothing else: no app, no build, no server. + const + npmCmd = os.platform() === 'win32' ? 'npm.cmd' : 'npm', + npxCmd = os.platform() === 'win32' ? 'npx.cmd' : 'npx', + workspace = path.resolve(cwd, programOpts.workspace ?? '.'); + + if (!fs.existsSync(path.join(workspace, 'package.json'))) { + console.error(chalk.red(`neo-app --agent-only: ${workspace} has no package.json; name an existing workspace with -w.`)); + process.exit(1); + } + + const changed = createAgentLayer.addToWorkspace(workspace, fs, os, path); + + spawnSync(npmCmd, ['i'], {encoding: 'utf-8', env: process.env, cwd: workspace, shell: true, stdio: 'inherit'}); + + if (createAgentLayer.generateAgentsMd(workspace, fs, path, spawnSync, npxCmd)) { + changed.push('AGENTS.md'); + } + + console.log(changed.length ? `Added the coding-agent layer: ${changed.join(', ')}` : 'The workspace already has the coding-agent layer.'); } else { console.log(chalk.bold('Welcome to the Neo.mjs App Generator!')); console.log(`current version of ${packageJson.name}: ${packageJson.version}`); // npm binary based on OS - const npmCmd = os.platform() === 'win32' ? 'npm.cmd' : 'npm'; + const npmCmd = os.platform() === 'win32' ? 'npm.cmd' : 'npm', + npxCmd = os.platform() === 'win32' ? 'npx.cmd' : 'npx'; const questions = []; @@ -163,12 +188,14 @@ if (programOpts.info) { createIndexHtml .init(appName, appPath, fs, os, path); createViewport .init(appName, appPath, fs, os, path); createMyAppsJson .init(appName, workspace, fs, os, path); - createNeoConfigJson.init(appName, appPath, fs, mainThreadAddons, os, path, themes, useSharedWorkers, useServiceWorker); - createPackageJson .init(appName, workspace, fs, os, path); + createNeoConfigJson.init(appName, appPath, fs, mainThreadAddons, os, path, themes, useSharedWorkers, useServiceWorker, programOpts.agent); + createPackageJson .init(appName, workspace, fs, os, path, programOpts.agent); createScssResources.init(appName, workspace, fs, os, path); createServiceWorker.init(appName, workspace, fs, os, path); createSrcFolder .init(path.join(cwd, workspace), fs, os, path, __dirname); + programOpts.agent && createAgentLayer.writeClaudeMd(workspace, fs, path); + fs.copySync( path.join(__dirname, '../resources/copyExamples.mjs'), path.join(workspace, 'buildScripts/copyExamples.mjs') @@ -176,9 +203,11 @@ if (programOpts.info) { const cpOpts = { encoding: 'utf-8', env: process.env, cwd: workspace, shell: true, stdio: 'inherit' }; - // npm install + // npm install — its `prepare` links the app skills when the agent layer is on spawnSync(npmCmd, ['i'], cpOpts); + programOpts.agent && createAgentLayer.generateAgentsMd(workspace, fs, path, spawnSync, npxCmd); + copyDocsApp.init(fs, os, path, workspace); spawnSync('node', ['./node_modules/neo.mjs/buildScripts/build/all.mjs', '-n', '-l', 'no', '-p', 'no'], {