front-end progress; Socket.IO messaging API; inquirer for credentials

This commit is contained in:
Rob Colbert 2026-04-29 00:24:38 -04:00
parent 56ba613cc7
commit db0d1586d6
19 changed files with 788 additions and 78 deletions

View File

@ -29,6 +29,7 @@ The IDE uses the following color palette (CSS custom properties):
``` ```
Font stack: Font stack:
- Primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif - Primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif
- Code/Retro: Courier New, Courier, monospace - Code/Retro: Courier New, Courier, monospace
@ -80,14 +81,14 @@ Implementation: `frontend/src/components/StatusBar.tsx`
Between the header and status bars is the Content Area. It uses React Router for URL-based navigation: Between the header and status bars is the Content Area. It uses React Router for URL-based navigation:
| Route | View | | Route | View |
|-------|------| | ----------------- | ------------------------------------------------- |
| `/` | Home (authenticated dashboard or unauthenticated) | | `/` | Home (authenticated dashboard or unauthenticated) |
| `/projects` | Project Manager (list view) | | `/projects` | Project Manager (list view) |
| `/projects/:slug` | Project Manager (project selected) | | `/projects/:slug` | Project Manager (project selected) |
| `/projects/new` | New project form | | `/projects/new` | New project form |
| `/sign-in` | Sign in page | | `/sign-in` | Sign in page |
| `/sign-out` | Signs out and redirects to `/` | | `/sign-out` | Signs out and redirects to `/` |
## Unauthenticated Home View ## Unauthenticated Home View
@ -132,6 +133,7 @@ Select a project or chat session from the sidebar to get started.
``` ```
Components: Components:
1. Clock - local time in AM/PM format, current date 1. Clock - local time in AM/PM format, current date
2. Projects list - links to `/projects/:slug`, [+]/link navigates to `/projects` 2. Projects list - links to `/projects/:slug`, [+]/link navigates to `/projects`
3. Drones list - status indicator (green=available, yellow=busy, gray=offline) 3. Drones list - status indicator (green=available, yellow=busy, gray=offline)
@ -174,6 +176,7 @@ When a project is selected:
``` ```
Features: Features:
- Left sidebar: Project list with [+ New Project] button - Left sidebar: Project list with [+ New Project] button
- Project Inspector: Shows name, slug, gitUrl, status, createdAt - Project Inspector: Shows name, slug, gitUrl, status, createdAt
- Delete: Confirmation before deletion - Delete: Confirmation before deletion
@ -205,12 +208,13 @@ Triggered by [New Project] button or `/projects/new` route:
All API requests include the JWT in the Authorization header: All API requests include the JWT in the Authorization header:
```typescript ```typescript
headers['Authorization'] = `Bearer ${token}`; headers["Authorization"] = `Bearer ${token}`;
``` ```
### Backend Session Restoration ### Backend Session Restoration
The backend restores user sessions from: The backend restores user sessions from:
1. `Authorization: Bearer <token>` header (JWT) 1. `Authorization: Bearer <token>` header (JWT)
2. Express session (fallback) 2. Express session (fallback)
@ -221,6 +225,7 @@ The `requireUser()` middleware ensures endpoints are authenticated.
### Password Field Handling ### Password Field Handling
Password credentials are NEVER exposed: Password credentials are NEVER exposed:
- Mongoose `select: false` on password fields in models - Mongoose `select: false` on password fields in models
- Population uses `select: "-passwordSalt -password"` to exclude from queries - Population uses `select: "-passwordSalt -password"` to exclude from queries
- Frontend User interface only includes: `_id`, `email`, `displayName`, `flags` - Frontend User interface only includes: `_id`, `email`, `displayName`, `flags`
@ -233,16 +238,17 @@ The Chat Session View presents:
Work Area | Session Status Work Area | Session Status
----------------------------------------------|--------------- ----------------------------------------------|---------------
Chat Messages | Chat: name Chat Messages | Chat: name
| ID: ... | ID: ...
| Model: ... | Model: ...
------------------------------------------|--------------- ----------------------------------------------|---------------
[Prompt input ][Expand][Send] | TC | FO | SA [Prompt input ][Expand][Send]| TC | FO | SA
----------------------------------------------|--------------- ----------------------------------------------|---------------
Log | Files Log | Files
| |
``` ```
Implemented components: Implemented components:
- Chat Messages (stubbed) - Chat Messages (stubbed)
- Prompt Input (stubbed) - Prompt Input (stubbed)
- Session Status sidebar (stubbed) - Session Status sidebar (stubbed)

View File

@ -108,3 +108,17 @@ export const projectApi = {
api.put<Project>(`/api/v1/projects/${id}`, data), api.put<Project>(`/api/v1/projects/${id}`, data),
delete: (id: string) => api.delete<void>(`/api/v1/projects/${id}`), delete: (id: string) => api.delete<void>(`/api/v1/projects/${id}`),
}; };
export interface DroneRegistration {
_id: string;
hostname: string;
workspaceDir: string;
status: 'starting' | 'available' | 'busy' | 'offline';
user: string;
createdAt: string;
updatedAt: string;
}
export const droneApi = {
getAll: () => api.get<DroneRegistration[]>('/api/v1/drone/registration'),
};

