claude-code-station 0.2.2 → 0.2.3

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 CHANGED
@@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.3] — 2026-10-14
10
+
11
+ ### Added
12
+ - **Nested repos are grouped in the list** — a repo whose `path` sits inside another listed repo's `path` now comes right after that repo, indented (` └ 📁 child ‹parent›`), instead of wherever its name sorts. The nearest enclosing path wins (the same deepest-path rule that maps sessions to repos), deeper nesting indents further, and siblings stay name-sorted. The dim `‹parent›` suffix keeps the relation readable when a fzf query hides the parent row, and a query for the parent's name matches its children too. Derived from `path` at list time — no `repos.yml` key and no cache migration. A disabled parent is not listed, so its children stay top-level.
13
+
9
14
  ## [0.2.2] — 2026-06-21
10
15
 
11
16
  ### Fixed
package/README.md CHANGED
@@ -202,6 +202,14 @@ Notion: ✅ abc123
202
202
  **Visual cues / 見た目のガイド:**
203
203
  - NEW rows and RESUME rows are separated by a dim `── Past Sessions ──` divider. Selecting the divider is a no-op.
204
204
  - RESUME sessions mapped to a registered repo show that repo's icon (e.g., `🔄 📁 ClaudeCode`). Unmapped sessions (cwd that isn't in `repos.yml`) show `❓` — a visual cue that the session was either a one-off or that the repo isn't registered yet.
205
+ - A repo whose `path` sits inside another registered repo's `path` is listed right under that repo, indented, with the parent's name dimmed at the end. Nothing to configure — the nesting follows from `path`. The dim `‹parent›` keeps the relation visible when a search hides the parent row, and searching for the parent's name lists its children too. / `path` が別の登録リポの `path` の中にあるリポは、そのリポのすぐ下に字下げして並び、行の末尾に親の名前が薄く出る。設定は要らない(`path` から決まる)。絞り込みで親の行が隠れても `‹親›` で親子がわかり、親の名前で絞り込むと子も一緒に出る。
206
+
207
+ ```
208
+ 📁 my-company
209
+ └ 📁 web-app ‹my-company›
210
+ └ 📁 mobile-app ‹my-company›
211
+ 📁 side-project
212
+ ```
205
213
 
206
214
  ### Workflows / 主な使い方
207
215
 
