Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/spotlight/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"test:dev": "vitest",
"test:e2e": "playwright test"
},
"files": ["dist"],
"files": [
"dist"
],
"bin": {
"spotlight": "./dist/run.js"
},
Expand Down Expand Up @@ -60,6 +62,7 @@
"launch-editor": "^2.9.1",
"logfmt": "^1.4.0",
"mcp-proxy": "^5.6.0",
"pidusage": "^4.0.1",
"semver": "^7.7.3",
"uuidv7": "^1.0.2",
"yaml": "^2.8.1",
Expand All @@ -81,6 +84,7 @@
"@types/beautify": "^0.0.3",
"@types/logfmt": "^1.2.6",
"@types/node": "catalog:",
"@types/pidusage": "^2.0.5",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@typescript-eslint/eslint-plugin": "^6.21.0",
Expand All @@ -96,7 +100,6 @@
"dotenv": "^16.4.5",
"electron": "^35.7.5",
"electron-builder": "^24.13.3",
"vite-plugin-electron": "^0.29.0",
"eslint": "^8.57.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.12",
Expand All @@ -121,6 +124,7 @@
"usehooks-ts": "^2.16.0",
"vite": "catalog:",
"vite-plugin-dts": "^4.5.4",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-svgr": "^3.3.0",
"vitest": "catalog:",
"zustand": "^5.0.3"
Expand Down
2 changes: 2 additions & 0 deletions packages/spotlight/src/server/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import { parseArgs } from "node:util";
import showHelp from "./cli/help.ts";
import list from "./cli/list.ts";
import mcp from "./cli/mcp.ts";
import run from "./cli/run.ts";
import server from "./cli/server.ts";
Expand Down Expand Up @@ -118,6 +119,7 @@ export const CLI_CMD_MAP = new Map<string | undefined, CLIHandler>([
["mcp", mcp],
["tail", tail],
["run", run],
["list", list],
["server", server],
[undefined, server],
]);
Expand Down
9 changes: 8 additions & 1 deletion packages/spotlight/src/server/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Spotlight Sidecar - Development proxy server for Spotlight
Usage: spotlight [command] [options]

Commands:
server Start Spotlight sidecar server (default)
run [command...] Run a command with Spotlight enabled
list List all running Spotlight instances
Use --all to include unresponsive/orphaned instances
tail [types...] Tail Sentry events (default: everything)
Available types: ${[...Object.keys(NAME_TO_TYPE_MAPPING)].join(", ")}
Magic words: ${[...EVERYTHING_MAGIC_WORDS].join(", ")}
Expand All @@ -17,12 +21,15 @@ Commands:
Options:
-p, --port <port> Port to listen on (default: 8969, or 0 for random)
-d, --debug Enable debug logging
-f, --format <format> Output format for tail command (default: human)
-f, --format <format> Output format for commands like run, tail, and list command (default: human)
Available formats: ${[...AVAILABLE_FORMATTERS].join(", ")}
-h, --help Show this help message

Examples:
spotlight # Start on default port 8969
spotlight run npm run dev # Run npm dev with Spotlight
spotlight list # List all running instances
spotlight list --format json # List instances in JSON format
spotlight tail # Tail all event types (human format)
spotlight tail errors # Tail only errors
spotlight tail errors logs # Tail errors and logs
Expand Down
143 changes: 143 additions & 0 deletions packages/spotlight/src/server/cli/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import logfmt from "logfmt";
import { formatLogLine } from "../formatters/human/utils.ts";
import { logger } from "../logger.ts";
import { getRegistry } from "../registry/manager.ts";
import type { InstanceInfo } from "../registry/types.ts";
import type { CLIHandlerOptions } from "../types/cli.ts";

/**
* Format instance info in human-readable format (uses formatLogLine)
*/
function formatHuman(instance: InstanceInfo, isSelf: boolean): string {
const timestamp = new Date(instance.startTime).getTime() / 1000;
const projectName = isSelf ? `${instance.projectName} [self]` : instance.projectName;
const message = `${projectName}@${instance.port} (${instance.command}) - http://localhost:${instance.port}`;
return formatLogLine(timestamp, "server", "info", message);
}

/**
* Format instance info in JSON format
*/
function formatJson(instances: InstanceInfo[]): string {
return JSON.stringify(instances, null, 2);
}

/**
* Format instance info in logfmt format
*/
function formatLogfmt(instance: InstanceInfo, isSelf: boolean): string {
const fields: Record<string, string | number | null | boolean> = {
instanceId: instance.instanceId,
projectName: instance.projectName,
port: instance.port,
command: instance.command,
cwd: instance.cwd,
pid: instance.pid,
childPid: instance.childPid,
startTime: instance.startTime,
detectedType: instance.detectedType,
status: instance.status,
isSelf,
};

if (instance.uptime !== undefined) {
fields.uptime = Math.floor(instance.uptime / 1000); // seconds
}

return logfmt.stringify(fields);
}

/**
* Format instance info in markdown format
*/
function formatMarkdown(instances: InstanceInfo[], currentPid: number): string {
const lines: string[] = [];

// Header
lines.push("| Project | Port | Command | Started | PID | URL | Status |");
lines.push("|---------|------|---------|---------|-----|-----|--------|");

// Rows
for (const instance of instances) {
const isSelf = instance.pid === currentPid;
const projectName = isSelf ? `${instance.projectName} [self]` : instance.projectName;
const startTime = new Date(instance.startTime).toLocaleString();
const url = `http://localhost:${instance.port}`;
const uptime = instance.uptime ? `${Math.floor(instance.uptime / 1000)}s` : "?";

lines.push(
`| ${projectName} | ${instance.port} | ${instance.command} | ${startTime} (${uptime}) | ${instance.pid} | ${url} | ${instance.status} |`,
);
}

return lines.join("\n");
}

/**
* Formatter function type - takes instances and current PID, returns formatted output
*/
type InstanceFormatter = (instances: InstanceInfo[], currentPid: number) => void;

/**
* Map of format types to their formatter functions
*/
const FORMATTERS = new Map<string, InstanceFormatter>([
[
"human",
(instances, currentPid) => {
for (const instance of instances) {
const isSelf = instance.pid === currentPid;
console.log(formatHuman(instance, isSelf));
}
},
],
[
"json",
instances => {
console.log(formatJson(instances));
},
],
[
"logfmt",
(instances, currentPid) => {
for (const instance of instances) {
const isSelf = instance.pid === currentPid;
console.log(formatLogfmt(instance, isSelf));
}
},
],
[
"md",
(instances, currentPid) => {
console.log(formatMarkdown(instances, currentPid));
},
],
]);

/**
* List all registered Spotlight instances
*/
export default async function list({ format = "human" }: CLIHandlerOptions): Promise<void> {
const includeUnhealthy = process.argv.includes("--all");
const currentPid = process.pid;

try {
const instances = await getRegistry().list(includeUnhealthy);

if (instances.length === 0) {
logger.info("No Spotlight instances are currently running.");
return;
}

const formatter = FORMATTERS.get(format);
if (!formatter) {
logger.error(`Unsupported format: ${format}`);
process.exit(1);
}

formatter(instances, currentPid);
} catch (err) {
logger.error(`Failed to list instances: ${err}`);
process.exit(1);
}
}
Loading
Loading