driggsby 0.2.4 → 0.3.1
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 +9 -2
- package/dist/args-deploy.js +1 -4
- package/dist/args-dev.js +27 -0
- package/dist/args-init.js +2 -2
- package/dist/args-query.js +120 -0
- package/dist/args-shared.js +3 -0
- package/dist/args.js +9 -4
- package/dist/credentials/file-store.js +7 -20
- package/dist/deploy/api-session.js +3 -2
- package/dist/deploy/deploy-command.js +14 -11
- package/dist/deploy/rollback-command.js +7 -9
- package/dist/dev/dev-command.js +104 -12
- package/dist/dev/dev-commands.js +10 -0
- package/dist/dev/dev-servers.js +8 -0
- package/dist/dev/dev-state.js +196 -0
- package/dist/dev/dev-stop.js +76 -0
- package/dist/dev/display-folder.js +23 -0
- package/dist/dev/host-page.js +16 -9
- package/dist/dev/mcp-broker.js +23 -10
- package/dist/help.js +46 -6
- package/dist/init/init-command.js +2 -1
- package/dist/init/template-styles.js +248 -0
- package/dist/init/templates.js +59 -184
- package/dist/main.js +6 -1
- package/dist/owner-only-file.js +26 -0
- package/dist/query/query-command.js +78 -0
- package/dist/terminal-text.js +15 -0
- package/package.json +3 -3
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// The record of the one `driggsby dev` running on this machine:
|
|
2
|
+
// ~/.driggsby/dev.json, written after both ports bind and removed when the
|
|
3
|
+
// run ends. `dev --stop` reads it to find the process, and a second `dev`
|
|
4
|
+
// reads it to say which folder already holds the ports. A file whose pid is
|
|
5
|
+
// no longer alive (the machine rebooted, the process was killed hard) is
|
|
6
|
+
// stale and treated as absent. It holds no credentials.
|
|
7
|
+
import { lstat, readFile, rm } from "node:fs/promises";
|
|
8
|
+
import { Agent, request as httpRequest } from "node:http";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { driggsbyDirectory, writeOwnerOnlyFile } from "../owner-only-file.js";
|
|
11
|
+
const STATE_FILE_NAME = "dev.json";
|
|
12
|
+
// The host origin answers this with the pid of the dev serving it, so a
|
|
13
|
+
// record can be checked against the process actually holding the port.
|
|
14
|
+
export const DEV_IDENTITY_PATH = "/-/dev-identity";
|
|
15
|
+
const IDENTITY_TIMEOUT_MS = 1_000;
|
|
16
|
+
// Whole-probe deadline: the socket timeout above resets on every byte, so a
|
|
17
|
+
// port that dribbles bytes would otherwise hold the probe open for ever;
|
|
18
|
+
// the abort surfaces as a request error, which reads as no answer.
|
|
19
|
+
const IDENTITY_DEADLINE_MS = 2_000;
|
|
20
|
+
const IDENTITY_ATTEMPTS = 2;
|
|
21
|
+
const IDENTITY_RETRY_PAUSE_MS = 100;
|
|
22
|
+
const IDENTITY_MAX_BYTES = 4_096;
|
|
23
|
+
// The record is a few short fields; anything larger is not ours.
|
|
24
|
+
const STATE_MAX_BYTES = 4_096;
|
|
25
|
+
// Node routes fetch AND the default http agent through a proxy named in the
|
|
26
|
+
// environment (NODE_USE_ENV_PROXY with HTTP_PROXY); an agent built here
|
|
27
|
+
// carries no such configuration, so the identity probe below can never
|
|
28
|
+
// leave the loopback interface.
|
|
29
|
+
const LOOPBACK_AGENT = new Agent({ keepAlive: false });
|
|
30
|
+
export function devStatePath(homeDirectory) {
|
|
31
|
+
return join(driggsbyDirectory(homeDirectory), STATE_FILE_NAME);
|
|
32
|
+
}
|
|
33
|
+
export async function writeDevState(homeDirectory, state) {
|
|
34
|
+
await writeOwnerOnlyFile(homeDirectory, STATE_FILE_NAME, `${JSON.stringify(state, null, 2)}\n`);
|
|
35
|
+
}
|
|
36
|
+
export function defaultDevProbes() {
|
|
37
|
+
return { isAlive: processIsAlive, pidServing: devPidServing };
|
|
38
|
+
}
|
|
39
|
+
export async function readDevState(homeDirectory, probes = defaultDevProbes()) {
|
|
40
|
+
const state = await readDevStateFile(homeDirectory);
|
|
41
|
+
if (state === null) {
|
|
42
|
+
return { status: "none", state: null };
|
|
43
|
+
}
|
|
44
|
+
if (state === "malformed") {
|
|
45
|
+
await rm(devStatePath(homeDirectory), { force: true });
|
|
46
|
+
return { status: "none", state: null };
|
|
47
|
+
}
|
|
48
|
+
if (!probes.isAlive(state.pid)) {
|
|
49
|
+
await removeDevState(homeDirectory, state.pid);
|
|
50
|
+
return { status: "none", state: null };
|
|
51
|
+
}
|
|
52
|
+
return (await probes.pidServing(state.hostPort)) === state.pid
|
|
53
|
+
? { status: "live", state }
|
|
54
|
+
: { status: "unconfirmed", state };
|
|
55
|
+
}
|
|
56
|
+
// The state of a dev that is confirmed running, or null.
|
|
57
|
+
export async function readLiveDevState(homeDirectory, probes = defaultDevProbes()) {
|
|
58
|
+
const read = await readDevState(homeDirectory, probes);
|
|
59
|
+
return read.status === "live" ? read.state : null;
|
|
60
|
+
}
|
|
61
|
+
// Asks the host origin on `hostPort` which pid serves it; null when nothing
|
|
62
|
+
// answers, or whatever answers is not a driggsby dev. A short pause and one
|
|
63
|
+
// retry cover a transient socket failure.
|
|
64
|
+
export async function devPidServing(hostPort) {
|
|
65
|
+
for (let attempt = 0; attempt < IDENTITY_ATTEMPTS; attempt += 1) {
|
|
66
|
+
if (attempt > 0) {
|
|
67
|
+
await new Promise((resolve) => {
|
|
68
|
+
setTimeout(resolve, IDENTITY_RETRY_PAUSE_MS);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const pid = await askIdentity(hostPort);
|
|
72
|
+
if (pid !== null) {
|
|
73
|
+
return pid;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
// Loopback only, on LOOPBACK_AGENT. No redirect is followed (anything but
|
|
79
|
+
// 200 is no answer), and the body is read bounded, because whatever holds
|
|
80
|
+
// that port is untrusted.
|
|
81
|
+
function askIdentity(hostPort) {
|
|
82
|
+
return new Promise((settle) => {
|
|
83
|
+
const request = httpRequest({
|
|
84
|
+
host: "127.0.0.1",
|
|
85
|
+
port: hostPort,
|
|
86
|
+
path: DEV_IDENTITY_PATH,
|
|
87
|
+
method: "GET",
|
|
88
|
+
timeout: IDENTITY_TIMEOUT_MS,
|
|
89
|
+
signal: AbortSignal.timeout(IDENTITY_DEADLINE_MS),
|
|
90
|
+
agent: LOOPBACK_AGENT,
|
|
91
|
+
}, (response) => {
|
|
92
|
+
if (response.statusCode !== 200) {
|
|
93
|
+
response.resume();
|
|
94
|
+
settle(null);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const chunks = [];
|
|
98
|
+
let total = 0;
|
|
99
|
+
response.on("data", (chunk) => {
|
|
100
|
+
total += chunk.byteLength;
|
|
101
|
+
if (total > IDENTITY_MAX_BYTES) {
|
|
102
|
+
request.destroy();
|
|
103
|
+
settle(null);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
chunks.push(chunk);
|
|
107
|
+
});
|
|
108
|
+
response.on("end", () => {
|
|
109
|
+
settle(pidFromIdentityBody(Buffer.concat(chunks).toString("utf8")));
|
|
110
|
+
});
|
|
111
|
+
response.on("error", () => {
|
|
112
|
+
settle(null);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
request.on("timeout", () => {
|
|
116
|
+
request.destroy();
|
|
117
|
+
});
|
|
118
|
+
request.on("error", () => {
|
|
119
|
+
settle(null);
|
|
120
|
+
});
|
|
121
|
+
request.end();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function pidFromIdentityBody(body) {
|
|
125
|
+
try {
|
|
126
|
+
const payload = JSON.parse(body);
|
|
127
|
+
const pid = typeof payload === "object" && payload !== null ? payload.pid : null;
|
|
128
|
+
return isPid(pid) ? pid : null;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Removes the file only if it still names `pid`: a dev that ends must never
|
|
135
|
+
// erase the record of a newer dev that took the ports after it.
|
|
136
|
+
export async function removeDevState(homeDirectory, pid) {
|
|
137
|
+
const state = await readDevStateFile(homeDirectory);
|
|
138
|
+
if (state === null) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (state === "malformed" || state.pid === pid) {
|
|
142
|
+
await rm(devStatePath(homeDirectory), { force: true });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export function processIsAlive(pid) {
|
|
146
|
+
try {
|
|
147
|
+
process.kill(pid, 0);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
// EPERM means the process exists but belongs to someone else.
|
|
152
|
+
return error.code === "EPERM";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Only a small regular file is read: a symlink, a device, or anything
|
|
156
|
+
// oversized at that path is not our record.
|
|
157
|
+
async function readDevStateFile(homeDirectory) {
|
|
158
|
+
const path = devStatePath(homeDirectory);
|
|
159
|
+
let raw;
|
|
160
|
+
try {
|
|
161
|
+
const info = await lstat(path);
|
|
162
|
+
if (!info.isFile() || info.size > STATE_MAX_BYTES) {
|
|
163
|
+
return "malformed";
|
|
164
|
+
}
|
|
165
|
+
raw = await readFile(path, "utf8");
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const parsed = JSON.parse(raw);
|
|
172
|
+
return isDevState(parsed) ? parsed : "malformed";
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return "malformed";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function isDevState(value) {
|
|
179
|
+
if (typeof value !== "object" || value === null) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const record = value;
|
|
183
|
+
return (isPid(record.pid) &&
|
|
184
|
+
typeof record.folder === "string" &&
|
|
185
|
+
typeof record.startedAt === "string" &&
|
|
186
|
+
isPort(record.hostPort) &&
|
|
187
|
+
isPort(record.appPort));
|
|
188
|
+
}
|
|
189
|
+
// Only a real, positive pid: 0 and negatives are process groups to kill(2),
|
|
190
|
+
// and a record must never turn into a broadcast signal.
|
|
191
|
+
function isPid(value) {
|
|
192
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
193
|
+
}
|
|
194
|
+
function isPort(value) {
|
|
195
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65_535;
|
|
196
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// driggsby dev --stop: end the `driggsby dev` running on this machine from
|
|
2
|
+
// any terminal, so an agent that started one in the background can put the
|
|
3
|
+
// ports (and the live-data preview) away without hunting for the process.
|
|
4
|
+
// The record in ~/.driggsby/dev.json is trusted only after the recorded
|
|
5
|
+
// port confirms it is served by the recorded pid (see readDevState).
|
|
6
|
+
import { CliError } from "../cli-error.js";
|
|
7
|
+
import { DEV_START_COMMAND, DEV_STOP_COMMAND } from "./dev-commands.js";
|
|
8
|
+
import { defaultDevProbes, readDevState, removeDevState } from "./dev-state.js";
|
|
9
|
+
import { displayFolder } from "./display-folder.js";
|
|
10
|
+
// How many waits of `waitMs` a signalled dev gets to go before we report it.
|
|
11
|
+
const STOP_ATTEMPTS = 20;
|
|
12
|
+
function defaultDevStopDeps() {
|
|
13
|
+
return {
|
|
14
|
+
...defaultDevProbes(),
|
|
15
|
+
terminate: (pid) => {
|
|
16
|
+
process.kill(pid, "SIGTERM");
|
|
17
|
+
},
|
|
18
|
+
waitMs: 250,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function defaultDevStopIo() {
|
|
22
|
+
return {
|
|
23
|
+
out: (text) => {
|
|
24
|
+
process.stdout.write(text);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export async function runDevStop(homeDirectory, io = defaultDevStopIo(), deps = defaultDevStopDeps()) {
|
|
29
|
+
const read = await readDevState(homeDirectory, deps);
|
|
30
|
+
if (read.status === "none") {
|
|
31
|
+
io.out(nothingRunning());
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
const state = read.state;
|
|
35
|
+
const folder = displayFolder(state.folder);
|
|
36
|
+
if (read.status === "unconfirmed") {
|
|
37
|
+
// The pid is alive but the port did not answer as that dev: it may be
|
|
38
|
+
// busy, or the pid may have been recycled. Never signal a process the
|
|
39
|
+
// port has not vouched for, and never claim nothing is running.
|
|
40
|
+
throw new CliError(`driggsby dev is recorded for the app in:\n ${folder}\n\n` +
|
|
41
|
+
"but didn't answer, so it wasn't stopped. If it's still running, press\n" +
|
|
42
|
+
`Ctrl+C in the terminal where it runs, then try again:\n ${DEV_STOP_COMMAND}\n\n` +
|
|
43
|
+
`If nothing is running there, the next driggsby dev replaces this record.`, 1);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
deps.terminate(state.pid);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// Gone between the check and the signal, or a pid that is not ours to
|
|
50
|
+
// signal (another user's process): either way there is no dev to stop.
|
|
51
|
+
// Any other failure leaves the record, since the dev may well be running.
|
|
52
|
+
const code = error.code;
|
|
53
|
+
if (code === "ESRCH" || code === "EPERM") {
|
|
54
|
+
await removeDevState(homeDirectory, state.pid);
|
|
55
|
+
io.out(nothingRunning());
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
for (let attempt = 0; attempt < STOP_ATTEMPTS; attempt += 1) {
|
|
61
|
+
if (!deps.isAlive(state.pid)) {
|
|
62
|
+
await removeDevState(homeDirectory, state.pid);
|
|
63
|
+
io.out(`✓ Stopped driggsby dev for the app in:\n ${folder}\n\nStart it again with:\n ${DEV_START_COMMAND}\n`);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
await new Promise((resolve) => {
|
|
67
|
+
setTimeout(resolve, deps.waitMs);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
throw new CliError(`driggsby dev was asked to stop but is still running for the app in:\n ${folder}\n\n` +
|
|
71
|
+
"Press Ctrl+C in the terminal where it runs, then check again:\n" +
|
|
72
|
+
` ${DEV_STOP_COMMAND}`, 1);
|
|
73
|
+
}
|
|
74
|
+
function nothingRunning() {
|
|
75
|
+
return `No driggsby dev is running on this machine.\n\nStart one in an app's folder with:\n ${DEV_START_COMMAND}\n`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// A project folder as printed in `dev` messages: the person's home shortens
|
|
2
|
+
// to "~" so the common case fits a terminal line, and the result goes
|
|
3
|
+
// through quotedForTerminal because a path is text this CLI did not author.
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { isAbsolute, relative, sep } from "node:path";
|
|
6
|
+
import { quotedForTerminal } from "../terminal-text.js";
|
|
7
|
+
const MAX_FOLDER_CHARS = 200;
|
|
8
|
+
export function displayFolder(folder, home = homedir()) {
|
|
9
|
+
return quotedForTerminal(homeRelative(folder, home), MAX_FOLDER_CHARS);
|
|
10
|
+
}
|
|
11
|
+
function homeRelative(folder, home) {
|
|
12
|
+
if (home === "" || !isAbsolute(folder)) {
|
|
13
|
+
return folder;
|
|
14
|
+
}
|
|
15
|
+
const inside = relative(home, folder);
|
|
16
|
+
if (inside === "") {
|
|
17
|
+
return "~";
|
|
18
|
+
}
|
|
19
|
+
if (inside.startsWith("..") || isAbsolute(inside)) {
|
|
20
|
+
return folder;
|
|
21
|
+
}
|
|
22
|
+
return `~${sep}${inside}`;
|
|
23
|
+
}
|
package/dist/dev/host-page.js
CHANGED
|
@@ -23,33 +23,40 @@ export function hostPageHtml(slug, appOrigin, background) {
|
|
|
23
23
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
24
24
|
<title>${slug} — driggsby dev</title>
|
|
25
25
|
<style>
|
|
26
|
+
/* The strip above the frame is the console's title row, in its own
|
|
27
|
+
measured values: 46px tall on the black ground, 14px ink, a 14%
|
|
28
|
+
white hairline under it. */
|
|
26
29
|
* { box-sizing: border-box; }
|
|
30
|
+
html { color-scheme: dark; }
|
|
27
31
|
body {
|
|
28
32
|
margin: 0;
|
|
29
33
|
display: flex;
|
|
30
34
|
flex-direction: column;
|
|
31
35
|
height: 100vh;
|
|
32
|
-
background: #
|
|
33
|
-
color: #
|
|
34
|
-
font-family:
|
|
36
|
+
background: #000000;
|
|
37
|
+
color: #fafafa;
|
|
38
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
35
39
|
font-size: 14px;
|
|
40
|
+
line-height: 21px;
|
|
41
|
+
-webkit-font-smoothing: antialiased;
|
|
36
42
|
}
|
|
37
43
|
header {
|
|
38
44
|
display: flex;
|
|
39
|
-
align-items:
|
|
45
|
+
align-items: center;
|
|
40
46
|
justify-content: space-between;
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
height: 46px;
|
|
48
|
+
padding: 0 16px;
|
|
49
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.14);
|
|
43
50
|
}
|
|
44
|
-
header .slug { font-weight:
|
|
45
|
-
header .mode { color: #
|
|
51
|
+
header .slug { font-weight: 500; }
|
|
52
|
+
header .mode { color: #a1a1a1; font-size: 13px; }
|
|
46
53
|
iframe {
|
|
47
54
|
flex: 1;
|
|
48
55
|
width: 100%;
|
|
49
56
|
border: none;
|
|
50
57
|
/* The app's declared background from driggsby.json, painted while
|
|
51
58
|
the app loads — the same surface the production host shows. */
|
|
52
|
-
background: ${background ?? "#
|
|
59
|
+
background: ${background ?? "#000000"};
|
|
53
60
|
}
|
|
54
61
|
</style>
|
|
55
62
|
</head>
|
package/dist/dev/mcp-broker.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// { ok, result | error } envelope. Bounds mirror the production host: a few
|
|
5
5
|
// calls in flight, a bounded queue, and a hard per-call timeout, so a
|
|
6
6
|
// buggy app can hammer its own laptop but never Driggsby.
|
|
7
|
+
import { capForTerminal, sanitizeForTerminal } from "../terminal-text.js";
|
|
7
8
|
import { GENERIC_TOOL_TROUBLE } from "./dev-servers.js";
|
|
8
9
|
export const MAX_IN_FLIGHT_CALLS = 4;
|
|
9
10
|
export const MAX_QUEUED_CALLS = 64;
|
|
@@ -16,8 +17,9 @@ export const SIGN_IN_AGAIN_MESSAGE = "Your sign-in on this machine isn't valid a
|
|
|
16
17
|
// endpoint requires a short reason on most read tools; a locally previewed
|
|
17
18
|
// dashboard has exactly one.
|
|
18
19
|
const DEFAULT_REASON = "Rendering this data in the developer's own Driggsby app preview.";
|
|
19
|
-
// One broker per `driggsby dev` run
|
|
20
|
-
//
|
|
20
|
+
// One broker per `driggsby dev` run (or per `driggsby query`). With the
|
|
21
|
+
// default transportErrors, runToolCall never rejects — every failure
|
|
22
|
+
// becomes an { ok: false } envelope with a person-readable message.
|
|
21
23
|
export class McpBroker {
|
|
22
24
|
options;
|
|
23
25
|
queue = [];
|
|
@@ -30,8 +32,8 @@ export class McpBroker {
|
|
|
30
32
|
if (this.queue.length >= MAX_QUEUED_CALLS) {
|
|
31
33
|
return { ok: false, error: { message: GENERIC_TOOL_TROUBLE } };
|
|
32
34
|
}
|
|
33
|
-
return await new Promise((resolve) => {
|
|
34
|
-
this.queue.push({ tool, argumentsObject, resolve });
|
|
35
|
+
return await new Promise((resolve, reject) => {
|
|
36
|
+
this.queue.push({ tool, argumentsObject, resolve, reject });
|
|
35
37
|
this.pump();
|
|
36
38
|
});
|
|
37
39
|
}
|
|
@@ -53,7 +55,11 @@ export class McpBroker {
|
|
|
53
55
|
try {
|
|
54
56
|
result = await this.callMcp(call.tool, call.argumentsObject);
|
|
55
57
|
}
|
|
56
|
-
catch {
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (this.options.transportErrors === "throw") {
|
|
60
|
+
call.reject(error);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
57
63
|
result = { ok: false, error: { message: GENERIC_TOOL_TROUBLE } };
|
|
58
64
|
}
|
|
59
65
|
call.resolve(result);
|
|
@@ -61,7 +67,7 @@ export class McpBroker {
|
|
|
61
67
|
async callMcp(tool, argumentsObject) {
|
|
62
68
|
const toolArguments = { ...argumentsObject };
|
|
63
69
|
if (typeof toolArguments.reason !== "string" || toolArguments.reason.trim() === "") {
|
|
64
|
-
toolArguments.reason = DEFAULT_REASON;
|
|
70
|
+
toolArguments.reason = this.options.defaultReason ?? DEFAULT_REASON;
|
|
65
71
|
}
|
|
66
72
|
const requestId = this.nextRequestId;
|
|
67
73
|
this.nextRequestId += 1;
|
|
@@ -87,7 +93,7 @@ export class McpBroker {
|
|
|
87
93
|
signal: AbortSignal.timeout(this.options.timeoutMs ?? TOOL_CALL_TIMEOUT_MS),
|
|
88
94
|
});
|
|
89
95
|
if (response.status === 401) {
|
|
90
|
-
return { ok: false, error: { message: SIGN_IN_AGAIN_MESSAGE } };
|
|
96
|
+
return { ok: false, error: { message: this.options.signInAgainMessage ?? SIGN_IN_AGAIN_MESSAGE } };
|
|
91
97
|
}
|
|
92
98
|
if (!response.ok) {
|
|
93
99
|
return { ok: false, error: { message: GENERIC_TOOL_TROUBLE } };
|
|
@@ -142,8 +148,15 @@ function toolRefusalMessage(resultRecord) {
|
|
|
142
148
|
}
|
|
143
149
|
return GENERIC_TOOL_TROUBLE;
|
|
144
150
|
}
|
|
151
|
+
// The server's own error text is hostile input on every path that shows it
|
|
152
|
+
// (a terminal for `query`, a page for `dev`): strip control and bidi bytes
|
|
153
|
+
// and cap the length in one place, so no caller has to remember to.
|
|
145
154
|
function capMessage(message) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
155
|
+
const clean = sanitizeForTerminal(message);
|
|
156
|
+
// Text that was nothing but invisible code points sanitizes to nothing;
|
|
157
|
+
// a blank error helps nobody.
|
|
158
|
+
if (clean.trim() === "") {
|
|
159
|
+
return GENERIC_TOOL_TROUBLE;
|
|
160
|
+
}
|
|
161
|
+
return clean.length > MAX_ERROR_MESSAGE_CHARS ? `${capForTerminal(clean, MAX_ERROR_MESSAGE_CHARS)}…` : clean;
|
|
149
162
|
}
|
package/dist/help.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
// the long setup help, and the distinct short/long setup variants). The root
|
|
6
6
|
// help grew past the Rust CLI when login/logout landed; its fixture pins the
|
|
7
7
|
// current text.
|
|
8
|
+
import { DEV_IDLE_MINUTES } from "./dev/dev-commands.js";
|
|
9
|
+
import { APP_TOOL_ALLOWLIST } from "./dev/tool-allowlist.js";
|
|
10
|
+
import { wrapNames } from "./terminal-text.js";
|
|
8
11
|
export const ROOT_HELP = `Usage: npx driggsby@latest <COMMAND>
|
|
9
12
|
|
|
10
13
|
Commands:
|
|
@@ -16,6 +19,7 @@ Commands:
|
|
|
16
19
|
deploy Deploy the app in this directory to Driggsby.
|
|
17
20
|
rollback Switch which deployed version of your app is live.
|
|
18
21
|
versions List your deployed app's versions.
|
|
22
|
+
query Run one Driggsby data tool and print its result as JSON.
|
|
19
23
|
|
|
20
24
|
Options:
|
|
21
25
|
-h, --help Print help
|
|
@@ -30,6 +34,7 @@ Examples:
|
|
|
30
34
|
npx driggsby@latest init money-dash
|
|
31
35
|
npx driggsby@latest dev
|
|
32
36
|
npx driggsby@latest deploy
|
|
37
|
+
npx driggsby@latest query get_overview
|
|
33
38
|
`;
|
|
34
39
|
export const INIT_HELP = `Create a new Driggsby app in a new folder.
|
|
35
40
|
|
|
@@ -80,22 +85,27 @@ files reload the page on save.
|
|
|
80
85
|
|
|
81
86
|
While dev runs, the preview serves your financial data to this machine —
|
|
82
87
|
other websites can't reach it, but anything running locally can, so stop
|
|
83
|
-
it when you're done
|
|
88
|
+
it when you're done: press Ctrl+C, or from any terminal run
|
|
89
|
+
npx driggsby@latest dev --stop. It also stops on its own after
|
|
90
|
+
${String(DEV_IDLE_MINUTES)} minutes with no page open.
|
|
84
91
|
|
|
85
92
|
Sign in first with npx driggsby@latest login.
|
|
86
93
|
|
|
87
|
-
Usage: npx driggsby@latest dev
|
|
94
|
+
Usage: npx driggsby@latest dev [--stop]
|
|
88
95
|
|
|
89
96
|
Options:
|
|
97
|
+
--stop Stop the driggsby dev running on this machine.
|
|
90
98
|
-h, --help Print help
|
|
91
99
|
`;
|
|
92
100
|
export const DEPLOY_HELP = `Deploy the app in this directory to Driggsby.
|
|
93
101
|
|
|
94
102
|
This reads driggsby.json next to your app files, uploads only what changed,
|
|
95
|
-
and makes the new version live
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
and makes the new version live. It prints the app's page in Driggsby, where
|
|
104
|
+
it runs with your data, then the app's own driggsby.dev address, which
|
|
105
|
+
shows no Driggsby data when opened directly. The first deploy creates the
|
|
106
|
+
app: Driggsby assigns its address (your name plus a unique ending) and
|
|
107
|
+
saves it back into driggsby.json. With --preview, the version is uploaded
|
|
108
|
+
and kept ready without changing what's live.
|
|
99
109
|
|
|
100
110
|
Sign in first with npx driggsby@latest login.
|
|
101
111
|
|
|
@@ -128,6 +138,36 @@ Usage: npx driggsby@latest versions
|
|
|
128
138
|
Options:
|
|
129
139
|
-h, --help Print help
|
|
130
140
|
`;
|
|
141
|
+
// The tool list is the allowlist itself, so the help can never name a tool
|
|
142
|
+
// a Driggsby app cannot watch.
|
|
143
|
+
const QUERY_TOOL_LINES = wrapNames([...APP_TOOL_ALLOWLIST], " ");
|
|
144
|
+
export const QUERY_HELP = `Run one Driggsby data tool and print its result as JSON.
|
|
145
|
+
|
|
146
|
+
The output is exactly what driggsby.watch hands an app's callback for the
|
|
147
|
+
same tool and params, so you can read a result's shape before writing any
|
|
148
|
+
render code. Only the read-only tools an app can watch are accepted. The
|
|
149
|
+
JSON goes to stdout and nothing else does; messages go to stderr.
|
|
150
|
+
|
|
151
|
+
Sign in first with npx driggsby@latest login.
|
|
152
|
+
|
|
153
|
+
Usage: npx driggsby@latest query <TOOL> [--sql <SQL>] [--params <JSON>]
|
|
154
|
+
|
|
155
|
+
Arguments:
|
|
156
|
+
<TOOL> One of:
|
|
157
|
+
${QUERY_TOOL_LINES}
|
|
158
|
+
|
|
159
|
+
Options:
|
|
160
|
+
--sql <SQL> The SQL for query_cash_sql or query_investment_sql.
|
|
161
|
+
Overrides a sql key given in --params.
|
|
162
|
+
--params <JSON> Other params as a JSON object, for example
|
|
163
|
+
'{"history_type":"liabilities"}'.
|
|
164
|
+
-h, --help Print help
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
npx driggsby@latest query get_overview
|
|
168
|
+
npx driggsby@latest query list_recurring_transactions
|
|
169
|
+
npx driggsby@latest query query_cash_sql --sql "SELECT 1"
|
|
170
|
+
`;
|
|
131
171
|
export const MCP_HELP = `Set up Driggsby MCP clients.
|
|
132
172
|
|
|
133
173
|
Usage: npx driggsby@latest mcp <COMMAND>
|
|
@@ -9,7 +9,8 @@ import { join } from "node:path";
|
|
|
9
9
|
import { slugProblem } from "@driggsby/deploy";
|
|
10
10
|
import { CliError } from "../cli-error.js";
|
|
11
11
|
import { quotedForTerminal, wrapProse } from "../terminal-text.js";
|
|
12
|
-
import {
|
|
12
|
+
import { STYLES_CSS } from "./template-styles.js";
|
|
13
|
+
import { APP_JS, driggsbyJsonText, indexHtml } from "./templates.js";
|
|
13
14
|
// Argv and prompt answers echo back inside quotes with this cap; a slug can
|
|
14
15
|
// never be longer than 63 characters, so the cap only trims hostile input.
|
|
15
16
|
const MAX_SLUG_ECHO_CHARS = 80;
|