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
package/README.md
CHANGED
|
@@ -60,7 +60,8 @@ This runs the app locally, embedded exactly the way Driggsby embeds it
|
|
|
60
60
|
after a deploy, and reloads the page when you save a file. While dev runs,
|
|
61
61
|
the preview serves your financial data to this machine — other websites
|
|
62
62
|
can't reach it, but anything running locally can, so stop it when you're
|
|
63
|
-
done
|
|
63
|
+
done: press Ctrl+C, or from any terminal run `npx driggsby@latest dev --stop`.
|
|
64
|
+
It also stops on its own after 30 minutes with no page open.
|
|
64
65
|
|
|
65
66
|
## Deploying an app
|
|
66
67
|
|
|
@@ -75,6 +76,9 @@ The first deploy creates the app: Driggsby assigns its address — the name
|
|
|
75
76
|
you picked plus a unique ending, like
|
|
76
77
|
`your-app-name-x7k2qf.driggsby.dev` — and saves the assigned name back
|
|
77
78
|
into `driggsby.json`, so any readable name works and names never collide.
|
|
79
|
+
`deploy` prints the Driggsby page for the app first; that is where it runs
|
|
80
|
+
with your data. The `driggsby.dev` address is the app on its own, which
|
|
81
|
+
shows no Driggsby data when opened directly.
|
|
78
82
|
If the project lives in git, commit the updated `driggsby.json` — it is the
|
|
79
83
|
only record of your app's address, and a fresh checkout without it would
|
|
80
84
|
create a second app.
|
|
@@ -82,7 +86,10 @@ Every deploy after that uploads only the files that changed. Deploy with
|
|
|
82
86
|
`--preview` to upload a version without
|
|
83
87
|
changing what visitors see, `npx driggsby@latest versions` to list every
|
|
84
88
|
kept version, and `npx driggsby@latest rollback` to switch which one is
|
|
85
|
-
live — switching re-uploads nothing.
|
|
89
|
+
live — switching re-uploads nothing. `npx driggsby@latest query <tool>`
|
|
90
|
+
runs one read-only data tool and prints its result as JSON, the same
|
|
91
|
+
shape a `driggsby.watch` callback receives, so you can see the data
|
|
92
|
+
before writing any render code.
|
|
86
93
|
|
|
87
94
|
The CLI is pure TypeScript and installs from the npm registry alone — no
|
|
88
95
|
binary downloads, no install scripts — and runs on macOS, Linux, and Windows
|
package/dist/args-deploy.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Argv parsing for the deploy command family: deploy, rollback, versions.
|
|
2
2
|
// Same behavior contract as the rest of the tree — help on -h/--help, usage
|
|
3
3
|
// errors exit 2, echoed argv is sanitized — without chasing clap byte quirks.
|
|
4
|
-
import { unexpectedArgument, unexpectedFlagValue, } from "./args-shared.js";
|
|
4
|
+
import { helpCommand, unexpectedArgument, unexpectedFlagValue, } from "./args-shared.js";
|
|
5
5
|
import { CliError } from "./cli-error.js";
|
|
6
6
|
import { DEPLOY_HELP, ROLLBACK_HELP, VERSIONS_HELP } from "./help.js";
|
|
7
7
|
import { sanitizeForTerminal } from "./terminal-text.js";
|
|
@@ -73,9 +73,6 @@ export function parseVersions(argv) {
|
|
|
73
73
|
}
|
|
74
74
|
return { kind: "versions" };
|
|
75
75
|
}
|
|
76
|
-
function helpCommand(text) {
|
|
77
|
-
return { kind: "print-help", text, stream: "stdout", exitCode: 0 };
|
|
78
|
-
}
|
|
79
76
|
// Version numbers are the small positive integers shown by `versions` (v1,
|
|
80
77
|
// v2, ...). Anything else is a usage mistake, reported like other bad argv —
|
|
81
78
|
// including a digit string too large for an exact integer, which would
|
package/dist/args-dev.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Argv parsing for `driggsby dev [--stop]`. Same contract as the rest of the
|
|
2
|
+
// tree: help on -h/--help, usage errors exit 2, echoed argv is sanitized.
|
|
3
|
+
import { helpCommand, unexpectedArgument, unexpectedFlagValue } from "./args-shared.js";
|
|
4
|
+
import { DEV_HELP } from "./help.js";
|
|
5
|
+
export const DEV_USAGE = "Usage: npx driggsby@latest dev [--stop]";
|
|
6
|
+
export function parseDev(argv) {
|
|
7
|
+
let stop = false;
|
|
8
|
+
let optionsEnded = false;
|
|
9
|
+
for (const token of argv) {
|
|
10
|
+
if (!optionsEnded && token === "--") {
|
|
11
|
+
optionsEnded = true;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (!optionsEnded && (token === "-h" || token === "--help")) {
|
|
15
|
+
return helpCommand(DEV_HELP);
|
|
16
|
+
}
|
|
17
|
+
if (!optionsEnded && token === "--stop") {
|
|
18
|
+
stop = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (!optionsEnded && token.startsWith("--stop=")) {
|
|
22
|
+
throw unexpectedFlagValue("--stop", token.slice("--stop=".length), DEV_USAGE);
|
|
23
|
+
}
|
|
24
|
+
throw unexpectedArgument(token, DEV_USAGE);
|
|
25
|
+
}
|
|
26
|
+
return { kind: "dev", stop };
|
|
27
|
+
}
|
package/dist/args-init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Argv parsing for driggsby init: one optional positional (the app's name),
|
|
2
2
|
// help, and the shared usage-error shape — usage errors exit 2, echoed argv
|
|
3
3
|
// is sanitized by the shared builders.
|
|
4
|
-
import { unexpectedArgument } from "./args-shared.js";
|
|
4
|
+
import { helpCommand, unexpectedArgument } from "./args-shared.js";
|
|
5
5
|
import { INIT_HELP } from "./help.js";
|
|
6
6
|
const INIT_USAGE = "Usage: npx driggsby@latest init [NAME]";
|
|
7
7
|
export function parseInit(argv) {
|
|
@@ -13,7 +13,7 @@ export function parseInit(argv) {
|
|
|
13
13
|
continue;
|
|
14
14
|
}
|
|
15
15
|
if (!optionsEnded && (token === "-h" || token === "--help")) {
|
|
16
|
-
return
|
|
16
|
+
return helpCommand(INIT_HELP);
|
|
17
17
|
}
|
|
18
18
|
// Any other flag-looking token is a mistake, not a name; after "--"
|
|
19
19
|
// everything is positional and slug validation refuses bad names.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Argv parsing for `driggsby query <TOOL> [--sql <SQL>] [--params <JSON>]`.
|
|
2
|
+
// Same behavior contract as the rest of the tree: help on -h/--help, usage
|
|
3
|
+
// errors exit 2, echoed argv is sanitized. The tool is checked against the
|
|
4
|
+
// allowlist here so a typo is a usage error naming the twelve, not a
|
|
5
|
+
// server round trip.
|
|
6
|
+
import { helpCommand, unexpectedArgument } from "./args-shared.js";
|
|
7
|
+
import { didYouMean } from "./clap-suggestions.js";
|
|
8
|
+
import { CliError } from "./cli-error.js";
|
|
9
|
+
import { APP_TOOL_ALLOWLIST } from "./dev/tool-allowlist.js";
|
|
10
|
+
import { QUERY_HELP } from "./help.js";
|
|
11
|
+
import { quotedForTerminal, wrapNames } from "./terminal-text.js";
|
|
12
|
+
// The two tools that take SQL: --sql applies only to these, and a retry
|
|
13
|
+
// line names --sql only for them.
|
|
14
|
+
export const SQL_TOOLS = new Set(["query_cash_sql", "query_investment_sql"]);
|
|
15
|
+
const QUERY_USAGE = "Usage: npx driggsby@latest query <TOOL> [--sql <SQL>] [--params <JSON>]";
|
|
16
|
+
export function parseQuery(argv) {
|
|
17
|
+
let tool = null;
|
|
18
|
+
let sql = null;
|
|
19
|
+
let params = null;
|
|
20
|
+
let optionsEnded = false;
|
|
21
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
22
|
+
const token = argv[index] ?? "";
|
|
23
|
+
if (!optionsEnded && token === "--") {
|
|
24
|
+
optionsEnded = true;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!optionsEnded && (token === "-h" || token === "--help")) {
|
|
28
|
+
return helpCommand(QUERY_HELP);
|
|
29
|
+
}
|
|
30
|
+
if (!optionsEnded && token === "--sql") {
|
|
31
|
+
sql = requireFlagValue(argv, index, "--sql");
|
|
32
|
+
index += 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!optionsEnded && token.startsWith("--sql=")) {
|
|
36
|
+
sql = token.slice("--sql=".length);
|
|
37
|
+
if (sql.trim() === "") {
|
|
38
|
+
throw missingFlagValue("--sql");
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!optionsEnded && token === "--params") {
|
|
43
|
+
params = parseParams(requireFlagValue(argv, index, "--params", { allowBlank: true }));
|
|
44
|
+
index += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!optionsEnded && token.startsWith("--params=")) {
|
|
48
|
+
params = parseParams(token.slice("--params=".length));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (tool === null && (optionsEnded || !token.startsWith("-"))) {
|
|
52
|
+
tool = token;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
throw unexpectedArgument(token, QUERY_USAGE);
|
|
56
|
+
}
|
|
57
|
+
if (tool === null) {
|
|
58
|
+
throw missingTool();
|
|
59
|
+
}
|
|
60
|
+
if (!APP_TOOL_ALLOWLIST.has(tool)) {
|
|
61
|
+
throw unknownTool(tool);
|
|
62
|
+
}
|
|
63
|
+
if (sql !== null && !SQL_TOOLS.has(tool)) {
|
|
64
|
+
throw sqlForNonSqlTool(tool);
|
|
65
|
+
}
|
|
66
|
+
const merged = { ...(params ?? {}) };
|
|
67
|
+
if (sql !== null) {
|
|
68
|
+
merged.sql = sql;
|
|
69
|
+
}
|
|
70
|
+
return { kind: "query", tool, params: merged };
|
|
71
|
+
}
|
|
72
|
+
// A following flag is never a value: `--sql --params '{}'` is a missing SQL,
|
|
73
|
+
// not SQL that reads "--params" (SQL that must start with "-" can use the
|
|
74
|
+
// --sql= spelling).
|
|
75
|
+
function requireFlagValue(argv, index, flag, options = {}) {
|
|
76
|
+
const value = argv[index + 1];
|
|
77
|
+
const looksLikeFlag = value !== undefined && value.length > 1 && value.startsWith("-");
|
|
78
|
+
const blank = value === undefined || looksLikeFlag || (value.trim() === "" && options.allowBlank !== true);
|
|
79
|
+
if (blank) {
|
|
80
|
+
throw missingFlagValue(flag);
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
function missingFlagValue(flag) {
|
|
85
|
+
return new CliError(`error: a value is required for '${flag}' but none was supplied\n\n${QUERY_USAGE}\n\nFor more information, try '--help'.`, 2);
|
|
86
|
+
}
|
|
87
|
+
// --params is the tool's params object, verbatim; anything that is not a
|
|
88
|
+
// JSON object (an array, a string, unparseable text) is a usage mistake.
|
|
89
|
+
function parseParams(rawValue) {
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(rawValue);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw invalidParams();
|
|
96
|
+
}
|
|
97
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
98
|
+
throw invalidParams();
|
|
99
|
+
}
|
|
100
|
+
return parsed;
|
|
101
|
+
}
|
|
102
|
+
function invalidParams() {
|
|
103
|
+
return new CliError(`error: invalid value for '--params':\n'--params' must be a JSON object, like '{"history_type":"liabilities"}'\n\n` +
|
|
104
|
+
`${QUERY_USAGE}\n\nFor more information, try '--help'.`, 2);
|
|
105
|
+
}
|
|
106
|
+
function missingTool() {
|
|
107
|
+
return new CliError(`error: the following required arguments were not provided:\n <TOOL>\n\n${QUERY_USAGE}\n\nFor more information, try '--help'.`, 2);
|
|
108
|
+
}
|
|
109
|
+
function unknownTool(tool) {
|
|
110
|
+
const similar = didYouMean(tool, [...APP_TOOL_ALLOWLIST]);
|
|
111
|
+
const tip = similar === undefined ? "" : ` tip: a similar tool exists: '${similar}'\n\n`;
|
|
112
|
+
return new CliError(`error: ${quotedForTerminal(tool, 60)} isn't a tool a Driggsby app can call.\n` +
|
|
113
|
+
`Apps can watch these read-only tools:\n` +
|
|
114
|
+
`${wrapNames([...APP_TOOL_ALLOWLIST], " ")}\n\n${tip}${QUERY_USAGE}\n\nFor more information, try '--help'.`, 2);
|
|
115
|
+
}
|
|
116
|
+
function sqlForNonSqlTool(tool) {
|
|
117
|
+
return new CliError(`error: '--sql' only applies to query_cash_sql or query_investment_sql;\n` +
|
|
118
|
+
`'${tool}' takes no SQL.\n\n` +
|
|
119
|
+
`${QUERY_USAGE}\n\nFor more information, try '--help'.`, 2);
|
|
120
|
+
}
|
package/dist/args-shared.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
import { CliError } from "./cli-error.js";
|
|
6
6
|
import {} from "./mcp-setup/known-client.js";
|
|
7
7
|
import { sanitizeForTerminal } from "./terminal-text.js";
|
|
8
|
+
export function helpCommand(text) {
|
|
9
|
+
return { kind: "print-help", text, stream: "stdout", exitCode: 0 };
|
|
10
|
+
}
|
|
8
11
|
// "--print=true" / "--help=x" / "--version=x": these flags take no value.
|
|
9
12
|
export function unexpectedFlagValue(flag, rawValue, usage) {
|
|
10
13
|
const value = sanitizeForTerminal(rawValue);
|
package/dist/args.js
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
// (login, logout) keep the same behavior and exit codes without chasing
|
|
7
7
|
// clap's byte-level quirks.
|
|
8
8
|
import { parseDeploy, parseRollback, parseVersions } from "./args-deploy.js";
|
|
9
|
+
import { parseDev } from "./args-dev.js";
|
|
9
10
|
import { parseInit } from "./args-init.js";
|
|
10
11
|
import { parseMcpSetup } from "./args-mcp-setup.js";
|
|
12
|
+
import { parseQuery } from "./args-query.js";
|
|
11
13
|
import { removeDashesTip, similarArgumentTip, unexpectedArgument, unexpectedFlagValue, } from "./args-shared.js";
|
|
12
14
|
import { didYouMean } from "./clap-suggestions.js";
|
|
13
15
|
import { CliError } from "./cli-error.js";
|
|
14
|
-
import {
|
|
16
|
+
import { LOGIN_HELP, LOGOUT_HELP, MCP_HELP, ROOT_HELP } from "./help.js";
|
|
15
17
|
import { sanitizeForTerminal } from "./terminal-text.js";
|
|
16
18
|
export {} from "./args-shared.js";
|
|
17
19
|
const ROOT_USAGE = "Usage: npx driggsby@latest <COMMAND>";
|
|
@@ -19,17 +21,18 @@ const MCP_USAGE = "Usage: npx driggsby@latest mcp <COMMAND>";
|
|
|
19
21
|
const ROOT_LEVEL = {
|
|
20
22
|
helpText: ROOT_HELP,
|
|
21
23
|
usage: ROOT_USAGE,
|
|
22
|
-
subcommands: ["mcp", "login", "logout", "init", "dev", "deploy", "rollback", "versions"],
|
|
24
|
+
subcommands: ["mcp", "login", "logout", "init", "dev", "deploy", "rollback", "versions", "query"],
|
|
23
25
|
longFlags: ["help", "version"],
|
|
24
26
|
subcommandFlags: [
|
|
25
27
|
{ name: "mcp", longFlags: ["help"] },
|
|
26
28
|
{ name: "login", longFlags: ["help"] },
|
|
27
29
|
{ name: "logout", longFlags: ["help"] },
|
|
28
30
|
{ name: "init", longFlags: ["help"] },
|
|
29
|
-
{ name: "dev", longFlags: ["help"] },
|
|
31
|
+
{ name: "dev", longFlags: ["stop", "help"] },
|
|
30
32
|
{ name: "deploy", longFlags: ["preview", "help"] },
|
|
31
33
|
{ name: "rollback", longFlags: ["to", "help"] },
|
|
32
34
|
{ name: "versions", longFlags: ["help"] },
|
|
35
|
+
{ name: "query", longFlags: ["sql", "params", "help"] },
|
|
33
36
|
],
|
|
34
37
|
usagePrefix: "npx driggsby@latest",
|
|
35
38
|
hasVersion: true,
|
|
@@ -88,13 +91,15 @@ function dispatchSubcommand(name, rest) {
|
|
|
88
91
|
case "init":
|
|
89
92
|
return parseInit(rest);
|
|
90
93
|
case "dev":
|
|
91
|
-
return
|
|
94
|
+
return parseDev(rest);
|
|
92
95
|
case "deploy":
|
|
93
96
|
return parseDeploy(rest);
|
|
94
97
|
case "rollback":
|
|
95
98
|
return parseRollback(rest);
|
|
96
99
|
case "versions":
|
|
97
100
|
return parseVersions(rest);
|
|
101
|
+
case "query":
|
|
102
|
+
return parseQuery(rest);
|
|
98
103
|
case "setup":
|
|
99
104
|
return parseMcpSetup(rest);
|
|
100
105
|
default:
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// atomically with owner-only permissions. This is the only backend on
|
|
3
3
|
// Windows (no readable credential CLI exists there; the profile directory's
|
|
4
4
|
// default user-only ACL is the boundary) and the fallback everywhere else.
|
|
5
|
-
import {
|
|
6
|
-
import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { readdir, readFile, rm } from "node:fs/promises";
|
|
7
6
|
import { join } from "node:path";
|
|
7
|
+
import { driggsbyDirectory, writeOwnerOnlyFile } from "../owner-only-file.js";
|
|
8
8
|
import {} from "./clear-outcome.js";
|
|
9
|
+
const CREDENTIALS_FILE_NAME = "credentials.json";
|
|
9
10
|
export function credentialsFilePath(homeDirectory) {
|
|
10
|
-
return join(homeDirectory,
|
|
11
|
+
return join(driggsbyDirectory(homeDirectory), CREDENTIALS_FILE_NAME);
|
|
11
12
|
}
|
|
12
13
|
export async function readFileToken(homeDirectory) {
|
|
13
14
|
let raw;
|
|
@@ -30,21 +31,7 @@ export async function readFileToken(homeDirectory) {
|
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
export async function writeFileToken(homeDirectory, token) {
|
|
33
|
-
|
|
34
|
-
// mode applies only when the directory is created; an existing ~/.driggsby
|
|
35
|
-
// keeps whatever permissions the user already gave it.
|
|
36
|
-
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
37
|
-
const finalPath = credentialsFilePath(homeDirectory);
|
|
38
|
-
const temporaryPath = join(directory, `credentials.json.${randomUUID()}.tmp`);
|
|
39
|
-
const body = `${JSON.stringify({ app_token: token }, null, 2)}\n`;
|
|
40
|
-
try {
|
|
41
|
-
await writeFile(temporaryPath, body, { mode: 0o600 });
|
|
42
|
-
await rename(temporaryPath, finalPath);
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
await rm(temporaryPath, { force: true });
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
34
|
+
await writeOwnerOnlyFile(homeDirectory, CREDENTIALS_FILE_NAME, `${JSON.stringify({ app_token: token }, null, 2)}\n`);
|
|
48
35
|
}
|
|
49
36
|
export async function clearFileToken(homeDirectory) {
|
|
50
37
|
let outcome;
|
|
@@ -59,10 +46,10 @@ export async function clearFileToken(homeDirectory) {
|
|
|
59
46
|
// readable token in a `credentials.json.<uuid>.tmp` file; removal must not
|
|
60
47
|
// leave one behind while reporting the token gone.
|
|
61
48
|
try {
|
|
62
|
-
for (const entry of await readdir(
|
|
49
|
+
for (const entry of await readdir(driggsbyDirectory(homeDirectory))) {
|
|
63
50
|
if (entry.startsWith("credentials.json.") && entry.endsWith(".tmp")) {
|
|
64
51
|
try {
|
|
65
|
-
await rm(join(homeDirectory,
|
|
52
|
+
await rm(join(driggsbyDirectory(homeDirectory), entry), { force: true });
|
|
66
53
|
}
|
|
67
54
|
catch {
|
|
68
55
|
outcome = "failed";
|
|
@@ -9,6 +9,8 @@ import { CliError } from "../cli-error.js";
|
|
|
9
9
|
import { readStoredToken, } from "../credentials/store.js";
|
|
10
10
|
import { capForTerminal, wrapProse } from "../terminal-text.js";
|
|
11
11
|
const LOGIN_COMMAND = "npx driggsby@latest login";
|
|
12
|
+
// The one wording for a saved sign-in the server no longer accepts.
|
|
13
|
+
export const SIGN_IN_AGAIN_MESSAGE = "Your sign-in on this machine isn't valid anymore. Sign in again:\n" + ` ${LOGIN_COMMAND}`;
|
|
12
14
|
const MAX_ERROR_DESCRIPTION_CHARS = 300;
|
|
13
15
|
// Server-supplied display strings go through capForTerminal (sanitize plus
|
|
14
16
|
// a hard length bound) with bounds sized to each string's real contract so
|
|
@@ -67,8 +69,7 @@ function apiFailure(error, retryCommand) {
|
|
|
67
69
|
return new CliError(`Driggsby is briefly unavailable just now. Please try again:\n ${retryCommand}`, 1);
|
|
68
70
|
}
|
|
69
71
|
if (error.status === 401) {
|
|
70
|
-
return new CliError(
|
|
71
|
-
` ${LOGIN_COMMAND}`, 1);
|
|
72
|
+
return new CliError(SIGN_IN_AGAIN_MESSAGE, 1);
|
|
72
73
|
}
|
|
73
74
|
if (error.code === "deploy_scope_required") {
|
|
74
75
|
return new CliError("Your saved sign-in can read your Driggsby data but wasn't approved for\n" +
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// driggsby deploy: read driggsby.json, hash the serve folder, upload what
|
|
2
2
|
// Driggsby doesn't have, and make the new version live (or hold it as a
|
|
3
3
|
// ready-but-not-live version with --preview).
|
|
4
|
-
import { collectDeployFiles, deployProjectFiles, formatBytes, readProjectConfig, } from "@driggsby/deploy";
|
|
4
|
+
import { collectDeployFiles, deployProjectFiles, formatBytes, hasLiveAddress, liveAddresses, liveAddressLines, readProjectConfig, } from "@driggsby/deploy";
|
|
5
5
|
import { apiBaseUrl } from "../api/base-url.js";
|
|
6
6
|
import { defaultCredentialEnvironment, } from "../credentials/store.js";
|
|
7
|
-
import {
|
|
8
|
-
import { deployFailure, MAX_SERVER_TEXT_CHARS,
|
|
7
|
+
import { quotedForTerminal, wrapProse } from "../terminal-text.js";
|
|
8
|
+
import { deployFailure, MAX_SERVER_TEXT_CHARS, requireDeploySession, } from "./api-session.js";
|
|
9
9
|
const DEPLOY_RETRY_COMMAND = "npx driggsby@latest deploy";
|
|
10
10
|
function defaultDeployIo() {
|
|
11
11
|
return {
|
|
@@ -48,9 +48,12 @@ export async function runDeploy(options, environment = defaultCredentialEnvironm
|
|
|
48
48
|
const seconds = (io.now() - startedAt) / 1_000;
|
|
49
49
|
io.out(`${changedNote(outcome)}\n`);
|
|
50
50
|
io.out(`${uploadedLine(outcome, seconds)}\n`);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
const addresses = liveAddresses(outcome.url, outcome.consoleUrl, session.baseUrl);
|
|
52
|
+
io.out(`${resultLine(outcome, addresses)}\n`);
|
|
53
|
+
// A preview has no address to open: the Driggsby page shows the live
|
|
54
|
+
// version, which a preview leaves unchanged.
|
|
55
|
+
if (outcome.live) {
|
|
56
|
+
io.out(liveAddressLines(addresses));
|
|
54
57
|
}
|
|
55
58
|
if (collected.skippedNodeModules.length > 0) {
|
|
56
59
|
io.out(`\n${wrapProse("Note: node_modules doesn't deploy — Driggsby serves your app as static files, so it isn't needed.")}\n`);
|
|
@@ -89,12 +92,12 @@ function uploadedLine(outcome, seconds) {
|
|
|
89
92
|
}
|
|
90
93
|
return `✓ Uploaded ${formatBytes(outcome.uploadedBytes)} in ${formatSeconds(seconds)}`;
|
|
91
94
|
}
|
|
92
|
-
function resultLine(outcome) {
|
|
95
|
+
function resultLine(outcome, addresses) {
|
|
93
96
|
if (outcome.live) {
|
|
94
|
-
// "at:" only when
|
|
95
|
-
return
|
|
96
|
-
? `✓ Live v${outcome.versionNumber}
|
|
97
|
-
: `✓ Live v${outcome.versionNumber}
|
|
97
|
+
// "at:" only when an address that line promises actually follows.
|
|
98
|
+
return hasLiveAddress(addresses)
|
|
99
|
+
? `✓ Live v${outcome.versionNumber}, at:`
|
|
100
|
+
: `✓ Live v${outcome.versionNumber}`;
|
|
98
101
|
}
|
|
99
102
|
return `✓ Ready v${outcome.versionNumber} is uploaded but not live — what visitors see is unchanged`;
|
|
100
103
|
}
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// re-uploads — the switch is a pointer change on Driggsby's side. With --to N
|
|
3
3
|
// it acts directly; without it, it shows the choices and asks (or, when not
|
|
4
4
|
// attached to a person, explains how to re-run with --to).
|
|
5
|
-
import {
|
|
5
|
+
import { hasLiveAddress, liveAddresses, liveAddressLines, readProjectConfig, setLiveVersion, } from "@driggsby/deploy";
|
|
6
6
|
import { apiBaseUrl } from "../api/base-url.js";
|
|
7
7
|
import { CliError } from "../cli-error.js";
|
|
8
8
|
import { defaultCredentialEnvironment, } from "../credentials/store.js";
|
|
9
|
-
import {
|
|
10
|
-
import { deployFailure,
|
|
9
|
+
import { wrapProse } from "../terminal-text.js";
|
|
10
|
+
import { deployFailure, requireDeploySession, } from "./api-session.js";
|
|
11
11
|
import { fetchVersionList, renderVersionRows } from "./versions-command.js";
|
|
12
12
|
const ROLLBACK_RETRY_COMMAND = "npx driggsby@latest rollback";
|
|
13
13
|
function defaultRollbackIo() {
|
|
@@ -56,13 +56,11 @@ export async function runRollback(options, environment = defaultCredentialEnviro
|
|
|
56
56
|
const previousLive = list.liveVersionNumber;
|
|
57
57
|
const result = await setLiveVersion(api, config.slug, target);
|
|
58
58
|
const was = previousLive === null ? "" : ` (was v${previousLive})`;
|
|
59
|
-
// "at:" only when
|
|
60
|
-
|
|
61
|
-
const atSuffix =
|
|
59
|
+
// "at:" only when an address that line promises actually follows.
|
|
60
|
+
const addresses = liveAddresses(result.url, result.consoleUrl, session.baseUrl);
|
|
61
|
+
const atSuffix = hasLiveAddress(addresses) ? ", at:" : "";
|
|
62
62
|
io.out(`✓ Live v${result.versionNumber} is what visitors see now${was}${atSuffix}\n`);
|
|
63
|
-
|
|
64
|
-
io.out(`\n ${capForTerminal(result.url, MAX_SERVER_URL_CHARS)}\n`);
|
|
65
|
-
}
|
|
63
|
+
io.out(liveAddressLines(addresses));
|
|
66
64
|
io.out(`\n${wrapProse(`Nothing re-uploaded — Driggsby already had v${result.versionNumber} in full.`)}\n` +
|
|
67
65
|
"\nNext:\n See every version with npx driggsby@latest versions\n");
|
|
68
66
|
return 0;
|
package/dist/dev/dev-command.js
CHANGED
|
@@ -13,12 +13,16 @@ import { defaultCredentialEnvironment, } from "../credentials/store.js";
|
|
|
13
13
|
import { requireDeploySession } from "../deploy/api-session.js";
|
|
14
14
|
import { tryOpenUrl } from "../login/open-url.js";
|
|
15
15
|
import { wrapProse } from "../terminal-text.js";
|
|
16
|
+
import { DEV_IDLE_MINUTES, DEV_START_COMMAND, DEV_STOP_COMMAND } from "./dev-commands.js";
|
|
16
17
|
import { startDevServers } from "./dev-servers.js";
|
|
18
|
+
import { readLiveDevState, removeDevState, writeDevState } from "./dev-state.js";
|
|
19
|
+
import { displayFolder } from "./display-folder.js";
|
|
17
20
|
import { McpBroker } from "./mcp-broker.js";
|
|
18
21
|
import { watchDirectory } from "./watcher.js";
|
|
19
22
|
export const DEV_HOST_PORT = 4111;
|
|
20
23
|
export const DEV_APP_PORT = 4112;
|
|
21
|
-
const
|
|
24
|
+
const DEV_IDLE_TIMEOUT_MS = DEV_IDLE_MINUTES * 60 * 1000;
|
|
25
|
+
const DEV_IDLE_CHECK_MS = 15 * 1000;
|
|
22
26
|
function defaultDevIo() {
|
|
23
27
|
return {
|
|
24
28
|
out: (text) => {
|
|
@@ -33,6 +37,13 @@ function defaultDevIo() {
|
|
|
33
37
|
process.once("SIGTERM", () => {
|
|
34
38
|
resolve();
|
|
35
39
|
});
|
|
40
|
+
// A closed terminal window: still a clean stop, so the record and
|
|
41
|
+
// the ports are released like any other exit. (On Windows, a signal
|
|
42
|
+
// from `dev --stop` ends the process outright instead; `dev --stop`
|
|
43
|
+
// then removes the record itself once the pid is gone.)
|
|
44
|
+
process.once("SIGHUP", () => {
|
|
45
|
+
resolve();
|
|
46
|
+
});
|
|
36
47
|
});
|
|
37
48
|
},
|
|
38
49
|
};
|
|
@@ -61,25 +72,100 @@ export async function runDev(options = {}, environment = defaultCredentialEnviro
|
|
|
61
72
|
});
|
|
62
73
|
}
|
|
63
74
|
catch (error) {
|
|
64
|
-
throw devStartFailure(error, baseUrl);
|
|
75
|
+
throw await devStartFailure(error, baseUrl, environment.homeDirectory);
|
|
65
76
|
}
|
|
66
77
|
const stopWatching = watchDirectory(config.serveDirectory, () => {
|
|
67
78
|
servers.notifyChange();
|
|
68
79
|
});
|
|
80
|
+
const idleTimeoutMs = options.idleTimeoutMs ?? DEV_IDLE_TIMEOUT_MS;
|
|
81
|
+
const idle = idleWatch(servers, idleTimeoutMs, options.idleCheckMs ?? DEV_IDLE_CHECK_MS);
|
|
69
82
|
try {
|
|
83
|
+
// The record is what `dev --stop` finds; a home that can't be written
|
|
84
|
+
// costs that convenience, not the preview.
|
|
85
|
+
const recorded = await tryWriteDevState(environment.homeDirectory, projectDirectory, servers);
|
|
70
86
|
const opened = await io.openUrl(servers.hostOrigin);
|
|
71
|
-
io.out(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
io.out(readyText(config.slug, servers, opened, recorded, idleTimeoutMs));
|
|
88
|
+
const ending = await Promise.race([io.waitForShutdown().then(() => "shutdown"), idle.expired]);
|
|
89
|
+
if (ending === "idle") {
|
|
90
|
+
io.out(`✓ Stopped No page was open for ${idleWindowWords(idleTimeoutMs)}, so ` +
|
|
91
|
+
`driggsby dev stopped itself.\n\nStart it again with:\n ${DEV_START_COMMAND}\n`);
|
|
92
|
+
}
|
|
76
93
|
return 0;
|
|
77
94
|
}
|
|
78
95
|
finally {
|
|
96
|
+
idle.cancel();
|
|
79
97
|
stopWatching();
|
|
98
|
+
// The record goes before the ports are freed, so a dev starting the
|
|
99
|
+
// instant these close never finds a record this one is about to remove.
|
|
100
|
+
// (A `dev --stop` waiting on this exit polls the pid for five seconds,
|
|
101
|
+
// and closing takes well under one.) The record is a convenience: a
|
|
102
|
+
// failure to remove it must never keep the ports open.
|
|
103
|
+
try {
|
|
104
|
+
await removeDevState(environment.homeDirectory, process.pid);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Left behind; the next read finds a dead pid and removes it.
|
|
108
|
+
}
|
|
80
109
|
await servers.close();
|
|
81
110
|
}
|
|
82
111
|
}
|
|
112
|
+
async function tryWriteDevState(homeDirectory, folder, servers) {
|
|
113
|
+
try {
|
|
114
|
+
await writeDevState(homeDirectory, {
|
|
115
|
+
pid: process.pid,
|
|
116
|
+
folder,
|
|
117
|
+
startedAt: new Date().toISOString(),
|
|
118
|
+
hostPort: servers.hostPort,
|
|
119
|
+
appPort: servers.appPort,
|
|
120
|
+
});
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function readyText(slug, servers, opened, recorded, idleTimeoutMs) {
|
|
128
|
+
const stopLine = recorded
|
|
129
|
+
? `${wrapProse("Edit the app's files and the page reloads on save. Press Ctrl+C to stop, or from any terminal:")}\n ${DEV_STOP_COMMAND}\n`
|
|
130
|
+
: `${wrapProse("Edit the app's files and the page reloads on save. Press Ctrl+C to stop.")}\n`;
|
|
131
|
+
return (`✓ Ready ${slug} is running with your live Driggsby data${opened ? ", at:" : ":"}\n\n` +
|
|
132
|
+
` ${servers.hostOrigin}\n\n` +
|
|
133
|
+
`${wrapProse(`That page embeds the app from ${servers.appOrigin}. Opened on its own, the app gets no Driggsby data, so use the address above.`)}\n\n` +
|
|
134
|
+
`${stopLine}\n` +
|
|
135
|
+
`${wrapProse(`It stops on its own after ${idleWindowWords(idleTimeoutMs)} with no page open.`)}\n` +
|
|
136
|
+
"\nNext:\n Put it live on driggsby.dev with npx driggsby@latest deploy\n");
|
|
137
|
+
}
|
|
138
|
+
export function idleWindowWords(timeoutMs) {
|
|
139
|
+
const minutes = Math.round(timeoutMs / 60_000);
|
|
140
|
+
if (minutes < 1) {
|
|
141
|
+
return "a moment";
|
|
142
|
+
}
|
|
143
|
+
return minutes === 1 ? "1 minute" : `${String(minutes)} minutes`;
|
|
144
|
+
}
|
|
145
|
+
function idleWatch(servers, timeoutMs, checkMs) {
|
|
146
|
+
let resolveExpired = () => undefined;
|
|
147
|
+
const expired = new Promise((resolve) => {
|
|
148
|
+
resolveExpired = resolve;
|
|
149
|
+
});
|
|
150
|
+
let idleSince = Date.now();
|
|
151
|
+
const timer = setInterval(() => {
|
|
152
|
+
if (servers.clientCount() > 0) {
|
|
153
|
+
idleSince = Date.now();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (Date.now() - idleSince >= timeoutMs) {
|
|
157
|
+
resolveExpired("idle");
|
|
158
|
+
}
|
|
159
|
+
}, checkMs);
|
|
160
|
+
// A pending check must never hold the process open once the run ends.
|
|
161
|
+
timer.unref();
|
|
162
|
+
return {
|
|
163
|
+
expired,
|
|
164
|
+
cancel: () => {
|
|
165
|
+
clearInterval(timer);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
83
169
|
// The Driggsby SDK bundle ships inside @driggsby/sdk; dev serves it on the
|
|
84
170
|
// app origin at the same path production serves it, so the scaffold's
|
|
85
171
|
// script tag works identically in both places.
|
|
@@ -88,20 +174,26 @@ async function loadSdkBundle() {
|
|
|
88
174
|
const bundlePath = require.resolve("@driggsby/sdk/driggsby-sdk.js");
|
|
89
175
|
return await readFile(bundlePath, "utf8");
|
|
90
176
|
}
|
|
91
|
-
function devStartFailure(error, baseUrl) {
|
|
177
|
+
async function devStartFailure(error, baseUrl, homeDirectory) {
|
|
92
178
|
if (error instanceof CliError) {
|
|
93
179
|
return error;
|
|
94
180
|
}
|
|
95
181
|
if (error.code === "EADDRINUSE") {
|
|
96
|
-
|
|
97
|
-
|
|
182
|
+
const running = await readLiveDevState(homeDirectory);
|
|
183
|
+
if (running !== null) {
|
|
184
|
+
return new CliError(`driggsby dev is already running for the app in:\n ${displayFolder(running.folder)}\n\n` +
|
|
185
|
+
`Only one can run at a time. Stop it first:\n ${DEV_STOP_COMMAND}\n\n` +
|
|
186
|
+
`Then try again here:\n ${DEV_START_COMMAND}`, 1);
|
|
187
|
+
}
|
|
188
|
+
return new CliError(`${wrapProse(`Ports ${String(DEV_HOST_PORT)} and ${String(DEV_APP_PORT)} are how driggsby dev serves the preview, and something on this machine is already using one of them. Stop that program and try again:`)}\n` +
|
|
189
|
+
` ${DEV_START_COMMAND}`, 1);
|
|
98
190
|
}
|
|
99
191
|
const blocked = blockedNetworkError(error, {
|
|
100
192
|
host: apiHost(baseUrl),
|
|
101
|
-
retryCommand:
|
|
193
|
+
retryCommand: DEV_START_COMMAND,
|
|
102
194
|
});
|
|
103
195
|
if (blocked !== null) {
|
|
104
196
|
return blocked;
|
|
105
197
|
}
|
|
106
|
-
return new CliError(`We weren't able to start the preview just now. Please try again:\n ${
|
|
198
|
+
return new CliError(`We weren't able to start the preview just now. Please try again:\n ${DEV_START_COMMAND}`, 1);
|
|
107
199
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The commands `driggsby dev` names in its own output and help, and the
|
|
2
|
+
// idle window it enforces, in one place so no two surfaces can disagree.
|
|
3
|
+
export const DEV_START_COMMAND = "npx driggsby@latest dev";
|
|
4
|
+
export const DEV_STOP_COMMAND = "npx driggsby@latest dev --stop";
|
|
5
|
+
// A preview nobody has open for this long stops itself, so a dev started in
|
|
6
|
+
// the background (often by an agent) does not keep serving live financial
|
|
7
|
+
// data on this machine indefinitely.
|
|
8
|
+
// Spelled out as prose in README.md and packages/driggsby/README.md too;
|
|
9
|
+
// change those with this.
|
|
10
|
+
export const DEV_IDLE_MINUTES = 30;
|
package/dist/dev/dev-servers.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// only. The app token never appears here — the broker callback holds it in
|
|
6
6
|
// the CLI process.
|
|
7
7
|
import { createServer } from "node:http";
|
|
8
|
+
import { DEV_IDENTITY_PATH } from "./dev-state.js";
|
|
8
9
|
import { hostPageHtml, HOST_PAGE_JS } from "./host-page.js";
|
|
9
10
|
import { serveStaticFile } from "./static-files.js";
|
|
10
11
|
import { APP_TOOL_ALLOWLIST } from "./tool-allowlist.js";
|
|
@@ -55,6 +56,7 @@ export async function startDevServers(options) {
|
|
|
55
56
|
client.write("event: change\ndata: files-changed\n\n");
|
|
56
57
|
}
|
|
57
58
|
},
|
|
59
|
+
clientCount: () => sseClients.size,
|
|
58
60
|
close: async () => {
|
|
59
61
|
for (const client of sseClients) {
|
|
60
62
|
client.end();
|
|
@@ -116,6 +118,12 @@ async function handleHostRequest(options, hostPort, appOrigin, sseClients, reque
|
|
|
116
118
|
return;
|
|
117
119
|
}
|
|
118
120
|
const path = requestPath(request);
|
|
121
|
+
if (request.method === "GET" && path === DEV_IDENTITY_PATH) {
|
|
122
|
+
// Lets `dev --stop` (and a colliding `dev`) confirm that the process in
|
|
123
|
+
// ~/.driggsby/dev.json is the one actually serving this port.
|
|
124
|
+
sendJson(response, 200, { pid: process.pid });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
119
127
|
if (request.method === "GET" && path === "/") {
|
|
120
128
|
sendHostPage(response, hostPageHtml(options.slug, appOrigin, options.background), appOrigin);
|
|
121
129
|
return;
|