@indigo-gr/claude-code-station 0.4.1 → 0.5.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 +37 -0
- package/README.md +67 -1
- package/bin/ccs +572 -26
- package/bin/ccs-config-edit.ts +431 -0
- package/bin/ccs-config.ts +324 -44
- package/bin/ccs-delete.sh +2 -1
- package/bin/ccs-doctor.ts +432 -0
- package/bin/ccs-groups.ts +167 -0
- package/bin/ccs-init.ts +58 -14
- package/bin/ccs-list-json.ts +124 -0
- package/bin/ccs-list.ts +107 -7
- package/bin/ccs-machine.ts +467 -0
- package/bin/ccs-open.ts +304 -0
- package/bin/ccs-preview-session.ts +4 -1
- package/bin/ccs-preview.ts +68 -1
- package/bin/ccs-repo.ts +433 -0
- package/bin/ccs-scan.ts +41 -4
- package/bin/ccs-sessions.ts +174 -0
- package/bin/ccs-tsx.sh +16 -0
- package/bin/package.json +3 -0
- package/docs/design/machine-interface.md +860 -0
- package/docs/design/repos-yml-schema.md +225 -76
- package/package.json +1 -1
package/bin/ccs-init.ts
CHANGED
|
@@ -14,13 +14,15 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { readdir } from "node:fs/promises";
|
|
17
|
-
import { readFileSync
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
18
|
import { homedir } from "node:os";
|
|
19
19
|
import { basename, join, relative } from "node:path";
|
|
20
20
|
import { parseDocument, isSeq } from "yaml";
|
|
21
21
|
|
|
22
22
|
import { ensureConfigDir, getPaths, loadConfig } from "./ccs-config.ts";
|
|
23
|
+
import { EditStop, runEditSync } from "./ccs-config-edit.ts";
|
|
23
24
|
import { hasShellMetachars } from "./ccs-sanitize.ts";
|
|
25
|
+
import { pathToFileURL } from "node:url";
|
|
24
26
|
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
26
28
|
// Discovery
|
|
@@ -143,6 +145,27 @@ export function buildCandidates(
|
|
|
143
145
|
// YAML append (comment/format-preserving)
|
|
144
146
|
// ---------------------------------------------------------------------------
|
|
145
147
|
|
|
148
|
+
/**
|
|
149
|
+
* `repos: [{name: a, path: ~/a}]` (one line, YAML flow style) is valid, but
|
|
150
|
+
* appending to it keeps it on one line, and tools that read repos.yml line by
|
|
151
|
+
* line (`name:` and `path:` under each `- ` item) see none of it. ccs init
|
|
152
|
+
* only writes the block form, so it refuses here and leaves the file as is.
|
|
153
|
+
* `flow_style_repos` is the reason word for scripts to match on.
|
|
154
|
+
*/
|
|
155
|
+
export function rejectFlowStyleRepos(repos: unknown): void {
|
|
156
|
+
if (isSeq(repos) && repos.flow) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"flow_style_repos: repos.yml lists `repos:` on one line ([...]); ccs init only adds to a block list. " +
|
|
159
|
+
"Rewrite it with one `- name: ...` / `path: ...` item per entry, then run ccs init --auto-discover again. repos.yml unchanged",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The repos.yml source's `repos:` node, for the checks before any scan. */
|
|
165
|
+
function reposNodeOf(source: string): unknown {
|
|
166
|
+
return parseDocument(source).get("repos", true);
|
|
167
|
+
}
|
|
168
|
+
|
|
146
169
|
/**
|
|
147
170
|
* Append candidates to the `repos:` sequence of a repos.yml source string.
|
|
148
171
|
* parseDocument round-trips comments and the user's existing formatting —
|
|
@@ -157,6 +180,7 @@ export function appendReposToYaml(
|
|
|
157
180
|
if (!isSeq(repos)) {
|
|
158
181
|
throw new Error("repos.yml: top-level `repos` sequence not found");
|
|
159
182
|
}
|
|
183
|
+
rejectFlowStyleRepos(repos);
|
|
160
184
|
for (const c of candidates) {
|
|
161
185
|
repos.add(doc.createNode({ name: c.name, path: c.path }));
|
|
162
186
|
}
|
|
@@ -168,25 +192,41 @@ export function appendReposToYaml(
|
|
|
168
192
|
}
|
|
169
193
|
|
|
170
194
|
/**
|
|
171
|
-
* Write candidates into repos.yml
|
|
172
|
-
*
|
|
173
|
-
*
|
|
195
|
+
* Write candidates into repos.yml through the common write path
|
|
196
|
+
* (ccs-config-edit: lock, check, `.bak`, write in place, check again, put the
|
|
197
|
+
* old text back on failure) — `ccs init` must never leave a config the
|
|
198
|
+
* launcher cannot load. A one-line `repos:` is refused before anything else,
|
|
199
|
+
* even when the file fails the other checks.
|
|
174
200
|
*/
|
|
175
201
|
export function appendAndValidate(
|
|
176
202
|
reposYmlPath: string,
|
|
177
203
|
candidates: Candidate[],
|
|
178
204
|
): void {
|
|
179
|
-
|
|
180
|
-
const updated = appendReposToYaml(source, candidates);
|
|
181
|
-
writeFileSync(reposYmlPath + ".bak", source, { mode: 0o600 });
|
|
182
|
-
writeFileSync(reposYmlPath, updated, { mode: 0o600 });
|
|
205
|
+
rejectFlowStyleRepos(reposNodeOf(readFileSync(reposYmlPath, "utf-8")));
|
|
183
206
|
try {
|
|
184
|
-
|
|
207
|
+
runEditSync({
|
|
208
|
+
reposYml: reposYmlPath,
|
|
209
|
+
transform: ({ doc }) => {
|
|
210
|
+
const repos = doc.get("repos", true);
|
|
211
|
+
if (!isSeq(repos)) {
|
|
212
|
+
throw new Error("repos.yml: top-level `repos` sequence not found");
|
|
213
|
+
}
|
|
214
|
+
rejectFlowStyleRepos(repos);
|
|
215
|
+
for (const c of candidates) {
|
|
216
|
+
repos.add(doc.createNode({ name: c.name, path: c.path }));
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
});
|
|
185
220
|
} catch (err) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
221
|
+
if (err instanceof EditStop && err.code === "validation_after_write_failed") {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`[ccs-init] validation failed after append — repos.yml restored (backup kept at .bak): ${err.message}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (err instanceof EditStop) {
|
|
227
|
+
throw new Error(`${err.code}: ${err.message}`);
|
|
228
|
+
}
|
|
229
|
+
throw err;
|
|
190
230
|
}
|
|
191
231
|
}
|
|
192
232
|
|
|
@@ -208,6 +248,8 @@ async function cliDiscover(argv: string[]): Promise<number> {
|
|
|
208
248
|
const maxDepth = parseDepth(argv);
|
|
209
249
|
ensureConfigDir();
|
|
210
250
|
const config = loadConfig(); // throws ConfigError on a broken repos.yml
|
|
251
|
+
// Refuse before the $HOME scan and the picker, not after the user chose.
|
|
252
|
+
rejectFlowStyleRepos(reposNodeOf(readFileSync(getPaths().reposYml, "utf-8")));
|
|
211
253
|
const existing: ExistingConfig = {
|
|
212
254
|
names: new Set(config.repos.map((r) => r.name)),
|
|
213
255
|
paths: new Set(config.repos.map((r) => r.path)),
|
|
@@ -274,7 +316,9 @@ async function main(): Promise<number> {
|
|
|
274
316
|
}
|
|
275
317
|
}
|
|
276
318
|
|
|
277
|
-
|
|
319
|
+
// pathToFileURL, not `file://${argv[1]}`: a folder with a space (or any
|
|
320
|
+
// character a URL escapes) in its name would never match.
|
|
321
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
278
322
|
main().then(
|
|
279
323
|
// exitCode, not process.exit(): bin/ccs captures discover's candidates
|
|
280
324
|
// with $(...), a pipe, and exiting right after the writes dropped every
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ccs-list-json.ts — `ccs list --json [--with-paths]`.
|
|
3
|
+
*
|
|
4
|
+
* repos.yml after ccs's checks, read directly (not the cache: no wait for a
|
|
5
|
+
* scan, nothing stale after --no-scan). Names, labels and descriptions are
|
|
6
|
+
* printed — that is what this command is for — but paths only with
|
|
7
|
+
* --with-paths, and never `command`, `cwd`, `custom` or `scan`.
|
|
8
|
+
* Contract: docs/design/machine-interface.md 2.2.
|
|
9
|
+
*
|
|
10
|
+
* Creates nothing: a missing repos.yml is exit 4 (config_missing), not a
|
|
11
|
+
* template.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { statSync } from "node:fs";
|
|
15
|
+
|
|
16
|
+
import { readConfig, type RepoEntry } from "./ccs-config.ts";
|
|
17
|
+
import {
|
|
18
|
+
cliOutcome,
|
|
19
|
+
configFailure,
|
|
20
|
+
emit,
|
|
21
|
+
okOutcome,
|
|
22
|
+
onInterrupt,
|
|
23
|
+
parseFlags,
|
|
24
|
+
wantsJson,
|
|
25
|
+
type Outcome,
|
|
26
|
+
} from "./ccs-machine.ts";
|
|
27
|
+
import { readLogicalGroups } from "./ccs-groups.ts";
|
|
28
|
+
import { findParentRepo } from "./ccs-utils.ts";
|
|
29
|
+
|
|
30
|
+
const COMMAND = "list";
|
|
31
|
+
|
|
32
|
+
function isDirectory(p: string): boolean {
|
|
33
|
+
if (!p) return false;
|
|
34
|
+
try {
|
|
35
|
+
return statSync(p).isDirectory();
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One repos[] item of the output, in the documented key order. */
|
|
42
|
+
export function listItem(
|
|
43
|
+
repo: RepoEntry,
|
|
44
|
+
parent: RepoEntry | null,
|
|
45
|
+
withPaths: boolean,
|
|
46
|
+
): Record<string, unknown> {
|
|
47
|
+
const item: Record<string, unknown> = {
|
|
48
|
+
name: repo.name,
|
|
49
|
+
label: repo.label,
|
|
50
|
+
description: repo.description,
|
|
51
|
+
icon: repo.icon,
|
|
52
|
+
kind: repo.launch ? "repo" : "group",
|
|
53
|
+
launch: repo.launch,
|
|
54
|
+
lead: repo.lead,
|
|
55
|
+
disabled: repo.disabled,
|
|
56
|
+
parent: parent ? parent.name : null,
|
|
57
|
+
tags: [...repo.tags],
|
|
58
|
+
exists: isDirectory(repo.path),
|
|
59
|
+
};
|
|
60
|
+
if (withPaths) item.path = repo.path || null;
|
|
61
|
+
return item;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `groups[]`: the logical groups in defaults.groups_dir, read now (not from
|
|
66
|
+
* the cache), with their members' repos.yml names in the file's order.
|
|
67
|
+
*/
|
|
68
|
+
function logicalGroupsJson(dir: string, repos: RepoEntry[]): Record<string, unknown>[] {
|
|
69
|
+
return readLogicalGroups(dir, repos).groups.map((g) => ({
|
|
70
|
+
id: g.id,
|
|
71
|
+
label: g.label,
|
|
72
|
+
kind: "logical",
|
|
73
|
+
members: g.members,
|
|
74
|
+
unregistered_members: g.unregistered,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The outcome of `ccs list` for these arguments (no printing). */
|
|
79
|
+
export function listOutcome(args: readonly string[]): Outcome {
|
|
80
|
+
const flags = parseFlags(args, { booleans: ["--json", "--with-paths"] });
|
|
81
|
+
if (!flags.ok) return cliOutcome(flags.code);
|
|
82
|
+
if (flags.positionals.length > 0 || flags.rest.length > 0) {
|
|
83
|
+
return cliOutcome("extra_argument");
|
|
84
|
+
}
|
|
85
|
+
if (!flags.booleans.has("--json")) {
|
|
86
|
+
return cliOutcome(
|
|
87
|
+
"json_required",
|
|
88
|
+
{},
|
|
89
|
+
"`ccs list` needs --json (to pick a repo by hand, run ccs)",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const withPaths = flags.booleans.has("--with-paths");
|
|
93
|
+
|
|
94
|
+
let repos: RepoEntry[];
|
|
95
|
+
let groupsDir: string | null = null;
|
|
96
|
+
try {
|
|
97
|
+
const result = readConfig();
|
|
98
|
+
if (!result.found) {
|
|
99
|
+
return cliOutcome(
|
|
100
|
+
"config_missing",
|
|
101
|
+
{},
|
|
102
|
+
"no repos.yml yet (config_missing); `ccs init` creates one",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
repos = result.config.repos;
|
|
106
|
+
groupsDir = result.config.settings.groupsDir;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return configFailure(err);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The list tree's rule (ccs-list): the deepest enabled entry whose path
|
|
112
|
+
// holds this one; a disabled entry is never anyone's parent.
|
|
113
|
+
const enabled = repos.filter((r) => !r.disabled);
|
|
114
|
+
return okOutcome({
|
|
115
|
+
repos: repos.map((r) => listItem(r, findParentRepo(r, enabled), withPaths)),
|
|
116
|
+
groups: groupsDir ? logicalGroupsJson(groupsDir, repos) : [],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function runList(args: readonly string[]): number {
|
|
121
|
+
const json = wantsJson(args);
|
|
122
|
+
onInterrupt(COMMAND, json);
|
|
123
|
+
return emit(COMMAND, listOutcome(args), json);
|
|
124
|
+
}
|
package/bin/ccs-list.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `launch: false`), `resume` (past session) or `separator`.
|
|
10
10
|
*
|
|
11
11
|
* Modes:
|
|
12
|
-
* --current-only sessions whose cwd matches process.cwd()
|
|
12
|
+
* --current-only sessions whose cwd matches CCS_CWD (else process.cwd())
|
|
13
13
|
* --repos-only only NEW repo rows (and group headers)
|
|
14
14
|
* --sessions-only only RESUME session rows
|
|
15
15
|
* (default) NEW first (alpha, nested repos in a tree under their
|
|
@@ -23,10 +23,13 @@
|
|
|
23
23
|
* row; no badge column, no tree glyphs, plain-word times
|
|
24
24
|
*
|
|
25
25
|
* --print-ui print the `defaults.ui` the last scan stored, then exit
|
|
26
|
+
* --print-settings print `ui=<mode>` and `update_check=<on|off>` as the
|
|
27
|
+
* last scan stored them, then exit
|
|
26
28
|
*/
|
|
27
29
|
|
|
28
30
|
import { homedir } from "node:os";
|
|
29
31
|
import { openDb, getMeta, reposV3Columns } from "./ccs-db.ts";
|
|
32
|
+
import { groupsFromMeta } from "./ccs-groups.ts";
|
|
30
33
|
import { getPaths } from "./ccs-config.ts";
|
|
31
34
|
import { SHELL_METACHARS } from "./ccs-sanitize.ts";
|
|
32
35
|
import {
|
|
@@ -44,6 +47,7 @@ import {
|
|
|
44
47
|
uiFromArgv,
|
|
45
48
|
type UiMode,
|
|
46
49
|
} from "./ccs-utils.ts";
|
|
50
|
+
import { pathToFileURL } from "node:url";
|
|
47
51
|
|
|
48
52
|
// ---------------------------------------------------------------------------
|
|
49
53
|
// Constants
|
|
@@ -71,6 +75,7 @@ interface Flags {
|
|
|
71
75
|
sessionsOnly: boolean;
|
|
72
76
|
ui: UiMode;
|
|
73
77
|
printUi: boolean;
|
|
78
|
+
printSettings: boolean;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
function parseArgs(argv: string[]): Flags {
|
|
@@ -80,6 +85,7 @@ function parseArgs(argv: string[]): Flags {
|
|
|
80
85
|
sessionsOnly: argv.includes("--sessions-only"),
|
|
81
86
|
ui: uiFromArgv(argv),
|
|
82
87
|
printUi: argv.includes("--print-ui"),
|
|
88
|
+
printSettings: argv.includes("--print-settings"),
|
|
83
89
|
};
|
|
84
90
|
}
|
|
85
91
|
|
|
@@ -452,17 +458,77 @@ function simpleRepoRow(n: SimpleNode, nameCol: number): string {
|
|
|
452
458
|
return [label, desc, "", `new:${r.name}`, cwd, r.command || "claude"].join("\t");
|
|
453
459
|
}
|
|
454
460
|
|
|
455
|
-
|
|
461
|
+
/** A logical group (defaults.groups_dir) with the listed repos it holds. */
|
|
462
|
+
interface LogicalView {
|
|
463
|
+
id: string;
|
|
464
|
+
label: string;
|
|
465
|
+
members: RepoRowFull[];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Match the stored groups to the listed (enabled) repos; empty groups go. */
|
|
469
|
+
function logicalGroupsOf(
|
|
470
|
+
groups: { id: string; label: string; members: string[] }[],
|
|
471
|
+
repos: RepoRowFull[],
|
|
472
|
+
): LogicalView[] {
|
|
473
|
+
const byName = new Map(repos.map((r) => [r.name, r]));
|
|
474
|
+
return groups
|
|
475
|
+
.map((g) => ({
|
|
476
|
+
id: g.id,
|
|
477
|
+
label: g.label,
|
|
478
|
+
members: g.members.map((n) => byName.get(n)).filter((r): r is RepoRowFull => r !== undefined),
|
|
479
|
+
}))
|
|
480
|
+
.filter((g) => g.members.length > 0);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** The icon of a logical group's header (not a repos.yml entry, so no icon of its own). */
|
|
484
|
+
const LOGICAL_ICON = "🗂";
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Classic rows of the logical groups: a display-only header (KIND=group, key
|
|
488
|
+
* `@<id>`, so the preview looks in the logical groups first) and the members
|
|
489
|
+
* under it, drawn as a tree.
|
|
490
|
+
*/
|
|
491
|
+
function logicalGroupRows(groups: LogicalView[]): string[] {
|
|
492
|
+
const lines: string[] = [];
|
|
493
|
+
for (const g of groups) {
|
|
494
|
+
const label = `${displayIcon(LOGICAL_ICON)} ${truncate(g.label, 40)}${LABEL_SEP}`;
|
|
495
|
+
lines.push([label, "", "", `group:@${g.id}`, "", ""].join("\t"));
|
|
496
|
+
g.members.forEach((r, i) => {
|
|
497
|
+
const last = i === g.members.length - 1;
|
|
498
|
+
lines.push(repoToRow({ repo: r, parent: null, prefix: ` ${last ? "└ " : "├ "}` }));
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return lines;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Simple rows of a logical group: the bold header, then its members one level in. */
|
|
505
|
+
function simpleLogicalRows(g: LogicalView, nameCol: number): string[] {
|
|
506
|
+
const head = `${padDisplay(LOGICAL_ICON, 2)} ${truncate(g.label, 40)}`;
|
|
507
|
+
const gap = " ".repeat(Math.max(0, nameCol - displayWidth(head)));
|
|
508
|
+
const styled = `${padDisplay(LOGICAL_ICON, 2)} ${BOLD}${truncate(g.label, 40)}${RESET}${gap}`;
|
|
509
|
+
return [
|
|
510
|
+
[`${styled} `, "", "", `group:@${g.id}`, "", ""].join("\t"),
|
|
511
|
+
...g.members.map((r) => simpleRepoRow({ repo: r, parent: null, depth: 1 }, nameCol)),
|
|
512
|
+
];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function simpleRepoRows(repos: RepoRowFull[], logical: LogicalView[] = []): string[] {
|
|
456
516
|
const { groups, projects } = simpleSections(repos);
|
|
457
517
|
const all = [...groups, ...projects];
|
|
518
|
+
const logicalHeads = logical.flatMap((g) => [
|
|
519
|
+
`${padDisplay(LOGICAL_ICON, 2)} ${truncate(g.label, 40)}`,
|
|
520
|
+
...g.members.map((r) => simpleHead({ repo: r, parent: null, depth: 1 })),
|
|
521
|
+
]);
|
|
458
522
|
const nameCol = Math.min(
|
|
459
523
|
SIMPLE_NAME_COL_MAX,
|
|
460
|
-
Math.max(0, ...all.map((n) => displayWidth(simpleHead(n)))),
|
|
524
|
+
Math.max(0, ...all.map((n) => displayWidth(simpleHead(n))), ...logicalHeads.map(displayWidth)),
|
|
461
525
|
);
|
|
462
526
|
const lines: string[] = [];
|
|
463
|
-
if (groups.length > 0) {
|
|
527
|
+
if (groups.length > 0 || logical.length > 0) {
|
|
464
528
|
lines.push(simpleHeading("グループ"));
|
|
465
529
|
for (const n of groups) lines.push(simpleRepoRow(n, nameCol));
|
|
530
|
+
// After the launch: false headers.
|
|
531
|
+
for (const g of logical) lines.push(...simpleLogicalRows(g, nameCol));
|
|
466
532
|
}
|
|
467
533
|
if (projects.length > 0) {
|
|
468
534
|
lines.push(simpleHeading("プロジェクト"));
|
|
@@ -554,10 +620,35 @@ function printUi(stateDb: string): number {
|
|
|
554
620
|
return 0;
|
|
555
621
|
}
|
|
556
622
|
|
|
623
|
+
/**
|
|
624
|
+
* `--print-settings`: what the last scan stored, as the pre-scan prints it
|
|
625
|
+
* (`ui=<mode>` and `update_check=<on|off>`). Any failure prints the defaults
|
|
626
|
+
* (classic, on): the launcher must always get an answer it can use.
|
|
627
|
+
*/
|
|
628
|
+
function printSettings(stateDb: string): number {
|
|
629
|
+
let ui: UiMode = "classic";
|
|
630
|
+
let updateCheck = "on";
|
|
631
|
+
try {
|
|
632
|
+
const handle = openDb(stateDb, { readonly: true, skipMigrate: true });
|
|
633
|
+
try {
|
|
634
|
+
const v = getMeta(handle.db, "defaults_ui");
|
|
635
|
+
if (isUiMode(v)) ui = v;
|
|
636
|
+
if (getMeta(handle.db, "defaults_update_check") === "off") updateCheck = "off";
|
|
637
|
+
} finally {
|
|
638
|
+
handle.close();
|
|
639
|
+
}
|
|
640
|
+
} catch {
|
|
641
|
+
// keep the defaults
|
|
642
|
+
}
|
|
643
|
+
process.stdout.write(`ui=${ui}\nupdate_check=${updateCheck}\n`);
|
|
644
|
+
return 0;
|
|
645
|
+
}
|
|
646
|
+
|
|
557
647
|
function main(): number {
|
|
558
648
|
const flags = parseArgs(process.argv.slice(2));
|
|
559
649
|
const paths = getPaths();
|
|
560
650
|
if (flags.printUi) return printUi(paths.stateDb);
|
|
651
|
+
if (flags.printSettings) return printSettings(paths.stateDb);
|
|
561
652
|
const simple = flags.ui === "simple";
|
|
562
653
|
// openDb() is called INSIDE the try so that a cold-start failure (state.db
|
|
563
654
|
// missing before first scan) is caught and rendered as a friendly hint
|
|
@@ -589,12 +680,17 @@ function main(): number {
|
|
|
589
680
|
ORDER BY r.name COLLATE NOCASE ASC`,
|
|
590
681
|
)
|
|
591
682
|
.all() as RepoRowFull[];
|
|
683
|
+
// defaults.groups_dir: logical groups the last scan matched to these
|
|
684
|
+
// repos (none when the setting is off).
|
|
685
|
+
const logical = logicalGroupsOf(groupsFromMeta(getMeta(db, "logical_groups")), repos);
|
|
592
686
|
if (simple) {
|
|
593
|
-
lines.push(...simpleRepoRows(repos));
|
|
687
|
+
lines.push(...simpleRepoRows(repos, logical));
|
|
594
688
|
} else {
|
|
595
689
|
for (const g of groupRepos(repos)) {
|
|
596
690
|
lines.push(repoToRow(g));
|
|
597
691
|
}
|
|
692
|
+
// After every project row, before the conversations.
|
|
693
|
+
lines.push(...logicalGroupRows(logical));
|
|
598
694
|
}
|
|
599
695
|
}
|
|
600
696
|
|
|
@@ -610,7 +706,9 @@ function main(): number {
|
|
|
610
706
|
? metaCommand
|
|
611
707
|
: "claude";
|
|
612
708
|
|
|
613
|
-
|
|
709
|
+
// CCS_CWD: the folder ccs was run in (bin/ccs starts this module from
|
|
710
|
+
// its own folder; see ccs-tsx.sh).
|
|
711
|
+
const filterCwd = flags.currentOnly ? process.env.CCS_CWD || process.cwd() : null;
|
|
614
712
|
const baseSelect = `SELECT s.uuid, s.repo_name, s.cwd, s.last_activity_at, s.topic,
|
|
615
713
|
r.name AS repo_display, r.icon AS repo_icon, r.command AS repo_command
|
|
616
714
|
FROM sessions s
|
|
@@ -701,7 +799,9 @@ function main(): number {
|
|
|
701
799
|
}
|
|
702
800
|
}
|
|
703
801
|
|
|
704
|
-
|
|
802
|
+
// pathToFileURL, not `file://${argv[1]}`: a folder with a space (or any
|
|
803
|
+
// character a URL escapes) in its name would never match.
|
|
804
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
705
805
|
// exitCode, not process.exit(): stdout is a pipe into fzf, and pipe writes
|
|
706
806
|
// are async on macOS. Exiting right after the write dropped everything past
|
|
707
807
|
// the pipe buffer — as little as 512 bytes, so the list ended after 3 repos
|