feat: git sync improvements (#6182)

* init checkpoint

* ui second pass...

* round 1 backend + saving settings + detecting changes...

* checkpoint

* fix openapi

* saving + correct wmill.yaml diff

* cli refactor

* cli and tests refactor done

* cli multi workspace support

* cli support skip core types to align with ui

* new test framework

* sqlx

* openapi spec

* frontend

* sync + settings changes

* some fixes

* some fixes

* security: Remove hardcoded EE license key, use environment variable only

- Remove hardcoded license key from containerized test backend
- Environment variable EE_LICENSE_KEY now required for EE features
- License key no longer stored in database during tests

* sqlx

* tests

* fixing tests

* fix tests

* checkpoint

* checkpoint

* cli build

* frontend - cli exchange

* settings match

* ee repo ref

* npm check

* openapi

* tests

* checkpoint

* cli + tests

* reset to preview on changes

* merge issue ee

* cleanup

* hubscript

* simplifications

* ee repo ref

* cli fixes

* fix sync and add tests

* extra test

* git sync settings / key change aware

* ee-repo ref

* ee-repo ref

* ee repo ref

* ee ref

* review 1

* ee ref

* Update frontend/src/lib/components/PullGitRepoPopover.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* ee ref

* remove extra includes from ui

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
Alexander Petric
2025-07-15 12:25:31 -04:00
committed by GitHub
parent 574f14b349
commit 27bf4e34d8
41 changed files with 7892 additions and 924 deletions

View File

@@ -2,7 +2,7 @@
import { GlobalOptions } from "./types.ts";
import { getRootStore } from "./store.ts";
import { loginInteractive, tryGetLoginInfo } from "./login.ts";
import { colors, Command, Input, log, setClient, Table } from "./deps.ts";
import { colors, Command, Confirm, Input, log, setClient, Table } from "./deps.ts";
import * as wmill from "./gen/services.gen.ts";
import { requireLogin } from "./context.ts";
@@ -14,9 +14,9 @@ export interface Workspace {
token: string;
}
export async function allWorkspaces(): Promise<Workspace[]> {
export async function allWorkspaces(configDirOverride?: string): Promise<Workspace[]> {
try {
const file = (await getRootStore()) + "remotes.ndjson";
const file = (await getRootStore(configDirOverride)) + "remotes.ndjson";
const txt = await Deno.readTextFile(file);
return txt
.split("\n")
@@ -40,7 +40,7 @@ async function getActiveWorkspaceName(
return opts?.workspace;
}
try {
return await Deno.readTextFile((await getRootStore()) + "/activeWorkspace");
return await Deno.readTextFile((await getRootStore(opts?.configDir)) + "/activeWorkspace");
} catch {
return undefined;
}
@@ -53,13 +53,14 @@ export async function getActiveWorkspace(
if (!name) {
return undefined;
}
return await getWorkspaceByName(name);
return await getWorkspaceByName(name, opts?.configDir);
}
export async function getWorkspaceByName(
workspaceName: string
workspaceName: string,
configDirOverride?: string
): Promise<Workspace | undefined> {
const workspaceStream = await allWorkspaces();
const workspaceStream = await allWorkspaces(configDirOverride);
for await (const workspace of workspaceStream) {
if (workspace.name === workspaceName) {
return workspace;
@@ -69,7 +70,7 @@ export async function getWorkspaceByName(
}
async function list(opts: GlobalOptions) {
const workspaces = await allWorkspaces();
const workspaces = await allWorkspaces(opts.configDir);
const activeName = await getActiveWorkspaceName(opts);
new Table()
@@ -101,7 +102,7 @@ async function switchC(opts: GlobalOptions, workspaceName: string) {
return;
}
const all = await allWorkspaces();
const all = await allWorkspaces(opts.configDir);
if (all.findIndex((x) => x.name === workspaceName) === -1) {
log.info(
colors.red.bold(
@@ -115,13 +116,13 @@ async function switchC(opts: GlobalOptions, workspaceName: string) {
return;
}
await setActiveWorkspace(workspaceName);
await setActiveWorkspace(workspaceName, opts.configDir);
return;
}
export async function setActiveWorkspace(workspaceName: string) {
export async function setActiveWorkspace(workspaceName: string, configDirOverride?: string) {
await Deno.writeTextFile(
(await getRootStore()) + "/activeWorkspace",
(await getRootStore(configDirOverride)) + "/activeWorkspace",
workspaceName
);
}
@@ -246,7 +247,7 @@ export async function add(
},
opts
);
await setActiveWorkspace(workspaceName);
await setActiveWorkspace(workspaceName, opts.configDir);
log.info(
colors.green.underline(
@@ -257,8 +258,85 @@ export async function add(
export async function addWorkspace(workspace: Workspace, opts: any) {
workspace.remote = new URL(workspace.remote).toString(); // add trailing slash in all cases!
// Check for conflicts before adding
const existingWorkspaces = await allWorkspaces(opts.configDir);
const isInteractive = Deno.stdin.isTerminal() && Deno.stdout.isTerminal() && !opts.force;
// Check 1: Workspace name already exists
const nameConflict = existingWorkspaces.find(w => w.name === workspace.name);
if (nameConflict) {
// If it's the exact same workspace (same remote + workspaceId), just update the token
if (nameConflict.remote === workspace.remote && nameConflict.workspaceId === workspace.workspaceId) {
log.info(colors.yellow(`Updating token for existing workspace "${workspace.name}"`));
} else {
// Different remote or workspaceId - this is a conflict
log.info(colors.red.bold(`❌ Workspace name "${workspace.name}" already exists!`));
log.info(` Existing: ${nameConflict.workspaceId} on ${nameConflict.remote}`);
log.info(` New: ${workspace.workspaceId} on ${workspace.remote}`);
if (!isInteractive) {
// In non-interactive mode (tests, scripts), auto-overwrite with force flag
if (opts.force) {
log.info(colors.yellow("Force flag enabled, overwriting existing workspace."));
} else {
throw new Error("Workspace name conflict. Use --force to overwrite or choose a different name.");
}
} else {
const overwrite = await Confirm.prompt({
message: "Do you want to overwrite the existing workspace?",
default: false,
});
if (!overwrite) {
log.info(colors.yellow("Operation cancelled."));
return;
}
}
}
}
// Check 2: Same (remote, workspaceId) tuple already exists under different name
const tupleConflict = existingWorkspaces.find(w =>
w.remote === workspace.remote &&
w.workspaceId === workspace.workspaceId &&
w.name !== workspace.name
);
if (tupleConflict) {
log.info(colors.red.bold(`❌ Workspace ${workspace.workspaceId} on ${workspace.remote} already exists!`));
log.info(` Existing name: "${tupleConflict.name}"`);
log.info(` New name: "${workspace.name}"`);
log.info(colors.yellow(`\nNote: Backend constraint prevents duplicate (remote, workspaceId) combinations.`));
if (!isInteractive) {
// In non-interactive mode (tests, scripts), auto-overwrite with force flag
if (opts.force) {
log.info(colors.yellow(`Force flag enabled, overwriting existing workspace "${tupleConflict.name}".`));
} else {
throw new Error(`Backend constraint violation: (${workspace.remote}, ${workspace.workspaceId}) already exists as "${tupleConflict.name}". Use --force to overwrite.`);
}
} else {
const overwrite = await Confirm.prompt({
message: `Do you want to overwrite the existing workspace "${tupleConflict.name}"?`,
default: false,
});
if (!overwrite) {
log.info(colors.yellow("Operation cancelled."));
return;
}
}
// Remove the conflicting workspace
await removeWorkspace(tupleConflict.name, true, opts);
}
// Remove existing workspace with same name (if updating)
await removeWorkspace(workspace.name, true, opts);
const file = await Deno.open((await getRootStore()) + "remotes.ndjson", {
// Add the new workspace
const file = await Deno.open((await getRootStore(opts.configDir)) + "remotes.ndjson", {
append: true,
write: true,
read: true,
@@ -274,7 +352,7 @@ export async function removeWorkspace(
silent: boolean,
opts: any
) {
const orgWorkspaces = await allWorkspaces();
const orgWorkspaces = await allWorkspaces(opts.configDir);
if (orgWorkspaces.findIndex((x) => x.name === name) === -1) {
if (!silent) {
log.info(
@@ -290,7 +368,7 @@ export async function removeWorkspace(
}
await Deno.writeTextFile(
(await getRootStore()) + "remotes.ndjson",
(await getRootStore(opts.configDir)) + "remotes.ndjson",
orgWorkspaces
.filter((x) => x.name !== name)
.map((x) => JSON.stringify(x))