@joeldmtz/prtl 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { Command } from "commander";
2
+ export interface CliIO {
3
+ write(text: string): void;
4
+ error(text: string): void;
5
+ isInteractive: boolean;
6
+ }
7
+ export declare const createProgram: (io?: CliIO) => Command;
8
+ export declare const defaultIO: () => CliIO;
9
+ export declare const runCli: (argv?: string[], io?: CliIO) => Promise<void>;
10
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,MAAM,WAAW,KAAK;IACpB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,OAAO,CAAC;CACxB;AAOD,eAAO,MAAM,aAAa,GAAI,KAAI,KAAmB,KAAG,OA6EvD,CAAC;AAEF,eAAO,MAAM,SAAS,QAAO,KAI3B,CAAC;AAEH,eAAO,MAAM,MAAM,GAAU,OAAM,MAAM,EAAiB,EAAE,KAAK,KAAK,KAAG,OAAO,CAAC,IAAI,CAmBpF,CAAC"}
package/dist/cli.js ADDED
@@ -0,0 +1,106 @@
1
+ import { deleteTarget, listSessions, openTarget, resolveTarget } from "@joeldmtz/prtl-core";
2
+ import { Command } from "commander";
3
+ import { formatHumanList, formatOperationResult } from "./format.js";
4
+ import { confirm } from "./prompt.js";
5
+ import { renderInteractive } from "./tui/index.js";
6
+ const providerOption = (value) => {
7
+ if (value === "herdr" || value === "tmux")
8
+ return value;
9
+ throw new Error("provider must be herdr or tmux");
10
+ };
11
+ export const createProgram = (io = defaultIO()) => {
12
+ const program = new Command();
13
+ program
14
+ .name("prtl")
15
+ .description("Portal for Herdr and tmux sessions")
16
+ .version("0.1.0")
17
+ .showHelpAfterError()
18
+ .exitOverride();
19
+ program
20
+ .command("interactive")
21
+ .aliases(["i", "select"])
22
+ .description("Open the interactive session selector")
23
+ .action(() => renderInteractive());
24
+ program
25
+ .command("list")
26
+ .alias("ls")
27
+ .description("List Herdr and tmux sessions")
28
+ .option("--json", "Output normalized prtl JSON")
29
+ .option("--provider <provider>", "Filter provider: herdr|tmux", providerOption)
30
+ .action(async (options) => {
31
+ const result = await listSessions({ provider: options.provider, timeoutMs: 900 });
32
+ io.write(options.json ? `${JSON.stringify(result, null, 2)}\n` : formatHumanList(result));
33
+ });
34
+ program
35
+ .command("open <name>")
36
+ .alias("o")
37
+ .description("Open a session or pane by name")
38
+ .option("--provider <provider>", "Filter provider: herdr|tmux", providerOption)
39
+ .action(async (name, options) => {
40
+ const listed = await listSessions({ provider: options.provider, timeoutMs: 900 });
41
+ const resolved = resolveTarget(listed.items, name);
42
+ if (resolved.type === "ambiguous" && io.isInteractive) {
43
+ renderInteractive({ items: resolved.matches, query: name });
44
+ return;
45
+ }
46
+ const result = resolved.type === "resolved" ? await openTarget(name, { items: listed.items }) : resolved;
47
+ io.write(formatOperationResult(result));
48
+ if (result.type === "not_found" || result.type === "ambiguous" || result.type === "failed")
49
+ process.exitCode = 1;
50
+ });
51
+ program
52
+ .command("delete <name>")
53
+ .aliases(["rm", "del"])
54
+ .description("Delete a session or pane by name")
55
+ .option("--provider <provider>", "Filter provider: herdr|tmux", providerOption)
56
+ .option("--yes", "Skip confirmation prompt")
57
+ .action(async (name, options) => {
58
+ const listed = await listSessions({ provider: options.provider, timeoutMs: 900 });
59
+ const resolved = resolveTarget(listed.items, name);
60
+ if (resolved.type !== "resolved") {
61
+ io.write(formatOperationResult(resolved));
62
+ process.exitCode = 1;
63
+ return;
64
+ }
65
+ if (!options.yes) {
66
+ if (!io.isInteractive) {
67
+ io.error("Refusing to delete without --yes in noninteractive mode.\n");
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ const accepted = await confirm(`Delete ${resolved.target.provider} ${resolved.target.id}?`);
72
+ if (!accepted) {
73
+ io.write("Delete cancelled.\n");
74
+ return;
75
+ }
76
+ }
77
+ const result = await deleteTarget(name, { items: listed.items });
78
+ io.write(formatOperationResult(result));
79
+ if (result.type === "failed")
80
+ process.exitCode = 1;
81
+ });
82
+ return program;
83
+ };
84
+ export const defaultIO = () => ({
85
+ write: (text) => process.stdout.write(text),
86
+ error: (text) => process.stderr.write(text),
87
+ isInteractive: Boolean(process.stdin.isTTY && process.stdout.isTTY)
88
+ });
89
+ export const runCli = async (argv = process.argv, io) => {
90
+ if (argv.length <= 2) {
91
+ renderInteractive();
92
+ return;
93
+ }
94
+ const program = createProgram(io);
95
+ try {
96
+ await program.parseAsync(argv);
97
+ }
98
+ catch (error) {
99
+ if (error instanceof Error &&
100
+ "code" in error &&
101
+ error.code === "commander.helpDisplayed") {
102
+ return;
103
+ }
104
+ throw error;
105
+ }
106
+ };
@@ -0,0 +1,5 @@
1
+ import type { PrtlListResult, PrtlPane, ProviderOperationResult } from "@joeldmtz/prtl-core";
2
+ export declare const describeTarget: (item: PrtlPane) => string;
3
+ export declare const formatHumanList: (result: PrtlListResult) => string;
4
+ export declare const formatOperationResult: (result: ProviderOperationResult) => string;
5
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,QAAQ,EACR,uBAAuB,EAExB,MAAM,qBAAqB,CAAC;AAI7B,eAAO,MAAM,cAAc,GAAI,MAAM,QAAQ,KAAG,MAW/C,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,QAAQ,cAAc,KAAG,MAuCxD,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,QAAQ,uBAAuB,KAAG,MAevE,CAAC"}
package/dist/format.js ADDED
@@ -0,0 +1,57 @@
1
+ const providerLabel = (item) => item.provider.toUpperCase();
2
+ export const describeTarget = (item) => {
3
+ const hierarchy = item.provider === "herdr"
4
+ ? [item.workspace, item.tab, item.name ?? item.id]
5
+ : [item.session, item.tab, item.name ?? item.id];
6
+ const context = hierarchy.filter(Boolean).join(" > ");
7
+ const agent = item.agent?.name
8
+ ? ` agent=${item.agent.name}${item.agent.status ? `:${item.agent.status}` : ""}`
9
+ : "";
10
+ const cwd = item.cwd ? ` cwd=${item.cwd}` : "";
11
+ return `${providerLabel(item)} ${item.id} ${context}${agent}${cwd}`.trim();
12
+ };
13
+ export const formatHumanList = (result) => {
14
+ const lines = [];
15
+ lines.push(`prtl ${result.version} · ${result.items.length} panes · ${result.generatedAt}`);
16
+ for (const [provider, status] of Object.entries(result.metadata.providers)) {
17
+ const source = status.source ? ` via ${status.source}` : "";
18
+ lines.push(`${provider.toUpperCase()}: ${status.available ? "available" : "unavailable"}${source}`);
19
+ }
20
+ const grouped = result.items.reduce((groups, item) => {
21
+ groups[item.provider] ??= [];
22
+ groups[item.provider].push(item);
23
+ return groups;
24
+ }, {});
25
+ for (const [provider, items] of Object.entries(grouped)) {
26
+ lines.push("");
27
+ lines.push(provider.toUpperCase());
28
+ for (const item of items) {
29
+ const hierarchy = item.provider === "herdr"
30
+ ? `${item.workspace ?? "?"} > ${item.tab ?? "?"}`
31
+ : `${item.session ?? "?"} > ${item.tab ?? "?"}`;
32
+ const label = item.name ?? item.id;
33
+ const agent = item.agent?.name
34
+ ? ` · ${item.agent.name}${item.agent.status ? ` ${item.agent.status}` : ""}`
35
+ : "";
36
+ const detail = item.command ?? item.cwd ?? "";
37
+ lines.push(` ${item.focused ? "*" : " "} ${item.id} ${hierarchy} > ${label}${agent}${detail ? ` · ${detail}` : ""}`);
38
+ }
39
+ }
40
+ return `${lines.join("\n")}\n`;
41
+ };
42
+ export const formatOperationResult = (result) => {
43
+ switch (result.type) {
44
+ case "opened":
45
+ return `Opened ${describeTarget(result.target)}\n`;
46
+ case "deleted":
47
+ return `Deleted ${describeTarget(result.target)}\n`;
48
+ case "failed":
49
+ return `Failed ${describeTarget(result.target)}: ${result.error}\n`;
50
+ case "ambiguous":
51
+ return `Ambiguous target '${result.query}'. Matches:\n${result.matches.map((item) => ` - ${describeTarget(item)}`).join("\n")}\n`;
52
+ case "not_found":
53
+ return `No target found for '${result.query}'.\n`;
54
+ default:
55
+ return "Unknown operation result.\n";
56
+ }
57
+ };
package/dist/main.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ export {};
3
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
package/dist/main.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ import { runCli } from "./cli.js";
3
+ await runCli();
@@ -0,0 +1,2 @@
1
+ export declare const confirm: (message: string) => Promise<boolean>;
2
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC,OAAO,CAO9D,CAAC"}
package/dist/prompt.js ADDED
@@ -0,0 +1,8 @@
1
+ export const confirm = async (message) => {
2
+ process.stdout.write(`${message} [y/N] `);
3
+ for await (const chunk of process.stdin) {
4
+ const answer = chunk.toString().trim().toLowerCase();
5
+ return answer === "y" || answer === "yes";
6
+ }
7
+ return false;
8
+ };
@@ -0,0 +1,10 @@
1
+ import { type PrtlPane } from "@joeldmtz/prtl-core";
2
+ import React from "react";
3
+ interface AppProps {
4
+ initialItems?: PrtlPane[];
5
+ initialQuery?: string;
6
+ onOpen?: (item: PrtlPane, items: PrtlPane[]) => Promise<void> | void;
7
+ }
8
+ export declare const App: ({ initialItems, initialQuery, onOpen }: AppProps) => React.ReactElement;
9
+ export {};
10
+ //# sourceMappingURL=App.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/tui/App.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,QAAQ,EACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAA8B,MAAM,OAAO,CAAC;AAMnD,UAAU,QAAQ;IAChB,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACtE;AAgBD,eAAO,MAAM,GAAG,GAAI,wCAA6C,QAAQ,KAAG,KAAK,CAAC,YAyLjF,CAAC"}
@@ -0,0 +1,121 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { deleteTarget, filterItems, listSessions, openTarget } from "@joeldmtz/prtl-core";
3
+ import { useEffect, useState } from "react";
4
+ import { Box, Text, useApp, useInput } from "ink";
5
+ import TextInput from "ink-text-input";
6
+ const truncate = (value, max) => {
7
+ if (!value)
8
+ return "";
9
+ return value.length > max ? `${value.slice(0, Math.max(0, max - 1))}…` : value;
10
+ };
11
+ const statusLine = (result) => {
12
+ if (!result)
13
+ return "loading providers";
14
+ return Object.entries(result.metadata.providers)
15
+ .map(([provider, status]) => `${provider}:${status.available ? (status.source ?? "ok") : "down"}`)
16
+ .join(" ");
17
+ };
18
+ export const App = ({ initialItems, initialQuery = "", onOpen }) => {
19
+ const { exit } = useApp();
20
+ const columns = process.stdout.columns ?? 80;
21
+ const rows = process.stdout.rows ?? 24;
22
+ const [result, setResult] = useState(initialItems
23
+ ? {
24
+ version: "0.1.0",
25
+ generatedAt: new Date().toISOString(),
26
+ items: initialItems,
27
+ metadata: { providers: {} }
28
+ }
29
+ : undefined);
30
+ const [query, setQuery] = useState(initialQuery);
31
+ const [selected, setSelected] = useState(0);
32
+ const [mode, setMode] = useState("list");
33
+ const [message, setMessage] = useState();
34
+ const load = async () => {
35
+ setMessage(undefined);
36
+ setResult(await listSessions({ timeoutMs: 900 }));
37
+ setSelected(0);
38
+ };
39
+ useEffect(() => {
40
+ if (!initialItems)
41
+ void load();
42
+ }, []);
43
+ const items = filterItems(result?.items ?? [], query);
44
+ const current = items[Math.min(selected, Math.max(0, items.length - 1))];
45
+ const isWide = columns >= 100;
46
+ const isNarrow = columns < 70;
47
+ const visibleRows = Math.max(4, rows - 8);
48
+ const start = Math.max(0, Math.min(selected - Math.floor(visibleRows / 2), Math.max(0, items.length - visibleRows)));
49
+ const visibleItems = items.slice(start, start + visibleRows);
50
+ useInput((input, key) => {
51
+ if (mode === "search") {
52
+ if (key.escape || key.return)
53
+ setMode("list");
54
+ return;
55
+ }
56
+ if (mode === "metadata") {
57
+ if (key.escape || input === "q" || input === "m")
58
+ setMode("list");
59
+ return;
60
+ }
61
+ if (mode === "delete") {
62
+ if (key.escape || input === "n" || input === "q")
63
+ setMode("list");
64
+ if (input === "y" && current) {
65
+ void deleteTarget(current.id, { items }).then((deleted) => {
66
+ setMessage(deleted.type === "deleted" ? `Deleted ${current.id}` : deleted.type);
67
+ setMode("list");
68
+ void load();
69
+ });
70
+ }
71
+ return;
72
+ }
73
+ if (input === "q" || key.escape)
74
+ exit();
75
+ if (input === "/")
76
+ setMode("search");
77
+ if (input === "r")
78
+ void load();
79
+ if (input === "m" && current)
80
+ setMode("metadata");
81
+ if (input === "d" && current)
82
+ setMode("delete");
83
+ if (key.downArrow || input === "j")
84
+ setSelected((value) => Math.min(items.length - 1, value + 1));
85
+ if (key.upArrow || input === "k")
86
+ setSelected((value) => Math.max(0, value - 1));
87
+ if (key.return && current) {
88
+ if (onOpen) {
89
+ void Promise.resolve(onOpen(current, items));
90
+ return;
91
+ }
92
+ void openTarget(current.id, { items }).then((opened) => {
93
+ if (opened.type === "opened")
94
+ exit();
95
+ else
96
+ setMessage(opened.type === "failed" ? opened.error : opened.type);
97
+ });
98
+ }
99
+ });
100
+ const renderRow = (item, index) => {
101
+ const absolute = start + index;
102
+ const hierarchy = item.provider === "herdr"
103
+ ? `${item.workspace ?? "?"} > ${item.tab ?? "?"}`
104
+ : `${item.session ?? "?"} > ${item.tab ?? "?"}`;
105
+ const label = item.name ?? item.id;
106
+ const agent = item.agent?.name
107
+ ? `${item.agent.name}${item.agent.status ? ` ${item.agent.status}` : ""}`
108
+ : item.command;
109
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: absolute === selected ? "cyan" : undefined, children: [absolute === selected ? "›" : " ", " "] }), _jsxs(Text, { color: item.provider === "herdr" ? "magenta" : "green", children: [item.provider.toUpperCase(), " "] }), _jsxs(Text, { children: [truncate(hierarchy, isNarrow ? 20 : 34), " "] }), _jsxs(Text, { bold: true, children: [truncate(label, isNarrow ? 16 : 22), " "] }), !isNarrow && _jsx(Text, { dimColor: true, children: truncate(agent, 28) })] }, `${item.provider}:${item.id}`));
110
+ };
111
+ const detail = current ? (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, width: isWide ? Math.floor(columns * 0.35) : undefined, children: [_jsx(Text, { bold: true, children: "Details" }), _jsxs(Text, { children: ["Provider: ", current.provider] }), _jsxs(Text, { children: ["Source: ", current.metadata.source] }), _jsxs(Text, { children: ["ID: ", current.id] }), _jsxs(Text, { children: ["Name: ", current.name ?? "-"] }), _jsxs(Text, { children: ["CWD: ", truncate(current.cwd, 46) || "-"] }), _jsxs(Text, { children: ["Command: ", current.command ?? "-"] }), _jsxs(Text, { children: ["Agent: ", current.agent?.name ?? "-"] })] })) : null;
112
+ if (mode === "metadata" && current) {
113
+ const rawKeys = current.metadata.raw && typeof current.metadata.raw === "object"
114
+ ? Object.keys(current.metadata.raw).join(", ")
115
+ : typeof current.metadata.raw;
116
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "prtl metadata" }), _jsxs(Text, { children: ["Provider: ", current.provider] }), _jsxs(Text, { children: ["Source: ", current.metadata.source] }), _jsxs(Text, { children: ["Collected: ", current.metadata.collectedAt] }), _jsxs(Text, { children: ["ID: ", current.id] }), _jsxs(Text, { children: ["Raw: ", truncate(rawKeys, columns - 6)] }), _jsx(Text, { dimColor: true, children: "Esc/q/m back" })] }));
117
+ }
118
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "prtl \u00B7 sessions portal" }), _jsx(Text, { dimColor: true, children: statusLine(result) })] }), _jsxs(Box, { children: [_jsx(Text, { children: "Search: " }), mode === "search" ? (_jsx(TextInput, { value: query, onChange: setQuery })) : (_jsx(Text, { children: query || "-" }))] }), _jsxs(Box, { flexDirection: isWide ? "row" : "column", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, flexGrow: 1, children: [result ? visibleItems.map(renderRow) : _jsx(Text, { children: "Loading Herdr and tmux\u2026" }), result && items.length === 0 && _jsx(Text, { dimColor: true, children: "No targets match this search." })] }), isWide
119
+ ? detail
120
+ : current && (_jsx(Text, { dimColor: true, children: truncate(`${current.id} ${current.cwd ?? current.command ?? ""}`, columns - 2) }))] }), mode === "delete" && current && _jsxs(Text, { color: "red", children: ["Delete ", current.id, "? y/N"] }), message && _jsx(Text, { color: "yellow", children: message }), _jsx(Text, { dimColor: true, children: "Enter open \u00B7 d delete \u00B7 m metadata \u00B7 r refresh \u00B7 / search \u00B7 q quit" })] }));
121
+ };
@@ -0,0 +1,6 @@
1
+ import { type PrtlPane } from "@joeldmtz/prtl-core";
2
+ export declare const renderInteractive: (options?: {
3
+ items?: PrtlPane[];
4
+ query?: string;
5
+ }) => void;
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tui/index.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEhE,eAAO,MAAM,iBAAiB,GAAI,UAAU;IAAE,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KAAG,IAiBpF,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from "ink";
3
+ import { App } from "./App.js";
4
+ import { openTarget } from "@joeldmtz/prtl-core";
5
+ export const renderInteractive = (options) => {
6
+ let unmount = () => undefined;
7
+ const instance = render(_jsx(App, { initialItems: options?.items, initialQuery: options?.query, onOpen: async (item, items) => {
8
+ unmount();
9
+ const result = await openTarget(item.id, { items });
10
+ if (result.type === "failed") {
11
+ process.stderr.write(`${result.error}\n`);
12
+ process.exitCode = 1;
13
+ }
14
+ } }));
15
+ unmount = () => instance.unmount();
16
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@joeldmtz/prtl",
3
+ "version": "0.1.2",
4
+ "type": "module",
5
+ "bin": {
6
+ "prtl": "dist/main.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "engines": {
15
+ "bun": ">=1.0.0"
16
+ },
17
+ "dependencies": {
18
+ "@joeldmtz/prtl-core": "0.1.2",
19
+ "commander": "latest",
20
+ "ink": "latest",
21
+ "ink-text-input": "latest",
22
+ "react": "latest",
23
+ "string-width": "latest"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "prepublishOnly": "bun ../../scripts/resolve-workspace.ts",
28
+ "postpublish": "bun ../../scripts/restore-workspace.ts"
29
+ },
30
+ "devDependencies": {
31
+ "@types/bun": "latest",
32
+ "@types/react": "latest",
33
+ "ink-testing-library": "latest"
34
+ }
35
+ }