Files
stack/packages/cli/src/project.mjs
T
jason.woltje 98d29769e2 bus, cli: tester reads and the W1-W7 tester writes, recorded as tester; scripts/mosaic sends plugins, name and names to the CLI (row 175, #1673)
TESTER_VERBS gains usage, names, limits, plugins, instances, project.list,
thread.list, name.set and project.create. A tester reads limits, plugins
and instances and sends plugins.set, instances.set and routes.set, each
logged with actor tester. The project file verbs refuse a tester
tester-refused. The broker and CLI drop the [mos test]
prefix rule (mos-0872 Q1). The CLI takes --tester on the seven reads and
the W1-W7 write forms and refuses it (exit 4) elsewhere, bus start and
stop included.

project init --tester checks the project area, refuses the area root
itself, binds the tester with one agents read, then commits the layout as
tester (mos-0872 Q2). scripts/mosaic sends plugins, name, names, follow,
threads and thread to packages/cli (sage-1081). The row 164 pins are
untouched.

Built by Dewey on 9e74925c2 (three commits to 989699f7c, manifest
2e30f2a1...29d5f), approved by Filbert in round 3 (#1673 comment 29279),
Sage's gate green on 716ddcf1e plus the patch.
2026-10-19 19:04:50 -05:00

76 lines
4.1 KiB
JavaScript

import { configDir, loadBusiness } from '../../business/src/index.mjs';
import { checkProject, initProject, ProjectError, testerProjectArea } from '../../project/src/index.mjs';
import { loadSystem, rolesDir } from './config.mjs';
import { CliError } from './errors.mjs';
import { shortId } from './format.mjs';
import { parseArgs } from './thread.mjs';
import { refuseInsideAgent } from './transport.mjs';
export const PROJECT_USAGE = 'mosaic project init <business> <project> [--tester] | check <business> <project>';
// Row 128 (#1622): create and list work on the bus record (project.create, project.list); init
// and check above work on the repository on disk and never contact the bus, except that
// init --tester (row 175, #1673) binds the tester once before it writes.
export const PROJECT_BUS_USAGE = [
'mosaic project create <name> [--repo <repo>] [--business <id>] [--tester]',
'mosaic project list [--business <id>] [--json] [--tester]',
'A thread project named for a business repository must carry that repo: --repo <name>.',
].join('\n ');
export async function projectBusCommand(args, io, callFor) {
const [sub, ...rest] = args;
const parsed = parseArgs(rest, PROJECT_BUS_USAGE, sub === 'create'
? { flags: ['--tester'], values: ['--repo', '--business'] }
: { flags: ['--json', '--tester'], values: ['--business'] });
if (parsed.positional.length !== (sub === 'create' ? 1 : 0))
throw new CliError(`project ${sub} takes ${sub === 'create' ? 'one name' : 'no arguments'}\n${PROJECT_BUS_USAGE}`, 4);
const call = callFor(parsed);
if (sub === 'create') {
const [name] = parsed.positional;
let made;
try {
made = await call('project.create', { name, ...(parsed.values['--repo'] === undefined ? {} : { repo: parsed.values['--repo'] }) });
} catch (error) {
if (error.code === 'project-name-is-repo')
throw new CliError(`${JSON.stringify(name)} is a repository in the business file; add --repo ${name} to make its thread project, or pick another name`, 2);
throw error;
}
return io.stdout.write(`created project ${name} id ${made.id}\n`);
}
const projects = await call('project.list');
if (parsed.flags.has('--json')) return io.stdout.write(`${JSON.stringify(projects, null, 2)}\n`);
io.stdout.write(projects.length === 0 ? 'no projects\n' : projects.map(p => `${p.name} ${p.repo ?? '-'} ${shortId(p.id)}\n`).join(''));
}
// Row 175 (#1673, Q173, mos-0872 Q2): `init --tester` may run inside an agent. Before anything
// touches the disk it refuses a repository outside <dataRoot>/projects/<business>/
// (tester-project-area, exit 2), then binds the tester with one agents read, so tester-off and
// tester-required refuse (exit 3), then commits the layout as `tester`.
export async function projectCommand(args, io, deps = {}, callFor) {
const tester = args[0] === 'init' && args.filter(arg => arg === '--tester').length === 1;
const words = tester ? args.filter(arg => arg !== '--tester') : args;
if (words.length !== 3 || !['init', 'check'].includes(words[0]) || words.some(arg => arg.startsWith('--')))
throw new CliError(PROJECT_USAGE, 4);
const [verb, id, project] = words;
if (verb === 'init' && !tester) refuseInsideAgent(io.env);
try {
const system = deps.system ?? loadSystem({ env: io.env });
const business = loadBusiness(id, { dir: configDir(io.env), rolesDir: rolesDir(io.env) });
const context = { business, project, dataRoot: system.dataRoot };
if (tester) {
testerProjectArea(context);
await callFor({ positional: [], flags: new Set(['--tester']), values: { '--business': id } })('agents', {});
}
if (verb === 'init') {
const result = await initProject(context, tester ? { author: 'tester' } : {});
io.stdout.write(`initialized ${result.repo}\ncommit ${result.commit}\n`);
} else {
const result = await checkProject(context);
if (!result.ok) throw new CliError(result.problems.join('\n'), 2);
io.stdout.write(`checked ${result.repo}: valid\n`);
}
} catch (error) {
if (error instanceof CliError) throw error;
throw new CliError(error.message, error instanceof ProjectError ? 2 : 3);
}
}