fix(settings): expose remaining import errors

This commit is contained in:
Mohamed Boudra
2026-07-17 17:47:16 +02:00
parent 35d4ae201a
commit 5e75e463ec
5 changed files with 133 additions and 17 deletions
@@ -248,6 +248,31 @@ cwd = "apps/local-web"
});
});
test("merges root conductor.json before scoped JSON until TOML migration", () => {
const repo = makeRepo();
writeFileSync(
join(repo, "conductor.json"),
JSON.stringify({ scripts: { setup: "legacy setup" }, runScriptMode: "nonconcurrent" }),
);
writeSharedJson(repo, { scripts: { run: { dev: { command: "npm run dev" } } } });
const preview = inspect(repo);
expect(preview.inputs).toEqual([
{ role: "legacy", relativePath: "conductor.json" },
{ role: "shared", relativePath: ".conductor/settings.json" },
]);
expect(preview.preview).toMatchObject({
worktree: { setup: "legacy setup" },
scripts: { dev: { command: "npm run dev" } },
});
expect(preview.items).toEqual(
expect.arrayContaining([
expect.objectContaining({ key: "runScriptMode", outcome: "unsupported" }),
]),
);
});
test("reports missing and empty repository-local Conductor configs", () => {
const missingRepo = makeRepo();
const emptyRepo = makeRepo();
@@ -420,6 +445,47 @@ command = "npm run dev -- --port \${CONDUCTOR_PORT:-3000}"
});
});
test("rewrites bare Conductor ports inside shell arithmetic", () => {
const repo = makeRepo();
writeSharedToml(
repo,
`
[scripts.run.dev]
command = "npm run dev -- --hmr-port $((CONDUCTOR_PORT + 1))"
`,
);
expect(inspect(repo).preview).toMatchObject({
scripts: {
dev: {
type: "service",
command: "npm run dev -- --hmr-port $((PASEO_PORT + 1))",
},
},
});
});
test("preserves shell arithmetic expansion in script arguments", () => {
const repo = makeRepo();
writeSharedToml(
repo,
`
[scripts.run.dev]
command = "npm run dev"
args = ["--hmr-port=$((CONDUCTOR_PORT + 1))"]
`,
);
expect(inspect(repo).preview).toMatchObject({
scripts: {
dev: {
type: "service",
command: `npm run dev '--hmr-port='"$((PASEO_PORT + 1))"`,
},
},
});
});
test("does not import scripts available only in Conductor cloud", () => {
const repo = makeRepo();
writeSharedToml(
@@ -113,15 +113,15 @@ function discoverConductorSources(
const worktreeIncludePath = join(repoRoot, ".worktreeinclude");
const files: SourceFile[] = [];
if (!existsSync(sharedTomlPath) && existsSync(rootLegacyPath)) {
files.push(readSourceFile(repoRoot, rootLegacyPath, "legacy", true, source));
}
if (existsSync(sharedJsonPath)) {
files.push(readSourceFile(repoRoot, sharedJsonPath, "shared", true, source));
}
if (existsSync(sharedTomlPath)) {
files.push(readSourceFile(repoRoot, sharedTomlPath, "shared", true, source));
}
if (!existsSync(sharedJsonPath) && !existsSync(sharedTomlPath) && existsSync(rootLegacyPath)) {
files.push(readSourceFile(repoRoot, rootLegacyPath, "legacy", true, source));
}
if (existsSync(localJsonPath)) {
files.push(readSourceFile(repoRoot, localJsonPath, "local", true, source));
}
@@ -516,12 +516,34 @@ function rewriteVariables(
function replaceShellVariable(command: string, from: string, to: string): string {
const pattern = new RegExp(`\\$\\{${from}(?=[}:#%+\\-=?])|\\$${from}(?![A-Za-z0-9_])`, "g");
return command.replace(pattern, (match) => (match.startsWith("${") ? `\${${to}` : `$${to}`));
return replaceArithmeticVariable(
command.replace(pattern, (match) => (match.startsWith("${") ? `\${${to}` : `$${to}`)),
from,
to,
);
}
function containsShellVariable(command: string, name: string): boolean {
const pattern = new RegExp(`\\$\\{${name}(?=[}:#%+\\-=?])|\\$${name}(?![A-Za-z0-9_])`);
return pattern.test(command);
return pattern.test(command) || containsArithmeticVariable(command, name);
}
function replaceArithmeticVariable(command: string, from: string, to: string): string {
return command.replace(/\$\(\(([\s\S]*?)\)\)/g, (expression, body: string) => {
const identifier = new RegExp(`(^|[^A-Za-z0-9_])${from}(?![A-Za-z0-9_])`, "g");
const rewrittenBody = body.replace(identifier, (_match, prefix: string) => `${prefix}${to}`);
return rewrittenBody === body ? expression : `$((` + rewrittenBody + `))`;
});
}
function containsArithmeticVariable(command: string, name: string): boolean {
const identifier = new RegExp(`(^|[^A-Za-z0-9_])${name}(?![A-Za-z0-9_])`);
for (const match of command.matchAll(/\$\(\(([\s\S]*?)\)\)/g)) {
if (identifier.test(match[1])) {
return true;
}
}
return false;
}
function appendArgs(command: string, args: string[]): string {
@@ -565,7 +587,7 @@ function normalizeAvailableIn(value: unknown): string | string[] | undefined {
function shellQuoteArgument(value: string): string {
const variablePattern =
/\$(?:\{[A-Za-z_][A-Za-z0-9_]*(?:(?:[^{}])|\{[^{}]*\})*\}|[A-Za-z_][A-Za-z0-9_]*)/g;
/\$\(\([\s\S]*?\)\)|\$(?:\{[A-Za-z_][A-Za-z0-9_]*(?:(?:[^{}])|\{[^{}]*\})*\}|[A-Za-z_][A-Za-z0-9_]*)/g;
const parts: string[] = [];
let offset = 0;
for (const match of value.matchAll(variablePattern)) {