ccdesk 0.2.2 → 0.2.4

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.
Files changed (131) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +7 -4
  3. package/dist/core/agent-edit.d.ts +6 -0
  4. package/dist/{server → core}/agent-edit.js +15 -21
  5. package/dist/core/backups.d.ts +19 -0
  6. package/dist/{server → core}/backups.js +64 -48
  7. package/dist/core/core.d.ts +96 -0
  8. package/dist/core/core.js +57 -0
  9. package/dist/core/index.d.ts +22 -0
  10. package/dist/core/index.js +38 -0
  11. package/dist/core/mcp-edit.d.ts +23 -0
  12. package/dist/{server → core}/mcp-edit.js +121 -100
  13. package/dist/core/mutations.d.ts +20 -0
  14. package/dist/{server → core}/mutations.js +8 -15
  15. package/dist/core/open.d.ts +28 -0
  16. package/dist/{server → core}/open.js +38 -13
  17. package/dist/core/parse-edit.d.ts +12 -0
  18. package/dist/core/parse-edit.js +64 -0
  19. package/dist/core/reference.d.ts +3 -0
  20. package/dist/core/reference.js +11 -0
  21. package/dist/core/scan/compare.d.ts +7 -0
  22. package/dist/{server → core}/scan/compare.js +2 -2
  23. package/dist/core/scan/context.d.ts +13 -0
  24. package/dist/{server → core}/scan/context.js +55 -32
  25. package/dist/core/scan/data.d.ts +14 -0
  26. package/dist/core/scan/data.js +26 -0
  27. package/dist/core/scan/env.d.ts +28 -0
  28. package/dist/{server → core}/scan/env.js +15 -4
  29. package/dist/core/scan/files.d.ts +68 -0
  30. package/dist/core/scan/files.js +270 -0
  31. package/dist/core/scan/git.d.ts +20 -0
  32. package/dist/core/scan/git.js +106 -0
  33. package/dist/core/scan/index.d.ts +5 -0
  34. package/dist/{server → core}/scan/index.js +119 -82
  35. package/dist/core/scan/instructions.d.ts +54 -0
  36. package/dist/{server → core}/scan/instructions.js +141 -73
  37. package/dist/core/scan/inventory.d.ts +38 -0
  38. package/dist/{server → core}/scan/inventory.js +134 -115
  39. package/dist/core/scan/mcp.d.ts +28 -0
  40. package/dist/{server → core}/scan/mcp.js +153 -95
  41. package/dist/core/scan/memory.d.ts +8 -0
  42. package/dist/{server → core}/scan/memory.js +8 -8
  43. package/dist/core/scan/path-test.d.ts +9 -0
  44. package/dist/core/scan/path-test.js +156 -0
  45. package/dist/core/scan/permissions.d.ts +31 -0
  46. package/dist/{server → core}/scan/permissions.js +5 -8
  47. package/dist/core/scan/plugins.d.ts +39 -0
  48. package/dist/{server → core}/scan/plugins.js +36 -27
  49. package/dist/core/scan/projects.d.ts +9 -0
  50. package/dist/{server → core}/scan/projects.js +7 -5
  51. package/dist/core/scan/sandbox.d.ts +14 -0
  52. package/dist/{server → core}/scan/sandbox.js +87 -59
  53. package/dist/core/scan/settings.d.ts +37 -0
  54. package/dist/{server → core}/scan/settings.js +98 -93
  55. package/dist/core/scan/tools.d.ts +10 -0
  56. package/dist/{server → core}/scan/tools.js +28 -20
  57. package/dist/core/scan/usage.d.ts +19 -0
  58. package/dist/{server → core}/scan/usage.js +122 -97
  59. package/dist/core/schema.d.ts +8 -0
  60. package/dist/core/schema.js +67 -0
  61. package/dist/core/secrets.d.ts +45 -0
  62. package/dist/core/secrets.js +207 -0
  63. package/dist/core/settings-edit.d.ts +9 -0
  64. package/dist/{server → core}/settings-edit.js +91 -112
  65. package/dist/core/watch.d.ts +45 -0
  66. package/dist/{server → core}/watch.js +54 -17
  67. package/dist/core/write.d.ts +117 -0
  68. package/dist/{server → core}/write.js +61 -8
  69. package/dist/server/args.d.ts +32 -0
  70. package/dist/server/args.js +106 -0
  71. package/dist/server/cli.d.ts +2 -0
  72. package/dist/server/cli.js +58 -83
  73. package/dist/server/commands.d.ts +8 -0
  74. package/dist/server/commands.js +39 -0
  75. package/dist/server/server.d.ts +12 -0
  76. package/dist/server/server.js +80 -195
  77. package/dist/server/start.d.ts +13 -0
  78. package/dist/server/start.js +21 -0
  79. package/dist/shared/compare.d.ts +25 -0
  80. package/dist/shared/memory.d.ts +6 -0
  81. package/dist/shared/memory.js +18 -2
  82. package/dist/shared/requests.d.ts +104 -0
  83. package/dist/shared/requests.js +73 -0
  84. package/dist/shared/scopes.d.ts +10 -0
  85. package/dist/shared/settings-catalog.json +2199 -0
  86. package/dist/shared/types.d.ts +535 -0
  87. package/dist/shared/types.js +11 -0
  88. package/dist/ui/assets/index-C9qqEI5Z.js +60 -0
  89. package/dist/ui/index.html +1 -1
  90. package/dist/view/diff.d.ts +16 -0
  91. package/dist/view/diff.js +85 -0
  92. package/dist/view/format.d.ts +17 -0
  93. package/dist/view/format.js +47 -0
  94. package/dist/view/index.d.ts +11 -0
  95. package/dist/view/index.js +13 -0
  96. package/dist/view/lines.d.ts +9 -0
  97. package/dist/view/lines.js +12 -0
  98. package/dist/view/path-test.d.ts +3 -0
  99. package/dist/view/path-test.js +7 -0
  100. package/dist/view/sections/catalog.d.ts +9 -0
  101. package/dist/view/sections/catalog.js +47 -0
  102. package/dist/view/sections/extensions.d.ts +1 -0
  103. package/dist/view/sections/extensions.js +48 -0
  104. package/dist/view/sections/hooks.d.ts +1 -0
  105. package/dist/view/sections/hooks.js +32 -0
  106. package/dist/view/sections/index.d.ts +6 -0
  107. package/dist/view/sections/index.js +27 -0
  108. package/dist/view/sections/instructions.d.ts +6 -0
  109. package/dist/view/sections/instructions.js +38 -0
  110. package/dist/view/sections/mcp.d.ts +3 -0
  111. package/dist/view/sections/mcp.js +38 -0
  112. package/dist/view/sections/memory.d.ts +1 -0
  113. package/dist/view/sections/memory.js +31 -0
  114. package/dist/view/sections/permissions.d.ts +17 -0
  115. package/dist/view/sections/permissions.js +87 -0
  116. package/dist/view/sections/settings.d.ts +1 -0
  117. package/dist/view/sections/settings.js +33 -0
  118. package/dist/view/sections/tools.d.ts +6 -0
  119. package/dist/view/sections/tools.js +57 -0
  120. package/dist/view/sections/types.d.ts +123 -0
  121. package/dist/view/sections/types.js +10 -0
  122. package/dist/view/settings.d.ts +12 -0
  123. package/dist/view/settings.js +31 -0
  124. package/dist/view/usage.d.ts +8 -0
  125. package/dist/view/usage.js +18 -0
  126. package/package.json +13 -2
  127. package/dist/server/scan/files.js +0 -480
  128. package/dist/server/scan/path-test.js +0 -136
  129. package/dist/server/schema.js +0 -15
  130. package/dist/server/secrets.js +0 -63
  131. package/dist/ui/assets/index-Dg9dxngR.js +0 -60
