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.
- package/CHANGELOG.md +46 -0
- package/README.md +7 -4
- package/dist/core/agent-edit.d.ts +6 -0
- package/dist/{server → core}/agent-edit.js +15 -21
- package/dist/core/backups.d.ts +19 -0
- package/dist/{server → core}/backups.js +64 -48
- package/dist/core/core.d.ts +96 -0
- package/dist/core/core.js +57 -0
- package/dist/core/index.d.ts +22 -0
- package/dist/core/index.js +38 -0
- package/dist/core/mcp-edit.d.ts +23 -0
- package/dist/{server → core}/mcp-edit.js +121 -100
- package/dist/core/mutations.d.ts +20 -0
- package/dist/{server → core}/mutations.js +8 -15
- package/dist/core/open.d.ts +28 -0
- package/dist/{server → core}/open.js +38 -13
- package/dist/core/parse-edit.d.ts +12 -0
- package/dist/core/parse-edit.js +64 -0
- package/dist/core/reference.d.ts +3 -0
- package/dist/core/reference.js +11 -0
- package/dist/core/scan/compare.d.ts +7 -0
- package/dist/{server → core}/scan/compare.js +2 -2
- package/dist/core/scan/context.d.ts +13 -0
- package/dist/{server → core}/scan/context.js +55 -32
- package/dist/core/scan/data.d.ts +14 -0
- package/dist/core/scan/data.js +26 -0
- package/dist/core/scan/env.d.ts +28 -0
- package/dist/{server → core}/scan/env.js +15 -4
- package/dist/core/scan/files.d.ts +68 -0
- package/dist/core/scan/files.js +270 -0
- package/dist/core/scan/git.d.ts +20 -0
- package/dist/core/scan/git.js +106 -0
- package/dist/core/scan/index.d.ts +5 -0
- package/dist/{server → core}/scan/index.js +119 -82
- package/dist/core/scan/instructions.d.ts +54 -0
- package/dist/{server → core}/scan/instructions.js +141 -73
- package/dist/core/scan/inventory.d.ts +38 -0
- package/dist/{server → core}/scan/inventory.js +134 -115
- package/dist/core/scan/mcp.d.ts +28 -0
- package/dist/{server → core}/scan/mcp.js +153 -95
- package/dist/core/scan/memory.d.ts +8 -0
- package/dist/{server → core}/scan/memory.js +8 -8
- package/dist/core/scan/path-test.d.ts +9 -0
- package/dist/core/scan/path-test.js +156 -0
- package/dist/core/scan/permissions.d.ts +31 -0
- package/dist/{server → core}/scan/permissions.js +5 -8
- package/dist/core/scan/plugins.d.ts +39 -0
- package/dist/{server → core}/scan/plugins.js +36 -27
- package/dist/core/scan/projects.d.ts +9 -0
- package/dist/{server → core}/scan/projects.js +7 -5
- package/dist/core/scan/sandbox.d.ts +14 -0
- package/dist/{server → core}/scan/sandbox.js +87 -59
- package/dist/core/scan/settings.d.ts +37 -0
- package/dist/{server → core}/scan/settings.js +98 -93
- package/dist/core/scan/tools.d.ts +10 -0
- package/dist/{server → core}/scan/tools.js +28 -20
- package/dist/core/scan/usage.d.ts +19 -0
- package/dist/{server → core}/scan/usage.js +122 -97
- package/dist/core/schema.d.ts +8 -0
- package/dist/core/schema.js +67 -0
- package/dist/core/secrets.d.ts +45 -0
- package/dist/core/secrets.js +207 -0
- package/dist/core/settings-edit.d.ts +9 -0
- package/dist/{server → core}/settings-edit.js +91 -112
- package/dist/core/watch.d.ts +45 -0
- package/dist/{server → core}/watch.js +54 -17
- package/dist/core/write.d.ts +117 -0
- package/dist/{server → core}/write.js +61 -8
- package/dist/server/args.d.ts +32 -0
- package/dist/server/args.js +106 -0
- package/dist/server/cli.d.ts +2 -0
- package/dist/server/cli.js +58 -83
- package/dist/server/commands.d.ts +8 -0
- package/dist/server/commands.js +39 -0
- package/dist/server/server.d.ts +12 -0
- package/dist/server/server.js +80 -195
- package/dist/server/start.d.ts +13 -0
- package/dist/server/start.js +21 -0
- package/dist/shared/compare.d.ts +25 -0
- package/dist/shared/memory.d.ts +6 -0
- package/dist/shared/memory.js +18 -2
- package/dist/shared/requests.d.ts +104 -0
- package/dist/shared/requests.js +73 -0
- package/dist/shared/scopes.d.ts +10 -0
- package/dist/shared/settings-catalog.json +2199 -0
- package/dist/shared/types.d.ts +535 -0
- package/dist/shared/types.js +11 -0
- package/dist/ui/assets/index-C9qqEI5Z.js +60 -0
- package/dist/ui/index.html +1 -1
- package/dist/view/diff.d.ts +16 -0
- package/dist/view/diff.js +85 -0
- package/dist/view/format.d.ts +17 -0
- package/dist/view/format.js +47 -0
- package/dist/view/index.d.ts +11 -0
- package/dist/view/index.js +13 -0
- package/dist/view/lines.d.ts +9 -0
- package/dist/view/lines.js +12 -0
- package/dist/view/path-test.d.ts +3 -0
- package/dist/view/path-test.js +7 -0
- package/dist/view/sections/catalog.d.ts +9 -0
- package/dist/view/sections/catalog.js +47 -0
- package/dist/view/sections/extensions.d.ts +1 -0
- package/dist/view/sections/extensions.js +48 -0
- package/dist/view/sections/hooks.d.ts +1 -0
- package/dist/view/sections/hooks.js +32 -0
- package/dist/view/sections/index.d.ts +6 -0
- package/dist/view/sections/index.js +27 -0
- package/dist/view/sections/instructions.d.ts +6 -0
- package/dist/view/sections/instructions.js +38 -0
- package/dist/view/sections/mcp.d.ts +3 -0
- package/dist/view/sections/mcp.js +38 -0
- package/dist/view/sections/memory.d.ts +1 -0
- package/dist/view/sections/memory.js +31 -0
- package/dist/view/sections/permissions.d.ts +17 -0
- package/dist/view/sections/permissions.js +87 -0
- package/dist/view/sections/settings.d.ts +1 -0
- package/dist/view/sections/settings.js +33 -0
- package/dist/view/sections/tools.d.ts +6 -0
- package/dist/view/sections/tools.js +57 -0
- package/dist/view/sections/types.d.ts +123 -0
- package/dist/view/sections/types.js +10 -0
- package/dist/view/settings.d.ts +12 -0
- package/dist/view/settings.js +31 -0
- package/dist/view/usage.d.ts +8 -0
- package/dist/view/usage.js +18 -0
- package/package.json +13 -2
- package/dist/server/scan/files.js +0 -480
- package/dist/server/scan/path-test.js +0 -136
- package/dist/server/schema.js +0 -15
- package/dist/server/secrets.js +0 -63
- 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 "
|
|
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
|
-
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
}
|
package/dist/server/cli.js
CHANGED
|
@@ -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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
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
|
|
12
|
+
function isFolder(path) {
|
|
13
|
+
return statSync(path, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
14
|
+
}
|
|
15
|
+
async function main() {
|
|
16
|
+
let command;
|
|
26
17
|
try {
|
|
27
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
57
|
-
if (
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
});
|
|
69
|
-
server
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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;
|