Files
stack/packages/project/src/paths.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

59 lines
2.8 KiB
JavaScript

import { lstatSync } from 'node:fs';
import { dirname, isAbsolute, join, normalize, relative, sep } from 'node:path';
export class ProjectError extends Error {
constructor(code, message) { super(`${code}: ${message}`); this.code = code; }
}
export const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
export const checkedId = value => {
if (typeof value !== 'string' || !ID.test(value)) throw new ProjectError('project-id', 'invalid id');
return value;
};
// The files a person opens and saves in the Console (row 148): the spec, and Markdown directly
// under docs/. Reads and the human writer share this one definition.
export const documentPath = path => typeof path === 'string' &&
(path === 'spec/north-star.md' || /^docs\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.md$/.test(path));
export function absolute(path) {
if (typeof path !== 'string' || !isAbsolute(path) || normalize(path) !== path || path.includes('\0'))
throw new ProjectError('project-path', 'expected normalized absolute path');
return path;
}
export function noLinks(path) {
for (let part = path; ; part = dirname(part)) {
try {
const st = lstatSync(part);
if (!st.isDirectory() || st.isSymbolicLink()) throw new ProjectError('project-path', 'path ancestor must be a directory, never a link');
} catch (e) { if (e.code !== 'ENOENT') throw e; }
if (part === dirname(part)) break;
}
}
const contains = (base, child) => {
const rel = relative(base, child);
return rel === '' || (rel !== '..' && !rel.startsWith('..' + sep) && !isAbsolute(rel));
};
export function projectRepo({ business, project, dataRoot }) {
checkedId(business?.id); checkedId(project);
if (!Object.hasOwn(business?.projects ?? {}, project))
throw new ProjectError('project-not-declared', `business does not declare ${project}`);
absolute(dataRoot);
const entry = business.projects[project];
const repo = absolute(entry.repo ?? join(dataRoot, 'projects', business.id, project));
for (const project of Object.values(business.projects)) {
const code = absolute(project.root);
if (contains(code, repo) || contains(repo, code))
throw new ProjectError('project-path', 'record repository and code checkout overlap');
}
noLinks(repo);
return repo;
}
// Row 175 (#1673, mos-0872 Q2): the tester initializes only a repository in the data root's
// project area, <dataRoot>/projects/<business id>/. It writes nothing; projectRepo only lstats the path.
// The area itself is refused too: a repository there would hold every other project's default one.
export function testerProjectArea(context) {
const repo = projectRepo(context);
const area = join(context.dataRoot, 'projects', context.business.id);
if (relative(area, repo) === '' || !contains(area, repo))
throw new ProjectError('tester-project-area', `the tester initializes only a repository under ${area}`);
return repo;
}