feat: add main features

Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
2026-06-06 01:27:01 +02:00
parent 55b3bee2c8
commit 307ae71857
273 changed files with 48740 additions and 0 deletions

View File

@ -0,0 +1,103 @@
/**
* Pure, framework-free helpers for the first-run wizard (testable without React
* or Tauri, ARCHITECTURE §1.3). Validation of a custom/edited profile and small
* factories live here; the components only render.
*/
import type {
AgentProfile,
ContextInjection,
InjectionStrategy,
} from "@/domain";
/** A field-keyed validation error map (empty ⇒ valid). */
export type ProfileErrors = Partial<
Record<"name" | "command" | "target" | "flag" | "var", string>
>;
/** Whether a path is a relative, traversal-free file name (mirror of backend). */
export function isRelativeSafe(path: string): boolean {
if (path.length === 0) return false;
if (path.startsWith("/") || path.startsWith("\\")) return false;
// Windows drive (C:) / UNC.
if (/^[a-zA-Z]:/.test(path)) return false;
return !path.split(/[/\\]/).includes("..");
}
/** Whether a string is a valid environment-variable identifier. */
export function isValidEnvVar(v: string): boolean {
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(v);
}
/**
* Validates a profile draft the way the backend would, so the wizard can surface
* errors before any `invoke`. Returns an empty object when the draft is valid.
*/
export function validateProfile(p: AgentProfile): ProfileErrors {
const errors: ProfileErrors = {};
if (p.name.trim().length === 0) errors.name = "Name is required.";
if (p.command.trim().length === 0) errors.command = "Command is required.";
const ci = p.contextInjection;
if (ci.strategy === "conventionFile") {
if (!isRelativeSafe(ci.target)) {
errors.target = "Target must be a relative file name (no .. or absolute).";
}
} else if (ci.strategy === "flag") {
if (ci.flag.trim().length === 0) errors.flag = "Flag is required.";
} else if (ci.strategy === "env") {
if (!isValidEnvVar(ci.var)) errors.var = "Must be a valid env var identifier.";
}
return errors;
}
/** Whether a profile draft is valid (no errors). */
export function isProfileValid(p: AgentProfile): boolean {
return Object.keys(validateProfile(p)).length === 0;
}
/**
* Builds a default {@link ContextInjection} for a strategy, so switching the
* strategy dropdown produces a sensible editable shape.
*/
export function defaultInjection(strategy: InjectionStrategy): ContextInjection {
switch (strategy) {
case "conventionFile":
return { strategy, target: "CONTEXT.md" };
case "flag":
return { strategy, flag: "--context-file {path}" };
case "env":
return { strategy, var: "AGENT_CONTEXT_FILE" };
case "stdin":
return { strategy };
}
}
/** A fresh, empty custom-profile draft (id minted client-side). */
export function emptyCustomProfile(): AgentProfile {
return {
id: newProfileId(),
name: "",
command: "",
args: [],
contextInjection: { strategy: "conventionFile", target: "CONTEXT.md" },
detect: null,
cwdTemplate: "{projectRoot}",
};
}
/** Generates a UUID for a client-created profile (crypto when available). */
export function newProfileId(): string {
const c = globalThis.crypto as Crypto | undefined;
if (c && typeof c.randomUUID === "function") return c.randomUUID();
// Fallback for non-secure contexts/tests.
return `profile-${Math.random().toString(36).slice(2, 10)}`;
}
/** Parses a whitespace-separated args string into a trimmed, non-empty list. */
export function parseArgs(raw: string): string[] {
return raw
.split(/\s+/)
.map((s) => s.trim())
.filter((s) => s.length > 0);
}