@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/doctor.js
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
/**
|
|
4
|
-
* `mend doctor`: one read-only pass over everything a first run depends on, printed
|
|
5
|
-
* as mono status lines (DESIGN.md §4 — a mark plus a word, never a badge). Every
|
|
6
|
-
* line states what was observed; a line that needs an action ends with the single
|
|
7
|
-
* command that takes it. Nothing here writes, launches, or repairs anything.
|
|
8
|
-
*
|
|
9
|
-
* No request waits longer than 3s: a doctor that hangs is worse than a doctor that
|
|
10
|
-
* reports "not checked".
|
|
11
|
-
*/
|
|
12
|
-
const TIMEOUT_MS = 3_000;
|
|
13
|
-
const MARKS = { ok: "✓", todo: "○", failed: "✗" };
|
|
14
|
-
const LABEL_WIDTH = 11;
|
|
15
|
-
/** One status line. The mark is painted by the caller so the formatter stays testable. */
|
|
16
|
-
export const formatCheck = (check, paint = (_state, mark) => mark) => `${paint(check.state, MARKS[check.state])} ${check.label.padEnd(LABEL_WIDTH)} ${check.detail}${check.fix === null ? "" : ` → ${check.fix}`}`;
|
|
17
|
-
/** A read that never throws and never blocks: the outcome is a value the checklist can print. */
|
|
18
|
-
const getJson = async (config, route) => {
|
|
19
|
-
try {
|
|
20
|
-
const response = await fetch(`${config.url}/api${route}`, {
|
|
21
|
-
headers: config.token === null ? {} : { authorization: `Bearer ${config.token}` },
|
|
22
|
-
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
23
|
-
});
|
|
24
|
-
if (!response.ok)
|
|
25
|
-
return { value: null, status: response.status };
|
|
26
|
-
return { value: (await response.json()), status: response.status };
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return { value: null, status: null };
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
/** Where each provider's own CLI writes the credential Mend forwards (mirrors `mend connect`). */
|
|
33
|
-
const LOGIN_COMMANDS = {
|
|
34
|
-
claude: "claude setup-token",
|
|
35
|
-
codex: "codex login",
|
|
36
|
-
github: "gh auth login",
|
|
37
|
-
};
|
|
38
|
-
const HARNESS_CLIS = [
|
|
39
|
-
{ command: "claude", provider: "claude" },
|
|
40
|
-
{ command: "codex", provider: "codex" },
|
|
41
|
-
{ command: "gh", provider: "github" },
|
|
42
|
-
];
|
|
43
|
-
const notChecked = (label) => ({
|
|
44
|
-
label,
|
|
45
|
-
state: "todo",
|
|
46
|
-
detail: "not checked",
|
|
47
|
-
fix: null,
|
|
48
|
-
});
|
|
49
|
-
const identityOf = (account) => {
|
|
50
|
-
const meta = account.metadata;
|
|
51
|
-
for (const key of ["login", "email", "accountEmail", "accountId"]) {
|
|
52
|
-
const value = meta[key];
|
|
53
|
-
if (typeof value === "string" && value !== "")
|
|
54
|
-
return value;
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
};
|
|
58
|
-
/** Every fact the checklist prints, in the order a first run needs them. */
|
|
59
|
-
export const runChecks = async (config, probes) => {
|
|
60
|
-
const checks = [];
|
|
61
|
-
const health = await getJson(config, "/health");
|
|
62
|
-
checks.push(health.value === null
|
|
63
|
-
? {
|
|
64
|
-
label: "server",
|
|
65
|
-
state: "failed",
|
|
66
|
-
detail: `cannot reach ${config.url}`,
|
|
67
|
-
fix: "start the Mend server",
|
|
68
|
-
}
|
|
69
|
-
: {
|
|
70
|
-
label: "server",
|
|
71
|
-
state: "ok",
|
|
72
|
-
detail: `${config.url} · mend ${health.value.version}`,
|
|
73
|
-
fix: null,
|
|
74
|
-
});
|
|
75
|
-
// Projects double as the cheapest authenticated read there is: it proves the token
|
|
76
|
-
// without asking the platform anything.
|
|
77
|
-
const projects = health.value !== null && config.token !== null
|
|
78
|
-
? await getJson(config, "/projects")
|
|
79
|
-
: null;
|
|
80
|
-
if (health.value === null)
|
|
81
|
-
checks.push(notChecked("signed in"));
|
|
82
|
-
else if (config.token === null) {
|
|
83
|
-
checks.push({
|
|
84
|
-
label: "signed in",
|
|
85
|
-
state: "failed",
|
|
86
|
-
detail: "no token saved",
|
|
87
|
-
fix: "mend login",
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
else if (projects !== null && projects.value !== null) {
|
|
91
|
-
checks.push({ label: "signed in", state: "ok", detail: "token accepted", fix: null });
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
const status = projects === null ? null : projects.status;
|
|
95
|
-
checks.push(status === 401 || status === 403
|
|
96
|
-
? { label: "signed in", state: "failed", detail: "token rejected", fix: "mend login" }
|
|
97
|
-
: {
|
|
98
|
-
label: "signed in",
|
|
99
|
-
state: "failed",
|
|
100
|
-
detail: `GET /projects → ${status ?? "no answer"}`,
|
|
101
|
-
fix: null,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
const signedIn = projects !== null && projects.value !== null;
|
|
105
|
-
const connection = signedIn ? await getJson(config, "/sealant/connection") : null;
|
|
106
|
-
if (connection === null)
|
|
107
|
-
checks.push(notChecked("sealant"));
|
|
108
|
-
else if (connection.value === null) {
|
|
109
|
-
// 404 is a server older than the endpoint: nothing was observed, so nothing is claimed.
|
|
110
|
-
checks.push(connection.status === 404
|
|
111
|
-
? notChecked("sealant")
|
|
112
|
-
: {
|
|
113
|
-
label: "sealant",
|
|
114
|
-
state: "failed",
|
|
115
|
-
detail: `GET /sealant/connection → ${connection.status ?? "no answer"}`,
|
|
116
|
-
fix: null,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
else if (connection.value.status === "connected") {
|
|
120
|
-
checks.push({
|
|
121
|
-
label: "sealant",
|
|
122
|
-
state: "ok",
|
|
123
|
-
detail: `connected · ${connection.value.baseUrl}`,
|
|
124
|
-
fix: null,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
checks.push({
|
|
129
|
-
label: "sealant",
|
|
130
|
-
state: "failed",
|
|
131
|
-
detail: `${connection.value.status} · ${connection.value.detail ?? connection.value.baseUrl}`,
|
|
132
|
-
fix: null,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
const identity = signedIn ? await getJson(config, "/me/sealant") : null;
|
|
136
|
-
const platform = identity === null ? null : identity.value;
|
|
137
|
-
for (const { provider } of HARNESS_CLIS) {
|
|
138
|
-
if (platform === null) {
|
|
139
|
-
checks.push(notChecked(provider));
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
const account = platform.accounts.find((row) => row.provider === provider && row.name === "default") ??
|
|
143
|
-
platform.accounts.find((row) => row.provider === provider);
|
|
144
|
-
if (account === undefined) {
|
|
145
|
-
checks.push({
|
|
146
|
-
label: provider,
|
|
147
|
-
state: "todo",
|
|
148
|
-
detail: "not connected",
|
|
149
|
-
fix: `mend connect ${provider}`,
|
|
150
|
-
});
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
const who = identityOf(account);
|
|
154
|
-
checks.push(account.status === "active"
|
|
155
|
-
? {
|
|
156
|
-
label: provider,
|
|
157
|
-
state: "ok",
|
|
158
|
-
detail: who === null ? "connected" : `connected · ${who}`,
|
|
159
|
-
fix: null,
|
|
160
|
-
}
|
|
161
|
-
: {
|
|
162
|
-
label: provider,
|
|
163
|
-
state: "todo",
|
|
164
|
-
detail: account.status,
|
|
165
|
-
fix: `mend connect ${provider}`,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
const adopted = projects === null ? null : projects.value;
|
|
169
|
-
if (adopted === null)
|
|
170
|
-
checks.push(notChecked("projects"));
|
|
171
|
-
else if (adopted.length === 0) {
|
|
172
|
-
checks.push({ label: "projects", state: "todo", detail: "none adopted", fix: "mend adopt" });
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
checks.push({ label: "projects", state: "ok", detail: `${adopted.length} adopted`, fix: null });
|
|
176
|
-
}
|
|
177
|
-
for (const { command, provider } of HARNESS_CLIS) {
|
|
178
|
-
const label = `${command} cli`;
|
|
179
|
-
if (!probes.onPath(command)) {
|
|
180
|
-
checks.push({ label, state: "todo", detail: "not on PATH", fix: null });
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
const credential = probes.localCredential(provider);
|
|
184
|
-
checks.push(credential === null
|
|
185
|
-
? {
|
|
186
|
-
label,
|
|
187
|
-
state: "todo",
|
|
188
|
-
detail: "on PATH · no credential here",
|
|
189
|
-
fix: LOGIN_COMMANDS[provider],
|
|
190
|
-
}
|
|
191
|
-
: { label, state: "ok", detail: "on PATH · credential present", fix: null });
|
|
192
|
-
}
|
|
193
|
-
const machine = signedIn ? await getJson(config, "/machine") : null;
|
|
194
|
-
const tailnet = machine === null || machine.value === null ? null : machine.value.tailnet;
|
|
195
|
-
if (tailnet === null)
|
|
196
|
-
checks.push(notChecked("tailnet"));
|
|
197
|
-
else if (tailnet.status === "reachable" && tailnet.address !== null) {
|
|
198
|
-
checks.push({ label: "tailnet", state: "ok", detail: tailnet.address, fix: null });
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
checks.push({ label: "tailnet", state: "todo", detail: "not detected", fix: null });
|
|
202
|
-
}
|
|
203
|
-
return checks;
|
|
204
|
-
};
|
|
205
|
-
/** An executable of that name on PATH — the question `command -v` asks, without a subprocess. */
|
|
206
|
-
export const onPath = (command) => {
|
|
207
|
-
for (const directory of (process.env["PATH"] ?? "").split(path.delimiter)) {
|
|
208
|
-
if (directory === "")
|
|
209
|
-
continue;
|
|
210
|
-
try {
|
|
211
|
-
fs.accessSync(path.join(directory, command), fs.constants.X_OK);
|
|
212
|
-
return true;
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
// keep looking
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return false;
|
|
219
|
-
};
|
|
220
|
-
/** Green observed · amber not started · red a blocker (DESIGN.md §4); plain text on a pipe. */
|
|
221
|
-
const paintMark = (state, mark) => {
|
|
222
|
-
if (process.stdout.isTTY !== true)
|
|
223
|
-
return mark;
|
|
224
|
-
const code = state === "ok" ? "32" : state === "todo" ? "33" : "31";
|
|
225
|
-
return `[${code}m${mark}[0m`;
|
|
226
|
-
};
|
|
227
|
-
export const doctorCommand = async (config, localCredential) => {
|
|
228
|
-
const checks = await runChecks(config, { localCredential, onPath });
|
|
229
|
-
for (const check of checks)
|
|
230
|
-
process.stdout.write(`${formatCheck(check, paintMark)}\n`);
|
|
231
|
-
if (checks.some((check) => check.state === "failed"))
|
|
232
|
-
process.exitCode = 1;
|
|
233
|
-
};
|
package/dist/dotfiles.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
/**
|
|
4
|
-
* Local dotfiles scanning for `mend dotfiles sync`. This runs on the machine that HAS the
|
|
5
|
-
* files — the whole point: the Mend server may be a VPS whose home directory is a service
|
|
6
|
-
* account's, so contents are captured here and streamed to the server's per-user dotfiles store.
|
|
7
|
-
*
|
|
8
|
-
* The candidate list mirrors the product's curated set (plain configuration only — never keys,
|
|
9
|
-
* credential stores, or histories). The CLI stays dependency-light by design, so the list is
|
|
10
|
-
* duplicated here rather than imported from a workspace package.
|
|
11
|
-
*/
|
|
12
|
-
export const DOTFILE_CANDIDATES = [
|
|
13
|
-
{
|
|
14
|
-
group: "shell",
|
|
15
|
-
paths: [
|
|
16
|
-
".zshrc",
|
|
17
|
-
".zshenv",
|
|
18
|
-
".zprofile",
|
|
19
|
-
".bashrc",
|
|
20
|
-
".bash_profile",
|
|
21
|
-
".profile",
|
|
22
|
-
".aliases",
|
|
23
|
-
".config/fish/config.fish",
|
|
24
|
-
".config/starship.toml",
|
|
25
|
-
],
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
group: "git",
|
|
29
|
-
paths: [".gitconfig", ".gitignore_global", ".config/git/config", ".config/git/ignore"],
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
group: "editors",
|
|
33
|
-
paths: [".vimrc", ".ideavimrc", ".editorconfig", ".config/helix/config.toml"],
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
group: "terminal",
|
|
37
|
-
paths: [".tmux.conf", ".config/tmux/tmux.conf", ".inputrc", ".dir_colors"],
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
group: "tools",
|
|
41
|
-
paths: [
|
|
42
|
-
".ripgreprc",
|
|
43
|
-
".config/bat/config",
|
|
44
|
-
".config/lazygit/config.yml",
|
|
45
|
-
".config/mise/config.toml",
|
|
46
|
-
".config/atuin/config.toml",
|
|
47
|
-
".config/zellij/config.kdl",
|
|
48
|
-
],
|
|
49
|
-
},
|
|
50
|
-
];
|
|
51
|
-
/** The per-file cap the server enforces; checked here so a mistake fails before the upload. */
|
|
52
|
-
export const MAX_FILE_BYTES = 1024 * 1024;
|
|
53
|
-
const statFile = (home, relative) => {
|
|
54
|
-
try {
|
|
55
|
-
const stat = fs.statSync(path.join(home, relative));
|
|
56
|
-
return stat.isFile() ? stat : null;
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
/** Probe the curated candidates under `home`; only existing regular files appear. */
|
|
63
|
-
export const scanDotfileCandidates = (home) => DOTFILE_CANDIDATES.flatMap(({ group, paths }) => paths.flatMap((relative) => {
|
|
64
|
-
const stat = statFile(home, relative);
|
|
65
|
-
return stat === null ? [] : [{ path: relative, group, bytes: stat.size }];
|
|
66
|
-
}));
|
|
67
|
-
/**
|
|
68
|
-
* Read the selected paths for upload. Explicitly requested paths must exist (a typo should fail,
|
|
69
|
-
* not silently sync nothing); oversized files fail with the server's own rule. Modes ride along
|
|
70
|
-
* so an executable script stays executable.
|
|
71
|
-
*/
|
|
72
|
-
export const readSyncFiles = (home, paths_) => {
|
|
73
|
-
const files = [];
|
|
74
|
-
for (const relative of paths_) {
|
|
75
|
-
const stat = statFile(home, relative);
|
|
76
|
-
if (stat === null) {
|
|
77
|
-
return { error: `${relative} is not a file under ${home}` };
|
|
78
|
-
}
|
|
79
|
-
if (stat.size > MAX_FILE_BYTES) {
|
|
80
|
-
return { error: `${relative} is over 1MB — dotfiles are text; trim the selection` };
|
|
81
|
-
}
|
|
82
|
-
files.push({
|
|
83
|
-
path: relative,
|
|
84
|
-
contentsBase64: fs.readFileSync(path.join(home, relative)).toString("base64"),
|
|
85
|
-
// eslint-disable-next-line no-bitwise -- permission bits are the point
|
|
86
|
-
mode: (stat.mode & 0o777).toString(8),
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
return { files };
|
|
90
|
-
};
|
package/dist/env.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `mend env`: load a dotenv file into the project's env store, and show what is stored. Parsing
|
|
3
|
-
* AND routing happen SERVER-SIDE (one parser for the CLI and the browser); the CLI ships the file
|
|
4
|
-
* text once and prints the per-name report. Values are never printed.
|
|
5
|
-
*/
|
|
6
|
-
/** Render the report as terse status lines — names, lanes, actions, reasons; never a value. */
|
|
7
|
-
export const formatLoadReport = (report, paint) => {
|
|
8
|
-
const width = Math.max(0, ...report.loaded.map((entry) => entry.name.length), ...report.rejected.map((entry) => entry.name.length));
|
|
9
|
-
const lines = [];
|
|
10
|
-
for (const entry of report.loaded) {
|
|
11
|
-
lines.push(` ${entry.name.padEnd(width)} ${entry.lane === "secret" ? "secret" : "configuration"} ${paint.dim(`· ${entry.action}${entry.lane === "configuration" ? " · plaintext" : ""}`)}`);
|
|
12
|
-
}
|
|
13
|
-
for (const entry of report.rejected) {
|
|
14
|
-
lines.push(` ${entry.name.padEnd(width)} ${paint.warn("rejected")} ${paint.dim(`· ${entry.reason}`)}`);
|
|
15
|
-
}
|
|
16
|
-
return lines;
|
|
17
|
-
};
|