driggsby 0.1.41 → 0.2.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.
Files changed (50) hide show
  1. package/README.md +59 -0
  2. package/bin/driggsby.js +2 -0
  3. package/dist/api/base-url.js +31 -0
  4. package/dist/api/network-error.js +55 -0
  5. package/dist/args-deploy.js +94 -0
  6. package/dist/args-init.js +29 -0
  7. package/dist/args-mcp-setup.js +258 -0
  8. package/dist/args-shared.js +28 -0
  9. package/dist/args.js +177 -0
  10. package/dist/clap-suggestions.js +66 -0
  11. package/dist/cli-error.js +12 -0
  12. package/dist/credentials/clear-outcome.js +1 -0
  13. package/dist/credentials/file-store.js +77 -0
  14. package/dist/credentials/keychain.js +54 -0
  15. package/dist/credentials/secret-tool.js +48 -0
  16. package/dist/credentials/store.js +164 -0
  17. package/dist/credentials/tool-runner.js +55 -0
  18. package/dist/deploy/api-session.js +85 -0
  19. package/dist/deploy/deploy-command.js +126 -0
  20. package/dist/deploy/rollback-command.js +100 -0
  21. package/dist/deploy/versions-command.js +97 -0
  22. package/dist/dev/dev-command.js +106 -0
  23. package/dist/dev/dev-servers.js +274 -0
  24. package/dist/dev/host-page.js +147 -0
  25. package/dist/dev/mcp-broker.js +149 -0
  26. package/dist/dev/static-files.js +104 -0
  27. package/dist/dev/tool-allowlist.js +18 -0
  28. package/dist/dev/watcher.js +76 -0
  29. package/dist/help.js +180 -0
  30. package/dist/init/init-command.js +114 -0
  31. package/dist/init/templates.js +271 -0
  32. package/dist/login/claim-client.js +151 -0
  33. package/dist/login/login.js +146 -0
  34. package/dist/login/logout.js +37 -0
  35. package/dist/login/open-url.js +79 -0
  36. package/dist/main.js +57 -0
  37. package/dist/mcp-setup/classify.js +37 -0
  38. package/dist/mcp-setup/client-command.js +133 -0
  39. package/dist/mcp-setup/commands.js +53 -0
  40. package/dist/mcp-setup/known-client.js +38 -0
  41. package/dist/mcp-setup/setup.js +137 -0
  42. package/dist/mcp-setup/spawn-plan.js +91 -0
  43. package/dist/mcp-setup/text.js +83 -0
  44. package/dist/terminal-text.js +19 -0
  45. package/dist/version.js +6 -0
  46. package/package.json +22 -41
  47. package/bin/driggsby +0 -10
  48. package/install.js +0 -159
  49. package/lib/artifacts.js +0 -49
  50. package/lib/process.js +0 -65
