Skip to content
Merged
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
14 changes: 14 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,20 @@ View details of a specific trace
- `-w, --web - Open in browser`
- `--spans <value> - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")`

### Init

Initialize Sentry in your project

#### `sentry init <directory>`

Initialize Sentry in your project

**Flags:**
- `--force - Continue even if Sentry is already installed`
- `-y, --yes - Non-interactive mode (accept defaults)`
- `--dry-run - Preview changes without applying them`
- `--features <value> - Comma-separated features: errors,tracing,logs,replay,metrics`

### Issues

List issues in a project
Expand Down
4 changes: 1 addition & 3 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ export const initCommand = buildCommand<InitFlags, [string?], SentryContext>({
},
},
async func(this: SentryContext, flags: InitFlags, directory?: string) {
const targetDir = directory
? path.resolve(this.cwd, directory)
: this.cwd;
const targetDir = directory ? path.resolve(this.cwd, directory) : this.cwd;
const featuresList = flags.features
?.split(",")
.map((f) => f.trim())
Expand Down
77 changes: 52 additions & 25 deletions src/lib/init/local-ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@
* All operations are sandboxed to the workflow's cwd directory.
*/

import { spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
import {
DEFAULT_COMMAND_TIMEOUT_MS,
MAX_FILE_BYTES,
MAX_STDOUT_BYTES,
DEFAULT_COMMAND_TIMEOUT_MS,
} from "./constants.js";
import type {
WizardOptions,
ApplyPatchsetPayload,
FileExistsBatchPayload,
ListDirPayload,
LocalOpPayload,
LocalOpResult,
ListDirPayload,
ReadFilesPayload,
FileExistsBatchPayload,
RunCommandsPayload,
ApplyPatchsetPayload,
WizardOptions,
} from "./types.js";

/**
Expand All @@ -31,15 +31,18 @@ import type {
function safePath(cwd: string, relative: string): string {
const resolved = path.resolve(cwd, relative);
const normalizedCwd = path.resolve(cwd);
if (!resolved.startsWith(normalizedCwd + path.sep) && resolved !== normalizedCwd) {
if (
!resolved.startsWith(normalizedCwd + path.sep) &&
resolved !== normalizedCwd
) {
throw new Error(`Path "${relative}" resolves outside project directory`);
}
return resolved;
}

export async function handleLocalOp(
payload: LocalOpPayload,
_options: WizardOptions,
_options: WizardOptions
): Promise<LocalOpResult> {
try {
switch (payload.operation) {
Expand All @@ -54,7 +57,13 @@ export async function handleLocalOp(
case "apply-patchset":
return await applyPatchset(payload);
default:
return { ok: false, error: `Unknown operation: ${(payload as any).operation}` };
return {
ok: false,
error: `Unknown operation: ${
// biome-ignore lint/suspicious/noExplicitAny: payload is of type LocalOpPayload
(payload as any).operation
}`,
};
}
} catch (error) {
return {
Expand All @@ -64,18 +73,24 @@ export async function handleLocalOp(
}
}

async function listDir(payload: ListDirPayload): Promise<LocalOpResult> {
function listDir(payload: ListDirPayload): LocalOpResult {
const { cwd, params } = payload;
const targetPath = safePath(cwd, params.path);
const maxDepth = params.maxDepth ?? 3;
const maxEntries = params.maxEntries ?? 500;
const recursive = params.recursive ?? false;

const entries: Array<{ name: string; path: string; type: "file" | "directory" }> = [];
const entries: Array<{
name: string;
path: string;
type: "file" | "directory";
}> = [];

// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: walking the directory tree is a complex operation
function walk(dir: string, depth: number): void {
if (entries.length >= maxEntries) return;
if (depth > maxDepth) return;
if (entries.length >= maxEntries || depth > maxDepth) {
return;
}

let dirEntries: fs.Dirent[];
try {
Expand All @@ -85,13 +100,20 @@ async function listDir(payload: ListDirPayload): Promise<LocalOpResult> {
}

for (const entry of dirEntries) {
if (entries.length >= maxEntries) return;
if (entries.length >= maxEntries) {
return;
}

const relPath = path.relative(cwd, path.join(dir, entry.name));
const type = entry.isDirectory() ? "directory" : "file";
entries.push({ name: entry.name, path: relPath, type });

if (recursive && entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
if (
recursive &&
entry.isDirectory() &&
!entry.name.startsWith(".") &&
entry.name !== "node_modules"
) {
walk(path.join(dir, entry.name), depth + 1);
}
}
Expand All @@ -101,7 +123,7 @@ async function listDir(payload: ListDirPayload): Promise<LocalOpResult> {
return { ok: true, data: { entries } };
}

async function readFiles(payload: ReadFilesPayload): Promise<LocalOpResult> {
function readFiles(payload: ReadFilesPayload): LocalOpResult {
const { cwd, params } = payload;
const maxBytes = params.maxBytes ?? MAX_FILE_BYTES;
const files: Record<string, string | null> = {};
Expand All @@ -128,9 +150,7 @@ async function readFiles(payload: ReadFilesPayload): Promise<LocalOpResult> {
return { ok: true, data: { files } };
}

async function fileExistsBatch(
payload: FileExistsBatchPayload,
): Promise<LocalOpResult> {
function fileExistsBatch(payload: FileExistsBatchPayload): LocalOpResult {
const { cwd, params } = payload;
const exists: Record<string, boolean> = {};

Expand All @@ -146,7 +166,9 @@ async function fileExistsBatch(
return { ok: true, data: { exists } };
}

async function runCommands(payload: RunCommandsPayload): Promise<LocalOpResult> {
async function runCommands(
payload: RunCommandsPayload
): Promise<LocalOpResult> {
const { cwd, params } = payload;
const timeoutMs = params.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;

Expand Down Expand Up @@ -175,8 +197,13 @@ async function runCommands(payload: RunCommandsPayload): Promise<LocalOpResult>
function runSingleCommand(
command: string,
cwd: string,
timeoutMs: number,
): Promise<{ command: string; exitCode: number; stdout: string; stderr: string }> {
timeoutMs: number
): Promise<{
command: string;
exitCode: number;
stdout: string;
stderr: string;
}> {
return new Promise((resolve) => {
const child = spawn("sh", ["-c", command], {
cwd,
Expand Down Expand Up @@ -224,9 +251,7 @@ function runSingleCommand(
});
}

async function applyPatchset(
payload: ApplyPatchsetPayload,
): Promise<LocalOpResult> {
function applyPatchset(payload: ApplyPatchsetPayload): LocalOpResult {
const { cwd, params } = payload;
const applied: Array<{ path: string; action: string }> = [];

Expand Down Expand Up @@ -260,6 +285,8 @@ async function applyPatchset(
applied.push({ path: patch.path, action: "delete" });
break;
}
default:
break;
}
}

Expand Down
36 changes: 18 additions & 18 deletions src/lib/init/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Writer } from "../../types/index.js";

export interface WizardOptions {
export type WizardOptions = {
directory: string;
force: boolean;
yes: boolean;
Expand All @@ -9,7 +9,7 @@ export interface WizardOptions {
stdout: Writer;
stderr: Writer;
stdin: NodeJS.ReadStream & { fd: 0 };
}
};

// ── Local-op suspend payloads ──────────────────────────────

Expand All @@ -20,7 +20,7 @@ export type LocalOpPayload =
| RunCommandsPayload
| ApplyPatchsetPayload;

export interface ListDirPayload {
export type ListDirPayload = {
type: "local-op";
operation: "list-dir";
cwd: string;
Expand All @@ -30,38 +30,38 @@ export interface ListDirPayload {
maxDepth?: number;
maxEntries?: number;
};
}
};

export interface ReadFilesPayload {
export type ReadFilesPayload = {
type: "local-op";
operation: "read-files";
cwd: string;
params: {
paths: string[];
maxBytes?: number;
};
}
};

export interface FileExistsBatchPayload {
export type FileExistsBatchPayload = {
type: "local-op";
operation: "file-exists-batch";
cwd: string;
params: {
paths: string[];
};
}
};

export interface RunCommandsPayload {
export type RunCommandsPayload = {
type: "local-op";
operation: "run-commands";
cwd: string;
params: {
commands: string[];
timeoutMs?: number;
};
}
};

export interface ApplyPatchsetPayload {
export type ApplyPatchsetPayload = {
type: "local-op";
operation: "apply-patchset";
cwd: string;
Expand All @@ -72,30 +72,30 @@ export interface ApplyPatchsetPayload {
patch: string;
}>;
};
}
};

export interface LocalOpResult {
export type LocalOpResult = {
ok: boolean;
error?: string;
data?: unknown;
}
};

// ── Interactive suspend payloads ───────────────────────────

export interface InteractivePayload {
export type InteractivePayload = {
type: "interactive";
prompt: string;
kind: "select" | "multi-select" | "confirm";
[key: string]: unknown;
}
};

// ── Workflow run result ────────────────────────────────────

export interface WorkflowRunResult {
export type WorkflowRunResult = {
status: "suspended" | "success" | "failed";
suspended?: string[][];
steps?: Record<string, { suspendPayload?: unknown }>;
suspendPayload?: unknown;
result?: unknown;
error?: string;
}
};
7 changes: 6 additions & 1 deletion src/lib/init/wizard-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { randomBytes } from "node:crypto";
import { cancel, intro, log, spinner } from "@clack/prompts";
import { MastraClient } from "@mastra/client-js";
import { CLI_VERSION } from "../constants.js";
import { getAuthToken } from "../db/auth.js";
import { formatBanner } from "../help.js";
import { STEP_LABELS, WizardCancelledError } from "./clack-utils.js";
import { MASTRA_API_URL, SENTRY_DOCS_URL, WORKFLOW_ID } from "./constants.js";
Expand Down Expand Up @@ -126,7 +127,11 @@ export async function runWizard(options: WizardOptions): Promise<void> {
},
};

const client = new MastraClient({ baseUrl: MASTRA_API_URL });
const token = getAuthToken();
const client = new MastraClient({
baseUrl: MASTRA_API_URL,
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
const workflow = client.getWorkflow(WORKFLOW_ID);
const run = await workflow.createRun();

Expand Down
Loading