package/bin/ccs-init.ts CHANGED
@@ -276,7 +276,12 @@ async function main(): Promise<number> {
276
276
 
277
277
  if (import.meta.url === `file://${process.argv[1]}`) {
278
278
  main().then(
279
- (code) => process.exit(code),
279
+ // exitCode, not process.exit(): bin/ccs captures discover's candidates
280
+ // with $(...), a pipe, and exiting right after the writes dropped every
281
+ // candidate past the pipe buffer (same bug as ccs-list, 2026-10-07).
282
+ (code) => {
283
+ process.exitCode = code;
284
+ },
280
285
  (err) => {
281
286
  process.stderr.write(
282
287
  `[ccs-init] fatal: ${err instanceof Error ? err.message : String(err)}\n`,
package/bin/ccs-list.ts CHANGED
@@ -9,7 +9,8 @@
9
9
  * --current-only sessions whose cwd matches process.cwd()
10
10
  * --repos-only only NEW repo rows
11
11
  * --sessions-only only RESUME session rows
12
- * (default) NEW first (alpha), then RESUME (last_activity_at DESC)
12
+ * (default) NEW first (alpha, nested repos under their parent),
13
+ * then RESUME (last_activity_at DESC)
13
14
  */
14
15
 
15
16
  import { homedir } from "node:os";
@@ -175,8 +176,51 @@ function buildRepoBadges(r: RepoRowFull): string {
175
176
  // invoked with --ansi so these codes render as styling.
176
177
  const LABEL_SEP = " \x1b[2m│\x1b[0m";
177
178
 
178
- function repoToRow(r: RepoRowFull): string {
179
- const label = `${r.icon || "📁"} ${r.name}${LABEL_SEP}`;
179
+ interface GroupedRepo {
180
+ repo: RepoRowFull;
181
+ parentName: string | null;
182
+ depth: number;
183
+ }
184
+
185
+ function isUnder(child: string, parent: string): boolean {
186
+ return child.startsWith(parent.endsWith("/") ? parent : parent + "/");
187
+ }
188
+
189
+ // Nest each repo under the listed repo whose path most deeply contains its
190
+ // own — the same deepest-path-wins rule buildRepoResolver uses for sessions,
191
+ // so no repos.yml key is needed. Children follow their parent, siblings keep
192
+ // the incoming (SQL, name-sorted) order. A disabled parent is not in the
193
+ // list, so its children fall back to the top level.
194
+ function groupRepos(repos: RepoRowFull[]): GroupedRepo[] {
195
+ const children = new Map<RepoRowFull | null, RepoRowFull[]>();
196
+ for (const r of repos) {
197
+ let parent: RepoRowFull | null = null;
198
+ for (const p of repos) {
199
+ if (p === r || !isUnder(r.path, p.path)) continue;
200
+ if (!parent || p.path.length > parent.path.length) parent = p;
201
+ }
202
+ const siblings = children.get(parent) ?? [];
203
+ siblings.push(r);
204
+ children.set(parent, siblings);
205
+ }
206
+ const out: GroupedRepo[] = [];
207
+ const walk = (parent: RepoRowFull | null, depth: number): void => {
208
+ for (const r of children.get(parent) ?? []) {
209
+ out.push({ repo: r, parentName: parent?.name ?? null, depth });
210
+ walk(r, depth + 1);
211
+ }
212
+ };
213
+ walk(null, 0);
214
+ return out;
215
+ }
216
+
217
+ function repoToRow({ repo: r, parentName, depth }: GroupedRepo): string {
218
+ // Children are indented under their parent. The dim ‹parent› suffix keeps
219
+ // the relation readable when a fzf query hides the parent row, and lets a
220
+ // query for the parent's name match its children too.
221
+ const indent = depth > 0 ? `${" ".repeat(depth)}└ ` : "";
222
+ const parentHint = parentName ? ` \x1b[2m‹${parentName}›\x1b[0m` : "";
223
+ const label = `${indent}${r.icon || "📁"} ${r.name}${parentHint}${LABEL_SEP}`;
180
224
  const desc = truncate(r.description || "", DESC_MAX);
181
225
  const badges = buildRepoBadges(r);
182
226
  const cwd = r.cwd && r.cwd.length > 0 ? r.cwd : r.path;
@@ -271,8 +315,8 @@ function main(): number {
271
315
  ORDER BY r.name COLLATE NOCASE ASC`,
272
316
  )
273
317
  .all() as RepoRowFull[];
274
- for (const r of repos) {
275
- lines.push(repoToRow(r));
318
+ for (const g of groupRepos(repos)) {
319
+ lines.push(repoToRow(g));
276
320
  }
277
321
  }
278
322
 
@@ -359,5 +403,18 @@ function main(): number {
359
403
  }
360
404
 
361
405
  if (import.meta.url === `file://${process.argv[1]}`) {
362
- process.exit(main());
406
+ // exitCode, not process.exit(): stdout is a pipe into fzf, and pipe writes
407
+ // are async on macOS. Exiting right after the write dropped everything past
408
+ // the pipe buffer — as little as 512 bytes, so the list ended after 3 repos
409
+ // (2026-10-07). Returning lets Node flush the whole list first.
410
+ //
411
+ // The flip side: fzf may exit (pick / Esc) before reading every row, and
412
+ // the pending write then fails with EPIPE. Those rows have no reader, so
413
+ // end quietly with main()'s status — an unhandled EPIPE would print a stack
414
+ // trace and exit 1, which pipefail in bin/ccs turns into a dropped launch.
415
+ process.stdout.on("error", (err: NodeJS.ErrnoException) => {
416
+ if (err.code === "EPIPE") process.exit(process.exitCode ?? 0);
417
+ throw err;
418
+ });
419
+ process.exitCode = main();
363
420
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-station",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Claude Code Station — fzf-powered launcher and session picker for Claude Code with multi-repo support and SQLite-backed state cache",
5
5
  "type": "module",
6
6
  "bin": {