@@ -1,9 +1,12 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { chmod, link, lstat, mkdir, open, readFile, realpath, rename, rm, writeFile, } from "node:fs/promises";
3
3
  import { basename, dirname, join } from "node:path";
4
- import {} from "../shared/types.js";
4
+ import { Type } from "typebox";
5
+ import { BACKUP_KINDS } from "../shared/types.js";
6
+ import { isRecord, parseJson } from "./scan/data.js";
5
7
  import {} from "./scan/env.js";
6
8
  import { isCredentials, isSecretPath, revisionOf } from "./scan/files.js";
9
+ import { jsonSyntaxError } from "./secrets.js";
7
10
  /** A change ccdesk refuses on purpose; the server answers 409 with the message. */
8
11
  export class RefusedError extends Error {
9
12
  }
@@ -12,15 +15,30 @@ export class StaleError extends RefusedError {
12
15
  }
13
16
  const CREDENTIALS = "ccdesk does not read or change Claude Code's credentials.";
14
17
  const STATE_FILE = "ccdesk does not change ~/.claude.json; Claude Code rewrites it all the time.";
15
- let queue = Promise.resolve();
18
+ /** A new, empty queue of changes. Each core has its own. */
19
+ export function changeQueue() {
20
+ let queue = Promise.resolve();
21
+ return (change) => {
22
+ const run = queue.then(change);
23
+ queue = run.catch(() => undefined);
24
+ return run;
25
+ };
26
+ }
16
27
  /**
17
- * Runs changes one at a time, so two of them can never both pass a revision check before either
18
- * writes. Every exported change goes through here; helpers it calls must not.
28
+ * Plans a change again and makes it, but only when it has no errors and everything it was worked
29
+ * out from is as reviewed; `changed` is the refusal otherwise. Planning and saving share one turn
30
+ * of the queue, so no other change lands between the check and the write.
19
31
  */