View File

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import type { User, Project } from '../lib/api'; import type { User, Project, DroneRegistration } from '../lib/api';
import { projectApi } from '../lib/api'; import { projectApi, droneApi } from '../lib/api';
import Clock from '../components/Clock'; import Clock from '../components/Clock';
const ansiColors = [ const ansiColors = [
@ -51,23 +51,79 @@ function AnsiLogo() {
interface DashboardSidebarProps { interface DashboardSidebarProps {
onNavigate: (view: string, data?: unknown) => void; onNavigate: (view: string, data?: unknown) => void;
selectedDrone: DroneRegistration | null;
onSelectDrone: (drone: DroneRegistration | null) => void;
} }
function DashboardSidebar({ onNavigate }: DashboardSidebarProps) { function DroneInspector({ drone, onClose }: { drone: DroneRegistration; onClose: () => void }) {
return (
<div className="flex-1 flex items-center justify-center p-8">
<div className="max-w-lg w-full">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold">Drone Inspector</h2>
<button
onClick={onClose}
className="text-text-secondary hover:text-text-primary text-sm"
>
Back to Dashboard
</button>
</div>
<div className="bg-bg-secondary border border-border-default rounded p-4 space-y-4">
<div>
<div className="text-sm text-text-muted">Hostname</div>
<div className="font-mono text-text-primary">{drone.hostname}</div>
</div>
<div>
<div className="text-sm text-text-muted">Workspace</div>
<div className="font-mono text-text-primary text-sm truncate">{drone.workspaceDir}</div>
</div>
<div>
<div className="text-sm text-text-muted">Status</div>
<div className="flex items-center gap-2">
<span
className={`w-2 h-2 rounded-full ${
drone.status === 'available'
? 'bg-green-500'
: drone.status === 'busy'
? 'bg-yellow-500'
: 'bg-gray-600'
}`}
/>
<span className="text-text-primary capitalize">{drone.status}</span>
</div>
</div>
<div>
<div className="text-sm text-text-muted">Registered</div>
<div className="text-text-primary text-sm">
{new Date(drone.createdAt).toLocaleString()}
</div>
</div>
</div>
</div>
</div>
);
}
function DashboardSidebar({ onNavigate, selectedDrone, onSelectDrone }: DashboardSidebarProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
const [drones, setDrones] = useState<DroneRegistration[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
loadProjects(); loadData();
}, []); }, []);
const loadProjects = async () => { const loadData = async () => {
try { try {
const data = await projectApi.getAll(); const [projectsData, dronesData] = await Promise.all([
setProjects(data); projectApi.getAll(),
droneApi.getAll(),
]);
setProjects(projectsData);
setDrones(dronesData);
} catch (err) { } catch (err) {
console.error('Failed to load projects', err); console.error('Failed to load dashboard data', err);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -110,9 +166,51 @@ function DashboardSidebar({ onNavigate }: DashboardSidebarProps) {
<div className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2"> <div className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
Drones Drones
</div> </div>
<div className="space-y-1"> {loading ? (
<p className="text-sm text-text-muted px-2">Loading...</p> <p className="text-sm text-text-muted px-2">Loading...</p>
</div> ) : drones.length === 0 ? (
<p className="text-sm text-text-muted px-2">No drones available.</p>
) : (
<div className="space-y-1">
{drones
.filter((d) => d.status === 'available' || d.status === 'busy')
.map((drone) => (
<button
key={drone._id}
onClick={() => onSelectDrone(drone)}
className={`w-full text-left px-2 py-1 rounded transition-colors ${
selectedDrone?._id === drone._id
? 'bg-brand text-white'
: 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
}`}
>
<div className="flex items-center gap-2">
<span
className={`w-2 h-2 rounded-full shrink-0 ${
drone.status === 'available'
? 'bg-green-500'
: 'bg-yellow-500'
}`}
/>
<div className="overflow-hidden">
<div className="text-sm truncate">{drone.hostname}</div>
{drone.workspaceDir && (
<div className={`text-xs truncate ${
selectedDrone?._id === drone._id
? 'text-white/70'
: 'text-text-muted'
}`}>
{drone.workspaceDir.length > 24
? '...' + drone.workspaceDir.slice(-21)
: drone.workspaceDir}
</div>
)}
</div>
</div>
</button>
))}
</div>
)}
</div> </div>
<div className="p-3 border-t border-border-subtle"> <div className="p-3 border-t border-border-subtle">
@ -133,6 +231,7 @@ interface HomeProps {
export default function Home({ user }: HomeProps) { export default function Home({ user }: HomeProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [selectedDrone, setSelectedDrone] = useState<DroneRegistration | null>(null);
if (!user) { if (!user) {
return ( return (
@ -144,33 +243,41 @@ export default function Home({ user }: HomeProps) {
return ( return (
<div className="flex-1 flex bg-bg-primary"> <div className="flex-1 flex bg-bg-primary">
<div className="flex-1 flex items-center justify-center p-8"> {selectedDrone ? (
<div className="text-center"> <DroneInspector drone={selectedDrone} onClose={() => setSelectedDrone(null)} />
<h1 className="text-2xl font-semibold mb-4"> ) : (
Welcome, {user.displayName}! <div className="flex-1 flex items-center justify-center p-8">
</h1> <div className="text-center">
<p className="text-text-secondary mb-4"> <h1 className="text-2xl font-semibold mb-4">
Your dashboard is under construction. Welcome, {user.displayName}!
</p> </h1>
<p className="text-text-muted text-sm mb-6"> <p className="text-text-secondary mb-4">
Select a project or chat session from the sidebar to get started. Your dashboard is under construction.
</p> </p>
<div className="flex gap-3 justify-center"> <p className="text-text-muted text-sm mb-6">
<Link Select a project or chat session from the sidebar to get started.
to="/projects" </p>
className="px-4 py-2 border border-border-default text-text-secondary hover:bg-bg-tertiary hover:text-text-primary rounded transition-colors" <div className="flex gap-3 justify-center">
> <Link
Open Project Manager to="/projects"
</Link> className="px-4 py-2 border border-border-default text-text-secondary hover:bg-bg-tertiary hover:text-text-primary rounded transition-colors"
>
Open Project Manager
</Link>
</div>
</div> </div>
</div> </div>
</div> )}
<DashboardSidebar onNavigate={(view) => { <DashboardSidebar
if (view === 'project' && typeof navigate === 'function') { onNavigate={(view) => {
navigate('/projects'); if (view === 'project' && typeof navigate === 'function') {
} navigate('/projects');
}} /> }
}}
selectedDrone={selectedDrone}
onSelectDrone={setSelectedDrone}
/>
</div> </div>
); );
} }

View File

@ -229,27 +229,41 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
</button> </button>
</div> </div>
<div className="flex-1 overflow-y-auto p-2"> <div className="flex-1 overflow-y-auto p-2">
<div className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2 px-2"> {showNewForm ? (
Projects ({projects.length}) <div className="p-2">
</div> <p className="text-sm text-text-muted mb-2">Creating new project...</p>
{projects.length === 0 ? ( <button
<p className="text-sm text-text-muted px-2">No projects yet.</p> onClick={() => setShowNewForm(false)}
) : ( className="text-sm text-text-secondary hover:text-text-primary transition-colors"
<div className="space-y-1"> >
{projects.map((project) => ( Cancel
<button </button>
key={project._id}
onClick={() => handleSelectProject(project)}
className={`w-full text-left px-2 py-1.5 text-sm rounded truncate transition-colors ${
selectedProject?.slug === project.slug
? 'bg-brand text-white'
: 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
}`}
>
{project.name}
</button>
))}
</div> </div>
) : (
<>
<div className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2 px-2">
Projects ({projects.length})
</div>
{projects.length === 0 ? (
<p className="text-sm text-text-muted px-2">No projects yet.</p>
) : (
<div className="space-y-1">
{projects.map((project) => (
<button
key={project._id}
onClick={() => handleSelectProject(project)}
className={`w-full text-left px-2 py-1.5 text-sm rounded truncate transition-colors ${
selectedProject?.slug === project.slug
? 'bg-brand text-white'
: 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
}`}
>
{project.name}
</button>
))}
</div>
)}
</>
)} )}
</div> </div>
</aside> </aside>

View File

@ -26,7 +26,9 @@ export class DroneApiControllerV1 extends DtpController {
} }
async start(): Promise<void> { async start(): Promise<void> {
const requireUser = this.requireUser();
const multer = this.createMulter(this.slug, {}); const multer = this.createMulter(this.slug, {});
this.router.param("registrationId", populateDroneRegistrationById(this)); this.router.param("registrationId", populateDroneRegistrationById(this));
this.router.post( this.router.post(
@ -40,6 +42,12 @@ export class DroneApiControllerV1 extends DtpController {
this.putRegistrationStatus.bind(this), this.putRegistrationStatus.bind(this),
); );
this.router.get(
"/registration",
requireUser,
this.getRegistrations.bind(this),
);
this.router.delete( this.router.delete(
"/registration/:registrationId", "/registration/:registrationId",
this.deleteRegistration.bind(this), this.deleteRegistration.bind(this),
@ -82,6 +90,24 @@ export class DroneApiControllerV1 extends DtpController {
} }
} }
async getRegistrations(req: Request, res: Response): Promise<void> {
try {
const registrations = await DroneService.getForUser(req.user);
res.status(200).json({
success: true,
data: registrations,
});
} catch (error) {
this.log.error("failed to present drone registrations for User", {
error,
});
res.status((error as Error).statusCode || 500).json({
success: false,
message: (error as Error).message,
});
}
}
async deleteRegistration(_req: Request, res: Response): Promise<void> { async deleteRegistration(_req: Request, res: Response): Promise<void> {
try { try {
await DroneService.unregister(res.locals.registration); await DroneService.unregister(res.locals.registration);

View File

@ -17,6 +17,7 @@ export const DroneRegistrationSchema = new Schema<IDroneRegistration>({
default: DroneStatus.Starting, default: DroneStatus.Starting,
required: true, required: true,
}, },
chatSessionId: { type: String, required: false },
currentJobId: { type: String, required: false }, currentJobId: { type: String, required: false },
}); });

View File

@ -4,13 +4,19 @@
import { PopulateOptions, Types } from "mongoose"; import { PopulateOptions, Types } from "mongoose";
import { IUser, DroneStatus, IDroneRegistration } from "@gadget/api"; import {
IUser,
DroneStatus,
IDroneRegistration,
IChatSession,
} from "@gadget/api";
import DroneRegistration from "@/models/drone-registration.js"; import DroneRegistration from "@/models/drone-registration.js";
import { DtpService } from "../lib/service.js"; import { DtpService } from "../lib/service.js";
export interface IDroneDefinition { export interface IDroneDefinition {
hostname: string; hostname: string;
workspaceDir: string;
} }
class DroneService extends DtpService { class DroneService extends DtpService {
@ -42,6 +48,13 @@ class DroneService extends DtpService {
this.log.info("service stopped"); this.log.info("service stopped");
} }
/**
* The drone calls this service to register itself into the platform as
* belonging to a specific User. Users can only ever access their own drones.
* @param user The user for which a drone is being registered
* @param definition The definition of the drone being registered
* @returns the new drone registration
*/
async register( async register(
user: IUser, user: IUser,
definition: IDroneDefinition, definition: IDroneDefinition,
@ -53,6 +66,7 @@ class DroneService extends DtpService {
registration.updatedAt = NOW; registration.updatedAt = NOW;
registration.user = user._id; registration.user = user._id;
registration.hostname = definition.hostname; registration.hostname = definition.hostname;
registration.workspaceDir = definition.workspaceDir;
registration.status = DroneStatus.Starting; registration.status = DroneStatus.Starting;
this.log.info("registering drone", { hostname: registration.hostname }); this.log.info("registering drone", { hostname: registration.hostname });
@ -61,6 +75,13 @@ class DroneService extends DtpService {
return registration.populate(this.populateDroneRegistration); return registration.populate(this.populateDroneRegistration);
} }
/**
* The drone calls this service to unregister from the platform when shutting
* down. This will set the status to offline and remove any associated
* tasks.
* @param registration The drone registration being unregistered (offline)
* @returns the updated drone registration
*/
async unregister( async unregister(
registration: IDroneRegistration, registration: IDroneRegistration,
): Promise<IDroneRegistration> { ): Promise<IDroneRegistration> {
@ -68,6 +89,12 @@ class DroneService extends DtpService {
return this.setStatus(registration, DroneStatus.Offline); return this.setStatus(registration, DroneStatus.Offline);
} }
/**
* Retrieve a drone registration by _id. Throws if the registration doesn't
* exist.
* @param registrationId The _id of the drone registration to be fetched
* @returns the drone registration document
*/
async getById(registrationId: Types.ObjectId): Promise<IDroneRegistration> { async getById(registrationId: Types.ObjectId): Promise<IDroneRegistration> {
const registration = await DroneRegistration.findById( const registration = await DroneRegistration.findById(
registrationId, registrationId,
@ -80,6 +107,11 @@ class DroneService extends DtpService {
return registration; return registration;
} }
/**
* Request a User's list of registered drones.
* @param user The user for which a list of registered drones is being requested
* @returns The list of drone registrations for the user.
*/
async getForUser(user: IUser): Promise<IDroneRegistration[]> { async getForUser(user: IUser): Promise<IDroneRegistration[]> {
const registrations = await DroneRegistration.find({ user: user._id }) const registrations = await DroneRegistration.find({ user: user._id })
.sort({ hostname: 1, workspaceDir: 1 }) .sort({ hostname: 1, workspaceDir: 1 })
@ -87,6 +119,12 @@ class DroneService extends DtpService {
return registrations; return registrations;
} }
/**
* The drone calls this to update it's status in the database.
* @param registration The registration for which a status is being updated.
* @param status The new status of the drone.
* @returns the updated drone registration
*/
async setStatus( async setStatus(
registration: IDroneRegistration, registration: IDroneRegistration,
status: DroneStatus, status: DroneStatus,
@ -108,6 +146,31 @@ class DroneService extends DtpService {
} }
return newRegistration; return newRegistration;
} }
async requestChatSessionLock(
registration: IDroneRegistration,
session: IChatSession,
): Promise<IDroneRegistration> {
/*
* TODO: Send socket message to drone requesting session lock
* If drone acknowledges lock, update the registration with the chatSessionId.
* If the drone denies the lock, throw a descriptive error.
*/
// Update the registration with the chatSessionId
const updatedRegistration = await DroneRegistration.findOneAndUpdate(
{ _id: registration._id },
{ $set: { chatSessionId: session._id } },
{ new: true, populate: this.populateDroneRegistration },
);
if (!updatedRegistration) {
const error = new Error("drone registration has been removed");
error.statusCode = 404;
throw error;
}
return updatedRegistration;
}
} }
export default new DroneService(); export default new DroneService();

View File

@ -58,6 +58,11 @@ import { User } from "./models/user.js";
import { SocketSessionType } from "./lib/socket-session.js"; import { SocketSessionType } from "./lib/socket-session.js";
import { CodeSession } from "./lib/code-session.js"; import { CodeSession } from "./lib/code-session.js";
import { DroneSession } from "./lib/drone-session.js"; import { DroneSession } from "./lib/drone-session.js";
import {
ClientToServerEvents,
ServerToClientEvents,
SocketData,
} from "@gadget/api";
class DtpWebAppServer implements DtpComponent { class DtpWebAppServer implements DtpComponent {
private log: DtpLog; private log: DtpLog;
@ -272,7 +277,12 @@ class DtpWebAppServer implements DtpComponent {
/* /*
* Create Socket.io server * Create Socket.io server
*/ */
this.io = new SocketIOServer(this.server, { this.io = new SocketIOServer<
ClientToServerEvents,
ServerToClientEvents,
never,
SocketData
>(this.server, {
cors: { cors: {
origin: "*", origin: "*",
methods: ["GET", "POST"], methods: ["GET", "POST"],

View File

@ -0,0 +1,80 @@
import { test, expect } from '@playwright/test';
test('Home page should display user drones from API', async ({ page }) => {
await page.goto('https://code-dev.g4dge7.com:5174/sign-in');
await page.waitForLoadState('networkidle');
await page.fill('#email', 'rob@digitaltelepresence.com');
await page.fill('#password', 'ionfrali');
await page.click('button[type="submit"]');
await page.waitForTimeout(3000);
await page.goto('https://code-dev.g4dge7.com:5174/');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
const sidebar = page.locator('aside').first();
const sidebarText = await sidebar.textContent();
expect(sidebarText).toContain('Drones');
expect(sidebarText).toContain('mysterymachine');
});
test('DroneInspector shows drone details', async ({ page }) => {
await page.goto('https://code-dev.g4dge7.com:5174/sign-in');
await page.waitForLoadState('networkidle');
await page.fill('#email', 'rob@digitaltelepresence.com');
await page.fill('#password', 'ionfrali');
await page.click('button[type="submit"]');
await page.waitForTimeout(3000);
await page.goto('https://code-dev.g4dge7.com:5174/');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
const sidebar = page.locator('aside').first();
const droneButton = sidebar.locator('button').filter({ hasText: 'mysterymachine' }).first();
await droneButton.click();
await page.waitForTimeout(500);
const inspector = page.locator('text=Drone Inspector');
await expect(inspector).toBeVisible();
const backButton = page.locator('text=← Back to Dashboard');
await expect(backButton).toBeVisible();
const hostname = page.locator('text=Hostname');
await expect(hostname).toBeVisible();
const workspace = page.locator('text=Workspace');
await expect(workspace).toBeVisible();
await backButton.click();
await page.waitForTimeout(500);
await expect(inspector).not.toBeVisible();
});
test('Drones API returns available and busy drones', async ({ page }) => {
await page.goto('https://code-dev.g4dge7.com:5174/sign-in');
await page.waitForLoadState('networkidle');
await page.fill('#email', 'rob@digitaltelepresence.com');
await page.fill('#password', 'ionfrali');
await page.click('button[type="submit"]');
await page.waitForTimeout(2000);
const token = await page.evaluate(() => localStorage.getItem('dtp_auth_token'));
expect(token).toBeDefined();
const dronesResponse = await page.request.get('https://code-dev.g4dge7.com:5174/api/v1/drone/registration', {
headers: { 'Authorization': `Bearer ${token}` },
});
expect(dronesResponse.status()).toBe(200);
const data = await dronesResponse.json() as { success: boolean; data: Array<{ status: string }> };
expect(data.success).toBe(true);
expect(data.data).toBeDefined();
const availableOrBusy = data.data.filter(d => d.status === 'available' || d.status === 'busy');
expect(availableOrBusy.length).toBeGreaterThan(0);
});

View File

@ -22,6 +22,7 @@
"dependencies": { "dependencies": {
"@gadget/ai": "workspace:*", "@gadget/ai": "workspace:*",
"@gadget/api": "workspace:*", "@gadget/api": "workspace:*",
"@inquirer/prompts": "^8.4.2",
"ansicolor": "^2.0.3", "ansicolor": "^2.0.3",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",

View File

@ -6,6 +6,7 @@ import env from "./config/env.ts";
import assert from "node:assert"; import assert from "node:assert";
import { io, ManagerOptions, SocketOptions, Socket } from "socket.io-client"; import { io, ManagerOptions, SocketOptions, Socket } from "socket.io-client";
import { input as inqInput, password as inqPassword } from "@inquirer/prompts";
import AgentService, { IAgentWorkOrder } from "./services/agent.ts"; import AgentService, { IAgentWorkOrder } from "./services/agent.ts";
import AiService from "./services/ai.ts"; import AiService from "./services/ai.ts";
@ -15,10 +16,18 @@ import PlatformService, {
} from "./services/platform.ts"; } from "./services/platform.ts";
import { GadgetProcess } from "./lib/process.ts"; import { GadgetProcess } from "./lib/process.ts";
import { ClientToServerEvents, ServerToClientEvents } from "@gadget/api";
interface UserCredentials {
email: string;
password: string;
}
type ClientSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
class GadgetDrone extends GadgetProcess { class GadgetDrone extends GadgetProcess {
private registration: PlatformRegistration | undefined; private registration: PlatformRegistration | undefined;
private socket: Socket | undefined; private socket: ClientSocket | undefined;
private isShuttingDown: boolean = false; private isShuttingDown: boolean = false;
get name(): string { get name(): string {
@ -45,9 +54,13 @@ class GadgetDrone extends GadgetProcess {
* Register this Drone with the Gadget Code web services platform. * Register this Drone with the Gadget Code web services platform.
*/ */
const email = "rob@digitaltelepresence.com"; const credentials = await this.getUserCredentials();
const password = "ionfrali"; const workspaceDir = process.cwd();
this.registration = await PlatformService.register(email, password); this.registration = await PlatformService.register(
credentials.email,
credentials.password,
workspaceDir,
);
this.log.info("registered with platform", { this.log.info("registered with platform", {
registration: this.registration, registration: this.registration,
}); });
@ -159,6 +172,13 @@ class GadgetDrone extends GadgetProcess {
process.exit(exitCode); process.exit(exitCode);
}); });
} }
async getUserCredentials(): Promise<UserCredentials> {
return {
email: await inqInput({ message: "📧 Enter Drone Email: " }),
password: await inqPassword({ message: "🔑 Enter Password: " }),
};
}
} }
(async () => { (async () => {

View File

@ -55,12 +55,14 @@ class PlatformService extends GadgetService {
async register( async register(
email: string, email: string,
password: string, password: string,
workspaceDir: string,
): Promise<PlatformRegistration> { ): Promise<PlatformRegistration> {
const url = this.getApiUrl("/drone/registration"); const url = this.getApiUrl("/drone/registration");
const body = JSON.stringify({ const body = JSON.stringify({
email, email,
password, password,
hostname: os.hostname(), hostname: os.hostname(),
workspaceDir,
}); });
const response = await fetch(url, { const response = await fetch(url, {
method: "POST", method: "POST",

View File

@ -2,13 +2,22 @@
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us> // Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// All Rights Reserved // All Rights Reserved
export * from "./interfaces/ai-provider.ts"; /*
* Data Model Interfaces
*/
export * from "./interfaces/ai-provider.ts";
export * from "./interfaces/user.ts"; export * from "./interfaces/user.ts";
export * from "./interfaces/project.ts"; export * from "./interfaces/project.ts";
export * from "./interfaces/drone-registration.ts"; export * from "./interfaces/drone-registration.ts";
export * from "./interfaces/drone-monitor.ts"; export * from "./interfaces/drone-monitor.ts";
export * from "./interfaces/chat-session.ts"; export * from "./interfaces/chat-session.ts";
export * from "./interfaces/chat-turn.ts"; export * from "./interfaces/chat-turn.ts";
/*
* Socket.IO Interfaces
*/
export * from "./messages/ide.ts";
export * from "./messages/drone.ts";
export * from "./messages/socket.ts";

View File

@ -20,5 +20,6 @@ export interface IDroneRegistration extends Document {
hostname: string; hostname: string;
workspaceDir: string; workspaceDir: string;
status: DroneStatus; status: DroneStatus;
chatSessionId?: string;
currentJobId?: string; currentJobId?: string;
} }

View File

@ -0,0 +1,7 @@
export type ThinkingMessage = (content: string) => void;
export type ResponseMessage = (content: string) => void;
export type ToolCallMessage = (
name: string,
params: string,
response: string,
) => void;

View File

@ -0,0 +1,9 @@
import { IChatSession } from "../interfaces/chat-session.ts";
import { IProject } from "../interfaces/project.ts";
export type RequestSessionLockMessage = (
project: IProject,
chatSession: IChatSession,
cb: (success: boolean, chatSessionId: string) => void,
) => void;
export type SubmitPromptMessage = (prompt: string) => void;

View File

@ -0,0 +1,42 @@
// src/messages/gadget-code.ts
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// Licensed under the Apache License, Version 2.0
import { ResponseMessage, ThinkingMessage, ToolCallMessage } from "./drone.ts";
import { RequestSessionLockMessage, SubmitPromptMessage } from "./ide.ts";
export interface ServerToClientEvents {
/*
* GadgetCode => IDE
*/
thinking: ThinkingMessage;
response: ResponseMessage;
toolCall: ToolCallMessage;
/*
* Gadget Code => Drone
*/
requestSessionLock: RequestSessionLockMessage;
submitPrompt: SubmitPromptMessage;
}
export interface ClientToServerEvents {
/*
* IDE => Gadget Code
*/
requestSessionLock: RequestSessionLockMessage;
submitPrompt: SubmitPromptMessage;
/*
* Drone => Gadget Code
*/
thinking: ThinkingMessage;
response: ResponseMessage;
toolCall: ToolCallMessage;
}
export interface SocketData {}

View File

@ -251,6 +251,9 @@ importers:
'@gadget/api': '@gadget/api':
specifier: workspace:* specifier: workspace:*
version: link:../packages/api version: link:../packages/api
'@inquirer/prompts':
specifier: ^8.4.2
version: 8.4.2(@types/node@25.6.0)
ansicolor: ansicolor:
specifier: ^2.0.3 specifier: ^2.0.3
version: 2.0.3 version: 2.0.3
@ -750,6 +753,140 @@ packages:
resolution: {integrity: sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==} resolution: {integrity: sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==}
engines: {node: '>=6'} engines: {node: '>=6'}
'@inquirer/ansi@2.0.5':
resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
'@inquirer/checkbox@5.1.4':
resolution: {integrity: sha512-w6KF8ZYRvqHhROkOTHXYC3qIV/KYEu5o12oLqQySvch61vrYtRxNSHTONSdJqWiFJPlCUQAHT5OgOIyuTr+MHQ==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/confirm@6.0.12':
resolution: {integrity: sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/core@11.1.9':
resolution: {integrity: sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/editor@5.1.1':
resolution: {integrity: sha512-6y11LgmNpmn5D2aB5FgnCfBUBK8ZstwLCalyJmORcJZ/WrhOjm16mu6eSqIx8DnErxDqSLr+Jkp+GP8/Nwd5tA==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/expand@5.0.13':
resolution: {integrity: sha512-dF2zvrFo9LshkcB23/O1il13kBkBltWIXzut1evfbuBLXMiGIuC45c+ZQ0uukjCDsvI8OWqun4FRYMnzFCQa3g==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/external-editor@3.0.0':
resolution: {integrity: sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/figures@2.0.5':
resolution: {integrity: sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
'@inquirer/input@5.0.12':
resolution: {integrity: sha512-uiMFBl4LqFzJClh80Q3f9hbOFJ6kgkDWI4LjAeBuyO6EanVVMF69AgOvpi1qdqjDSjDN6578B6nky9ceEpI+1Q==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/number@4.0.12':
resolution: {integrity: sha512-/vrwhEf7Xsuh+YlHF4IjSy3g1cyrQuPaSiHIxCEbLu8qnfvrcvJyCkoktOOF+xV9gSb77/G0n3h04RbMDW2sIg==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/password@5.0.12':
resolution: {integrity: sha512-CBh7YHju623lxJRcAOo498ZUwIuMy63bqW/vVq0tQAZVv+lkWlHkP9ealYE1utWSisEShY5VMdzIXRmyEODzcQ==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/prompts@8.4.2':
resolution: {integrity: sha512-XJmn/wY4AX56l1BRU+ZjDrFtg9+2uBEi4JvJQj82kwJDQKiPgSn4CEsbfGGygS4Gw6rkL4W18oATjfVfaqub2Q==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/rawlist@5.2.8':
resolution: {integrity: sha512-Su7FQvp5buZmCymN3PPoYv31ZQQX4ve2j02k7piGgKAWgE+AQRB5YoYVveGXcl3TZ9ldgRMSxj56YfDFmmaqLg==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/search@4.1.8':
resolution: {integrity: sha512-fGiHKGD6DyPIYUWxoXnQTeXeyYqSOUrasDMABBmMHUalH/LxkuzY0xVRtimXAt1sUeeyYkVuKQx1bebMuN11Kw==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/select@5.1.4':
resolution: {integrity: sha512-2kWcGKPMLAXAWRp1AH1SLsQmX+j0QjeljyXMUji9WMZC8nRDO0b7qquIGr6143E7KMLt3VAIGNXzwa/6PXQs4Q==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/type@4.0.5':
resolution: {integrity: sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@ioredis/commands@1.5.1': '@ioredis/commands@1.5.1':
resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==}
@ -1416,6 +1553,9 @@ packages:
character-parser@2.2.0: character-parser@2.2.0:
resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==}
chardet@2.1.1:
resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
chart.js@4.5.1: chart.js@4.5.1:
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
engines: {pnpm: '>=8'} engines: {pnpm: '>=8'}
@ -1424,6 +1564,10 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'} engines: {node: '>= 8.10.0'}
cli-width@4.1.0:
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
cliui@8.0.1: cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -1758,6 +1902,15 @@ packages:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'} engines: {node: '>=8.6.0'}
fast-string-truncated-width@3.0.3:
resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==}
fast-string-width@3.0.2:
resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
fast-wrap-ansi@0.2.0:
resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==}
fast-xml-builder@1.1.5: fast-xml-builder@1.1.5:
resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==}
@ -2348,6 +2501,10 @@ packages:
resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==}
engines: {node: '>= 10.16.0'} engines: {node: '>= 10.16.0'}
mute-stream@3.0.0:
resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==}
engines: {node: ^20.17.0 || >=22.9.0}
mylas@2.1.14: mylas@2.1.14:
resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==} resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
@ -2797,6 +2954,10 @@ packages:
siginfo@2.0.0: siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
slash@3.0.0: slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -3485,6 +3646,125 @@ snapshots:
'@fortawesome/fontawesome-free@6.7.2': {} '@fortawesome/fontawesome-free@6.7.2': {}
'@inquirer/ansi@2.0.5': {}
'@inquirer/checkbox@5.1.4(@types/node@25.6.0)':
dependencies:
'@inquirer/ansi': 2.0.5
'@inquirer/core': 11.1.9(@types/node@25.6.0)
'@inquirer/figures': 2.0.5
'@inquirer/type': 4.0.5(@types/node@25.6.0)
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/confirm@6.0.12(@types/node@25.6.0)':
dependencies:
'@inquirer/core': 11.1.9(@types/node@25.6.0)
'@inquirer/type': 4.0.5(@types/node@25.6.0)
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/core@11.1.9(@types/node@25.6.0)':
dependencies:
'@inquirer/ansi': 2.0.5
'@inquirer/figures': 2.0.5
'@inquirer/type': 4.0.5(@types/node@25.6.0)
cli-width: 4.1.0
fast-wrap-ansi: 0.2.0
mute-stream: 3.0.0
signal-exit: 4.1.0
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/editor@5.1.1(@types/node@25.6.0)':
dependencies:
'@inquirer/core': 11.1.9(@types/node@25.6.0)
'@inquirer/external-editor': 3.0.0(@types/node@25.6.0)
'@inquirer/type': 4.0.5(@types/node@25.6.0)
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/expand@5.0.13(@types/node@25.6.0)':
dependencies:
'@inquirer/core': 11.1.9(@types/node@25.6.0)
'@inquirer/type': 4.0.5(@types/node@25.6.0)
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/external-editor@3.0.0(@types/node@25.6.0)':
dependencies:
chardet: 2.1.1
iconv-lite: 0.7.2
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/figures@2.0.5': {}
'@inquirer/input@5.0.12(@types/node@25.6.0)':
dependencies:
'@inquirer/core': 11.1.9(@types/node@25.6.0)
'@inquirer/type': 4.0.5(@types/node@25.6.0)
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/number@4.0.12(@types/node@25.6.0)':
dependencies:
'@inquirer/core': 11.1.9(@types/node@25.6.0)
'@inquirer/type': 4.0.5(@types/node@25.6.0)
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/password@5.0.12(@types/node@25.6.0)':
dependencies:
'@inquirer/ansi': 2.0.5
'@inquirer/core': 11.1.9(@types/node@25.6.0)
'@inquirer/type': 4.0.5(@types/node@25.6.0)
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/prompts@8.4.2(@types/node@25.6.0)':
dependencies:
'@inquirer/checkbox': 5.1.4(@types/node@25.6.0)
'@inquirer/confirm': 6.0.12(@types/node@25.6.0)
'@inquirer/editor': 5.1.1(@types/node@25.6.0)
'@inquirer/expand': 5.0.13(@types/node@25.6.0)
'@inquirer/input': 5.0.12(@types/node@25.6.0)
'@inquirer/number': 4.0.12(@types/node@25.6.0)
'@inquirer/password': 5.0.12(@types/node@25.6.0)
'@inquirer/rawlist': 5.2.8(@types/node@25.6.0)
'@inquirer/search': 4.1.8(@types/node@25.6.0)
'@inquirer/select': 5.1.4(@types/node@25.6.0)
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/rawlist@5.2.8(@types/node@25.6.0)':
dependencies:
'@inquirer/core': 11.1.9(@types/node@25.6.0)
'@inquirer/type': 4.0.5(@types/node@25.6.0)
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/search@4.1.8(@types/node@25.6.0)':
dependencies:
'@inquirer/core': 11.1.9(@types/node@25.6.0)
'@inquirer/figures': 2.0.5
'@inquirer/type': 4.0.5(@types/node@25.6.0)
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/select@5.1.4(@types/node@25.6.0)':
dependencies:
'@inquirer/ansi': 2.0.5
'@inquirer/core': 11.1.9(@types/node@25.6.0)
'@inquirer/figures': 2.0.5
'@inquirer/type': 4.0.5(@types/node@25.6.0)
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/type@4.0.5(@types/node@25.6.0)':
optionalDependencies:
'@types/node': 25.6.0
'@ioredis/commands@1.5.1': {} '@ioredis/commands@1.5.1': {}
'@jridgewell/gen-mapping@0.3.13': '@jridgewell/gen-mapping@0.3.13':
@ -4132,6 +4412,8 @@ snapshots:
dependencies: dependencies:
is-regex: 1.2.1 is-regex: 1.2.1
chardet@2.1.1: {}
chart.js@4.5.1: chart.js@4.5.1:
dependencies: dependencies:
'@kurkle/color': 0.3.4 '@kurkle/color': 0.3.4
@ -4148,6 +4430,8 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
cli-width@4.1.0: {}
cliui@8.0.1: cliui@8.0.1:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3
@ -4536,6 +4820,16 @@ snapshots:
merge2: 1.4.1 merge2: 1.4.1
micromatch: 4.0.8 micromatch: 4.0.8
fast-string-truncated-width@3.0.3: {}
fast-string-width@3.0.2:
dependencies:
fast-string-truncated-width: 3.0.3
fast-wrap-ansi@0.2.0:
dependencies:
fast-string-width: 3.0.2
fast-xml-builder@1.1.5: fast-xml-builder@1.1.5:
dependencies: dependencies:
path-expression-matcher: 1.5.0 path-expression-matcher: 1.5.0
@ -5138,6 +5432,8 @@ snapshots:
concat-stream: 2.0.0 concat-stream: 2.0.0
type-is: 1.6.18 type-is: 1.6.18
mute-stream@3.0.0: {}
mylas@2.1.14: {} mylas@2.1.14: {}
nanoid@3.3.11: {} nanoid@3.3.11: {}
@ -5628,6 +5924,8 @@ snapshots:
siginfo@2.0.0: {} siginfo@2.0.0: {}
signal-exit@4.1.0: {}
slash@3.0.0: {} slash@3.0.0: {}
slug@11.0.1: {} slug@11.0.1: {}