pi-link 0.1.9 → 0.1.10

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/bin/pi-link.mjs CHANGED
@@ -1,13 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // pi-link CLI — resolve session by name and launch Pi with --link-name
3
+ // pi-link CLI — launch Pi with session resume by name
4
4
  //
5
5
  // Usage:
6
- // pi-link start <name> [pi-flags...]
7
- //
8
- // If a session named <name> exists, resumes it.
9
- // If not, creates a new session.
10
- // Always connects to the link as <name>.
6
+ // pi-link <name> [flags...] Resume or create a named session, connected to link.
7
+ // pi-link resolve <name> Print just the session path (machine-readable).
11
8
 
12
9
  import { readdir, stat } from "fs/promises";
13
10
  import { createReadStream } from "fs";
@@ -18,9 +15,7 @@ import { spawn } from "child_process";
18
15
 
19
16
  const SESSIONS_DIR = join(homedir(), ".pi", "agent", "sessions");
20
17
 
21
- // ── Session scanning ───────────────────────────────────────────────────────
22
-
23
- async function getSessionName(filePath) {
18
+ async function getSessionMeta(filePath) {
24
19
  let name;
25
20
  let cwd;
26
21
  const rl = createInterface({ input: createReadStream(filePath, "utf-8"), crlfDelay: Infinity });
@@ -28,9 +23,9 @@ async function getSessionName(filePath) {
28
23
  if (!line) continue;
29
24
  try {
30
25
  const entry = JSON.parse(line);
31
- if (entry.type === "session" && entry.cwd) cwd = entry.cwd;
32
- if (entry.type === "session_info" && entry.name !== undefined) {
33
- name = entry.name?.trim() || undefined;
26
+ if (entry.type === "session" && typeof entry.cwd === "string") cwd = entry.cwd;
27
+ if (entry.type === "session_info" && typeof entry.name === "string") {
28
+ name = entry.name.trim().replace(/\s+/g, " ") || undefined;
34
29
  }
35
30
  } catch {
36
31
  // skip malformed lines
@@ -64,7 +59,7 @@ async function findSessionsByName(targetName) {
64
59
  if (!file.endsWith(".jsonl")) continue;
65
60
  const filePath = join(dirPath, file);
66
61
  try {
67
- const { name, cwd } = await getSessionName(filePath);
62
+ const { name, cwd } = await getSessionMeta(filePath);
68
63
  if (name === targetName) {
69
64
  const stats = await stat(filePath);
70
65
  matches.push({ path: filePath, cwd: cwd || "?", modified: stats.mtime });
@@ -88,67 +83,82 @@ async function findSessionsByName(targetName) {
88
83
 
89
84
  // ── CLI ────────────────────────────────────────────────────────────────────
90
85
 
91
- const args = process.argv.slice(2);
92
- const command = args[0];
93
-
94
- if (command !== "start" || args.length < 2) {
95
- console.log(`Usage: pi-link start <name> [pi-flags...]
86
+ const [command, ...args] = process.argv.slice(2);
96
87
 
97
- Start Pi connected to the link as <name>.
98
- Resumes a session named <name> if one exists, otherwise creates a new session.
99
-
100
- Examples:
101
- pi-link start worker-1
102
- pi-link start worker-1 --model sonnet
103
- pi-link start worker-1 --model sonnet --thinking high`);
104
- process.exit(command === "start" ? 1 : 0);
105
- }
106
-
107
- const name = args[1].trim().replace(/\s+/g, " ");
108
- if (!name) {
109
- console.error("Error: name cannot be empty.");
88
+ function printCandidates(name, matches) {
89
+ console.error(`Multiple sessions named "${name}":\n`);
90
+ for (const m of matches) {
91
+ console.error(` ${m.modified.toISOString().slice(0, 19)} cwd: ${m.cwd}`);
92
+ console.error(` ${m.path}\n`);
93
+ }
94
+ console.error(`Use: pi --session <path> --link`);
110
95
  process.exit(1);
111
96
  }
112
97
 
113
- const extraFlags = args.slice(2);
114
- for (const flag of ["--session", "--link-name"]) {
115
- if (extraFlags.includes(flag)) {
116
- console.error(`Error: ${flag} is managed by pi-link start. Remove it.`);
98
+ if (command === "resolve") {
99
+ const name = args[0]?.trim().replace(/\s+/g, " ");
100
+ if (!name) {
101
+ console.error("Usage: pi-link resolve <name>");
102
+ process.exit(1);
103
+ }
104
+ const matches = await findSessionsByName(name);
105
+ if (matches.length === 1) {
106
+ process.stdout.write(matches[0].path);
107
+ } else if (matches.length > 1) {
108
+ printCandidates(name, matches);
109
+ }
110
+ } else if (command && command !== "--help" && command !== "-h") {
111
+ // pi-link <name> [flags...] — resolve and launch Pi
112
+ const name = command.trim().replace(/\s+/g, " ");
113
+ if (!name) {
114
+ console.error("Usage: pi-link <name> [pi flags...]");
117
115
  process.exit(1);
118
116
  }
119
- }
120
-
121
- console.log(`Searching for session "${name}"...`);
122
- const matches = await findSessionsByName(name);
123
-
124
- const piArgs = [];
125
117
 
126
- if (matches.length === 1) {
127
- console.log(`Resuming session: ${matches[0].path}`);
128
- piArgs.push("--session", matches[0].path);
129
- } else if (matches.length > 1) {
130
- console.error(`\nMultiple sessions named "${name}":\n`);
131
- for (const m of matches) {
132
- console.error(` ${m.modified.toISOString().slice(0, 19)} cwd: ${m.cwd}`);
133
- console.error(` ${m.path}\n`);
118
+ // Reject conflicting flags
119
+ for (const flag of args) {
120
+ const key = flag.split("=")[0];
121
+ if (["--session", "--continue", "-c", "--resume", "-r", "--fork", "--no-session", "--session-dir"].includes(key)) {
122
+ console.error(`Error: ${key} is managed by pi-link. Remove it.`);
123
+ process.exit(1);
124
+ }
125
+ if (key === "--link-name") {
126
+ console.error("Error: --link-name was removed. Use: pi-link <name>");
127
+ process.exit(1);
128
+ }
134
129
  }
135
- console.error(`Use pi --session <path> --link-name ${name} to pick one.`);
136
- process.exit(1);
137
- } else {
138
- console.log("No existing session found. Starting new session.");
139
- }
140
130
 
141
- piArgs.push("--link-name", name, ...extraFlags);
131
+ const matches = await findSessionsByName(name);
132
+ if (matches.length > 1) {
133
+ printCandidates(name, matches);
134
+ }
142
135
 
143
- // On Windows, resolve 'pi' through the shell so .cmd/.ps1 shims work
144
- const isWin = process.platform === "win32";
145
- const cmd = isWin ? "cmd" : "pi";
146
- const cmdArgs = isWin ? ["/c", "pi", ...piArgs] : piArgs;
136
+ const piArgs = [];
137
+ if (matches.length === 1) {
138
+ console.error(`Resuming session: ${matches[0].path}`);
139
+ piArgs.push("--session", matches[0].path);
140
+ } else {
141
+ console.error("No existing session found. Starting new session.");
142
+ }
143
+ piArgs.push("--link", ...args);
147
144
 
148
- const child = spawn(cmd, cmdArgs, { stdio: "inherit" });
145
+ const isWin = process.platform === "win32";
146
+ const cmd = isWin ? "cmd.exe" : "pi";
147
+ const cmdArgs = isWin ? ["/d", "/c", "pi", ...piArgs] : piArgs;
149
148
 
150
- child.on("exit", (code) => process.exit(code ?? 0));
151
- child.on("error", (err) => {
152
- console.error(`Failed to start pi: ${err.message}`);
153
- process.exit(1);
154
- });
149
+ const child = spawn(cmd, cmdArgs, {
150
+ stdio: "inherit",
151
+ env: { ...process.env, PI_LINK_NAME: name },
152
+ });
153
+ child.once("exit", (code, signal) => {
154
+ if (code !== null) process.exit(code);
155
+ process.exit(signal === "SIGINT" ? 130 : 1);
156
+ });
157
+ child.once("error", (err) => {
158
+ console.error(`Failed to start pi: ${err.message}`);
159
+ process.exit(1);
160
+ });
161
+ } else {
162
+ console.error("Usage: pi-link <name> [pi flags...]\n pi-link resolve <name>");
163
+ process.exit(0);
164
+ }