@sealant/mend 0.22.0 → 0.24.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/README.md +133 -6
- package/dist/chunk-4MX7ZC34.js +27894 -0
- package/dist/dashboard-YEU7SDYB.js +2951 -0
- package/dist/main.js +6917 -2580
- package/man/mend-accounts.1 +1 -1
- package/man/mend-adopt.1 +4 -4
- package/man/mend-attach.1 +1 -1
- package/man/mend-codex.1 +1 -1
- package/man/mend-completions.1 +1 -1
- package/man/mend-connect.1 +1 -1
- package/man/mend-continue.1 +1 -1
- package/man/mend-doctor.1 +1 -1
- package/man/mend-dotfiles-sync.1 +1 -1
- package/man/mend-dotfiles.1 +1 -1
- package/man/mend-env-cluster.1 +1 -1
- package/man/mend-env-load.1 +1 -1
- package/man/mend-env-show.1 +1 -1
- package/man/mend-help.1 +1 -1
- package/man/mend-keys-init.1 +1 -1
- package/man/mend-keys-mode.1 +1 -1
- package/man/mend-keys-share.1 +1 -1
- package/man/mend-keys-show.1 +1 -1
- package/man/mend-login.1 +1 -1
- package/man/mend-logout.1 +1 -1
- package/man/mend-man.1 +1 -1
- package/man/mend-pair.1 +1 -1
- package/man/mend-projects.1 +1 -1
- package/man/mend-refresh.1 +1 -1
- package/man/mend-rejoin.1 +1 -1
- package/man/mend-resume.1 +1 -1
- package/man/mend-run.1 +1 -1
- package/man/mend-server-logs.1 +15 -0
- package/man/mend-server-restart.1 +15 -0
- package/man/mend-server-setup.1 +64 -0
- package/man/mend-server-start.1 +15 -0
- package/man/mend-server-status.1 +11 -0
- package/man/mend-server-stop.1 +11 -0
- package/man/mend-server-upgrade.1 +30 -0
- package/man/mend-server.1 +11 -0
- package/man/mend-service-add.1 +1 -1
- package/man/mend-service-connect.1 +1 -1
- package/man/mend-service-init.1 +1 -1
- package/man/mend-service-list.1 +1 -1
- package/man/mend-service-logs.1 +1 -1
- package/man/mend-service-restart.1 +1 -1
- package/man/mend-service-run.1 +1 -1
- package/man/mend-service-stop.1 +1 -1
- package/man/mend-sessions.1 +1 -1
- package/man/mend-shell.1 +1 -1
- package/man/mend-skills-push.1 +1 -1
- package/man/mend-skills.1 +1 -1
- package/man/mend-ssh-setup.1 +6 -3
- package/man/mend-ssh.1 +1 -1
- package/man/mend-stop.1 +1 -1
- package/man/mend-ui.1 +1 -1
- package/man/mend-version.1 +1 -1
- package/man/mend-worktrees.1 +1 -1
- package/man/mend.1 +26 -2
- package/package.json +7 -3
- package/dist/agent-share.js +0 -150
- package/dist/clipboard.js +0 -73
- package/dist/dashboard-model.js +0 -308
- package/dist/dashboard.js +0 -1200
- package/dist/doctor.js +0 -233
- package/dist/dotfiles.js +0 -90
- package/dist/env.js +0 -17
- package/dist/help.js +0 -839
- package/dist/login.js +0 -240
- package/dist/pair.js +0 -83
- package/dist/review-model.js +0 -212
- package/dist/review-workflow.js +0 -8
- package/dist/review.js +0 -690
- package/dist/service-init.js +0 -227
- package/dist/shared.js +0 -338
- package/dist/skills.js +0 -134
- package/dist/ssh-setup.js +0 -181
- package/dist/terminal.js +0 -8
- package/dist/tui-theme.js +0 -41
- package/dist/version.js +0 -48
- package/dist/workbench-events.js +0 -72
package/dist/skills.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
/**
|
|
4
|
-
* Local skill-library scanning for `mend skills push`. Runs on the machine
|
|
5
|
-
* that HAS the skills: the shared agent-skills convention keeps them under
|
|
6
|
-
* `~/.agents/skills/<name>/SKILL.md` (plus support files), and the CLI
|
|
7
|
-
* captures the bundles here and streams them to the server's library. The
|
|
8
|
-
* CLI stays dependency-light by design, so the frontmatter peek below is a
|
|
9
|
-
* few lines of string handling, not a YAML parser — the server re-validates
|
|
10
|
-
* everything.
|
|
11
|
-
*/
|
|
12
|
-
/** Where the shared convention keeps user skills, relative to `$HOME`. */
|
|
13
|
-
export const DEFAULT_SKILLS_DIR = ".agents/skills";
|
|
14
|
-
/** The servers' caps, checked here so a mistake fails before the upload. */
|
|
15
|
-
export const MAX_SKILL_FILES = 64;
|
|
16
|
-
export const MAX_SKILL_FILE_BYTES = 512 * 1024;
|
|
17
|
-
const IGNORED_DIRS = new Set([".git", "node_modules", "__pycache__"]);
|
|
18
|
-
const isTextBuffer = (buffer) => !buffer.subarray(0, 8192).includes(0);
|
|
19
|
-
/**
|
|
20
|
-
* Pull `description:` out of a SKILL.md frontmatter block. Single-line plain
|
|
21
|
-
* or quoted scalars only — the common case by far; anything else reads as
|
|
22
|
-
* absent and the server keeps an empty description.
|
|
23
|
-
*/
|
|
24
|
-
export const readFrontmatterDescription = (contents) => {
|
|
25
|
-
if (!contents.startsWith("---\n") && !contents.startsWith("---\r\n"))
|
|
26
|
-
return "";
|
|
27
|
-
const end = contents.indexOf("\n---", 3);
|
|
28
|
-
if (end === -1)
|
|
29
|
-
return "";
|
|
30
|
-
for (const line of contents.slice(0, end).split("\n")) {
|
|
31
|
-
const match = /^description:\s*(.*)$/.exec(line);
|
|
32
|
-
if (match === null)
|
|
33
|
-
continue;
|
|
34
|
-
const raw = (match[1] ?? "").trim();
|
|
35
|
-
if (raw === "" || raw === "|" || raw === ">" || raw === "|-" || raw === ">-")
|
|
36
|
-
return "";
|
|
37
|
-
const quoted = /^(['"])(.*)\1$/.exec(raw);
|
|
38
|
-
return quoted?.[2] ?? raw;
|
|
39
|
-
}
|
|
40
|
-
return "";
|
|
41
|
-
};
|
|
42
|
-
const walkFiles = (root) => {
|
|
43
|
-
const found = [];
|
|
44
|
-
const visit = (dir) => {
|
|
45
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
46
|
-
if (IGNORED_DIRS.has(entry.name))
|
|
47
|
-
continue;
|
|
48
|
-
const absolute = path.join(dir, entry.name);
|
|
49
|
-
// stat (not the dirent) so symlinked files and directories both count.
|
|
50
|
-
let stat;
|
|
51
|
-
try {
|
|
52
|
-
stat = fs.statSync(absolute);
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
continue; // dangling symlink
|
|
56
|
-
}
|
|
57
|
-
if (stat.isDirectory())
|
|
58
|
-
visit(absolute);
|
|
59
|
-
else if (stat.isFile())
|
|
60
|
-
found.push(absolute);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
visit(root);
|
|
64
|
-
return found.toSorted();
|
|
65
|
-
};
|
|
66
|
-
/**
|
|
67
|
-
* Scan a skill library directory: every immediate subdirectory carrying a
|
|
68
|
-
* `SKILL.md` becomes one bundle. Oversized and binary files are skipped with
|
|
69
|
-
* a note; a directory without a SKILL.md is noted and left out entirely.
|
|
70
|
-
*/
|
|
71
|
-
export const scanSkillLibrary = (root) => {
|
|
72
|
-
let entries;
|
|
73
|
-
try {
|
|
74
|
-
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return { error: `${root} is not a readable directory` };
|
|
78
|
-
}
|
|
79
|
-
const skills = [];
|
|
80
|
-
const notes = [];
|
|
81
|
-
for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
|
|
82
|
-
if (entry.name.startsWith("."))
|
|
83
|
-
continue;
|
|
84
|
-
const skillDir = path.join(root, entry.name);
|
|
85
|
-
let stat;
|
|
86
|
-
try {
|
|
87
|
-
stat = fs.statSync(skillDir);
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (!stat.isDirectory())
|
|
93
|
-
continue;
|
|
94
|
-
if (!fs.existsSync(path.join(skillDir, "SKILL.md"))) {
|
|
95
|
-
notes.push({ skill: entry.name, message: "no SKILL.md — skipped" });
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
const files = [];
|
|
99
|
-
let bytes = 0;
|
|
100
|
-
for (const absolute of walkFiles(skillDir)) {
|
|
101
|
-
const relative = path.relative(skillDir, absolute).split(path.sep).join("/");
|
|
102
|
-
const buffer = fs.readFileSync(absolute);
|
|
103
|
-
if (buffer.byteLength > MAX_SKILL_FILE_BYTES) {
|
|
104
|
-
notes.push({ skill: entry.name, message: `${relative} is over 512KB — skipped` });
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (!isTextBuffer(buffer)) {
|
|
108
|
-
notes.push({ skill: entry.name, message: `${relative} is binary — skipped` });
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
files.push({ path: relative, contents: buffer.toString("utf8") });
|
|
112
|
-
bytes += buffer.byteLength;
|
|
113
|
-
}
|
|
114
|
-
if (files.length > MAX_SKILL_FILES) {
|
|
115
|
-
notes.push({
|
|
116
|
-
skill: entry.name,
|
|
117
|
-
message: `${files.length} files is over the ${MAX_SKILL_FILES}-file cap — skipped`,
|
|
118
|
-
});
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
const entryFile = files.find((file) => file.path === "SKILL.md");
|
|
122
|
-
if (entryFile === undefined) {
|
|
123
|
-
notes.push({ skill: entry.name, message: "SKILL.md was unreadable — skipped" });
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
skills.push({
|
|
127
|
-
name: entry.name.toLowerCase(),
|
|
128
|
-
description: readFrontmatterDescription(entryFile.contents),
|
|
129
|
-
files,
|
|
130
|
-
bytes,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return { skills, notes };
|
|
134
|
-
};
|
package/dist/ssh-setup.js
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `mend ssh`: workspace SSH for this machine (docs/WORKSPACE-SSH.md phase 1).
|
|
3
|
-
*
|
|
4
|
-
* `mend ssh` shows the observed state — the deployment's gateway, this user's registered keys,
|
|
5
|
-
* and whether `~/.ssh/config` carries the managed block. `mend ssh setup` makes this machine
|
|
6
|
-
* ready once: pick a public key (an explicit `--key` path; else the running ssh-agent's first
|
|
7
|
-
* key, so no new key material exists; else an existing dedicated key; else a freshly generated
|
|
8
|
-
* one), offer it to the platform under the signed-in user, and write one static Host block.
|
|
9
|
-
* The workspace id travels in the SSH username, so the block never changes per session:
|
|
10
|
-
* `ssh <prefix>-<workspaceId>@mend-ws` — and the VS Code extension connects the same way.
|
|
11
|
-
*/
|
|
12
|
-
import { spawnSync } from "node:child_process";
|
|
13
|
-
import * as fs from "node:fs";
|
|
14
|
-
import * as os from "node:os";
|
|
15
|
-
import * as path from "node:path";
|
|
16
|
-
export const SSH_HOST_ALIAS = "mend-ws";
|
|
17
|
-
const BLOCK_BEGIN = "# >>> mend workspace ssh (managed by `mend ssh setup`) >>>";
|
|
18
|
-
const BLOCK_END = "# <<< mend workspace ssh <<<";
|
|
19
|
-
/** The first key a running ssh-agent offers (`ssh-add -L` output), or null. */
|
|
20
|
-
export const firstAgentKey = (output) => {
|
|
21
|
-
for (const line of output.split("\n")) {
|
|
22
|
-
const trimmed = line.trim();
|
|
23
|
-
const parts = trimmed.split(/\s+/);
|
|
24
|
-
if (parts.length >= 2 && parts[0]?.startsWith("ssh-") === true)
|
|
25
|
-
return trimmed;
|
|
26
|
-
if (parts.length >= 2 && parts[0]?.startsWith("ecdsa-") === true)
|
|
27
|
-
return trimmed;
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
};
|
|
31
|
-
/** The managed Host block. `identityFile` null = the ssh-agent supplies the key. */
|
|
32
|
-
export const managedSshConfigBlock = (input) => [
|
|
33
|
-
BLOCK_BEGIN,
|
|
34
|
-
`Host ${SSH_HOST_ALIAS}`,
|
|
35
|
-
` HostName ${input.host}`,
|
|
36
|
-
` Port ${input.port}`,
|
|
37
|
-
...(input.identityFile === null
|
|
38
|
-
? []
|
|
39
|
-
: [` IdentityFile ${input.identityFile}`, " IdentitiesOnly yes"]),
|
|
40
|
-
" StrictHostKeyChecking accept-new",
|
|
41
|
-
BLOCK_END,
|
|
42
|
-
"",
|
|
43
|
-
].join("\n");
|
|
44
|
-
/** Replace the managed block in an ssh config, or append it; everything else stays verbatim. */
|
|
45
|
-
export const upsertManagedBlock = (existing, block) => {
|
|
46
|
-
const begin = existing.indexOf(BLOCK_BEGIN);
|
|
47
|
-
const end = existing.indexOf(BLOCK_END);
|
|
48
|
-
if (begin !== -1 && end !== -1 && end > begin) {
|
|
49
|
-
const after = existing.slice(end + BLOCK_END.length).replace(/^\n/, "");
|
|
50
|
-
return `${existing.slice(0, begin)}${block}${after}`;
|
|
51
|
-
}
|
|
52
|
-
if (existing === "")
|
|
53
|
-
return block;
|
|
54
|
-
return `${existing.replace(/\n*$/, "\n\n")}${block}`;
|
|
55
|
-
};
|
|
56
|
-
/** Whether an ssh config already carries the managed block. */
|
|
57
|
-
export const hasManagedBlock = (config) => config.includes(BLOCK_BEGIN);
|
|
58
|
-
const ansi = (code) => (text) => process.stdout.isTTY === true ? `[${code}m${text}[0m` : text;
|
|
59
|
-
const dim = ansi("2");
|
|
60
|
-
const green = ansi("32");
|
|
61
|
-
const warn = ansi("33");
|
|
62
|
-
const say = (line) => console.log(line);
|
|
63
|
-
const readIfPresent = (file) => {
|
|
64
|
-
try {
|
|
65
|
-
return fs.readFileSync(file, "utf8");
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return "";
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
/** Resolve the public key to offer, favoring what already exists over creating anything. */
|
|
72
|
-
const pickKey = (cliHome, keyFlag) => {
|
|
73
|
-
if (keyFlag !== null) {
|
|
74
|
-
const pubPath = keyFlag.endsWith(".pub") ? keyFlag : `${keyFlag}.pub`;
|
|
75
|
-
const publicKey = readIfPresent(pubPath).trim();
|
|
76
|
-
if (publicKey === "")
|
|
77
|
-
return { error: `no public key at ${pubPath}` };
|
|
78
|
-
return {
|
|
79
|
-
publicKey,
|
|
80
|
-
identityFile: pubPath.endsWith(".pub") ? pubPath.slice(0, -".pub".length) : pubPath,
|
|
81
|
-
source: "flag",
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
if (process.env["SSH_AUTH_SOCK"] !== undefined && process.env["SSH_AUTH_SOCK"] !== "") {
|
|
85
|
-
const listed = spawnSync("ssh-add", ["-L"], { encoding: "utf8" });
|
|
86
|
-
if (listed.status === 0) {
|
|
87
|
-
const key = firstAgentKey(listed.stdout);
|
|
88
|
-
if (key !== null)
|
|
89
|
-
return { publicKey: key, identityFile: null, source: "agent" };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const dedicated = path.join(cliHome, "ssh", "id_ed25519");
|
|
93
|
-
const existing = readIfPresent(`${dedicated}.pub`).trim();
|
|
94
|
-
if (existing !== "")
|
|
95
|
-
return { publicKey: existing, identityFile: dedicated, source: "existing" };
|
|
96
|
-
fs.mkdirSync(path.dirname(dedicated), { recursive: true, mode: 0o700 });
|
|
97
|
-
const generated = spawnSync("ssh-keygen", ["-t", "ed25519", "-f", dedicated, "-N", "", "-C", `mend-${os.hostname()}`], { encoding: "utf8" });
|
|
98
|
-
if (generated.status !== 0) {
|
|
99
|
-
return { error: `ssh-keygen failed: ${generated.stderr.trim() || "unknown error"}` };
|
|
100
|
-
}
|
|
101
|
-
const fresh = readIfPresent(`${dedicated}.pub`).trim();
|
|
102
|
-
if (fresh === "")
|
|
103
|
-
return { error: `ssh-keygen reported success but ${dedicated}.pub is missing` };
|
|
104
|
-
return { publicKey: fresh, identityFile: dedicated, source: "generated" };
|
|
105
|
-
};
|
|
106
|
-
const sshConfigPath = () => path.join(os.homedir(), ".ssh", "config");
|
|
107
|
-
const showStatus = async (api) => {
|
|
108
|
-
const view = await api("GET", "/workspace-ssh");
|
|
109
|
-
if (view.gateway === null) {
|
|
110
|
-
say(`workspace ssh ${warn("no gateway")} ${dim("· this deployment exposes none")}`);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
say(`gateway ${view.gateway.host}:${view.gateway.port} ${dim(`· username ${view.gateway.usernamePrefix}-<workspace-id>`)}`);
|
|
114
|
-
if (view.keys.length === 0) {
|
|
115
|
-
say(`keys ${warn("none registered")} ${dim("· run: mend ssh setup")}`);
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
for (const key of view.keys) {
|
|
119
|
-
say(`key ${key.name} ${dim(`· ${key.fingerprint}`)}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const configured = hasManagedBlock(readIfPresent(sshConfigPath()));
|
|
123
|
-
say(configured
|
|
124
|
-
? `ssh config ${green("●")} Host ${SSH_HOST_ALIAS} ${dim(`· ${sshConfigPath()}`)}`
|
|
125
|
-
: `ssh config ${warn("no managed block")} ${dim("· run: mend ssh setup")}`);
|
|
126
|
-
};
|
|
127
|
-
const setup = async (api, cliHome, args) => {
|
|
128
|
-
const keyFlagIndex = args.indexOf("--key");
|
|
129
|
-
const keyFlag = keyFlagIndex !== -1 && args[keyFlagIndex + 1] !== undefined
|
|
130
|
-
? String(args[keyFlagIndex + 1])
|
|
131
|
-
: null;
|
|
132
|
-
const view = await api("GET", "/workspace-ssh");
|
|
133
|
-
if (view.gateway === null) {
|
|
134
|
-
say(`mend: this deployment exposes no workspace SSH gateway`);
|
|
135
|
-
process.exitCode = 1;
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
const picked = pickKey(cliHome, keyFlag);
|
|
139
|
-
if ("error" in picked) {
|
|
140
|
-
say(`mend: ${picked.error}`);
|
|
141
|
-
process.exitCode = 1;
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const sourceLabel = {
|
|
145
|
-
flag: "from --key",
|
|
146
|
-
agent: "from your ssh-agent (nothing new created)",
|
|
147
|
-
existing: "existing dedicated key",
|
|
148
|
-
generated: "generated dedicated key",
|
|
149
|
-
}[picked.source];
|
|
150
|
-
const registered = await api("POST", "/workspace-ssh/keys", {
|
|
151
|
-
publicKey: picked.publicKey,
|
|
152
|
-
name: os.hostname(),
|
|
153
|
-
});
|
|
154
|
-
say(`key ${green("●")} ${registered.fingerprint} ${dim(`· ${sourceLabel}`)}`);
|
|
155
|
-
const configFile = sshConfigPath();
|
|
156
|
-
fs.mkdirSync(path.dirname(configFile), { recursive: true, mode: 0o700 });
|
|
157
|
-
const block = managedSshConfigBlock({
|
|
158
|
-
host: view.gateway.host,
|
|
159
|
-
port: view.gateway.port,
|
|
160
|
-
identityFile: picked.identityFile,
|
|
161
|
-
});
|
|
162
|
-
const before = readIfPresent(configFile);
|
|
163
|
-
fs.writeFileSync(configFile, upsertManagedBlock(before, block), { mode: 0o600 });
|
|
164
|
-
say(`ssh config ${green("●")} Host ${SSH_HOST_ALIAS} ${dim(`· ${configFile}`)}`);
|
|
165
|
-
say("");
|
|
166
|
-
say(`connect with ssh ${view.gateway.usernamePrefix}-<workspace-id>@${SSH_HOST_ALIAS} ${dim("· the VS Code extension uses this automatically")}`);
|
|
167
|
-
};
|
|
168
|
-
export const sshCommand = async (args, api, cliHome) => {
|
|
169
|
-
const [subcommand, ...rest] = args;
|
|
170
|
-
switch (subcommand) {
|
|
171
|
-
case undefined:
|
|
172
|
-
case "status":
|
|
173
|
-
return showStatus(api);
|
|
174
|
-
case "setup":
|
|
175
|
-
return setup(api, cliHome, rest);
|
|
176
|
-
default:
|
|
177
|
-
say(`mend: unknown ssh subcommand "${subcommand}" — try: mend ssh · mend ssh setup [--key <path>]`);
|
|
178
|
-
process.exitCode = 1;
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
};
|
package/dist/terminal.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
/** Open a Mend web surface without keeping the terminal process alive. */
|
|
3
|
-
export const openUrl = (target) => {
|
|
4
|
-
const opener = process.platform === "darwin" ? "open" : "xdg-open";
|
|
5
|
-
const child = spawn(opener, [target], { detached: true, stdio: "ignore" });
|
|
6
|
-
child.on("error", () => undefined);
|
|
7
|
-
child.unref();
|
|
8
|
-
};
|
package/dist/tui-theme.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The TUI's theme: a drawn interface with NO painted background — the
|
|
3
|
-
* terminal's own ground shows through everything, and the borders alone
|
|
4
|
-
* draw the panes. The only fills left are the ones that state something:
|
|
5
|
-
* the selection row and the diff add/delete washes. Chrome is near-mono;
|
|
6
|
-
* color appears only where it is a fact. Component code imports semantic
|
|
7
|
-
* names from here; this file is the CLI's single color authority.
|
|
8
|
-
*/
|
|
9
|
-
/** Primary text. */
|
|
10
|
-
export const INK = "#eeece8";
|
|
11
|
-
/** Secondary text. */
|
|
12
|
-
export const INK_2 = "#c9c7c2";
|
|
13
|
-
/** Tertiary text — labels, meta. */
|
|
14
|
-
export const MUTED = "#a3a09a";
|
|
15
|
-
/** The quietest legible step; anything darker reads as broken. */
|
|
16
|
-
export const FAINT = "#807d76";
|
|
17
|
-
/** Hairlines and panel borders — visible, not vanishing. */
|
|
18
|
-
export const RULE = "#37374a";
|
|
19
|
-
/**
|
|
20
|
-
* Interaction and selection — the ONE accent, and the only place chrome is
|
|
21
|
-
* allowed color: the focused pane border and the selection gutter bar.
|
|
22
|
-
*/
|
|
23
|
-
export const COBALT = "#6f96f5";
|
|
24
|
-
/** The selection wash: a neutral lifted row — visible, not colorful. */
|
|
25
|
-
export const WASH = "#2c2c35";
|
|
26
|
-
/**
|
|
27
|
-
* The one exception to the no-painted-background rule: a floating modal
|
|
28
|
-
* must occlude the content beneath it, so it owns its ground.
|
|
29
|
-
*/
|
|
30
|
-
export const SURFACE = "#17171d";
|
|
31
|
-
export const AMBER = "#e2ab42";
|
|
32
|
-
export const GREEN = "#69c189";
|
|
33
|
-
export const RED = "#e87f76";
|
|
34
|
-
/** Diff line washes — strong enough to scan, never louder than the ink. */
|
|
35
|
-
export const ADD_WASH = "#20362a";
|
|
36
|
-
export const DELETE_WASH = "#3c2b2e";
|
|
37
|
-
/** Syntax colors are centralized too; they are subordinate to semantic UI color. */
|
|
38
|
-
export const SYNTAX_KEYWORD = "#cda6f2";
|
|
39
|
-
export const SYNTAX_STRING = "#98cfa8";
|
|
40
|
-
export const SYNTAX_NUMBER = "#e6c37e";
|
|
41
|
-
export const SYNTAX_FUNCTION = "#94bdf7";
|
package/dist/version.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
/**
|
|
3
|
-
* `mend version`: this CLI's version, then the server's when it answers.
|
|
4
|
-
* The two drift apart in practice — an npm install on one machine, a cluster
|
|
5
|
-
* roll on another — and "which one am I on" is the first question any bug
|
|
6
|
-
* report needs answered. The server line is best-effort: a missing server is
|
|
7
|
-
* a fact to print, never an error.
|
|
8
|
-
*/
|
|
9
|
-
/** The published version, read from the package this file ships in (dist/ and src/ sit one level under it). */
|
|
10
|
-
export const cliVersion = () => {
|
|
11
|
-
const raw = fs.readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
12
|
-
const parsed = JSON.parse(raw);
|
|
13
|
-
if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
|
|
14
|
-
const version = parsed.version;
|
|
15
|
-
if (typeof version === "string")
|
|
16
|
-
return version;
|
|
17
|
-
}
|
|
18
|
-
return "unknown";
|
|
19
|
-
};
|
|
20
|
-
/** Ask the server's health route, briefly; a slow or absent server is `null`. */
|
|
21
|
-
export const fetchServerVersion = async (url, timeoutMs = 2_000) => {
|
|
22
|
-
try {
|
|
23
|
-
const response = await fetch(`${url}/api/health`, {
|
|
24
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
25
|
-
});
|
|
26
|
-
if (!response.ok)
|
|
27
|
-
return { url, version: null };
|
|
28
|
-
const body = await response.json();
|
|
29
|
-
const version = typeof body === "object" && body !== null && "version" in body ? body.version : null;
|
|
30
|
-
return { url, version: typeof version === "string" ? version : null };
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return { url, version: null };
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
/** The lines `mend version` prints — terse facts, no verdict beyond the mismatch note. */
|
|
37
|
-
export const versionLines = (cli, server) => {
|
|
38
|
-
const lines = [`mend ${cli}`];
|
|
39
|
-
if (server.version === null) {
|
|
40
|
-
lines.push(`server · unreachable · ${server.url}`);
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
lines.push(`server ${server.version} · ${server.url}`);
|
|
44
|
-
if (server.version !== cli)
|
|
45
|
-
lines.push("versions differ — the server's API wins");
|
|
46
|
-
}
|
|
47
|
-
return lines;
|
|
48
|
-
};
|
package/dist/workbench-events.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The dashboard's side of `/api/events` (the same SSE stream the web app
|
|
3
|
-
* reads): parse frames out of the byte stream, drop the `: ping` heartbeats,
|
|
4
|
-
* and map each pointer event to the query families it can actually stale —
|
|
5
|
-
* mirroring apps/web/src/lib/workbench-events.ts, reduced to the TUI's two
|
|
6
|
-
* cache families. Pure functions; dashboard.tsx owns the socket and the
|
|
7
|
-
* QueryClient.
|
|
8
|
-
*/
|
|
9
|
-
/**
|
|
10
|
-
* An incremental SSE frame splitter: feed decoded text chunks, get back the
|
|
11
|
-
* `data:` payloads of every frame completed so far. Comment-only frames (the
|
|
12
|
-
* heartbeat) produce nothing.
|
|
13
|
-
*/
|
|
14
|
-
export const createSseParser = () => {
|
|
15
|
-
let buffer = "";
|
|
16
|
-
return {
|
|
17
|
-
push: (chunk) => {
|
|
18
|
-
buffer += chunk;
|
|
19
|
-
const payloads = [];
|
|
20
|
-
for (;;) {
|
|
21
|
-
const boundary = buffer.indexOf("\n\n");
|
|
22
|
-
if (boundary === -1)
|
|
23
|
-
break;
|
|
24
|
-
const frame = buffer.slice(0, boundary);
|
|
25
|
-
buffer = buffer.slice(boundary + 2);
|
|
26
|
-
const data = frame
|
|
27
|
-
.split("\n")
|
|
28
|
-
.filter((line) => line.startsWith("data:"))
|
|
29
|
-
.map((line) => line.slice("data:".length).replace(/^ /, ""))
|
|
30
|
-
.join("\n");
|
|
31
|
-
if (data !== "")
|
|
32
|
-
payloads.push(data);
|
|
33
|
-
}
|
|
34
|
-
return payloads;
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* Which families one event payload stales. `session-progress` is deliberately
|
|
40
|
-
* nothing — it fires per record line while an agent works, and the dashboard
|
|
41
|
-
* renders no live lines; treating it as a pointer refetched the entire
|
|
42
|
-
* workbench on a 250ms loop for as long as a session was busy. Unknown types
|
|
43
|
-
* are also nothing, like the web: every state change the TUI renders emits
|
|
44
|
-
* one of the types below, and `r` remains the manual override.
|
|
45
|
-
*/
|
|
46
|
-
export const eventFamilies = (payload) => {
|
|
47
|
-
let parsed;
|
|
48
|
-
try {
|
|
49
|
-
parsed = JSON.parse(payload);
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
if (typeof parsed !== "object" || parsed === null || !("type" in parsed))
|
|
55
|
-
return [];
|
|
56
|
-
switch (parsed.type) {
|
|
57
|
-
case "project":
|
|
58
|
-
case "worktree":
|
|
59
|
-
case "session-process":
|
|
60
|
-
return ["workbench"];
|
|
61
|
-
// Session facts feed both surfaces: rows and annotations on the
|
|
62
|
-
// dashboard; status and the follow-up banner inside a review.
|
|
63
|
-
case "session":
|
|
64
|
-
case "agent-conversation":
|
|
65
|
-
// A comment moves the review AND the dashboard's open-comment counts.
|
|
66
|
-
case "session-change":
|
|
67
|
-
case "review-comment":
|
|
68
|
-
return ["workbench", "review"];
|
|
69
|
-
default:
|
|
70
|
-
return [];
|
|
71
|
-
}
|
|
72
|
-
};
|