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:
103
frontend/src/features/first-run/profile.ts
Normal file
103
frontend/src/features/first-run/profile.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user