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
5 changes: 3 additions & 2 deletions docs/pm/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ sidebarLabel: "Quick Start"
description: "Install @devintern/pm and create your first AI-drafted ticket in your tracker."
section: "PM"
order: 1
dateModified: 2026-08-12
dateModified: 2026-08-17
tags: ["devintern/pm", "quick start", "jira", "linear", "cli"]
---

# @devintern/pm Quick Start

**@devintern/pm** automates story and task creation across multiple project management tools with AI. Transform Figma designs, error logs, or requirements into well-structured issues in seconds.

For the primary visual workflow, [download DevIntern PM](https://devintern.com/pm-desktop/). Continue here if you prefer to work in the terminal.
For the primary visual workflow, [download DevIntern PM](https://devintern.com/pm-desktop/). The desktop app checks on launch that **Git** and **at least one supported agent CLI** are on your PATH (including common GUI-launch locations). If something is missing, install it and choose **Check again**. Continue here if you prefer to work in the terminal.

## Prerequisites

- **[Node.js](https://nodejs.org) 20 or newer**: Required to run @devintern/pm ([Bun](https://bun.sh) works too)
- **Git**: Required for project folders, GitHub connect, and update-from-remote
- AI agent CLI installed and configured (e.g., Claude Code, OpenCode, Codex, Cursor)
- Account with at least one supported PM tool (Jira, Linear, Trello, Azure DevOps, Asana, or GitHub)
- **For Figma functionality**: [Figma MCP server](https://developers.figma.com/docs/figma-mcp-server/remote-server-installation/) must be installed and configured in your AI agent (Claude Code only)
Expand Down
6 changes: 6 additions & 0 deletions packages/pm-desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ bun run build # compile only → out/
bun run test
```

## Required tools

The app is not a self-contained agent runtime: **Git** and **at least one supported agent CLI** (Claude Code, OpenCode, Codex, Cursor, …) must already be installed on the machine. On launch the app probes the same PATH it uses to spawn agents, including common GUI-launch locations (`~/.local/bin`, `~/.bun/bin`, Homebrew, …) that a dock/launcher start would otherwise miss.

If a required tool is missing, a blocking screen names it and how to install it. **Check again** (or switching back to the app after installing) re-runs the probe — there is no “everything is fine” step when the check passes. Optional extras such as sandbox CLIs are not required for core ticket workflows.

## App icon

Installer + dock/taskbar icon: `build/icon.png` (1024×1024 RGBA, with transparent rounded corners
Expand Down
2 changes: 1 addition & 1 deletion packages/pm-desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@devintern/pm-desktop",
"productName": "DevIntern PM",
"version": "0.9.10",
"version": "0.9.11",
"private": true,
"description": "Desktop app for @devintern/pm — multi-ticket AI task creation for your tracker.",
"author": "DevIntern <hello@getdevintern.com>",
Expand Down
20 changes: 20 additions & 0 deletions packages/pm-desktop/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
} from "./session.ts";
import { listRecentProjectDirs, recordRecentProjectDir } from "./recent-projects.ts";
import { readSettings, updateSettings } from "./settings.ts";
import { validateRequiredTools } from "./validate-tools.ts";

/** Reveal only known project dirs (bindings, recents, current session) — not arbitrary paths. */
async function isAllowedRevealPath(resolved: string): Promise<boolean> {
Expand Down Expand Up @@ -224,6 +225,25 @@ export function registerIpcHandlers(): void {
return settings.lastProjectDir ?? null;
});

handle(IPC_CHANNELS.validateRequiredTools, async () => {
const settings = await readSettings();
let agentEnv: Record<string, string> = {};
if (settings.lastProjectDir) {
try {
const { env } = await readProjectEnv(settings.lastProjectDir);
agentEnv = Object.fromEntries(
Object.entries(env).filter(
([key]) =>
key === "AGENT_HARNESS" || key === "AGENT_CLI_PATH" || key.endsWith("_CLI_PATH"),
),
);
} catch {
// A stale/unreadable remembered project must not hide process-level tools.
}
}
return validateRequiredTools({ envOverrides: agentEnv });
});

handle(IPC_CHANNELS.getRecentProjectDirs, async () => {
return listRecentProjectDirs();
});
Expand Down
206 changes: 206 additions & 0 deletions packages/pm-desktop/src/main/validate-tools.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { describe, expect, test } from "bun:test";
import type { AgentHarness } from "@devintern/agent-harness";
import { validateRequiredTools } from "./validate-tools.ts";

function fakeHarness(name: string, displayName: string, defaultPath: string): AgentHarness {
return {
name,
displayName,
defaultPath,
buildArgs: () => [],
};
}

const claude = fakeHarness("claude-code", "Claude Code", "claude");
const opencode = fakeHarness("opencode", "OpenCode", "opencode");

describe("validateRequiredTools", () => {
test("re-applies PATH augmentation before probing", () => {
let augmented = false;
validateRequiredTools({
augmentPath: () => {
augmented = true;
},
findGit: () => "/usr/bin/git",
probeGit: () => true,
listInstalled: () => [claude],
listAll: () => [claude, opencode],
});
expect(augmented).toBe(true);
});

test("is ok when git and at least one harness CLI are present", () => {
const result = validateRequiredTools({
augmentPath: () => {},
findGit: () => "/usr/bin/git",
probeGit: () => true,
listInstalled: () => [claude, opencode],
listAll: () => [claude, opencode],
});
expect(result.ok).toBe(true);
expect(result.warnings).toEqual([]);
expect(result.installedHarnesses).toEqual([
{ name: "claude-code", displayName: "Claude Code" },
{ name: "opencode", displayName: "OpenCode" },
]);
const git = result.tools.find((t) => t.id === "git");
const harness = result.tools.find((t) => t.id === "agent-harness");
expect(git).toMatchObject({ required: true, found: true, detail: "/usr/bin/git" });
expect(git?.hint).toBeUndefined();
expect(harness).toMatchObject({
required: true,
found: true,
detail: "Claude Code, OpenCode",
});
expect(harness?.hint).toBeUndefined();
});

test("fails with a Git install hint when git is missing", () => {
const result = validateRequiredTools({
augmentPath: () => {},
findGit: () => null,
listInstalled: () => [claude],
listAll: () => [claude],
platform: "darwin",
});
expect(result.ok).toBe(false);
const git = result.tools.find((t) => t.id === "git");
expect(git?.found).toBe(false);
expect(git?.hint).toContain("xcode-select --install");
expect(git?.docsUrl).toContain("git-scm.com");
expect(result.tools.find((t) => t.id === "agent-harness")?.found).toBe(true);
});

test("fails when no harness CLI is installed, even if git is present", () => {
const result = validateRequiredTools({
augmentPath: () => {},
findGit: () => "/usr/bin/git",
probeGit: () => true,
listInstalled: () => [],
listAll: () => [claude, opencode],
});
expect(result.ok).toBe(false);
const harness = result.tools.find((t) => t.id === "agent-harness");
expect(harness?.found).toBe(false);
expect(harness?.hint).toContain("Claude Code (`claude`)");
expect(harness?.hint).toContain("OpenCode (`opencode`)");
expect(result.installedHarnesses).toEqual([]);
});

test("one installed harness satisfies the agent requirement", () => {
const result = validateRequiredTools({
augmentPath: () => {},
findGit: () => "/opt/homebrew/bin/git",
probeGit: () => true,
listInstalled: () => [opencode],
listAll: () => [claude, opencode],
});
expect(result.ok).toBe(true);
expect(result.tools.every((t) => t.found)).toBe(true);
expect(result.installedHarnesses).toEqual([{ name: "opencode", displayName: "OpenCode" }]);
});

test("default probe returns a structured result against the real PATH", () => {
const original = process.env.PATH;
try {
const result = validateRequiredTools();
expect(result.tools.map((t) => t.id)).toEqual(["git", "agent-harness"]);
expect(typeof result.ok).toBe("boolean");
expect(Array.isArray(result.warnings)).toBe(true);
} finally {
process.env.PATH = original;
}
});

test("fails when Git resolves on PATH but cannot be invoked", () => {
const result = validateRequiredTools({
augmentPath: () => {},
findGit: () => "/usr/bin/git",
probeGit: () => false,
listInstalled: () => [claude],
listAll: () => [claude],
platform: "darwin",
});

expect(result.ok).toBe(false);
expect(result.tools.find((tool) => tool.id === "git")?.found).toBe(false);
});

test.each([
["global", { AGENT_HARNESS: "opencode", AGENT_CLI_PATH: "/custom/global-agent" }],
["harness-specific", { AGENT_HARNESS: "opencode", OPENCODE_CLI_PATH: "/custom/opencode" }],
])("honors a process-level %s CLI override for the active harness", (_kind, env) => {
const previousHarness = process.env.AGENT_HARNESS;
const previousGlobalPath = process.env.AGENT_CLI_PATH;
const previousHarnessPath = process.env.OPENCODE_CLI_PATH;
delete process.env.AGENT_CLI_PATH;
delete process.env.OPENCODE_CLI_PATH;
Object.assign(process.env, env);
try {
const result = validateRequiredTools({
augmentPath: () => {},
findGit: () => "/usr/bin/git",
probeGit: () => true,
listInstalled: ({ currentHarnessName } = {}) =>
currentHarnessName === "opencode" &&
(process.env.AGENT_CLI_PATH || process.env.OPENCODE_CLI_PATH)
? [opencode]
: [],
listAll: () => [claude, opencode],
});

expect(result.ok).toBe(true);
expect(result.installedHarnesses).toContainEqual({
name: "opencode",
displayName: "OpenCode",
});
} finally {
if (previousHarness === undefined) delete process.env.AGENT_HARNESS;
else process.env.AGENT_HARNESS = previousHarness;
if (previousGlobalPath === undefined) delete process.env.AGENT_CLI_PATH;
else process.env.AGENT_CLI_PATH = previousGlobalPath;
if (previousHarnessPath === undefined) delete process.env.OPENCODE_CLI_PATH;
else process.env.OPENCODE_CLI_PATH = previousHarnessPath;
}
});

test("honors project-local active harness and CLI path overrides", () => {
const originalHarness = process.env.AGENT_HARNESS;
const originalPath = process.env.AGENT_CLI_PATH;
const result = validateRequiredTools({
augmentPath: () => {},
findGit: () => "/usr/bin/git",
probeGit: () => true,
envOverrides: {
AGENT_HARNESS: "opencode",
AGENT_CLI_PATH: "/project/local-agent",
},
listInstalled: ({ currentHarnessName } = {}) =>
currentHarnessName === "opencode" && process.env.AGENT_CLI_PATH === "/project/local-agent"
? [opencode]
: [],
listAll: () => [claude, opencode],
});

expect(result.ok).toBe(true);
expect(result.installedHarnesses).toEqual([{ name: "opencode", displayName: "OpenCode" }]);
expect(process.env.AGENT_HARNESS).toBe(originalHarness);
expect(process.env.AGENT_CLI_PATH).toBe(originalPath);
});

test("reports both required tools missing without a stack trace", () => {
const result = validateRequiredTools({
augmentPath: () => {},
findGit: () => null,
listInstalled: () => [],
listAll: () => [claude],
platform: "linux",
});
expect(result.ok).toBe(false);
expect(result.tools.filter((t) => t.required && !t.found)).toHaveLength(2);
for (const tool of result.tools) {
expect(tool.hint).toBeTruthy();
expect(tool.hint).not.toMatch(/Error:|ENOENT|spawn /);
}
});
});
Loading
Loading