front-end progress; Socket.IO messaging API; inquirer for credentials
This commit is contained in:
parent
56ba613cc7
commit
db0d1586d6
@ -29,6 +29,7 @@ The IDE uses the following color palette (CSS custom properties):
|
||||
```
|
||||
|
||||
Font stack:
|
||||
|
||||
- Primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif
|
||||
- Code/Retro: Courier New, Courier, monospace
|
||||
|
||||
@ -81,7 +82,7 @@ Implementation: `frontend/src/components/StatusBar.tsx`
|
||||
Between the header and status bars is the Content Area. It uses React Router for URL-based navigation:
|
||||
|
||||
| Route | View |
|
||||
|-------|------|
|
||||
| ----------------- | ------------------------------------------------- |
|
||||
| `/` | Home (authenticated dashboard or unauthenticated) |
|
||||
| `/projects` | Project Manager (list view) |
|
||||
| `/projects/:slug` | Project Manager (project selected) |
|
||||
@ -132,6 +133,7 @@ Select a project or chat session from the sidebar to get started.
|
||||
```
|
||||
|
||||
Components:
|
||||
|
||||
1. Clock - local time in AM/PM format, current date
|
||||
2. Projects list - links to `/projects/:slug`, [+]/link navigates to `/projects`
|
||||
3. Drones list - status indicator (green=available, yellow=busy, gray=offline)
|
||||
@ -174,6 +176,7 @@ When a project is selected:
|
||||
```
|
||||
|
||||
Features:
|
||||
|
||||
- Left sidebar: Project list with [+ New Project] button
|
||||
- Project Inspector: Shows name, slug, gitUrl, status, createdAt
|
||||
- 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:
|
||||
|
||||
```typescript
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
```
|
||||
|
||||
### Backend Session Restoration
|
||||
|
||||
The backend restores user sessions from:
|
||||
|
||||
1. `Authorization: Bearer <token>` header (JWT)
|
||||
2. Express session (fallback)
|
||||
|
||||
@ -221,6 +225,7 @@ The `requireUser()` middleware ensures endpoints are authenticated.
|
||||
### Password Field Handling
|
||||
|
||||
Password credentials are NEVER exposed:
|
||||
|
||||
- Mongoose `select: false` on password fields in models
|
||||
- Population uses `select: "-passwordSalt -password"` to exclude from queries
|
||||
- Frontend User interface only includes: `_id`, `email`, `displayName`, `flags`
|
||||
@ -235,7 +240,7 @@ Work Area | Session Status
|
||||
Chat Messages | Chat: name
|
||||
| ID: ...
|
||||
| Model: ...
|
||||
------------------------------------------|---------------
|
||||
----------------------------------------------|---------------
|
||||
[Prompt input ][Expand][Send]| TC | FO | SA
|
||||
----------------------------------------------|---------------
|
||||
Log | Files
|
||||
@ -243,6 +248,7 @@ Log | Files
|
||||
```
|
||||
|
||||
Implemented components:
|
||||
|
||||
- Chat Messages (stubbed)
|
||||
- Prompt Input (stubbed)
|
||||
- Session Status sidebar (stubbed)
|
||||
|
||||
@ -108,3 +108,17 @@ export const projectApi = {
|
||||
api.put<Project>(`/api/v1/projects/${id}`, data),
|
||||
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'),
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import type { User, Project } from '../lib/api';
|
||||
import { projectApi } from '../lib/api';
|
||||
import type { User, Project, DroneRegistration } from '../lib/api';
|
||||
import { projectApi, droneApi } from '../lib/api';
|
||||
import Clock from '../components/Clock';
|
||||
|
||||
const ansiColors = [
|
||||
@ -51,23 +51,79 @@ function AnsiLogo() {
|
||||
|
||||
interface DashboardSidebarProps {
|
||||
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 [projects, setProjects] = useState<Project[]>([]);
|
||||
const [drones, setDrones] = useState<DroneRegistration[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadProjects();
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadProjects = async () => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const data = await projectApi.getAll();
|
||||
setProjects(data);
|
||||
const [projectsData, dronesData] = await Promise.all([
|
||||
projectApi.getAll(),
|
||||
droneApi.getAll(),
|
||||
]);
|
||||
setProjects(projectsData);
|
||||
setDrones(dronesData);
|
||||
} catch (err) {
|
||||
console.error('Failed to load projects', err);
|
||||
console.error('Failed to load dashboard data', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -110,9 +166,51 @@ function DashboardSidebar({ onNavigate }: DashboardSidebarProps) {
|
||||
<div className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
|
||||
Drones
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{loading ? (
|
||||
<p className="text-sm text-text-muted px-2">Loading...</p>
|
||||
) : 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 className="p-3 border-t border-border-subtle">
|
||||
@ -133,6 +231,7 @@ interface HomeProps {
|
||||
|
||||
export default function Home({ user }: HomeProps) {
|
||||
const navigate = useNavigate();
|
||||
const [selectedDrone, setSelectedDrone] = useState<DroneRegistration | null>(null);
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
@ -144,6 +243,9 @@ export default function Home({ user }: HomeProps) {
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex bg-bg-primary">
|
||||
{selectedDrone ? (
|
||||
<DroneInspector drone={selectedDrone} onClose={() => setSelectedDrone(null)} />
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center p-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-semibold mb-4">
|
||||
@ -165,12 +267,17 @@ export default function Home({ user }: HomeProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DashboardSidebar onNavigate={(view) => {
|
||||
<DashboardSidebar
|
||||
onNavigate={(view) => {
|
||||
if (view === 'project' && typeof navigate === 'function') {
|
||||
navigate('/projects');
|
||||
}
|
||||
}} />
|
||||
}}
|
||||
selectedDrone={selectedDrone}
|
||||
onSelectDrone={setSelectedDrone}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -229,6 +229,18 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{showNewForm ? (
|
||||
<div className="p-2">
|
||||
<p className="text-sm text-text-muted mb-2">Creating new project...</p>
|
||||
<button
|
||||
onClick={() => setShowNewForm(false)}
|
||||
className="text-sm text-text-secondary hover:text-text-primary transition-colors"
|
||||
>
|
||||
← Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2 px-2">
|
||||
Projects ({projects.length})
|
||||
</div>
|
||||
@ -251,6 +263,8 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
@ -26,7 +26,9 @@ export class DroneApiControllerV1 extends DtpController {
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
const requireUser = this.requireUser();
|
||||
const multer = this.createMulter(this.slug, {});
|
||||
|
||||
this.router.param("registrationId", populateDroneRegistrationById(this));
|
||||
|
||||
this.router.post(
|
||||
@ -40,6 +42,12 @@ export class DroneApiControllerV1 extends DtpController {
|
||||
this.putRegistrationStatus.bind(this),
|
||||
);
|
||||
|
||||
this.router.get(
|
||||
"/registration",
|
||||
requireUser,
|
||||
this.getRegistrations.bind(this),
|
||||
);
|
||||
|
||||
this.router.delete(
|
||||
"/registration/:registrationId",
|
||||
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> {
|
||||
try {
|
||||
await DroneService.unregister(res.locals.registration);
|
||||
|
||||
@ -17,6 +17,7 @@ export const DroneRegistrationSchema = new Schema<IDroneRegistration>({
|
||||
default: DroneStatus.Starting,
|
||||
required: true,
|
||||
},
|
||||
chatSessionId: { type: String, required: false },
|
||||
currentJobId: { type: String, required: false },
|
||||
});
|
||||
|
||||
|
||||
@ -4,13 +4,19 @@
|
||||
|
||||
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 { DtpService } from "../lib/service.js";
|
||||
|
||||
export interface IDroneDefinition {
|
||||
hostname: string;
|
||||
workspaceDir: string;
|
||||
}
|
||||
|
||||
class DroneService extends DtpService {
|
||||
@ -42,6 +48,13 @@ class DroneService extends DtpService {
|
||||
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(
|
||||
user: IUser,
|
||||
definition: IDroneDefinition,
|
||||
@ -53,6 +66,7 @@ class DroneService extends DtpService {
|
||||
registration.updatedAt = NOW;
|
||||
registration.user = user._id;
|
||||
registration.hostname = definition.hostname;
|
||||
registration.workspaceDir = definition.workspaceDir;
|
||||
registration.status = DroneStatus.Starting;
|
||||
|
||||
this.log.info("registering drone", { hostname: registration.hostname });
|
||||
@ -61,6 +75,13 @@ class DroneService extends DtpService {
|
||||
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(
|
||||
registration: IDroneRegistration,
|
||||
): Promise<IDroneRegistration> {
|
||||
@ -68,6 +89,12 @@ class DroneService extends DtpService {
|
||||
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> {
|
||||
const registration = await DroneRegistration.findById(
|
||||
registrationId,
|
||||
@ -80,6 +107,11 @@ class DroneService extends DtpService {
|
||||
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[]> {
|
||||
const registrations = await DroneRegistration.find({ user: user._id })
|
||||
.sort({ hostname: 1, workspaceDir: 1 })
|
||||
@ -87,6 +119,12 @@ class DroneService extends DtpService {
|
||||
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(
|
||||
registration: IDroneRegistration,
|
||||
status: DroneStatus,
|
||||
@ -108,6 +146,31 @@ class DroneService extends DtpService {
|
||||
}
|
||||
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();
|
||||
|
||||
@ -58,6 +58,11 @@ import { User } from "./models/user.js";
|
||||
import { SocketSessionType } from "./lib/socket-session.js";
|
||||
import { CodeSession } from "./lib/code-session.js";
|
||||
import { DroneSession } from "./lib/drone-session.js";
|
||||
import {
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
SocketData,
|
||||
} from "@gadget/api";
|
||||
|
||||
class DtpWebAppServer implements DtpComponent {
|
||||
private log: DtpLog;
|
||||
@ -272,7 +277,12 @@ class DtpWebAppServer implements DtpComponent {
|
||||
/*
|
||||
* Create Socket.io server
|
||||
*/
|
||||
this.io = new SocketIOServer(this.server, {
|
||||
this.io = new SocketIOServer<
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
never,
|
||||
SocketData
|
||||
>(this.server, {
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"],
|
||||
|
||||
80
gadget-code/tests/e2e/drones.test.ts
Normal file
80
gadget-code/tests/e2e/drones.test.ts
Normal 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);
|
||||
});
|
||||
@ -22,6 +22,7 @@
|
||||
"dependencies": {
|
||||
"@gadget/ai": "workspace:*",
|
||||
"@gadget/api": "workspace:*",
|
||||
"@inquirer/prompts": "^8.4.2",
|
||||
"ansicolor": "^2.0.3",
|
||||
"dayjs": "^1.11.20",
|
||||
"dotenv": "^17.4.2",
|
||||
|
||||
@ -6,6 +6,7 @@ import env from "./config/env.ts";
|
||||
import assert from "node:assert";
|
||||
|
||||
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 AiService from "./services/ai.ts";
|
||||
@ -15,10 +16,18 @@ import PlatformService, {
|
||||
} from "./services/platform.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 {
|
||||
private registration: PlatformRegistration | undefined;
|
||||
private socket: Socket | undefined;
|
||||
private socket: ClientSocket | undefined;
|
||||
private isShuttingDown: boolean = false;
|
||||
|
||||
get name(): string {
|
||||
@ -45,9 +54,13 @@ class GadgetDrone extends GadgetProcess {
|
||||
* Register this Drone with the Gadget Code web services platform.
|
||||
*/
|
||||
|
||||
const email = "rob@digitaltelepresence.com";
|
||||
const password = "ionfrali";
|
||||
this.registration = await PlatformService.register(email, password);
|
||||
const credentials = await this.getUserCredentials();
|
||||
const workspaceDir = process.cwd();
|
||||
this.registration = await PlatformService.register(
|
||||
credentials.email,
|
||||
credentials.password,
|
||||
workspaceDir,
|
||||
);
|
||||
this.log.info("registered with platform", {
|
||||
registration: this.registration,
|
||||
});
|
||||
@ -159,6 +172,13 @@ class GadgetDrone extends GadgetProcess {
|
||||
process.exit(exitCode);
|
||||
});
|
||||
}
|
||||
|
||||
async getUserCredentials(): Promise<UserCredentials> {
|
||||
return {
|
||||
email: await inqInput({ message: "📧 Enter Drone Email: " }),
|
||||
password: await inqPassword({ message: "🔑 Enter Password: " }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
|
||||
@ -55,12 +55,14 @@ class PlatformService extends GadgetService {
|
||||
async register(
|
||||
email: string,
|
||||
password: string,
|
||||
workspaceDir: string,
|
||||
): Promise<PlatformRegistration> {
|
||||
const url = this.getApiUrl("/drone/registration");
|
||||
const body = JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
hostname: os.hostname(),
|
||||
workspaceDir,
|
||||
});
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
|
||||
@ -2,13 +2,22 @@
|
||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||
// 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/project.ts";
|
||||
|
||||
export * from "./interfaces/drone-registration.ts";
|
||||
export * from "./interfaces/drone-monitor.ts";
|
||||
|
||||
export * from "./interfaces/chat-session.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";
|
||||
|
||||
@ -20,5 +20,6 @@ export interface IDroneRegistration extends Document {
|
||||
hostname: string;
|
||||
workspaceDir: string;
|
||||
status: DroneStatus;
|
||||
chatSessionId?: string;
|
||||
currentJobId?: string;
|
||||
}
|
||||
|
||||
7
packages/api/src/messages/drone.ts
Normal file
7
packages/api/src/messages/drone.ts
Normal 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;
|
||||
9
packages/api/src/messages/ide.ts
Normal file
9
packages/api/src/messages/ide.ts
Normal 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;
|
||||
42
packages/api/src/messages/socket.ts
Normal file
42
packages/api/src/messages/socket.ts
Normal 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 {}
|
||||
298
pnpm-lock.yaml
298
pnpm-lock.yaml
@ -251,6 +251,9 @@ importers:
|
||||
'@gadget/api':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/api
|
||||
'@inquirer/prompts':
|
||||
specifier: ^8.4.2
|
||||
version: 8.4.2(@types/node@25.6.0)
|
||||
ansicolor:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3
|
||||
@ -750,6 +753,140 @@ packages:
|
||||
resolution: {integrity: sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==}
|
||||
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':
|
||||
resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==}
|
||||
|
||||
@ -1416,6 +1553,9 @@ packages:
|
||||
character-parser@2.2.0:
|
||||
resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==}
|
||||
|
||||
chardet@2.1.1:
|
||||
resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
|
||||
|
||||
chart.js@4.5.1:
|
||||
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
|
||||
engines: {pnpm: '>=8'}
|
||||
@ -1424,6 +1564,10 @@ packages:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
cli-width@4.1.0:
|
||||
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
@ -1758,6 +1902,15 @@ packages:
|
||||
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==}
|
||||
|
||||
@ -2348,6 +2501,10 @@ packages:
|
||||
resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==}
|
||||
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:
|
||||
resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@ -2797,6 +2954,10 @@ packages:
|
||||
siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
slash@3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
@ -3485,6 +3646,125 @@ snapshots:
|
||||
|
||||
'@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': {}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
@ -4132,6 +4412,8 @@ snapshots:
|
||||
dependencies:
|
||||
is-regex: 1.2.1
|
||||
|
||||
chardet@2.1.1: {}
|
||||
|
||||
chart.js@4.5.1:
|
||||
dependencies:
|
||||
'@kurkle/color': 0.3.4
|
||||
@ -4148,6 +4430,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
cli-width@4.1.0: {}
|
||||
|
||||
cliui@8.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@ -4536,6 +4820,16 @@ snapshots:
|
||||
merge2: 1.4.1
|
||||
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:
|
||||
dependencies:
|
||||
path-expression-matcher: 1.5.0
|
||||
@ -5138,6 +5432,8 @@ snapshots:
|
||||
concat-stream: 2.0.0
|
||||
type-is: 1.6.18
|
||||
|
||||
mute-stream@3.0.0: {}
|
||||
|
||||
mylas@2.1.14: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
@ -5628,6 +5924,8 @@ snapshots:
|
||||
|
||||
siginfo@2.0.0: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
slug@11.0.1: {}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user