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/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
- async function getSessionMeta(filePath) {
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 rl = createInterface({ input: createReadStream(filePath, "utf-8"), crlfDelay: Infinity });
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") cwd = entry.cwd;
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.trim().replace(/\s+/g, " ") || undefined;
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.trim().replace(/\s+/g, " ");
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`. Custom
156
- // layout is flat (<dir>/*.jsonl); default layout has one subdir level per
157
- // encoded cwd (<dir>/<sub>/*.jsonl). Errors on individual files/dirs are
158
- // silently skipped — active or partially-written sessions are tolerated.
159
- async function scanSessions(dir, isCustom) {
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`. Returns both
190
- // local-cwd matches and all matches (cross-cwd) so the caller can default to
191
- // local while still surfacing a hint when non-local matches exist. Falls back
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
- async function findSessionsByName(targetName, dir, isCustom) {
195
- const localCwd = normalizePath(process.cwd());
196
- const all = (await scanSessions(dir, isCustom))
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). Default
205
- // scope is current cwd; `all` widens to every directory.
206
- async function listSessions({ all, dir, isCustom }) {
207
- const localCwd = normalizePath(process.cwd());
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) => all || (s.cwd && normalizePath(s.cwd) === localCwd))
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 6 (mode entry) and Phase 7 (after
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 printDeprecationWarning(form) {
297
- const canonical = form === "list" ? "--list" : "--resolve";
298
- console.error(
299
- `Warning: 'pi-link ${form}' is deprecated. Use 'pi-link ${canonical}' instead. ` +
300
- `(Subcommand form will be removed in a future release.)`,
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 (per
317
- // PLAN-cli-flags.md):
318
- // 1. Renamed-flag rejection (always-on for --all/-a)
319
- // 2. Global flags (--global, --help, --)
320
- // 3. Mode-selecting flags (--list, --resolve, --resolve=<name>)
321
- // 4. Deprecated subcommands (list, resolve <name>) — only at mode-null position
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: renamed-flag rejection.
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 3: mode-selecting flags.
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 4: deprecated subcommands (only at mode-null position).
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 6: launcher mode entry. state.mode === null here, no name set yet.
434
- // (lastWasFlag is still false here — only Phase 7 sets it, and Phase 7 requires launcher mode.)
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 7: launcher mode, name set. Tokens go to passthrough or get rejected.
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.trim().replace(/\s+/g, " ");
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.trim().replace(/\s+/g, " ");
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({ all: state.global, dir, isCustom });
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
- { header: "NAME", get: (s) => s.name },
521
- { header: "CWD", get: (s) => displayPath(s.cwd) },
522
- { header: "MODIFIED", get: (s) => relTime(s.modified), dim: true },
523
- { header: "MESSAGES", get: (s) => s.messages, dim: true },
524
- { header: "ID", get: (s) => s.id, dim: true },
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
- { header: "NAME", get: (s) => s.name },
528
- { header: "MODIFIED", get: (s) => relTime(s.modified), dim: true },
529
- { header: "MESSAGES", get: (s) => s.messages, dim: true },
530
- { header: "ID", get: (s) => s.id, dim: true },
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 { local, all } = await findSessionsByName(name, dir, isCustom);
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 && all.length > 0) {
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 { local, all } = await findSessionsByName(name, dir, isCustom);
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 && all.length > local.length) {
574
- const elsewhere = all.length - local.length;
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
  }