pi-link 0.1.17 → 0.3.0
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/CHANGELOG.md +62 -0
- package/README.md +156 -217
- package/bin/pi-link.mjs +121 -126
- package/index.ts +472 -550
- package/package.json +1 -1
- package/skills/pi-link-coordination/SKILL.md +89 -79
package/bin/pi-link.mjs
CHANGED
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
// pi-link --list [--global|-g] List pi-link sessions in current cwd (or everywhere).
|
|
9
9
|
// pi-link --resolve <name> [--global|-g]
|
|
10
10
|
// Print just the session path (machine-readable).
|
|
11
|
-
//
|
|
12
|
-
// Deprecated subcommand forms `pi-link list` / `pi-link resolve <name>` still
|
|
13
|
-
// work for one release with a stderr warning.
|
|
11
|
+
// pi-link --version Print the installed pi-link version.
|
|
14
12
|
|
|
15
13
|
import { readdir, stat } from "fs/promises";
|
|
16
14
|
import { createReadStream, existsSync, readFileSync } from "fs";
|
|
@@ -19,6 +17,12 @@ import { join } from "path";
|
|
|
19
17
|
import { homedir } from "os";
|
|
20
18
|
import { spawn } from "child_process";
|
|
21
19
|
|
|
20
|
+
// Canonicalize a link/session name: trim + collapse internal whitespace.
|
|
21
|
+
// Must match the extension's normalizeName (index.ts).
|
|
22
|
+
function normalizeName(s) {
|
|
23
|
+
return s.trim().replace(/\s+/g, " ");
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
// ── Pi config resolution ───────────────────────────────────────────────────
|
|
23
27
|
// Match Pi's session-dir lookup order so list/resolve/<name> see what Pi sees.
|
|
24
28
|
// Custom sessionDir → flat layout; default → <agentDir>/sessions/<encoded-cwd>.
|
|
@@ -67,32 +71,49 @@ function resolveSessionDir(cwd, agentDir) {
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
// Reads a session JSONL file and returns its display name, cwd, id, link
|
|
70
|
-
// status, and message count.
|
|
74
|
+
// status, and message count. Returns null when `scopeCwd` is given and the
|
|
75
|
+
// session's header names a different cwd.
|
|
71
76
|
//
|
|
72
77
|
// Name precedence: latest valid `link-name` custom entry wins as the
|
|
73
78
|
// authoritative pi-link name. `session_info.name` is only a fallback for
|
|
74
79
|
// sessions that never set a link-name. Historical link-names are not aliases.
|
|
75
|
-
|
|
80
|
+
//
|
|
81
|
+
// A scoped scan only ever keeps sessions from `scopeCwd`, so a session whose
|
|
82
|
+
// header names another cwd is abandoned there instead of being read to EOF for
|
|
83
|
+
// a name that would be filtered out anyway. Pi writes that header as the first
|
|
84
|
+
// complete line of the file, before any history.
|
|
85
|
+
//
|
|
86
|
+
// Only `undefined` means unscoped: a normalized scope is the empty string at
|
|
87
|
+
// POSIX root, which is a real scope and must not read as "no scope".
|
|
88
|
+
async function getSessionMeta(filePath, scopeCwd) {
|
|
76
89
|
let linkName;
|
|
77
90
|
let sessionName;
|
|
78
91
|
let cwd;
|
|
79
92
|
let id;
|
|
80
93
|
let hasLinkName = false;
|
|
81
94
|
let messages = 0;
|
|
82
|
-
const
|
|
95
|
+
const input = createReadStream(filePath, "utf-8");
|
|
96
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
83
97
|
for await (const line of rl) {
|
|
84
98
|
if (!line) continue;
|
|
85
99
|
try {
|
|
86
100
|
const entry = JSON.parse(line);
|
|
87
101
|
if (entry.type === "session") {
|
|
88
|
-
if (typeof entry.cwd === "string")
|
|
102
|
+
if (typeof entry.cwd === "string") {
|
|
103
|
+
if (scopeCwd !== undefined && normalizePath(entry.cwd) !== scopeCwd) {
|
|
104
|
+
rl.close();
|
|
105
|
+
input.destroy();
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
cwd = entry.cwd;
|
|
109
|
+
}
|
|
89
110
|
if (typeof entry.id === "string") id = entry.id;
|
|
90
111
|
} else if (entry.type === "session_info" && typeof entry.name === "string") {
|
|
91
|
-
sessionName = entry.name
|
|
112
|
+
sessionName = normalizeName(entry.name) || undefined;
|
|
92
113
|
} else if (entry.type === "custom" && entry.customType === "link-name") {
|
|
93
114
|
hasLinkName = true;
|
|
94
115
|
if (entry.data && typeof entry.data.name === "string") {
|
|
95
|
-
const n = entry.data.name
|
|
116
|
+
const n = normalizeName(entry.data.name);
|
|
96
117
|
if (n) linkName = n;
|
|
97
118
|
}
|
|
98
119
|
} else if (entry.type === "message" || entry.type === "user" || entry.type === "assistant") {
|
|
@@ -142,9 +163,10 @@ function relTime(d) {
|
|
|
142
163
|
return d.toISOString().slice(0, 10);
|
|
143
164
|
}
|
|
144
165
|
|
|
145
|
-
async function loadSessionRecord(filePath) {
|
|
166
|
+
async function loadSessionRecord(filePath, scopeCwd) {
|
|
146
167
|
try {
|
|
147
|
-
const meta = await getSessionMeta(filePath);
|
|
168
|
+
const meta = await getSessionMeta(filePath, scopeCwd);
|
|
169
|
+
if (!meta) return null; // known-foreign: not even worth a stat
|
|
148
170
|
const stats = await stat(filePath);
|
|
149
171
|
return { ...meta, modified: stats.mtime, path: filePath };
|
|
150
172
|
} catch {
|
|
@@ -152,11 +174,12 @@ async function loadSessionRecord(filePath) {
|
|
|
152
174
|
}
|
|
153
175
|
}
|
|
154
176
|
|
|
155
|
-
// Returns meta + mtime + path for every readable session in `dir
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
177
|
+
// Returns meta + mtime + path for every readable session in `dir`, or only
|
|
178
|
+
// those from `scopeCwd` when it is given. Custom layout is flat
|
|
179
|
+
// (<dir>/*.jsonl); default layout has one subdir level per encoded cwd
|
|
180
|
+
// (<dir>/<sub>/*.jsonl). Errors on individual files/dirs are silently skipped
|
|
181
|
+
// — active or partially-written sessions are tolerated.
|
|
182
|
+
async function scanSessions(dir, isCustom, scopeCwd) {
|
|
160
183
|
let entries;
|
|
161
184
|
try {
|
|
162
185
|
entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -168,7 +191,7 @@ async function scanSessions(dir, isCustom) {
|
|
|
168
191
|
if (isCustom) {
|
|
169
192
|
for (const entry of entries) {
|
|
170
193
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
171
|
-
tasks.push(loadSessionRecord(join(dir, entry.name)));
|
|
194
|
+
tasks.push(loadSessionRecord(join(dir, entry.name), scopeCwd));
|
|
172
195
|
}
|
|
173
196
|
} else {
|
|
174
197
|
for (const sub of entries) {
|
|
@@ -178,7 +201,7 @@ async function scanSessions(dir, isCustom) {
|
|
|
178
201
|
try { files = await readdir(subPath); } catch { continue; }
|
|
179
202
|
for (const file of files) {
|
|
180
203
|
if (!file.endsWith(".jsonl")) continue;
|
|
181
|
-
tasks.push(loadSessionRecord(join(subPath, file)));
|
|
204
|
+
tasks.push(loadSessionRecord(join(subPath, file), scopeCwd));
|
|
182
205
|
}
|
|
183
206
|
}
|
|
184
207
|
}
|
|
@@ -186,28 +209,28 @@ async function scanSessions(dir, isCustom) {
|
|
|
186
209
|
return (await Promise.all(tasks)).filter((s) => s !== null);
|
|
187
210
|
}
|
|
188
211
|
|
|
189
|
-
// Find sessions whose current display name matches `targetName
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
// to `session_info.name` for sessions without a link-name (so `pi-link <name>`
|
|
212
|
+
// Find sessions whose current display name matches `targetName`, restricted to
|
|
213
|
+
// `scopeCwd` when given and searched across every cwd when not. Falls back to
|
|
214
|
+
// `session_info.name` for sessions without a link-name (so `pi-link <name>`
|
|
193
215
|
// can attach link to a previously-unlinked named session).
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
216
|
+
//
|
|
217
|
+
// The cwd predicate is applied here as well as in the scan: a session whose
|
|
218
|
+
// header carries no cwd is not rejected early, and must still be left out of a
|
|
219
|
+
// scoped result.
|
|
220
|
+
async function findSessionsByName(targetName, dir, isCustom, scopeCwd) {
|
|
221
|
+
return (await scanSessions(dir, isCustom, scopeCwd))
|
|
197
222
|
.filter((s) => s.name === targetName)
|
|
223
|
+
.filter((s) => scopeCwd === undefined || (s.cwd && normalizePath(s.cwd) === scopeCwd))
|
|
198
224
|
.map((s) => ({ path: s.path, cwd: s.cwd || "?", modified: s.modified }))
|
|
199
225
|
.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
200
|
-
const local = all.filter((s) => normalizePath(s.cwd) === localCwd);
|
|
201
|
-
return { local, all };
|
|
202
226
|
}
|
|
203
227
|
|
|
204
|
-
// List pi-link sessions (those with at least one link-name entry)
|
|
205
|
-
//
|
|
206
|
-
async function listSessions({
|
|
207
|
-
|
|
208
|
-
return (await scanSessions(dir, isCustom))
|
|
228
|
+
// List pi-link sessions (those with at least one link-name entry), restricted
|
|
229
|
+
// to `scopeCwd` when given and covering every cwd when not.
|
|
230
|
+
async function listSessions({ dir, isCustom, scopeCwd }) {
|
|
231
|
+
return (await scanSessions(dir, isCustom, scopeCwd))
|
|
209
232
|
.filter((s) => s.hasLinkName)
|
|
210
|
-
.filter((s) =>
|
|
233
|
+
.filter((s) => scopeCwd === undefined || (s.cwd && normalizePath(s.cwd) === scopeCwd))
|
|
211
234
|
.map((s) => ({
|
|
212
235
|
name: s.name || "(unnamed)",
|
|
213
236
|
cwd: s.cwd || "?",
|
|
@@ -237,20 +260,9 @@ function renderTable(rows, columns) {
|
|
|
237
260
|
|
|
238
261
|
const rawArgs = process.argv.slice(2);
|
|
239
262
|
|
|
240
|
-
// Reject pi-link flags renamed in 0.1.12 with a clear pointer to the new name.
|
|
241
|
-
// Same intent as `rejectManagedFlag` (specific message > generic "Unknown argument")
|
|
242
|
-
// but for our own renames, not Pi-managed flags.
|
|
243
|
-
function rejectRenamedFlag(token) {
|
|
244
|
-
if (token === "--all" || token === "-a") {
|
|
245
|
-
const replacement = token === "-a" ? "-g" : "--global";
|
|
246
|
-
console.error(`Error: ${token} was renamed to ${replacement}.`);
|
|
247
|
-
process.exit(1);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
263
|
// Reject Pi flags that pi-link manages, plus --link-name (which exists at the
|
|
252
264
|
// `pi` level for link-only naming, but the wrapper's combined-mode contract
|
|
253
|
-
// conflicts with it). Called from Phase
|
|
265
|
+
// conflicts with it). Called from Phase 4 (mode entry) and Phase 5 (after
|
|
254
266
|
// launcher name), so it fires on both `pi-link --session foo` and
|
|
255
267
|
// `pi-link foo --session bar` with the friendly message.
|
|
256
268
|
function rejectManagedFlag(token) {
|
|
@@ -288,22 +300,27 @@ function printHelp() {
|
|
|
288
300
|
console.error("Usage: pi-link <name> [--global|-g] [pi flags...]");
|
|
289
301
|
console.error(" pi-link --list [--global|-g]");
|
|
290
302
|
console.error(" pi-link --resolve <name> [--global|-g]");
|
|
303
|
+
console.error(" pi-link --version");
|
|
291
304
|
console.error("");
|
|
292
305
|
console.error("By default, name lookup is scoped to the current cwd.");
|
|
293
306
|
console.error("--global / -g widens the search to sessions in any cwd.");
|
|
294
307
|
}
|
|
295
308
|
|
|
296
|
-
function
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
309
|
+
function printVersion() {
|
|
310
|
+
try {
|
|
311
|
+
const pkg = JSON.parse(
|
|
312
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf-8"),
|
|
313
|
+
);
|
|
314
|
+
console.log(pkg.version ?? "unknown");
|
|
315
|
+
} catch {
|
|
316
|
+
console.log("unknown");
|
|
317
|
+
}
|
|
302
318
|
}
|
|
303
319
|
|
|
304
320
|
function describeMode(mode) {
|
|
305
321
|
switch (mode) {
|
|
306
322
|
case "help": return "--help";
|
|
323
|
+
case "version": return "--version";
|
|
307
324
|
case "list": return "--list";
|
|
308
325
|
case "resolve": return "--resolve";
|
|
309
326
|
case "launcher": return "session name";
|
|
@@ -313,23 +330,19 @@ function describeMode(mode) {
|
|
|
313
330
|
|
|
314
331
|
// ── Parser ─────────────────────────────────────────────────────────────────
|
|
315
332
|
//
|
|
316
|
-
// Single sequential pass populates `state`; dispatcher reads it. Phases
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
// 5. Mode-specific extra-token rejection (with deprecated-resolve leniency)
|
|
323
|
-
// 6. Launcher mode entry (mode null + bare positional)
|
|
324
|
-
// 7. Launcher passthrough (mode launcher) with orphan-positional rejection
|
|
333
|
+
// Single sequential pass populates `state`; dispatcher reads it. Phases:
|
|
334
|
+
// 1. Global flags (--global, --help, --version, --)
|
|
335
|
+
// 2. Mode-selecting flags (--list, --resolve, --resolve=<name>)
|
|
336
|
+
// 3. Mode-specific extra-token rejection
|
|
337
|
+
// 4. Launcher mode entry (mode null + bare positional)
|
|
338
|
+
// 5. Launcher passthrough (mode launcher) with orphan-positional rejection
|
|
325
339
|
|
|
326
340
|
const state = {
|
|
327
|
-
mode: null, // null | "help" | "list" | "resolve" | "launcher"
|
|
341
|
+
mode: null, // null | "help" | "version" | "list" | "resolve" | "launcher"
|
|
328
342
|
resolveName: null,
|
|
329
343
|
launcherName: null,
|
|
330
344
|
global: false,
|
|
331
345
|
piPassthrough: [],
|
|
332
|
-
deprecated: null, // null | "list" | "resolve"
|
|
333
346
|
};
|
|
334
347
|
|
|
335
348
|
function setMode(mode) {
|
|
@@ -344,10 +357,7 @@ let lastWasFlag = false;
|
|
|
344
357
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
345
358
|
const a = rawArgs[i];
|
|
346
359
|
|
|
347
|
-
// Phase 1:
|
|
348
|
-
rejectRenamedFlag(a);
|
|
349
|
-
|
|
350
|
-
// Phase 2: global flags / scope-affecting tokens.
|
|
360
|
+
// Phase 1: global flags / scope-affecting tokens.
|
|
351
361
|
if (a === "--global" || a === "-g") {
|
|
352
362
|
state.global = true;
|
|
353
363
|
lastWasFlag = false;
|
|
@@ -357,6 +367,10 @@ for (let i = 0; i < rawArgs.length; i++) {
|
|
|
357
367
|
setMode("help"); // errors if combined with another mode
|
|
358
368
|
continue;
|
|
359
369
|
}
|
|
370
|
+
if (a === "--version") {
|
|
371
|
+
setMode("version"); // errors if combined with another mode
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
360
374
|
if (a === "--") {
|
|
361
375
|
// `--` only meaningful in launcher mode (separates pi flags from positionals).
|
|
362
376
|
if (state.mode !== "launcher") {
|
|
@@ -369,7 +383,7 @@ for (let i = 0; i < rawArgs.length; i++) {
|
|
|
369
383
|
break;
|
|
370
384
|
}
|
|
371
385
|
|
|
372
|
-
// Phase
|
|
386
|
+
// Phase 2: mode-selecting flags.
|
|
373
387
|
if (a === "--list") {
|
|
374
388
|
setMode("list");
|
|
375
389
|
continue;
|
|
@@ -392,57 +406,36 @@ for (let i = 0; i < rawArgs.length; i++) {
|
|
|
392
406
|
continue;
|
|
393
407
|
}
|
|
394
408
|
|
|
395
|
-
// Phase
|
|
396
|
-
if (state.mode === null && (a === "list" || a === "resolve")) {
|
|
397
|
-
state.deprecated = a;
|
|
398
|
-
if (a === "list") {
|
|
399
|
-
setMode("list");
|
|
400
|
-
} else {
|
|
401
|
-
setMode("resolve");
|
|
402
|
-
const next = rawArgs[i + 1];
|
|
403
|
-
if (next !== undefined && !next.startsWith("-")) {
|
|
404
|
-
state.resolveName = next;
|
|
405
|
-
i++;
|
|
406
|
-
}
|
|
407
|
-
// else: leave null; Phase 5 leniency or post-parse validation handles it.
|
|
408
|
-
}
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Phase 5: mode-specific extra-token rejection.
|
|
409
|
+
// Phase 3: mode-specific extra-token rejection.
|
|
413
410
|
if (state.mode === "help") {
|
|
414
411
|
fail(`--help does not accept arguments: ${a}`);
|
|
415
412
|
}
|
|
413
|
+
if (state.mode === "version") {
|
|
414
|
+
fail(`--version does not accept arguments: ${a}`);
|
|
415
|
+
}
|
|
416
416
|
if (state.mode === "list") {
|
|
417
417
|
fail(`--list does not accept argument: ${a}\n Usage: pi-link --list [--global|-g]`);
|
|
418
418
|
}
|
|
419
419
|
if (state.mode === "resolve") {
|
|
420
|
-
// Deprecated-form leniency: `pi-link resolve --global foo` was order-independent;
|
|
421
|
-
// if we entered via deprecated path and haven't bound a name yet, take this.
|
|
422
|
-
if (
|
|
423
|
-
state.deprecated === "resolve" &&
|
|
424
|
-
state.resolveName === null &&
|
|
425
|
-
!a.startsWith("-")
|
|
426
|
-
) {
|
|
427
|
-
state.resolveName = a;
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
420
|
fail(`--resolve accepts exactly one name; got extra: ${a}`);
|
|
431
421
|
}
|
|
432
422
|
|
|
433
|
-
// Phase
|
|
434
|
-
// (lastWasFlag is still false here — only Phase
|
|
423
|
+
// Phase 4: launcher mode entry. state.mode === null here, no name set yet.
|
|
424
|
+
// (lastWasFlag is still false here — only Phase 5 sets it, and Phase 5 requires launcher mode.)
|
|
435
425
|
if (state.mode === null) {
|
|
436
426
|
rejectManagedFlag(a);
|
|
437
427
|
if (a.startsWith("-")) {
|
|
438
428
|
fail(`Unknown argument: ${a}\n Usage: pi-link <name> [--global|-g] [pi flags...]`);
|
|
439
429
|
}
|
|
430
|
+
if (a === "list" || a === "resolve") {
|
|
431
|
+
fail(`'pi-link ${a}' was removed. Use 'pi-link --${a}'.`);
|
|
432
|
+
}
|
|
440
433
|
state.mode = "launcher";
|
|
441
434
|
state.launcherName = a;
|
|
442
435
|
continue;
|
|
443
436
|
}
|
|
444
437
|
|
|
445
|
-
// Phase
|
|
438
|
+
// Phase 5: launcher mode, name set. Tokens go to passthrough or get rejected.
|
|
446
439
|
rejectManagedFlag(a);
|
|
447
440
|
if (a.startsWith("-")) {
|
|
448
441
|
state.piPassthrough.push(a);
|
|
@@ -466,24 +459,20 @@ if (state.mode === "resolve") {
|
|
|
466
459
|
if (state.resolveName === null) {
|
|
467
460
|
fail(`--resolve requires a name argument.\n Usage: pi-link --resolve <name> [--global|-g]`);
|
|
468
461
|
}
|
|
469
|
-
const normalized = state.resolveName
|
|
462
|
+
const normalized = normalizeName(state.resolveName);
|
|
470
463
|
if (!normalized) {
|
|
471
464
|
fail(`--resolve requires a non-empty name argument.\n Usage: pi-link --resolve <name> [--global|-g]`);
|
|
472
465
|
}
|
|
473
466
|
state.resolveName = normalized;
|
|
474
467
|
}
|
|
475
468
|
if (state.mode === "launcher") {
|
|
476
|
-
const normalized = state.launcherName
|
|
469
|
+
const normalized = normalizeName(state.launcherName);
|
|
477
470
|
if (!normalized) {
|
|
478
471
|
fail(`session name cannot be empty.\n Usage: pi-link <name> [--global|-g] [pi flags...]`);
|
|
479
472
|
}
|
|
480
473
|
state.launcherName = normalized;
|
|
481
474
|
}
|
|
482
475
|
|
|
483
|
-
if (state.deprecated) {
|
|
484
|
-
printDeprecationWarning(state.deprecated);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
476
|
// ── Dispatch ───────────────────────────────────────────────────────────────
|
|
488
477
|
|
|
489
478
|
switch (state.mode) {
|
|
@@ -492,6 +481,10 @@ switch (state.mode) {
|
|
|
492
481
|
printHelp();
|
|
493
482
|
process.exit(0);
|
|
494
483
|
break; // unreachable; present to satisfy no-fallthrough lints
|
|
484
|
+
case "version":
|
|
485
|
+
printVersion();
|
|
486
|
+
process.exit(0);
|
|
487
|
+
break; // unreachable; present to satisfy no-fallthrough lints
|
|
495
488
|
case "list":
|
|
496
489
|
await runList(state);
|
|
497
490
|
break;
|
|
@@ -507,9 +500,14 @@ switch (state.mode) {
|
|
|
507
500
|
|
|
508
501
|
// ── Mode handlers ──────────────────────────────────────────────────────────
|
|
509
502
|
|
|
503
|
+
// A local operation scans only its own cwd; `--global` scans every cwd.
|
|
504
|
+
function localScope(state) {
|
|
505
|
+
return state.global ? undefined : normalizePath(process.cwd());
|
|
506
|
+
}
|
|
507
|
+
|
|
510
508
|
async function runList(state) {
|
|
511
509
|
const { dir, isCustom } = resolveSessionDir(process.cwd(), resolveAgentDir());
|
|
512
|
-
const sessions = await listSessions({
|
|
510
|
+
const sessions = await listSessions({ dir, isCustom, scopeCwd: localScope(state) });
|
|
513
511
|
if (sessions.length === 0) {
|
|
514
512
|
console.log(state.global ? "No pi-link sessions found." : "No pi-link sessions found in this cwd.");
|
|
515
513
|
console.log("Start one: pi-link <name>");
|
|
@@ -517,18 +515,18 @@ async function runList(state) {
|
|
|
517
515
|
}
|
|
518
516
|
const columns = state.global
|
|
519
517
|
? [
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
518
|
+
{ header: "NAME", get: (s) => s.name },
|
|
519
|
+
{ header: "CWD", get: (s) => displayPath(s.cwd) },
|
|
520
|
+
{ header: "MODIFIED", get: (s) => relTime(s.modified), dim: true },
|
|
521
|
+
{ header: "MESSAGES", get: (s) => s.messages, dim: true },
|
|
522
|
+
{ header: "ID", get: (s) => s.id, dim: true },
|
|
523
|
+
]
|
|
526
524
|
: [
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
525
|
+
{ header: "NAME", get: (s) => s.name },
|
|
526
|
+
{ header: "MODIFIED", get: (s) => relTime(s.modified), dim: true },
|
|
527
|
+
{ header: "MESSAGES", get: (s) => s.messages, dim: true },
|
|
528
|
+
{ header: "ID", get: (s) => s.id, dim: true },
|
|
529
|
+
];
|
|
532
530
|
console.log(renderTable(sessions, columns));
|
|
533
531
|
if (process.stdout.isTTY) {
|
|
534
532
|
console.log("");
|
|
@@ -539,8 +537,7 @@ async function runList(state) {
|
|
|
539
537
|
async function runResolve(state) {
|
|
540
538
|
const name = state.resolveName; // already normalized
|
|
541
539
|
const { dir, isCustom } = resolveSessionDir(process.cwd(), resolveAgentDir());
|
|
542
|
-
const
|
|
543
|
-
const matches = state.global ? all : local;
|
|
540
|
+
const matches = await findSessionsByName(name, dir, isCustom, localScope(state));
|
|
544
541
|
if (matches.length === 1) {
|
|
545
542
|
process.stdout.write(matches[0].path);
|
|
546
543
|
return; // exit 0
|
|
@@ -549,18 +546,17 @@ async function runResolve(state) {
|
|
|
549
546
|
printCandidates(name, matches); // exits 1
|
|
550
547
|
}
|
|
551
548
|
// matches.length === 0 → not found; exit 2 to distinguish from ambiguous.
|
|
549
|
+
// A local scan never reads names from other cwds, so the advice is offered
|
|
550
|
+
// without claiming anything is there.
|
|
552
551
|
console.error(`No session named "${name}" found${state.global ? "" : " in this cwd"}.`);
|
|
553
|
-
if (!state.global
|
|
554
|
-
console.error(`(${all.length} match${all.length === 1 ? "" : "es"} in other cwds — try --global to consider ${all.length === 1 ? "it" : "them"}.)`);
|
|
555
|
-
}
|
|
552
|
+
if (!state.global) console.error("Use --global to search other cwds.");
|
|
556
553
|
process.exit(2);
|
|
557
554
|
}
|
|
558
555
|
|
|
559
556
|
async function runLauncher(state) {
|
|
560
557
|
const name = state.launcherName; // already normalized
|
|
561
558
|
const { dir, isCustom } = resolveSessionDir(process.cwd(), resolveAgentDir());
|
|
562
|
-
const
|
|
563
|
-
const matches = state.global ? all : local;
|
|
559
|
+
const matches = await findSessionsByName(name, dir, isCustom, localScope(state));
|
|
564
560
|
if (matches.length > 1) {
|
|
565
561
|
printCandidates(name, matches);
|
|
566
562
|
}
|
|
@@ -570,9 +566,8 @@ async function runLauncher(state) {
|
|
|
570
566
|
console.error(`Resuming session: ${matches[0].path}`);
|
|
571
567
|
piArgs.push("--session", matches[0].path);
|
|
572
568
|
} else {
|
|
573
|
-
if (!state.global
|
|
574
|
-
|
|
575
|
-
console.error(`No "${name}" in this cwd. (${elsewhere} match${elsewhere === 1 ? "" : "es"} in other cwds — use --global to consider ${elsewhere === 1 ? "it" : "them"}.)`);
|
|
569
|
+
if (!state.global) {
|
|
570
|
+
console.error(`No "${name}" found in this cwd. Use --global to search other cwds.`);
|
|
576
571
|
}
|
|
577
572
|
console.error("Starting new session.");
|
|
578
573
|
}
|