20
- export function serially(change) {
21
- const run = queue.then(change);
22
- queue = run.catch(() => undefined);
23
- return run;
32
+ export function saveReviewed(queue, plan, expectedRevision, changed) {
33
+ return queue(async () => {
34
+ const { preview, save } = await plan();
35
+ const errors = preview.errors ?? [];
36
+ if (errors.length > 0)
37
+ throw new RefusedError(errors.join("; "));
38
+ if (preview.revision !== expectedRevision)
39
+ throw new RefusedError(changed);
40
+ return save();
41
+ });
24
42
  }
25
43
  export function backupRoot(env) {
26
44
  return join(env.claudeDir, "backups", "ccdesk");
@@ -60,6 +78,26 @@ export async function readIfExists(env, path) {
60
78
  throw error;
61
79
  }
62
80
  }
81
+ /**
82
+ * A JSON file that holds an object, as ccdesk edits it: missing or empty is `{}`, anything else
83
+ * that is not an object is refused. `reformats` says whether writing it back as 2-space JSON
84
+ * changes more than the edit, since its diff is rendered from the parsed file.
85
+ */
86
+ export async function readJsonObject(env, path) {
87
+ const raw = await readIfExists(env, path);
88
+ if (raw === null || raw.trim() === "")
89
+ return { raw, data: {}, reformats: false };
90
+ let parsed;
91
+ try {
92
+ parsed = parseJson(raw);
93
+ }
94
+ catch (error) {
95
+ throw new RefusedError(`${path} is ${jsonSyntaxError(error)}; fix it by hand first.`);
96
+ }
97
+ if (!isRecord(parsed))
98
+ throw new RefusedError(`${path} does not hold a JSON object.`);
99
+ return { raw, data: parsed, reformats: raw !== `${JSON.stringify(parsed, null, 2)}\n` };
100
+ }
63
101
  /**
64
102
  * Replaces a file's content, but only while it still has `expected` revision. The new text is
65
103
  * written to a temporary file first and the revision is checked again just before the rename, so
@@ -132,6 +170,21 @@ export async function removeExactly(path, expected) {
132
170
  await rm(aside);
133
171
  return false;
134
172
  }
173
+ /** What a backup folder records about its change, in its manifest. */
174
+ export const BackupManifestSchema = Type.Object({
175
+ kind: Type.Enum(BACKUP_KINDS),
176
+ /** The file that changed (for a deleted memory: the memory file). */
177
+ target: Type.String(),
178
+ createdAt: Type.String(),
179
+ /** Revision of the target before the change; "missing" when ccdesk created it. */
180
+ before: Type.String(),
181
+ /** Revision of the target right after the change; "pending" while it runs, "partial" when it failed halfway. */
182
+ after: Type.String(),
183
+ /** For a deleted memory: the MEMORY.md lines removed with it. */
184
+ removedIndexLines: Type.Optional(Type.Array(Type.String())),
185
+ /** For a change made through `claude mcp`: the commands that put it back, values hidden. */
186
+ undoCommands: Type.Optional(Type.Array(Type.String())),
187
+ });
135
188
  export const MANIFEST = "ccdesk-backup.json";
136
189
  /** Takes a change out of Recent changes, for one whose effect was undone again right away. */
137
190
  export function forgetChange(env, id) {
@@ -0,0 +1,32 @@
1
+ export declare const HELP = "ccdesk: see and change what Claude Code loads on this machine\n\nUsage:\n npx ccdesk [options] Open the page in your browser\n npx ccdesk scan [dir] --json Print what Claude Code loads in dir, as JSON\n npx ccdesk explain <file> [--dir <dir>] Print what loads once Claude reads or edits file\n\nOptions:\n --port <n> Port to listen on (default: a free port)\n --no-open Print the URL instead of opening a browser\n --config-dir <path> Claude folder to read, as CLAUDE_CONFIG_DIR sets it for Claude Code\n (wins over $CLAUDE_CONFIG_DIR)\n --dir <dir> For explain: the folder the session starts in (default: the current one)\n -h, --help Show this help\n\nccdesk reads ~/.claude (or --config-dir, or $CLAUDE_CONFIG_DIR). The page opens on the directory\nyou run ccdesk from and listens on 127.0.0.1 only. dir and file are relative to that directory.\nscan and explain write nothing and print no value the page hides.";
2
+ /** The port `--dev` listens on, which `pnpm dev:ui` proxies to. */
3
+ export declare const DEV_PORT = 4319;
4
+ /** What `ccdesk` was asked to do. */
5
+ export type Command = {
6
+ command: "help";
7
+ } | {
8
+ command: "serve";
9
+ port: number;
10
+ configDir: string | undefined;
11
+ open: boolean;
12
+ /** Serve the API only, on DEV_PORT with a fixed token, for the Vite dev server. */
13
+ dev: boolean;
14
+ }
15
+ /** Print the DirectoryView of `dir` (default: the current one) as JSON. */
16
+ | {
17
+ command: "scan";
18
+ dir: string | undefined;
19
+ configDir: string | undefined;
20
+ }
21
+ /** Print what loads once Claude touches `file` in a session started in `dir`. */
22
+ | {
23
+ command: "explain";
24
+ file: string;
25
+ dir: string | undefined;
26
+ configDir: string | undefined;
27
+ };
28
+ /** Arguments ccdesk cannot run with; the message is what to print. */
29
+ export declare class UsageError extends Error {
30
+ }
31
+ /** Reads the arguments after `ccdesk`, without touching the file system. */
32
+ export declare function parse(argv: string[]): Command;
@@ -0,0 +1,106 @@
1
+ import { parseArgs } from "node:util";
2
+ export const HELP = `ccdesk: see and change what Claude Code loads on this machine
3
+
4
+ Usage:
5
+ npx ccdesk [options] Open the page in your browser
6
+ npx ccdesk scan [dir] --json Print what Claude Code loads in dir, as JSON
7
+ npx ccdesk explain <file> [--dir <dir>] Print what loads once Claude reads or edits file
8
+
9
+ Options:
10
+ --port <n> Port to listen on (default: a free port)
11
+ --no-open Print the URL instead of opening a browser
12
+ --config-dir <path> Claude folder to read, as CLAUDE_CONFIG_DIR sets it for Claude Code
13
+ (wins over $CLAUDE_CONFIG_DIR)
14
+ --dir <dir> For explain: the folder the session starts in (default: the current one)
15
+ -h, --help Show this help
16
+
17
+ ccdesk reads ~/.claude (or --config-dir, or $CLAUDE_CONFIG_DIR). The page opens on the directory
18
+ you run ccdesk from and listens on 127.0.0.1 only. dir and file are relative to that directory.
19
+ scan and explain write nothing and print no value the page hides.`;
20
+ /** The port `--dev` listens on, which `pnpm dev:ui` proxies to. */
21
+ export const DEV_PORT = 4319;
22
+ /** Arguments ccdesk cannot run with; the message is what to print. */
23
+ export class UsageError extends Error {
24
+ }
25
+ /** The flags each command takes; --help goes with any. */
26
+ const FLAGS = {
27
+ serve: ["port", "open", "dev", "config-dir"],
28
+ scan: ["json", "config-dir"],
29
+ explain: ["dir", "config-dir"],
30
+ };
31
+ /** Reads the arguments after `ccdesk`, without touching the file system. */
32
+ export function parse(argv) {
33
+ let parsed;
34
+ try {
35
+ parsed = options(argv);
36
+ }
37
+ catch (error) {
38
+ throw usage(error.message);
39
+ }
40
+ const { values, positionals } = parsed;
41
+ if (values.help)
42
+ return { command: "help" };
43
+ const [first, ...rest] = positionals;
44
+ const name = commandOf(first);
45
+ for (const flag of Object.keys(values))
46
+ if (!FLAGS[name].includes(flag))
47
+ throw usage(`--${flag} does not go with ${name === "serve" ? "the page" : name}`);
48
+ const configDir = values["config-dir"];
49
+ if (name === "scan") {
50
+ if (rest.length > 1)
51
+ throw usage("scan takes one dir");
52
+ if (!values.json)
53
+ throw usage("scan prints JSON; pass --json");
54
+ return { command: "scan", dir: rest[0], configDir };
55
+ }
56
+ if (name === "explain") {
57
+ const [file, ...extra] = rest;
58
+ if (file === undefined || extra.length > 0)
59
+ throw usage("explain takes one file");
60
+ return { command: "explain", file, dir: values.dir, configDir };
61
+ }
62
+ return {
63
+ command: "serve",
64
+ port: portOf(values),
65
+ configDir,
66
+ open: values.open ?? true,
67
+ dev: values.dev ?? false,
68
+ };
69
+ }
70
+ /** The command the first word names; without one, the page. */
71
+ function commandOf(first) {
72
+ if (first === undefined)
73
+ return "serve";
74
+ if (first === "scan" || first === "explain")
75
+ return first;
76
+ throw usage(`unknown command ${first}`);
77
+ }
78
+ /** The port to serve on: --port, the dev server's with --dev, or else any free one. */
79
+ function portOf(values) {
80
+ if (!values.port)
81
+ return values.dev ? DEV_PORT : 0;
82
+ const port = Number(values.port);
83
+ if (!Number.isInteger(port) || port < 0 || port > 65535)
84
+ throw usage(`invalid --port ${values.port}`);
85
+ return port;
86
+ }
87
+ function usage(message) {
88
+ return new UsageError(`ccdesk: ${message}\n\n${HELP}`);
89
+ }
90
+ /** The flags as given: one left out is absent, so each command can refuse the others' flags. */
91
+ function options(args) {
92
+ return parseArgs({
93
+ args,
94
+ options: {
95
+ port: { type: "string" },
96
+ "config-dir": { type: "string" },
97
+ open: { type: "boolean" },
98
+ dev: { type: "boolean" },
99
+ json: { type: "boolean" },
100
+ dir: { type: "string" },
101
+ help: { type: "boolean", short: "h" },
102
+ },
103
+ allowPositionals: true,
104
+ allowNegative: true,
105
+ });
106
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -1,105 +1,80 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from "node:child_process";
3
2
  import { randomBytes } from "node:crypto";
4
3
  import { statSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { parseArgs } from "node:util";
7
- import { scanEnvFromProcess } from "./scan/env.js";
8
- import { createCcdeskServer } from "./server.js";
9
- const HELP = `ccdesk: see and change what Claude Code loads on this machine
10
-
11
- Usage: npx ccdesk [options]
12
-
13
- Options:
14
- --port <n> Port to listen on (default: a free port)
15
- --no-open Print the URL instead of opening a browser
16
- --config-dir <path> Claude folder to read, as CLAUDE_CONFIG_DIR sets it for Claude Code
17
- (wins over $CLAUDE_CONFIG_DIR)
18
- -h, --help Show this help
19
-
20
- ccdesk reads ~/.claude (or --config-dir, or $CLAUDE_CONFIG_DIR) and opens on the directory you
21
- run it from.
22
- It listens on 127.0.0.1 only.`;
23
- const DEV_PORT = 4319;
6
+ import { createCore } from "../core/core.js";
7
+ import { HELP, parse, UsageError } from "./args.js";
8
+ import { explainText, scanJson } from "./commands.js";
9
+ import { openBrowser } from "../core/open.js";
10
+ import { scanEnvFromProcess } from "../core/scan/env.js";
24
11
  const DEV_TOKEN = "dev";
25
- function options() {
12
+ function isFolder(path) {
13
+ return statSync(path, { throwIfNoEntry: false })?.isDirectory() ?? false;
14
+ }
15
+ async function main() {
16
+ let command;
26
17
  try {
27
- return parseArgs({
28
- options: {
29
- port: { type: "string" },
30
- "config-dir": { type: "string" },
31
- open: { type: "boolean", default: true },
32
- dev: { type: "boolean", default: false },
33
- help: { type: "boolean", short: "h", default: false },
34
- },
35
- allowNegative: true,
36
- }).values;
18
+ command = parse(process.argv.slice(2));
37
19
  }
38
20
  catch (error) {
39
- console.error(`ccdesk: ${error.message}\n\n${HELP}`);
40
- process.exit(2);
21
+ if (!(error instanceof UsageError))
22
+ throw error;
23
+ console.error(error.message);
24
+ process.exitCode = 2;
25
+ return;
41
26
  }
42
- }
43
- function main() {
44
- const values = options();
45
- if (values.help) {
27
+ if (command.command === "help") {
46
28
  console.log(HELP);
47
29
  return;
48
30
  }
49
- const dev = values.dev;
50
- const port = values.port ? Number(values.port) : dev ? DEV_PORT : 0;
51
- if (!Number.isInteger(port) || port < 0 || port > 65535) {
52
- console.error(`ccdesk: invalid --port ${values.port}`);
31
+ const { configDir } = command;
32
+ if (configDir !== undefined && !isFolder(configDir)) {
33
+ console.error(`ccdesk: --config-dir ${configDir} is not a folder`);
53
34
  process.exitCode = 2;
54
35
  return;
55
36
  }
56
- const configDir = values["config-dir"];
57
- if (configDir !== undefined && !statSync(configDir, { throwIfNoEntry: false })?.isDirectory()) {
58
- console.error(`ccdesk: --config-dir ${configDir} is not a folder`);
37
+ const core = createCore(scanEnvFromProcess(configDir));
38
+ if (command.command === "serve")
39
+ return serve(core, command);
40
+ const dir = resolve(command.dir ?? ".");
41
+ if (!isFolder(dir)) {
42
+ console.error(`ccdesk: ${command.dir} is not a folder`);
59
43
  process.exitCode = 2;
60
44
  return;
61
45
  }
62
- const token = dev ? DEV_TOKEN : randomBytes(18).toString("base64url");
63
- const server = createCcdeskServer({
64
- env: scanEnvFromProcess(configDir),
65
- launchDir: process.cwd(),
66
- token,
67
- uiDir: dev ? null : fileURLToPath(new URL("../ui", import.meta.url)),
68
- });
69
- server.on("error", (error) => {
70
- console.error(error.code === "EADDRINUSE"
71
- ? `ccdesk: port ${port} is already in use`
72
- : `ccdesk: ${error.message}`);
46
+ console.log(command.command === "scan"
47
+ ? await scanJson(core, dir)
48
+ : await explainText(core, dir, resolve(command.file)));
49
+ }
50
+ async function serve(core, { dev, port, open }) {
51
+ // Loaded here so that the subcommands, which only read, never load the HTTP server.
52
+ const { startServer } = await import("./start.js");
53
+ let server;
54
+ try {
55
+ server = await startServer({
56
+ core,
57
+ launchDir: process.cwd(),
58
+ token: dev ? DEV_TOKEN : randomBytes(18).toString("base64url"),
59
+ uiDir: dev ? null : fileURLToPath(new URL("../ui", import.meta.url)),
60
+ port,
61
+ });
62
+ }
63
+ catch (error) {
64
+ const { code, message } = error;
65
+ console.error(code === "EADDRINUSE" ? `ccdesk: port ${port} is already in use` : `ccdesk: ${message}`);
73
66
  process.exitCode = 1;
74
- });
75
- server.listen(port, "127.0.0.1", () => {
76
- const address = server.address();
77
- const actual = typeof address === "object" && address ? address.port : port;
78
- if (dev) {
79
- console.log(`ccdesk API on http://127.0.0.1:${actual} (dev token "${DEV_TOKEN}"); run \`pnpm dev:ui\` for the UI`);
80
- return;
81
- }
82
- const url = `http://127.0.0.1:${actual}/#token=${token}`;
83
- console.log(`ccdesk is running at ${url}\nPress Ctrl+C to stop.`);
84
- if (values.open)
85
- openBrowser(url);
86
- });
87
- const stop = () => {
88
- // An open browser tab keeps a connection alive, which would hold close() open.
89
- server.close(() => process.exit(0));
90
- server.closeAllConnections();
91
- };
67
+ return;
68
+ }
69
+ const stop = () => server.close().finally(() => process.exit(0));
92
70
  process.once("SIGINT", stop);
93
71
  process.once("SIGTERM", stop);
72
+ if (dev) {
73
+ console.log(`ccdesk API on ${new URL(server.url).origin} (dev token "${DEV_TOKEN}"); run \`pnpm dev:ui\` for the UI`);
74
+ return;
75
+ }
76
+ console.log(`ccdesk is running at ${server.url}\nPress Ctrl+C to stop.`);
77
+ if (open && (await openBrowser(server.url, core.env)) !== null)
78
+ console.log("Could not open a browser; open the URL above.");
94
79
  }
95
- function openBrowser(url) {
96
- const [command, args] = process.platform === "darwin"
97
- ? ["open", [url]]
98
- : process.platform === "win32"
99
- ? ["cmd", ["/c", "start", "", url]]
100
- : ["xdg-open", [url]];
101
- const child = spawn(command, args, { stdio: "ignore", detached: true });
102
- child.on("error", () => console.log("Could not open a browser; open the URL above."));
103
- child.unref();
104
- }
105
- main();
80
+ await main();
@@ -0,0 +1,8 @@
1
+ import { type Core } from "../core/index.ts";
2
+ /** `ccdesk scan --json`: the DirectoryView the page gets for `dir`. */
3
+ export declare function scanJson(core: Core, dir: string): Promise<string>;
4
+ /**
5
+ * `ccdesk explain`: what joins a session started in `dir` once Claude reads or edits `file`, as the
6
+ * page's path test lists it. Both paths are absolute.
7
+ */
8
+ export declare function explainText(core: Core, dir: string, file: string): Promise<string>;
@@ -0,0 +1,39 @@
1
+ import {} from "../core/index.js";
2
+ import { formatter, MATCH_KIND } from "../view/index.js";
3
+ // The subcommands of `ccdesk`. They only read, through the core, and print what the page would
4
+ // show: the scan is already redacted, so no value the page hides reaches the terminal.
5
+ /** `ccdesk scan --json`: the DirectoryView the page gets for `dir`. */
6
+ export async function scanJson(core, dir) {
7
+ return JSON.stringify(await core.scanDirectory(dir), null, 2);
8
+ }
9
+ /**
10
+ * `ccdesk explain`: what joins a session started in `dir` once Claude reads or edits `file`, as the
11
+ * page's path test lists it. Both paths are absolute.
12
+ */
13
+ export async function explainText(core, dir, file) {
14
+ const { tilde, relativeTo } = formatter(core.env.home);
15
+ const result = await core.testPath(dir, file);
16
+ if (!result.inside)
17
+ return `${tilde(result.file)} is outside ${tilde(dir)}, so nothing there applies to it.`;
18
+ const name = relativeTo(result.file, dir);
19
+ const note = result.exists ? "" : " (the file does not exist yet; paths are checked anyway)";
20
+ if (result.matches.length === 0)
21
+ return `Nothing more loads for ${name}${note}.`;
22
+ const shownName = (m) => m.name ?? relativeTo(m.path, dir);
23
+ const width = (cells) => Math.max(...cells.map((cell) => cell.length));
24
+ const scopeWidth = width(result.matches.map((m) => m.scope));
25
+ const kindWidth = width(result.matches.map((m) => MATCH_KIND[m.kind]));
26
+ const nameWidth = width(result.matches.map(shownName));
27
+ return [
28
+ `Once Claude reads or edits ${name} in ${tilde(dir)}${note}, these load:`,
29
+ ...result.matches.map((m) => [
30
+ "",
31
+ m.scope.padEnd(scopeWidth),
32
+ MATCH_KIND[m.kind].padEnd(kindWidth),
33
+ shownName(m).padEnd(nameWidth),
34
+ m.glob ? `matches ${m.glob}` : "",
35
+ ]
36
+ .join(" ")
37
+ .trimEnd()),
38
+ ].join("\n");
39
+ }
@@ -0,0 +1,12 @@
1
+ import { type Server } from "node:http";
2
+ import { type Core } from "../core/core.ts";
3
+ export interface ServerOptions {
4
+ /** What the routes read and change; tests make one with fakes so nothing is launched. */
5
+ core: Core;
6
+ launchDir: string;
7
+ /** Required on every API request; the browser gets it once, in the URL fragment. */
8
+ token: string;
9
+ /** Built UI directory; null serves the API only (the Vite dev server serves the UI). */
10
+ uiDir: string | null;
11
+ }
12
+ export declare function createCcdeskServer(options: ServerOptions): Server;