workspace mode management; drone status message socket events added

This commit is contained in:
Rob Colbert 2026-05-03 03:05:06 -04:00
parent d92d61024a
commit c5e5d16a51
11 changed files with 465 additions and 115 deletions

View File

@ -8,5 +8,8 @@
"build": "vite build"
},
"author": "Robert Colbert <rob.colbert@openplatform.us>",
"license": "Apache-2.0"
"license": "Apache-2.0",
"dependencies": {
"slug": "^11.0.1"
}
}

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import slug from "slug";
import type { User, Project } from "../lib/api";
import {
projectApi,
@ -114,13 +115,125 @@ function NewProjectForm({
);
}
interface EditProjectFormProps {
project: Project;
onCancel: () => void;
onSuccess: () => void;
}
function EditProjectForm({
project,
onCancel,
onSuccess,
}: EditProjectFormProps) {
const [name, setName] = useState(project.name);
const [slugValue, setSlugValue] = useState(project.slug);
const [gitUrl, setGitUrl] = useState(project.gitUrl || "");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name || !slugValue) {
setError("Name and slug are required");
return;
}
setSubmitting(true);
setError("");
try {
await projectApi.update(project._id, {
name,
slug: slug(slugValue),
gitUrl: gitUrl || undefined,
});
onSuccess();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to update project");
} finally {
setSubmitting(false);
}
};
return (
<div className="max-w-lg">
<h2 className="text-xl font-semibold mb-6">Edit Project</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm text-text-secondary mb-1">
Project Name *
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
placeholder="My Project"
/>
<p className="text-xs text-text-muted mt-1">
Used for display purposes only
</p>
</div>
<div>
<label className="block text-sm text-text-secondary mb-1">
Project Slug *
</label>
<input
type="text"
value={slugValue}
onChange={(e) => setSlugValue(e.target.value)}
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
placeholder="my-project"
/>
<p className="text-xs text-text-muted mt-1">
Unique identifier for the project directory. Changing this will
affect the workspace directory name on drones.
</p>
</div>
<div>
<label className="block text-sm text-text-secondary mb-1">
Git Repository URL
</label>
<input
type="text"
value={gitUrl}
onChange={(e) => setGitUrl(e.target.value)}
className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none"
placeholder="https://github.com/user/repo.git"
/>
</div>
{error && <p className="text-red-500 text-sm">{error}</p>}
<div className="flex gap-3 pt-2">
<button
type="submit"
disabled={submitting}
className="px-4 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50"
>
{submitting ? "Updating..." : "Save Changes"}
</button>
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border border-border-default text-text-secondary rounded hover:bg-bg-tertiary transition-colors"
>
Cancel
</button>
</div>
</form>
</div>
);
}
interface ProjectInspectorProps {
project: Project;
onDelete: () => void;
onUpdate: () => void;
}
function ProjectInspector({ project, onDelete }: ProjectInspectorProps) {
function ProjectInspector({ project, onDelete, onUpdate }: ProjectInspectorProps) {
const [deleting, setDeleting] = useState(false);
const [editing, setEditing] = useState(false);
const handleDelete = async () => {
if (
@ -145,6 +258,17 @@ function ProjectInspector({ project, onDelete }: ProjectInspectorProps) {
<div className="flex-1 overflow-y-auto p-6">
<div className="max-w-3xl">
<h2 className="text-xl font-semibold mb-6">Project Inspector</h2>
{editing ? (
<EditProjectForm
project={project}
onCancel={() => setEditing(false)}
onSuccess={() => {
setEditing(false);
onUpdate();
}}
/>
) : (
<>
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-bg-secondary border border-border-default rounded">
@ -192,7 +316,13 @@ function ProjectInspector({ project, onDelete }: ProjectInspectorProps) {
</div>
</div>
<div className="pt-6 border-t border-border-subtle">
<div className="pt-6 border-t border-border-subtle flex gap-3">
<button
onClick={() => setEditing(true)}
className="px-4 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors"
>
Edit Project
</button>
<button
onClick={handleDelete}
disabled={deleting}
@ -202,6 +332,8 @@ function ProjectInspector({ project, onDelete }: ProjectInspectorProps) {
</button>
</div>
</div>
</>
)}
</div>
</div>
);
@ -685,6 +817,10 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
navigate("/projects");
};
const handleProjectUpdated = () => {
loadProjects();
};
const handleSelectDrone = (drone: DroneRegistration) => {
setSelectedDrone(drone);
};
@ -795,6 +931,7 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
<ProjectInspector
project={selectedProject}
onDelete={handleProjectDeleted}
onUpdate={handleProjectUpdated}
/>
{/* Right Sidebar - Drones & Chat Sessions */}

View File

@ -229,6 +229,12 @@ importers:
specifier: ^4.1.5
version: 4.1.5(@types/node@24.0.4)(jsdom@29.0.2)(vite@8.0.10(@types/node@24.0.4)(esbuild@0.25.5)(jiti@2.6.1)(less@4.3.0)(tsx@4.21.0))
frontend:
dependencies:
slug:
specifier: ^11.0.1
version: 11.0.1
packages:
'@adobe/css-tools@4.4.4':
@ -2714,6 +2720,10 @@ packages:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
slug@11.0.1:
resolution: {integrity: sha512-VrM060OM/E7rdLQSnp6JHrzFfJFmqQBp0+TMhZStnEB8PfNliaZ9UWYjTHGHLUFVJorZ8TjVd/aKvIxHWU2O7g==}
hasBin: true
socket.io-adapter@2.5.5:
resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==}
@ -5604,6 +5614,8 @@ snapshots:
slash@3.0.0: {}
slug@11.0.1: {}
socket.io-adapter@2.5.5:
dependencies:
debug: 4.3.7

View File

@ -37,7 +37,10 @@ export class CodeSession extends SocketSession {
super.register();
this.socket.on("requestSessionLock", this.onRequestSessionLock.bind(this));
this.socket.on("requestWorkspaceMode", this.onRequestWorkspaceMode.bind(this));
this.socket.on(
"requestWorkspaceMode",
this.onRequestWorkspaceMode.bind(this),
);
this.socket.on("submitPrompt", this.onSubmitPrompt.bind(this));
}
@ -84,7 +87,6 @@ export class CodeSession extends SocketSession {
this.project = project;
SocketService.registerChatSession(chatSession._id, this);
droneSession.setChatSessionId(chatSession._id);
droneSession.setCodeSession(this);
}
cb(success, chatSessionId);
},
@ -192,4 +194,8 @@ export class CodeSession extends SocketSession {
this.workspaceMode = mode;
this.socket.emit("workspaceModeChanged", mode);
}
onStatus(content: string): void {
this.socket.emit("status", content);
}
}

View File

@ -23,7 +23,6 @@ export class DroneSession extends SocketSession {
chatSessionId: GadgetId | undefined;
currentTurnId: GadgetId | undefined;
workspaceMode: WorkspaceMode = WorkspaceMode.Idle;
codeSession: import("./code-session.js").CodeSession | undefined;
constructor(socket: GadgetSocket, registration: IDroneRegistration) {
super(socket, registration.user as IUser);
@ -33,16 +32,41 @@ export class DroneSession extends SocketSession {
register() {
super.register();
this.socket.on("status", this.onStatus.bind(this));
this.socket.on(
"workspaceModeChanged",
this.onWorkspaceModeChanged.bind(this),
);
this.socket.on("thinking", this.onThinking.bind(this));
this.socket.on("response", this.onResponse.bind(this));
this.socket.on("toolCall", this.onToolCall.bind(this));
this.socket.on("workOrderComplete", this.onWorkOrderComplete.bind(this));
this.socket.on(
"requestCrashRecovery",
this.onRequestCrashRecovery.bind(this),
);
this.socket.on("requestTermination", this.onRequestTermination.bind(this));
this.socket.on("workspaceModeChanged", this.onWorkspaceModeChanged.bind(this));
}
async onStatus(message: string): Promise<void> {
if (!this.chatSessionId) {
this.log.warn(
"drone status event received but no chat session is active",
);
return;
}
try {
const codeSession = SocketService.getCodeSessionByChatSessionId(
this.chatSessionId,
);
codeSession.socket.emit("status", message);
} catch (error) {
this.log.error("failed to route status message", { error });
}
}
/**
@ -183,23 +207,22 @@ export class DroneSession extends SocketSession {
this.currentTurnId = turnId;
}
/**
* Sets the associated code session for routing events back to the IDE.
*/
setCodeSession(codeSession: import("./code-session.js").CodeSession): void {
this.codeSession = codeSession;
}
/**
* Called when the drone emits a workspace mode change.
*/
async onWorkspaceModeChanged(mode: WorkspaceMode): Promise<void> {
if (!this.chatSessionId) {
return;
}
this.workspaceMode = mode;
this.log.info("workspace mode changed", { mode });
if (this.codeSession) {
this.codeSession.onWorkspaceModeChanged(mode);
}
const codeSession = SocketService.getCodeSessionByChatSessionId(
this.chatSessionId,
);
codeSession.onWorkspaceModeChanged(mode);
}
/**

View File

@ -32,6 +32,7 @@
"numeral": "^2.0.6",
"ollama": "^0.6.3",
"openai": "^6.34.0",
"simple-git": "^3.36.0",
"socket.io-client": "^4.8.3"
},
"devDependencies": {

View File

@ -226,6 +226,8 @@ class GadgetDrone extends GadgetProcess {
chatSession: IChatSession,
cb: RequestSessionLockCallback,
) {
assert(this.socket, "invalid application state");
/*
* Validate gadget-drone registration to ensure correct sync with IDE
*/
@ -286,8 +288,45 @@ class GadgetDrone extends GadgetProcess {
session: chatSession,
};
this.workspaceMode = WorkspaceMode.Idle;
this.socket.emit("status", "session lock granted");
/*
* Add the project to the workspace, lock to it, and deploy it.
*/
WorkspaceService.addProject({
_id: project._id,
slug: project.slug,
gitUrl: project.gitUrl,
});
WorkspaceService.updateLockedProject({
_id: project._id,
slug: project.slug,
gitUrl: project.gitUrl,
});
const projectDir = WorkspaceService.getProjectDirectory(project.slug);
let haveProjectDir = await WorkspaceService.exists(projectDir);
if (!haveProjectDir) {
this.socket.emit("status", `deploying project [slug=${project.slug}]`);
await WorkspaceService.deployProject(project);
/*
* Make sure a project directory got deployed
*/
haveProjectDir = await WorkspaceService.exists(projectDir);
if (!haveProjectDir) {
return cb(false, "failed to deploy project directory");
}
}
/*
* Commit the changes to the workspace.
*/
await WorkspaceService.writeWorkspaceData();
cb(true, chatSession._id);
this.socket.emit("status", "session lock granted");
}
async onRequestWorkspaceMode(
@ -434,17 +473,29 @@ class GadgetDrone extends GadgetProcess {
cb(true, "work order accepted"); // confirm that drone has the work order
const workspaceDir =
WorkspaceService.workspaceData?.workspaceDir || process.cwd();
const projectDir = WorkspaceService.getProjectDirectory(project.slug);
process.chdir(projectDir);
this.isProcessingWorkOrder = true;
try {
this.socket.emit("status", "processing work order");
await AgentService.process(order, this.socket);
this.socket.emit("status", "work order processing finished");
await WorkspaceService.removeWorkOrderCache();
} catch (error) {
const err = error as Error;
this.log.error("work order processing failed", {
error: err.message,
});
this.socket.emit(
"status",
`failed to process work order: ${(error as Error).message}`,
);
// Leave cache in place for recovery
} finally {
process.chdir(workspaceDir);
this.isProcessingWorkOrder = false;
this.log.info("work order processing complete", {
isProcessingWorkOrder: this.isProcessingWorkOrder,

View File

@ -2,11 +2,43 @@
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// Licensed under the Apache License, Version 2.0
import assert from "node:assert";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { simpleGit, SimpleGit, SimpleGitOptions } from "simple-git";
import { IChatTurn, IProject } from "@gadget/api";
import { GadgetService } from "../lib/service.ts";
import { IChatTurn } from "@gadget/api";
export interface WorkspaceChatSession {
_id: string; // MongoDB ChatSession._id
name: string; // Session name for display
lockedAt: string; // ISO 8601 timestamp
}
export interface WorkspaceLockedProject {
_id: string; // MongoDB Project._id
slug: string; // Project slug (directory name)
gitUrl?: string; // Remote git URL
lockedAt: string; // ISO 8601 timestamp
}
export interface WorkspaceProject {
_id: string;
slug: string;
gitUrl?: string;
clonedAt: string | null;
lastSyncAt: string | null;
}
export interface WorkspaceRegistration {
_id: string; // MongoDB DroneRegistration._id
status: string; // Current drone status
registeredAt: string; // ISO 8601 timestamp
}
export interface WorkspaceData {
workspaceId: string; // UUID v4, immutable once created
@ -15,35 +47,16 @@ export interface WorkspaceData {
workspaceDir: string; // Absolute path to workspace directory
// Active session state (null when idle)
chatSession: {
_id: string; // MongoDB ChatSession._id
name: string; // Session name for display
lockedAt: string; // ISO 8601 timestamp
} | null;
chatSession: WorkspaceChatSession | null;
// Project currently being worked on (null when idle)
lockedProject: {
_id: string; // MongoDB Project._id
slug: string; // Project slug (directory name)
gitUrl: string; // Remote git URL
lockedAt: string; // ISO 8601 timestamp
} | null;
lockedProject: WorkspaceLockedProject | null;
// All projects cloned into this workspace
projects: Array<{
_id: string;
slug: string;
gitUrl: string;
clonedAt: string;
lastSyncAt: string;
}>;
projects: Array<WorkspaceProject>;
// Drone registration (updated each startup)
registration: {
_id: string; // MongoDB DroneRegistration._id
status: string; // Current drone status
registeredAt: string; // ISO 8601 timestamp
} | null;
registration: WorkspaceRegistration | null;
}
export interface WorkOrderCache {
@ -57,9 +70,12 @@ export interface WorkOrderCache {
class WorkspaceService extends GadgetService {
private gadgetDir: string = "";
private cacheDir: string = "";
private workspaceFile: string = "";
private _workspaceData: WorkspaceData | null = null;
private repos = new Map<string, SimpleGit>();
get name(): string {
return "WorkspaceService";
}
@ -111,7 +127,7 @@ class WorkspaceService extends GadgetService {
*/
private async loadOrCreateWorkspaceData(workspaceDir: string): Promise<void> {
try {
if (await this.fileExists(this.workspaceFile)) {
if (await this.exists(this.workspaceFile)) {
// Load existing workspace
const content = await fs.promises.readFile(this.workspaceFile, "utf-8");
this._workspaceData = JSON.parse(content) as WorkspaceData;
@ -175,34 +191,59 @@ class WorkspaceService extends GadgetService {
* Updates the locked project in workspace data.
*/
updateLockedProject(
project: { _id: string; slug: string; gitUrl: string } | null,
project: {
_id: string; // MongoDB Project._id
slug: string; // Project slug (directory name)
gitUrl?: string; // Remote git URL
} | null,
): void {
if (!this._workspaceData) return;
this._workspaceData.lockedProject = project
? {
...project,
lockedAt: new Date().toISOString(),
if (!project) {
this._workspaceData.lockedProject = null;
return;
}
: null;
this._workspaceData.lockedProject = Object.assign(
{
lockedAt: new Date().toISOString(),
},
project,
);
}
/**
* Adds a project to the workspace projects list.
*/
addProject(project: { _id: string; slug: string; gitUrl: string }): void {
addProject(project: { _id: string; slug: string; gitUrl?: string }): void {
if (!this._workspaceData) return;
const existing = this._workspaceData.projects.find(
(p) => p.slug === project.slug,
);
if (!existing) {
if (existing) {
existing._id = project._id;
if (project.gitUrl) {
existing.gitUrl = project.gitUrl;
} else {
delete existing.gitUrl;
}
return;
}
this._workspaceData.projects.push({
...project,
clonedAt: new Date().toISOString(),
lastSyncAt: new Date().toISOString(),
clonedAt: null,
lastSyncAt: null,
});
}
async deployProject(project: IProject): Promise<void> {
assert(this.workspaceData, "workspace uninitialized");
if (project.gitUrl) {
const git: SimpleGit = this.getGitRepo(project.slug);
git.clone(project.gitUrl);
return;
}
}
/**
@ -258,7 +299,7 @@ class WorkspaceService extends GadgetService {
async removeWorkOrderCache(): Promise<void> {
const cacheFile = path.join(this.cacheDir, "work-order.json");
try {
if (await this.fileExists(cacheFile)) {
if (await this.exists(cacheFile)) {
await fs.promises.unlink(cacheFile);
this.log.info("work order cache removed");
}
@ -276,7 +317,7 @@ class WorkspaceService extends GadgetService {
async readWorkOrderCache(): Promise<WorkOrderCache | null> {
const cacheFile = path.join(this.cacheDir, "work-order.json");
try {
if (await this.fileExists(cacheFile)) {
if (await this.exists(cacheFile)) {
const content = await fs.promises.readFile(cacheFile, "utf-8");
return JSON.parse(content) as WorkOrderCache;
}
@ -290,9 +331,9 @@ class WorkspaceService extends GadgetService {
}
/**
* Checks if a file exists.
* Checks if a file or directory exists.
*/
private async fileExists(filePath: string): Promise<boolean> {
async exists(filePath: string): Promise<boolean> {
try {
await fs.promises.access(filePath);
return true;
@ -300,6 +341,35 @@ class WorkspaceService extends GadgetService {
return false;
}
}
getProjectDirectory(slug: string): string {
assert(this.workspaceData, "workspace uninitialized");
return path.join(this.workspaceData?.workspaceDir, slug);
}
getGitRepo(slug: string): SimpleGit {
assert(this.workspaceData, "workspace uninitialized");
/*
* If we have a SimpleGit instance for the project slug, return it.
*/
let git: SimpleGit | undefined = this.repos.get(slug);
if (git) {
return git;
}
/*
* Create a new SimpleGit instance rooted in the project's workspace
* directory, add it to the repo cache, and return it.
*/
const options: Partial<SimpleGitOptions> = {
baseDir: this.getProjectDirectory(slug),
};
git = simpleGit(options);
this.repos.set(slug, git);
return git;
}
}
export default new WorkspaceService();

View File

@ -18,6 +18,8 @@ export type ProcessWorkOrderMessage = (
cb: ProcessWorkOrderCallback,
) => void;
export type StatusMessage = (content: string) => void;
export type ThinkingMessage = (content: string) => void;
export type ResponseMessage = (content: string) => void;

View File

@ -3,6 +3,7 @@
// Licensed under the Apache License, Version 2.0
import {
StatusMessage,
ProcessWorkOrderMessage,
ResponseMessage,
ThinkingMessage,
@ -50,6 +51,7 @@ export interface ClientToServerEvents {
* gadget-drone => gadget-code:web
*/
status: StatusMessage;
thinking: ThinkingMessage;
response: ResponseMessage;
toolCall: ToolCallMessage;
@ -74,6 +76,7 @@ export interface ServerToClientEvents {
* gadget-code:web => gadget-code:ide
*/
status: StatusMessage;
thinking: ThinkingMessage;
response: ResponseMessage;
toolCall: ToolCallMessage;

View File

@ -287,6 +287,9 @@ importers:
openai:
specifier: ^6.34.0
version: 6.34.0(ws@8.18.3)
simple-git:
specifier: ^3.36.0
version: 3.36.0
socket.io-client:
specifier: ^4.8.3
version: 4.8.3
@ -943,6 +946,12 @@ packages:
'@kurkle/color@0.3.4':
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
'@kwsites/file-exists@1.1.1':
resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==}
'@kwsites/promise-deferred@1.1.1':
resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==}
'@mediapipe/tasks-vision@0.10.17':
resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==}
@ -1150,6 +1159,12 @@ packages:
'@rolldown/pluginutils@1.0.0-rc.7':
resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==}
'@simple-git/args-pathspec@1.0.3':
resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==}
'@simple-git/argv-parser@1.1.1':
resolution: {integrity: sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==}
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
@ -3171,6 +3186,9 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-git@3.36.0:
resolution: {integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@ -4103,6 +4121,14 @@ snapshots:
'@kurkle/color@0.3.4': {}
'@kwsites/file-exists@1.1.1':
dependencies:
debug: 4.4.3
transitivePeerDependencies:
- supports-color
'@kwsites/promise-deferred@1.1.1': {}
'@mediapipe/tasks-vision@0.10.17': {}
'@mongodb-js/saslprep@1.4.9':
@ -4265,6 +4291,12 @@ snapshots:
'@rolldown/pluginutils@1.0.0-rc.7': {}
'@simple-git/args-pathspec@1.0.3': {}
'@simple-git/argv-parser@1.1.1':
dependencies:
'@simple-git/args-pathspec': 1.0.3
'@socket.io/component-emitter@3.1.2': {}
'@standard-schema/spec@1.1.0': {}
@ -6421,6 +6453,16 @@ snapshots:
signal-exit@4.1.0: {}
simple-git@3.36.0:
dependencies:
'@kwsites/file-exists': 1.1.1
'@kwsites/promise-deferred': 1.1.1
'@simple-git/args-pathspec': 1.0.3
'@simple-git/argv-parser': 1.1.1
debug: 4.4.3
transitivePeerDependencies:
- supports-color
slash@3.0.0: {}
slug@11.0.1: {}