@@ -0,0 +1,18 @@
1
+ // The read-only tools a Driggsby app may call. This mirrors the allowlist
2
+ // Driggsby enforces server-side for embedded apps; the dev host enforces it
3
+ // too so an app that works locally works identically after deploy, and a
4
+ // disallowed call never even reaches Driggsby.
5
+ export const APP_TOOL_ALLOWLIST = new Set([
6
+ "get_history",
7
+ "get_overview",
8
+ "list_accounts",
9
+ "list_assets_and_liabilities",
10
+ "list_findings",
11
+ "list_investment_holdings",
12
+ "list_outstanding_debts",
13
+ "list_recurring_transactions",
14
+ "query_cash_sql",
15
+ "query_investment_sql",
16
+ "search_cash_transactions",
17
+ "search_investment_activity",
18
+ ]);
@@ -0,0 +1,76 @@
1
+ // Change detection for the serve folder. `driggsby dev` tells the embedded
2
+ // app to reload when a served file changes. Polling a directory signature —
3
+ // names, sizes, mtimes — is deliberately boring: it behaves identically on
4
+ // macOS, Linux, and Windows (recursive fs.watch does not), and a scaffolded
5
+ // static app is a handful of files, so a walk every second is nothing.
6
+ import { readdir, stat } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ export const WATCH_POLL_MS = 1_000;
9
+ // One stable string describing the current served files; any edit, add,
10
+ // delete, or rename changes it. Skips what dev never serves (dotfiles,
11
+ // node_modules), so churn in an ignored folder can't cause reloads.
12
+ export async function directorySignature(serveDirectory) {
13
+ const parts = [];
14
+ await collectSignature(serveDirectory, "", parts);
15
+ return parts.join("\n");
16
+ }
17
+ async function collectSignature(directory, prefix, parts) {
18
+ let entries;
19
+ try {
20
+ entries = await readdir(directory, { withFileTypes: true });
21
+ }
22
+ catch {
23
+ parts.push(`${prefix}<unreadable>`);
24
+ return;
25
+ }
26
+ entries.sort((a, b) => (a.name < b.name ? -1 : 1));
27
+ for (const entry of entries) {
28
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
29
+ continue;
30
+ }
31
+ const entryPath = join(directory, entry.name);
32
+ const entryKey = `${prefix}${entry.name}`;
33
+ if (entry.isDirectory()) {
34
+ await collectSignature(entryPath, `${entryKey}/`, parts);
35
+ continue;
36
+ }
37
+ if (!entry.isFile()) {
38
+ continue;
39
+ }
40
+ try {
41
+ const stats = await stat(entryPath);
42
+ parts.push(`${entryKey}:${String(stats.size)}:${String(stats.mtimeMs)}`);
43
+ }
44
+ catch {
45
+ parts.push(`${entryKey}:<gone>`);
46
+ }
47
+ }
48
+ }
49
+ // Polls until stopped; calls onChange once per observed signature change.
50
+ export function watchDirectory(serveDirectory, onChange, pollMs = WATCH_POLL_MS) {
51
+ let stopped = false;
52
+ let lastSignature = null;
53
+ let timer = null;
54
+ const poll = async () => {
55
+ const signature = await directorySignature(serveDirectory);
56
+ if (stopped) {
57
+ return;
58
+ }
59
+ if (lastSignature !== null && signature !== lastSignature) {
60
+ onChange();
61
+ }
62
+ lastSignature = signature;
63
+ timer = setTimeout(() => {
64
+ void poll();
65
+ }, pollMs);
66
+ // Never keep the process alive just to poll.
67
+ timer.unref();
68
+ };
69
+ void poll();
70
+ return () => {
71
+ stopped = true;
72
+ if (timer !== null) {
73
+ clearTimeout(timer);
74
+ }
75
+ };
76
+ }
package/dist/help.js ADDED
@@ -0,0 +1,180 @@
1
+ // Help text is contract, asserted byte-for-byte against the fixtures in
2
+ // __fixtures__/. The mcp and mcp setup blocks are still byte-identical to
3
+ // the Rust CLI's output (captured from driggsby 0.1.42), including the clap
4
+ // quirks (the trailing spaces on the blank line before [possible values] in
5
+ // the long setup help, and the distinct short/long setup variants). The root
6
+ // help grew past the Rust CLI when login/logout landed; its fixture pins the
7
+ // current text.
8
+ export const ROOT_HELP = `Usage: npx driggsby@latest <COMMAND>
9
+
10
+ Commands:
11
+ mcp Set up Driggsby MCP clients.
12
+ login Sign in to Driggsby and save an app token on this machine.
13
+ logout Remove the saved Driggsby app token.
14
+ init Create a new Driggsby app in a new folder.
15
+ dev Preview the app in this directory with your live Driggsby data.
16
+ deploy Deploy the app in this directory to Driggsby.
17
+ rollback Switch which deployed version of your app is live.
18
+ versions List your deployed app's versions.
19
+
20
+ Options:
21
+ -h, --help Print help
22
+ -V, --version Print version
23
+
24
+ Examples:
25
+ npx driggsby@latest mcp setup
26
+ npx driggsby@latest mcp setup claude-code
27
+ npx driggsby@latest mcp setup codex
28
+ npx driggsby@latest mcp setup other
29
+ npx driggsby@latest login
30
+ npx driggsby@latest init money-dash
31
+ npx driggsby@latest dev
32
+ npx driggsby@latest deploy
33
+ `;
34
+ export const INIT_HELP = `Create a new Driggsby app in a new folder.
35
+
36
+ This makes a small static app — index.html, app.js, styles.css — plus the
37
+ driggsby.json that names it. The app shows sample data on its own, and your
38
+ real Driggsby data when it runs inside Driggsby.
39
+
40
+ Usage: npx driggsby@latest init [NAME]
41
+
42
+ Arguments:
43
+ [NAME] The app's name: 3-63 lowercase letters, digits, and dashes.
44
+ Any readable name works — the first deploy assigns the app's
45
+ address by adding a unique ending, like
46
+ your-name-x7k2qf.driggsby.dev — so names never collide.
47
+ Asked for when omitted.
48
+
49
+ Options:
50
+ -h, --help Print help
51
+ `;
52
+ export const LOGIN_HELP = `Sign in to Driggsby and save an app token on this machine.
53
+
54
+ This opens a Driggsby approval page in your browser. Approving it issues an
55
+ app token that lets this machine read your Driggsby data and deploy Driggsby
56
+ apps. The token is saved locally for other driggsby commands and is never
57
+ printed.
58
+
59
+ Usage: npx driggsby@latest login
60
+
61
+ Options:
62
+ -h, --help Print help
63
+ `;
64
+ export const LOGOUT_HELP = `Remove the saved Driggsby app token.
65
+
66
+ This removes the app token that npx driggsby@latest login saved on this
67
+ machine. It does not sign you out of Driggsby in your browser or in your AI
68
+ clients.
69
+
70
+ Usage: npx driggsby@latest logout
71
+
72
+ Options:
73
+ -h, --help Print help
74
+ `;
75
+ export const DEV_HELP = `Preview the app in this directory with your live Driggsby data.
76
+
77
+ This runs the app locally, embedded the same way Driggsby embeds it after a
78
+ deploy, so what you see here is what a deploy will show. Edits to the app's
79
+ files reload the page on save.
80
+
81
+ While dev runs, the preview serves your financial data to this machine —
82
+ other websites can't reach it, but anything running locally can, so stop
83
+ it when you're done.
84
+
85
+ Sign in first with npx driggsby@latest login.
86
+
87
+ Usage: npx driggsby@latest dev
88
+
89
+ Options:
90
+ -h, --help Print help
91
+ `;
92
+ export const DEPLOY_HELP = `Deploy the app in this directory to Driggsby.
93
+
94
+ This reads driggsby.json next to your app files, uploads only what changed,
95
+ and makes the new version live at your app's driggsby.dev address. The
96
+ first deploy creates the app: Driggsby assigns its address (your name plus
97
+ a unique ending) and saves it back into driggsby.json. With --preview, the
98
+ version is uploaded and kept ready without changing what's live.
99
+
100
+ Sign in first with npx driggsby@latest login.
101
+
102
+ Usage: npx driggsby@latest deploy [--preview]
103
+
104
+ Options:
105
+ --preview Upload this version without making it live.
106
+ -h, --help Print help
107
+ `;
108
+ export const ROLLBACK_HELP = `Switch which deployed version of your app is live.
109
+
110
+ Driggsby keeps your app's recent versions, so going live with any of them —
111
+ older or newer — re-uploads nothing. Without --to, this lists the versions
112
+ you can choose from and asks for one.
113
+
114
+ Usage: npx driggsby@latest rollback [--to <VERSION>]
115
+
116
+ Options:
117
+ --to <VERSION> The version number to make live, as shown by
118
+ npx driggsby@latest versions.
119
+ -h, --help Print help
120
+ `;
121
+ export const VERSIONS_HELP = `List your deployed app's versions.
122
+
123
+ Shows every version Driggsby has kept for the app in this directory, which
124
+ one is live, and the version numbers npx driggsby@latest rollback accepts.
125
+
126
+ Usage: npx driggsby@latest versions
127
+
128
+ Options:
129
+ -h, --help Print help
130
+ `;
131
+ export const MCP_HELP = `Set up Driggsby MCP clients.
132
+
133
+ Usage: npx driggsby@latest mcp <COMMAND>
134
+
135
+ Commands:
136
+ setup Set up Driggsby for an AI client.
137
+
138
+ Options:
139
+ -h, --help Print help
140
+ `;
141
+ // mcp setup -h: clap's compact table.
142
+ export const MCP_SETUP_HELP_SHORT = `Set up Driggsby for an AI client.
143
+
144
+ Usage: npx driggsby@latest mcp setup [OPTIONS] [CLIENT]
145
+
146
+ Arguments:
147
+ [CLIENT] Client ID: claude-code, codex, or other.
148
+
149
+ Options:
150
+ --print Print the native setup command instead of running it.
151
+ -s <MCP_SCOPE> Claude Code only. Values: local, user (default). [possible values: local, user]
152
+ -h, --help Print help (see more with '--help')
153
+ `;
154
+ // mcp setup --help: clap's long form. The trailing spaces after the
155
+ // "Values: local, user (default)." line are clap's own rendering.
156
+ export const MCP_SETUP_HELP_LONG = `Set up Driggsby for an AI client.
157
+
158
+ Run once per client. This adds Driggsby's MCP URL to supported native client configs. Choose other
159
+ to print OAuth-based remote MCP setup instructions.
160
+
161
+ Supported clients: claude-code, codex, other.
162
+
163
+ Usage: npx driggsby@latest mcp setup [OPTIONS] [CLIENT]
164
+
165
+ Arguments:
166
+ [CLIENT]
167
+ Client ID: claude-code, codex, or other.
168
+
169
+ Options:
170
+ --print
171
+ Print the native setup command instead of running it.
172
+
173
+ -s <MCP_SCOPE>
174
+ Claude Code only. Values: local, user (default).
175
+ ${" "}
176
+ [possible values: local, user]
177
+
178
+ -h, --help
179
+ Print help (see a summary with '-h')
180
+ `;
@@ -0,0 +1,114 @@
1
+ // driggsby init: scaffold a new Driggsby app in a new folder. The result is
2
+ // a no-build static app (index.html + app.js + styles.css) that renders
3
+ // sample data on its own and live data inside Driggsby, plus the
4
+ // driggsby.json that names it. With a slug argument it acts directly;
5
+ // without one, it asks (a person) or explains the exact re-run (an agent or
6
+ // script).
7
+ import { mkdir, readdir, writeFile } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import { slugProblem } from "@driggsby/deploy";
10
+ import { CliError } from "../cli-error.js";
11
+ import { quotedForTerminal, wrapProse } from "../terminal-text.js";
12
+ import { APP_JS, STYLES_CSS, driggsbyJsonText, indexHtml } from "./templates.js";
13
+ // Argv and prompt answers echo back inside quotes with this cap; a slug can
14
+ // never be longer than 63 characters, so the cap only trims hostile input.
15
+ const MAX_SLUG_ECHO_CHARS = 80;
16
+ const INIT_RETRY_LINES = "Pick another name and re-run:\n npx driggsby@latest init <NAME>";
17
+ function defaultInitIo() {
18
+ return {
19
+ out: (text) => {
20
+ process.stdout.write(text);
21
+ },
22
+ ask: async (question) => {
23
+ const { createInterface } = await import("node:readline/promises");
24
+ const readline = createInterface({ input: process.stdin, output: process.stdout });
25
+ try {
26
+ return await readline.question(question);
27
+ }
28
+ finally {
29
+ readline.close();
30
+ }
31
+ },
32
+ interactive: process.stdin.isTTY && process.stdout.isTTY,
33
+ };
34
+ }
35
+ export async function runInit(options, io = defaultInitIo()) {
36
+ const parentDirectory = options.parentDirectory ?? process.cwd();
37
+ const slug = options.slug ?? (await askForSlug(io));
38
+ const problem = slugProblem(slug);
39
+ if (problem !== null) {
40
+ // slugProblem's wording spans lines; re-flow it as one paragraph. A bad
41
+ // argv slug is a usage error (exit 2); a bad prompt answer is the same
42
+ // conversational exit 1 the other interactive commands use.
43
+ const flatProblem = problem.replaceAll("\n", " ");
44
+ throw new CliError(`${wrapProse(`${quotedForTerminal(slug, MAX_SLUG_ECHO_CHARS)} won't work as an app name: ${flatProblem}.`)}\n\n${INIT_RETRY_LINES}`, options.slug === null ? 1 : 2);
45
+ }
46
+ // The slug passed the strict pattern above, so from here it is safe to
47
+ // print bare and to interpolate into the scaffolded index.html.
48
+ const appDirectory = join(parentDirectory, slug);
49
+ await ensureNewOrEmptyDirectory(appDirectory, slug);
50
+ await writeScaffold(appDirectory, slug);
51
+ io.out(`✓ Created ${slug}/\n\n` +
52
+ " index.html the page\n" +
53
+ " app.js fetches and renders your data — start editing here\n" +
54
+ " styles.css the look\n" +
55
+ " driggsby.json the app's name and which folder deploys\n\n" +
56
+ `${wrapProse("The app shows sample data on its own; dev and deploy both give it your real accounts.")}\n\n` +
57
+ "Next:\n" +
58
+ ` cd ${slug}\n` +
59
+ " npx driggsby@latest dev preview with your real data\n" +
60
+ " npx driggsby@latest deploy put it live on driggsby.dev\n");
61
+ return 0;
62
+ }
63
+ async function askForSlug(io) {
64
+ if (!io.interactive) {
65
+ throw new CliError("This terminal can't prompt for a name, so pass one directly:\n" +
66
+ " npx driggsby@latest init <NAME>\n\n" +
67
+ wrapProse("Any readable name works (3-63 lowercase letters, digits, and dashes) — Driggsby adds a unique ending at first deploy (<NAME>-x7k2qf.driggsby.dev), so names never collide."), 2);
68
+ }
69
+ io.out("The name starts your app's address — a unique ending is added at first\n" +
70
+ "deploy: <NAME>-x7k2qf.driggsby.dev\n\n");
71
+ const answer = await io.ask("Name your app (like money-dash): ");
72
+ return answer.trim();
73
+ }
74
+ // The scaffold only ever writes into a folder it created or one that exists
75
+ // empty — never into files someone already has.
76
+ async function ensureNewOrEmptyDirectory(appDirectory, slug) {
77
+ try {
78
+ await mkdir(appDirectory);
79
+ return;
80
+ }
81
+ catch (error) {
82
+ if (error.code !== "EEXIST") {
83
+ throw new CliError(wrapProse(`We couldn't create a ${slug} folder here. Check that this directory is writable and try again.`), 1);
84
+ }
85
+ }
86
+ let entries;
87
+ try {
88
+ entries = await readdir(appDirectory);
89
+ }
90
+ catch {
91
+ // The existing path isn't a readable directory (it's a file, or
92
+ // unreadable) — either way it isn't a safe scaffold target.
93
+ throw new CliError(existsMessage(slug), 1);
94
+ }
95
+ if (entries.length > 0) {
96
+ throw new CliError(existsMessage(slug), 1);
97
+ }
98
+ }
99
+ function existsMessage(slug) {
100
+ return `${wrapProse(`Something named ${slug} already exists here and isn't an empty folder. Pick another name, or move it first.`)}\n\n${INIT_RETRY_LINES}`;
101
+ }
102
+ async function writeScaffold(appDirectory, slug) {
103
+ // "wx" refuses to overwrite: even a file that appears between the
104
+ // emptiness check and this write stays untouched.
105
+ const writes = [
106
+ ["index.html", indexHtml(slug)],
107
+ ["app.js", APP_JS],
108
+ ["styles.css", STYLES_CSS],
109
+ ["driggsby.json", driggsbyJsonText(slug)],
110
+ ];
111
+ for (const [name, content] of writes) {
112
+ await writeFile(join(appDirectory, name), content, { flag: "wx" });
113
+ }
114
+ }
@@ -0,0 +1,271 @@
1
+ // The files `driggsby init` scaffolds: a no-build static app plus the
2
+ // driggsby.json that names it. The app renders obviously-synthetic sample
3
+ // data on its own, and live Driggsby data once a Driggsby host embeds it
4
+ // (`driggsby dev` locally, or the Driggsby console after a deploy). The
5
+ // sample values below are invented for the template and match the real
6
+ // tool result shapes only in structure, so the scaffold's render functions
7
+ // work unchanged when live data replaces them.
8
+ export function indexHtml(slug) {
9
+ return `<!doctype html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="utf-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1">
14
+ <title>${slug}</title>
15
+ <link rel="stylesheet" href="styles.css">
16
+ </head>
17
+ <body>
18
+ <main>
19
+ <header>
20
+ <h1>${slug}</h1>
21
+ <p class="sample-note" id="sample-note">Sample data — your real accounts
22
+ appear when this app runs inside Driggsby.</p>
23
+ </header>
24
+ <section aria-label="Overview">
25
+ <div class="stat-grid" id="overview"></div>
26
+ </section>
27
+ <section aria-label="Accounts">
28
+ <h2>Accounts</h2>
29
+ <div id="accounts"></div>
30
+ </section>
31
+ </main>
32
+ <script type="module" src="/-/driggsby-sdk.js"></script>
33
+ <script type="module" src="app.js"></script>
34
+ </body>
35
+ </html>
36
+ `;
37
+ }
38
+ export const APP_JS = `// Your app's code. Edit anything — driggsby dev reloads the page on save.
39
+ //
40
+ // Reading data is one call:
41
+ //
42
+ // driggsby.watch(tool, params, callback)
43
+ //
44
+ // Each watch is a live subscription: the callback runs with a fresh result
45
+ // whenever your Driggsby data changes. The sample data below matches the
46
+ // real result shapes, so the render functions work unchanged either way.
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Sample data. Every name and number here is invented. It renders only
50
+ // until real data arrives from Driggsby.
51
+ // ---------------------------------------------------------------------------
52
+
53
+ const SAMPLE_OVERVIEW = {
54
+ summary_rollups: {
55
+ net_worth_estimate: { amount: "52340.00", currency_code: "USD" },
56
+ cash: { amount: "8120.00", currency_code: "USD" },
57
+ total_assets: { amount: "61540.00", currency_code: "USD" },
58
+ total_liabilities: { amount: "9200.00", currency_code: "USD" }
59
+ }
60
+ };
61
+
62
+ const SAMPLE_ACCOUNTS = {
63
+ linked_accounts: [
64
+ {
65
+ institution_name: "Sample Bank",
66
+ account_display_name: "Everyday Checking",
67
+ account_mask_last4: "1111",
68
+ current_balance: { amount: "5120.00", currency_code: "USD" }
69
+ },
70
+ {
71
+ institution_name: "Sample Bank",
72
+ account_display_name: "Rainy Day Savings",
73
+ account_mask_last4: "2222",
74
+ current_balance: { amount: "3000.00", currency_code: "USD" }
75
+ },
76
+ {
77
+ institution_name: "Sample Card",
78
+ account_display_name: "Travel Card",
79
+ account_mask_last4: "1212",
80
+ current_balance: { amount: "9200.00", currency_code: "USD" }
81
+ }
82
+ ]
83
+ };
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Rendering. Data lands via textContent, never innerHTML, so a value can
87
+ // only ever read as text.
88
+ // ---------------------------------------------------------------------------
89
+
90
+ function formatMoney(value) {
91
+ if (!value || typeof value.amount !== "string") return "—";
92
+ const number = Number(value.amount);
93
+ if (!Number.isFinite(number)) return "—";
94
+ return new Intl.NumberFormat(undefined, {
95
+ style: "currency",
96
+ currency: value.currency_code || "USD"
97
+ }).format(number);
98
+ }
99
+
100
+ function element(tag, className, text) {
101
+ const node = document.createElement(tag);
102
+ if (className) node.className = className;
103
+ if (text !== undefined) node.textContent = text;
104
+ return node;
105
+ }
106
+
107
+ function renderOverview(result) {
108
+ const rollups = (result && result.summary_rollups) || {};
109
+ const container = document.getElementById("overview");
110
+ container.replaceChildren();
111
+ const stats = [
112
+ ["Net worth", rollups.net_worth_estimate],
113
+ ["Cash", rollups.cash],
114
+ ["Assets", rollups.total_assets],
115
+ ["Liabilities", rollups.total_liabilities]
116
+ ];
117
+ for (const [label, value] of stats) {
118
+ const stat = element("div", "stat");
119
+ stat.append(
120
+ element("div", "stat-label", label),
121
+ element("div", "stat-value", formatMoney(value))
122
+ );
123
+ container.append(stat);
124
+ }
125
+ }
126
+
127
+ function renderAccounts(result) {
128
+ const accounts = (result && result.linked_accounts) || [];
129
+ const container = document.getElementById("accounts");
130
+ container.replaceChildren();
131
+ for (const account of accounts) {
132
+ const row = element("div", "account-row");
133
+ const names = element("div", "account-names");
134
+ const mask = account.account_mask_last4;
135
+ names.append(
136
+ element("div", "account-name", account.account_display_name || "Account"),
137
+ element(
138
+ "div",
139
+ "account-institution",
140
+ (account.institution_name || "") + (mask ? " ····" + mask : "")
141
+ )
142
+ );
143
+ row.append(names, element("div", "account-balance", formatMoney(account.current_balance)));
144
+ container.append(row);
145
+ }
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Live data. window.driggsby exists when the Driggsby SDK loaded — inside
150
+ // Driggsby, or under driggsby dev. Standalone, the sample data stays and
151
+ // the note above explains why.
152
+ // ---------------------------------------------------------------------------
153
+
154
+ renderOverview(SAMPLE_OVERVIEW);
155
+ renderAccounts(SAMPLE_ACCOUNTS);
156
+
157
+ let overviewIsLive = false;
158
+ let accountsAreLive = false;
159
+
160
+ function removeSampleNoteWhenAllLive() {
161
+ if (!overviewIsLive || !accountsAreLive) return;
162
+ const note = document.getElementById("sample-note");
163
+ if (note) note.remove();
164
+ }
165
+
166
+ if (window.driggsby) {
167
+ driggsby.watch("get_overview", {}, (result) => {
168
+ overviewIsLive = true;
169
+ removeSampleNoteWhenAllLive();
170
+ renderOverview(result);
171
+ });
172
+ driggsby.watch("list_accounts", {}, (result) => {
173
+ accountsAreLive = true;
174
+ removeSampleNoteWhenAllLive();
175
+ renderAccounts(result);
176
+ });
177
+ }
178
+ `;
179
+ export const STYLES_CSS = `:root {
180
+ --background: #ffffff;
181
+ --text: #1f2430;
182
+ --text-muted: #6d7280;
183
+ --hairline: #e6e8ec;
184
+ --note-background: #fdf6e3;
185
+ --note-text: #7a5b1e;
186
+ }
187
+
188
+ * {
189
+ box-sizing: border-box;
190
+ }
191
+
192
+ body {
193
+ margin: 0;
194
+ background: var(--background);
195
+ color: var(--text);
196
+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
197
+ font-size: 15px;
198
+ line-height: 1.5;
199
+ }
200
+
201
+ main {
202
+ max-width: 720px;
203
+ margin: 0 auto;
204
+ padding: 48px 24px 64px;
205
+ }
206
+
207
+ h1 {
208
+ margin: 0;
209
+ font-size: 22px;
210
+ font-weight: 600;
211
+ }
212
+
213
+ h2 {
214
+ margin: 40px 0 4px;
215
+ font-size: 15px;
216
+ font-weight: 600;
217
+ }
218
+
219
+ .sample-note {
220
+ display: inline-block;
221
+ margin: 12px 0 0;
222
+ padding: 4px 10px;
223
+ border-radius: 6px;
224
+ background: var(--note-background);
225
+ color: var(--note-text);
226
+ font-size: 13px;
227
+ }
228
+
229
+ .stat-grid {
230
+ display: grid;
231
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
232
+ gap: 20px;
233
+ margin-top: 32px;
234
+ }
235
+
236
+ .stat-label {
237
+ font-size: 13px;
238
+ color: var(--text-muted);
239
+ }
240
+
241
+ .stat-value {
242
+ font-size: 20px;
243
+ font-weight: 600;
244
+ font-variant-numeric: tabular-nums;
245
+ }
246
+
247
+ .account-row {
248
+ display: flex;
249
+ align-items: center;
250
+ justify-content: space-between;
251
+ gap: 16px;
252
+ padding: 12px 0;
253
+ border-top: 1px solid var(--hairline);
254
+ }
255
+
256
+ .account-row:first-child {
257
+ border-top: none;
258
+ }
259
+
260
+ .account-institution {
261
+ font-size: 13px;
262
+ color: var(--text-muted);
263
+ }
264
+
265
+ .account-balance {
266
+ font-variant-numeric: tabular-nums;
267
+ }
268
+ `;
269
+ export function driggsbyJsonText(slug) {
270
+ return `{\n "slug": ${JSON.stringify(slug)},\n "serve": "."\n}\n`;
271
+ }