mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 10:12:44 +00:00
fix(settings): report remaining import gaps
This commit is contained in:
@@ -3664,6 +3664,8 @@ export const WriteProjectConfigResponseMessageSchema = z.object({
|
||||
|
||||
export const GetProjectConfigImportResponseMessageSchema = z.object({
|
||||
type: z.literal("project.config.get_import.response"),
|
||||
// zod-aot 0.2.0 miscompiles boolean discriminators as string options
|
||||
// (`"true"`/`"false"`), so keep this sequential until upstream fixes it.
|
||||
payload: z.union([
|
||||
ProjectConfigImportPreviewSchema.extend({
|
||||
requestId: z.string(),
|
||||
@@ -3680,6 +3682,8 @@ export const GetProjectConfigImportResponseMessageSchema = z.object({
|
||||
|
||||
export const ApplyProjectConfigImportResponseMessageSchema = z.object({
|
||||
type: z.literal("project.config.apply_import.response"),
|
||||
// zod-aot 0.2.0 miscompiles boolean discriminators as string options
|
||||
// (`"true"`/`"false"`), so keep this sequential until upstream fixes it.
|
||||
payload: z.union([
|
||||
z.object({
|
||||
requestId: z.string(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PaseoConfigRaw } from "@getpaseo/protocol/messages";
|
||||
import { normalizeServiceEnvName } from "../../../workspace-service-env.js";
|
||||
import type { ProjectConfigImportCandidate, ProjectConfigImportPreview } from "./service.js";
|
||||
|
||||
interface MergeProjectConfigImportInput {
|
||||
@@ -42,12 +43,26 @@ export function mergeProjectConfigImport(
|
||||
}
|
||||
|
||||
const patchScripts = input.candidate.patch.scripts ?? {};
|
||||
const serviceScriptByEnvName = collectServiceScriptsByEnvName(base.scripts ?? {});
|
||||
for (const [scriptId, script] of Object.entries(patchScripts)) {
|
||||
const key = `scripts.${scriptId}`;
|
||||
if (base.scripts && Object.hasOwn(base.scripts, scriptId)) {
|
||||
setOutcome(items, key, `Paseo already has a "${scriptId}" script.`);
|
||||
continue;
|
||||
}
|
||||
if (isServiceScript(script)) {
|
||||
const envName = normalizeServiceEnvName(scriptId);
|
||||
const existingScriptId = serviceScriptByEnvName.get(envName);
|
||||
if (existingScriptId) {
|
||||
setOutcome(
|
||||
items,
|
||||
key,
|
||||
`Service environment name collides with "${existingScriptId}" (${envName}).`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
serviceScriptByEnvName.set(envName, scriptId);
|
||||
}
|
||||
merged.scripts = { ...merged.scripts, [scriptId]: script };
|
||||
importedCount += 1;
|
||||
}
|
||||
@@ -64,6 +79,24 @@ export function mergeProjectConfigImport(
|
||||
};
|
||||
}
|
||||
|
||||
function collectServiceScriptsByEnvName(
|
||||
scripts: NonNullable<PaseoConfigRaw["scripts"]>,
|
||||
): Map<string, string> {
|
||||
const result = new Map<string, string>();
|
||||
for (const [scriptId, script] of Object.entries(scripts)) {
|
||||
if (isServiceScript(script)) {
|
||||
result.set(normalizeServiceEnvName(scriptId), scriptId);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function isServiceScript(script: unknown): boolean {
|
||||
return Boolean(
|
||||
script && typeof script === "object" && "type" in script && script.type === "service",
|
||||
);
|
||||
}
|
||||
|
||||
function hasLifecycle(value: unknown): boolean {
|
||||
if (typeof value === "string") {
|
||||
return value.trim().length > 0;
|
||||
|
||||
+85
@@ -476,6 +476,91 @@ hide = true
|
||||
);
|
||||
});
|
||||
|
||||
test("reports nested environment variables, prompts, and Git settings", () => {
|
||||
const repo = makeRepo();
|
||||
writeSharedToml(
|
||||
repo,
|
||||
`
|
||||
[environment_variables.local]
|
||||
LOCAL_TOKEN = "local-secret"
|
||||
|
||||
[environment_variables.cloud]
|
||||
CLOUD_TOKEN = "cloud-secret"
|
||||
|
||||
[prompts]
|
||||
system = "custom prompt"
|
||||
|
||||
[git]
|
||||
default_branch = "develop"
|
||||
`,
|
||||
);
|
||||
|
||||
expect(inspect(repo).items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: "environment_variables",
|
||||
outcome: "unsupported",
|
||||
detail: "Environment variable values are not imported. Found: CLOUD_TOKEN, LOCAL_TOKEN.",
|
||||
}),
|
||||
expect.objectContaining({ key: "prompts", outcome: "unsupported" }),
|
||||
expect.objectContaining({ key: "git", outcome: "unsupported" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test("reports default and icon fields on imported run scripts", () => {
|
||||
const repo = makeRepo();
|
||||
writeSharedToml(
|
||||
repo,
|
||||
`
|
||||
[scripts.run.dev]
|
||||
command = "npm run dev"
|
||||
default = true
|
||||
icon = "play"
|
||||
`,
|
||||
);
|
||||
|
||||
const preview = inspect(repo);
|
||||
|
||||
expect(preview.preview).toMatchObject({ scripts: { dev: { command: "npm run dev" } } });
|
||||
expect(preview.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ key: "scripts.dev.default", outcome: "unsupported" }),
|
||||
expect.objectContaining({ key: "scripts.dev.icon", outcome: "unsupported" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test("does not import services with colliding normalized environment names", () => {
|
||||
const repo = makeRepo();
|
||||
writeSharedToml(
|
||||
repo,
|
||||
`
|
||||
[scripts.run.app-server]
|
||||
command = "npm run app -- --port $CONDUCTOR_PORT"
|
||||
|
||||
[scripts.run."app.server"]
|
||||
command = "npm run other -- --port $CONDUCTOR_PORT"
|
||||
`,
|
||||
);
|
||||
|
||||
const preview = inspect(repo);
|
||||
|
||||
expect(preview.preview?.scripts).toMatchObject({
|
||||
"app-server": { type: "service", port: "$PASEO_PORT" },
|
||||
});
|
||||
expect(preview.preview?.scripts).not.toHaveProperty("app.server");
|
||||
expect(preview.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: "scripts.app.server",
|
||||
outcome: "collision",
|
||||
detail: 'Service environment name collides with "app-server" (APP_SERVER).',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test("malformed TOML identifies the safe relative source path", () => {
|
||||
const repo = makeRepo();
|
||||
writeSharedToml(repo, "[scripts\nsetup = nope");
|
||||
|
||||
+41
-12
@@ -30,6 +30,8 @@ interface ConductorSettings {
|
||||
file_include_globs?: unknown;
|
||||
environment_variables?: unknown;
|
||||
environment_variables_forward?: unknown;
|
||||
prompts?: unknown;
|
||||
git?: unknown;
|
||||
spotlight_testing?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -37,7 +39,9 @@ interface ConductorSettings {
|
||||
interface ConductorRunScript {
|
||||
command: string;
|
||||
args?: string[];
|
||||
default?: boolean;
|
||||
hide?: boolean;
|
||||
icon?: string;
|
||||
options?: {
|
||||
cwd?: string;
|
||||
};
|
||||
@@ -240,7 +244,9 @@ function normalizeRunScript(entry: Record<string, unknown>, command: string): Co
|
||||
return {
|
||||
command,
|
||||
...(args ? { args } : {}),
|
||||
...(typeof entry.default === "boolean" ? { default: entry.default } : {}),
|
||||
...(typeof entry.hide === "boolean" ? { hide: entry.hide } : {}),
|
||||
...(typeof entry.icon === "string" ? { icon: entry.icon } : {}),
|
||||
...(options && typeof options.cwd === "string" ? { options: { cwd: options.cwd } } : {}),
|
||||
...(availableIn ? { available_in: availableIn } : {}),
|
||||
};
|
||||
@@ -272,6 +278,13 @@ function mapRunScript(
|
||||
return;
|
||||
}
|
||||
|
||||
if (script.default !== undefined) {
|
||||
unsupported(items, `scripts.${scriptId}.default`, "Default script selection is not imported.");
|
||||
}
|
||||
if (script.icon !== undefined) {
|
||||
unsupported(items, `scripts.${scriptId}.icon`, "Script icons are not imported.");
|
||||
}
|
||||
|
||||
let command = appendArgs(script.command, script.args ?? []);
|
||||
if (script.options?.cwd) {
|
||||
const cwdPrefix = safeCwdPrefix(script.options.cwd);
|
||||
@@ -349,27 +362,43 @@ function reportUnsupported(
|
||||
"Paseo spotlight is a separate workflow, not project config.",
|
||||
);
|
||||
}
|
||||
if (settings.prompts !== undefined) {
|
||||
unsupported(items, "prompts", "Custom agent prompts are not imported.");
|
||||
}
|
||||
if (settings.git !== undefined) {
|
||||
unsupported(items, "git", "Conductor Git settings are not imported.");
|
||||
}
|
||||
}
|
||||
|
||||
function collectEnvironmentVariableNames(settings: ConductorSettings): string[] {
|
||||
const names = new Set<string>();
|
||||
for (const key of ["environment_variables", "environment_variables_forward"] as const) {
|
||||
const value = settings[key];
|
||||
if (isRecord(value)) {
|
||||
for (const name of Object.keys(value).sort()) {
|
||||
names.add(name);
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
for (const name of value) {
|
||||
if (typeof name === "string") {
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
collectEnvironmentVariableNamesFromValue(settings[key], names);
|
||||
}
|
||||
return Array.from(names).sort();
|
||||
}
|
||||
|
||||
function collectEnvironmentVariableNamesFromValue(value: unknown, names: Set<string>): void {
|
||||
if (Array.isArray(value)) {
|
||||
for (const name of value) {
|
||||
if (typeof name === "string") {
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!isRecord(value)) {
|
||||
return;
|
||||
}
|
||||
for (const [name, nestedValue] of Object.entries(value)) {
|
||||
if (isRecord(nestedValue)) {
|
||||
collectEnvironmentVariableNamesFromValue(nestedValue, names);
|
||||
} else {
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unsupported(items: ProjectConfigImportItem[], key: string, detail: string): void {
|
||||
items.push({ key, label: key, outcome: "unsupported", detail });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user