mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-31 05:32:58 +00:00
fix(settings): keep project imports current and complete
This commit is contained in:
@@ -52,13 +52,15 @@ export function projectConfigImportPreviewQueryKey(
|
||||
source: ProjectConfigImportSourceDescriptor | null,
|
||||
): ProjectConfigImportPreviewQueryKey {
|
||||
return [
|
||||
"project-config-import",
|
||||
serverId,
|
||||
repoRoot,
|
||||
...projectConfigImportPreviewQueryRoot(serverId, repoRoot),
|
||||
source ? stableProjectConfigImportSourceKey(source) : "none",
|
||||
] as const;
|
||||
}
|
||||
|
||||
export function projectConfigImportPreviewQueryRoot(serverId: string, repoRoot: string) {
|
||||
return ["project-config-import", serverId, repoRoot] as const;
|
||||
}
|
||||
|
||||
export function stableProjectConfigImportSourceKey(
|
||||
source: ProjectConfigImportSourceDescriptor,
|
||||
): string {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { fetchQueryOptions } from "@/data/query";
|
||||
import {
|
||||
projectConfigImportPreviewQueryInput,
|
||||
projectConfigImportPreviewQueryKey,
|
||||
projectConfigImportPreviewQueryRoot,
|
||||
stableProjectConfigImportSourceKey,
|
||||
} from "./preview-cache";
|
||||
import {
|
||||
@@ -127,9 +128,26 @@ describe("project config import retries", () => {
|
||||
}),
|
||||
).toBe("refresh");
|
||||
});
|
||||
|
||||
it("refreshes the preview after an apply-time source parse failure", () => {
|
||||
expect(
|
||||
projectConfigImportApplyFailureRetryAction({
|
||||
code: "invalid_source_config",
|
||||
source: protocolSource,
|
||||
relativePath: ".conductor/settings.toml",
|
||||
}),
|
||||
).toBe("refresh");
|
||||
});
|
||||
});
|
||||
|
||||
describe("project config import preview cache keys", () => {
|
||||
it("groups source previews under a repository cache root", () => {
|
||||
expect(projectConfigImportPreviewQueryKey("server", "/repo", fakeSource)).toEqual([
|
||||
...projectConfigImportPreviewQueryRoot("server", "/repo"),
|
||||
stableProjectConfigImportSourceKey(fakeSource),
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses the full source descriptor instead of kind alone", () => {
|
||||
const alpha = { kind: "variant-source", profile: "alpha" };
|
||||
const beta = { profile: "beta", kind: "variant-source" };
|
||||
|
||||
@@ -8,7 +8,8 @@ export function projectConfigImportApplyFailureRetryAction(
|
||||
return error.code === "stale_source_config" ||
|
||||
error.code === "stale_project_config" ||
|
||||
error.code === "nothing_to_import" ||
|
||||
error.code === "source_config_not_found"
|
||||
error.code === "source_config_not_found" ||
|
||||
error.code === "invalid_source_config"
|
||||
? "refresh"
|
||||
: "apply";
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
|
||||
import type { ProjectConfigImportIntent } from "@/project-config-import/route";
|
||||
import { ProjectConfigImportSection } from "@/project-config-import/project-config-import-section";
|
||||
import { projectConfigImportPreviewQueryRoot } from "@/project-config-import/preview-cache";
|
||||
import { SettingsTextAreaCard } from "@/components/settings-textarea";
|
||||
import { SettingsGroup } from "@/screens/settings/settings-group";
|
||||
import { SettingsSection } from "@/screens/settings/settings-section";
|
||||
@@ -216,6 +217,7 @@ function ProjectSettingsBody({
|
||||
importIntent,
|
||||
onImportIntentConsumed,
|
||||
}: ProjectSettingsBodyProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const queryKey = useMemo(
|
||||
() => ["project-config", selectedHost.serverId, selectedHost.repoRoot] as const,
|
||||
[selectedHost.serverId, selectedHost.repoRoot],
|
||||
@@ -245,8 +247,11 @@ function ProjectSettingsBody({
|
||||
const readState = resolveProjectConfigReadState(data);
|
||||
|
||||
const handleReload = useCallback(() => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: projectConfigImportPreviewQueryRoot(selectedHost.serverId, selectedHost.repoRoot),
|
||||
});
|
||||
void readQuery.refetch();
|
||||
}, [readQuery]);
|
||||
}, [queryClient, readQuery, selectedHost.repoRoot, selectedHost.serverId]);
|
||||
|
||||
const hasMultipleHosts = hosts.length > 1;
|
||||
|
||||
@@ -517,6 +522,9 @@ function ProjectConfigForm({
|
||||
});
|
||||
setWriteError(null);
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: projectConfigImportPreviewQueryRoot(serverId, repoRoot),
|
||||
});
|
||||
toast.show(t("settings.project.actions.saved"), { variant: "success" });
|
||||
} else {
|
||||
setWriteError(result.error);
|
||||
|
||||
+60
@@ -43,6 +43,16 @@ function writeLocalToml(repo: string, contents: string): void {
|
||||
writeFileSync(join(repo, ".conductor", "settings.local.toml"), contents);
|
||||
}
|
||||
|
||||
function writeSharedJson(repo: string, value: unknown): void {
|
||||
mkdirSync(join(repo, ".conductor"), { recursive: true });
|
||||
writeFileSync(join(repo, ".conductor", "settings.json"), JSON.stringify(value));
|
||||
}
|
||||
|
||||
function writeLocalJson(repo: string, value: unknown): void {
|
||||
mkdirSync(join(repo, ".conductor"), { recursive: true });
|
||||
writeFileSync(join(repo, ".conductor", "settings.local.json"), JSON.stringify(value));
|
||||
}
|
||||
|
||||
function inspect(repo: string, paseoConfig = {}) {
|
||||
return service.inspect({
|
||||
repoRoot: repo,
|
||||
@@ -151,6 +161,8 @@ setup = "local setup"
|
||||
command = "local dev"
|
||||
`,
|
||||
);
|
||||
writeSharedJson(repo, { scripts: { setup: "scoped legacy shared setup" } });
|
||||
writeLocalJson(repo, { scripts: { setup: "scoped legacy local setup" } });
|
||||
writeFileSync(
|
||||
join(repo, "conductor.json"),
|
||||
JSON.stringify({ scripts: { setup: "legacy setup", run: "legacy run" } }),
|
||||
@@ -168,6 +180,54 @@ command = "local dev"
|
||||
});
|
||||
});
|
||||
|
||||
test("deep-merges local run-script fields with shared commands", () => {
|
||||
const repo = makeRepo();
|
||||
writeSharedToml(
|
||||
repo,
|
||||
`
|
||||
[scripts.run.dev]
|
||||
command = "npm run dev"
|
||||
args = ["--shared"]
|
||||
[scripts.run.dev.options]
|
||||
cwd = "apps/web"
|
||||
`,
|
||||
);
|
||||
writeLocalToml(
|
||||
repo,
|
||||
`
|
||||
[scripts.run.dev]
|
||||
args = ["--local"]
|
||||
[scripts.run.dev.options]
|
||||
cwd = "apps/local-web"
|
||||
`,
|
||||
);
|
||||
|
||||
expect(inspect(repo).preview).toMatchObject({
|
||||
scripts: {
|
||||
dev: { command: "cd -- 'apps/local-web' && npm run dev '--local'" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("imports legacy scoped JSON settings with local overrides", () => {
|
||||
const repo = makeRepo();
|
||||
writeSharedJson(repo, {
|
||||
scripts: { setup: "shared setup", run: { dev: { command: "npm run dev" } } },
|
||||
});
|
||||
writeLocalJson(repo, { scripts: { run: { dev: { args: ["--local"] } } } });
|
||||
|
||||
const preview = inspect(repo);
|
||||
|
||||
expect(preview.inputs).toEqual([
|
||||
{ role: "shared", relativePath: ".conductor/settings.json" },
|
||||
{ role: "local", relativePath: ".conductor/settings.local.json" },
|
||||
]);
|
||||
expect(preview.preview).toMatchObject({
|
||||
worktree: { setup: "shared setup" },
|
||||
scripts: { dev: { command: "npm run dev '--local'" } },
|
||||
});
|
||||
});
|
||||
|
||||
test("imports legacy conductor.json when shared TOML is absent", () => {
|
||||
const repo = makeRepo();
|
||||
writeFileSync(
|
||||
|
||||
+33
-12
@@ -94,18 +94,24 @@ function inspectConductorImport(input: {
|
||||
}
|
||||
|
||||
function discoverConductorSources(repoRoot: string): SourceFile[] {
|
||||
const localPath = join(repoRoot, ".conductor", "settings.local.toml");
|
||||
const sharedPath = join(repoRoot, ".conductor", "settings.toml");
|
||||
const legacyPath = join(repoRoot, "conductor.json");
|
||||
const localTomlPath = join(repoRoot, ".conductor", "settings.local.toml");
|
||||
const localJsonPath = join(repoRoot, ".conductor", "settings.local.json");
|
||||
const sharedTomlPath = join(repoRoot, ".conductor", "settings.toml");
|
||||
const sharedJsonPath = join(repoRoot, ".conductor", "settings.json");
|
||||
const rootLegacyPath = join(repoRoot, "conductor.json");
|
||||
const files: SourceFile[] = [];
|
||||
|
||||
if (existsSync(sharedPath)) {
|
||||
files.push(readSourceFile(repoRoot, sharedPath, "shared"));
|
||||
} else if (existsSync(legacyPath)) {
|
||||
files.push(readSourceFile(repoRoot, legacyPath, "legacy"));
|
||||
if (existsSync(sharedTomlPath)) {
|
||||
files.push(readSourceFile(repoRoot, sharedTomlPath, "shared"));
|
||||
} else if (existsSync(sharedJsonPath)) {
|
||||
files.push(readSourceFile(repoRoot, sharedJsonPath, "shared"));
|
||||
} else if (existsSync(rootLegacyPath)) {
|
||||
files.push(readSourceFile(repoRoot, rootLegacyPath, "legacy"));
|
||||
}
|
||||
if (existsSync(localPath)) {
|
||||
files.push(readSourceFile(repoRoot, localPath, "local"));
|
||||
if (existsSync(localTomlPath)) {
|
||||
files.push(readSourceFile(repoRoot, localTomlPath, "local"));
|
||||
} else if (existsSync(localJsonPath)) {
|
||||
files.push(readSourceFile(repoRoot, localJsonPath, "local"));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -127,8 +133,7 @@ function loadConductorSettings(
|
||||
for (const file of sourceFiles) {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed =
|
||||
file.relativePath === "conductor.json" ? JSON.parse(file.bytes) : parseToml(file.bytes);
|
||||
parsed = file.relativePath.endsWith(".json") ? JSON.parse(file.bytes) : parseToml(file.bytes);
|
||||
} catch {
|
||||
throw new InvalidProjectConfigImportSourceError(source, file.relativePath);
|
||||
}
|
||||
@@ -159,7 +164,23 @@ function mergeRunScripts(base: unknown, override: unknown): unknown {
|
||||
if (!isRecord(base) || !isRecord(override)) {
|
||||
return override ?? base;
|
||||
}
|
||||
return { ...base, ...override };
|
||||
const merged: Record<string, unknown> = { ...base };
|
||||
for (const [scriptId, overrideEntry] of Object.entries(override)) {
|
||||
const baseEntry = base[scriptId];
|
||||
merged[scriptId] = mergeRunScriptEntry(baseEntry, overrideEntry);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function mergeRunScriptEntry(base: unknown, override: unknown): unknown {
|
||||
if (!isRecord(base) || !isRecord(override)) {
|
||||
return override ?? base;
|
||||
}
|
||||
const merged: Record<string, unknown> = { ...base, ...override };
|
||||
if (isRecord(base.options) && isRecord(override.options)) {
|
||||
merged.options = { ...base.options, ...override.options };
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function mapLifecycle(
|
||||
|
||||
Reference in New Issue
Block a user