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,248 @@
|
|
|
1
|
+
// The scaffold's styles.css. A deployed app lives inside Driggsby's dark
|
|
2
|
+
// chrome, so the page is dark by default and its values are Driggsby's
|
|
3
|
+
// own: the console's ground, card rung, hairline, ink ramp, radii, and
|
|
4
|
+
// type sizes, written out as plain colors because the app is its own
|
|
5
|
+
// origin and cannot read the console's stylesheet. --bg-app must stay
|
|
6
|
+
// equal to driggsby.json's "background" (init-command.test.ts pins the
|
|
7
|
+
// two together): Driggsby paints that color while the app loads.
|
|
8
|
+
export const STYLES_CSS = `/* Driggsby's console palette, so this page reads as part of the app it
|
|
9
|
+
opens inside. Change these together if you re-theme; keep --bg-app equal
|
|
10
|
+
to the "background" in driggsby.json. */
|
|
11
|
+
:root {
|
|
12
|
+
color-scheme: dark;
|
|
13
|
+
--bg-app: #000000;
|
|
14
|
+
--bg-card: #0a0a0a;
|
|
15
|
+
--bg-mute: #262626;
|
|
16
|
+
--hairline: rgba(255, 255, 255, 0.14);
|
|
17
|
+
--text-primary: #fafafa;
|
|
18
|
+
--text-secondary: #c4c4c4;
|
|
19
|
+
--text-tertiary: #a1a1a1;
|
|
20
|
+
--radius-card: 8px;
|
|
21
|
+
--radius-bar: 4px;
|
|
22
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
* {
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
margin: 0;
|
|
31
|
+
background: var(--bg-app);
|
|
32
|
+
color: var(--text-primary);
|
|
33
|
+
font-family: var(--font-sans);
|
|
34
|
+
font-size: 14px;
|
|
35
|
+
line-height: 21px;
|
|
36
|
+
-webkit-font-smoothing: antialiased;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
main {
|
|
40
|
+
max-width: 800px;
|
|
41
|
+
margin: 0 auto;
|
|
42
|
+
padding: 24px 24px 64px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
h1 {
|
|
46
|
+
margin: 0;
|
|
47
|
+
font-size: 24px;
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
line-height: 36px;
|
|
50
|
+
letter-spacing: -0.02em;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
h2 {
|
|
54
|
+
margin: 24px 0 12px;
|
|
55
|
+
font-size: 14px;
|
|
56
|
+
font-weight: 500;
|
|
57
|
+
line-height: 21px;
|
|
58
|
+
letter-spacing: -0.02em;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* One card, four cells; the 1px gap over the card's own hairline color
|
|
62
|
+
draws the divider between cells however the grid wraps. */
|
|
63
|
+
.stat-grid {
|
|
64
|
+
display: grid;
|
|
65
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
66
|
+
gap: 1px;
|
|
67
|
+
margin-top: 24px;
|
|
68
|
+
border: 1px solid var(--hairline);
|
|
69
|
+
border-radius: var(--radius-card);
|
|
70
|
+
background: var(--hairline);
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@media (max-width: 640px) {
|
|
75
|
+
.stat-grid {
|
|
76
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@media (max-width: 400px) {
|
|
81
|
+
.stat-grid {
|
|
82
|
+
grid-template-columns: minmax(0, 1fr);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.stat {
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
padding: 16px;
|
|
90
|
+
background: var(--bg-card);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.stat-label {
|
|
94
|
+
font-size: 13px;
|
|
95
|
+
line-height: 20px;
|
|
96
|
+
color: var(--text-tertiary);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.stat-value {
|
|
100
|
+
margin-top: 4px;
|
|
101
|
+
font-size: 20px;
|
|
102
|
+
font-weight: 600;
|
|
103
|
+
line-height: 28px;
|
|
104
|
+
letter-spacing: -0.02em;
|
|
105
|
+
font-variant-numeric: tabular-nums;
|
|
106
|
+
/* A currency string has no natural break, and a clipped balance is a
|
|
107
|
+
wrong number. If a value ever outgrows its cell, it wraps. */
|
|
108
|
+
overflow-wrap: anywhere;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#accounts {
|
|
112
|
+
border: 1px solid var(--hairline);
|
|
113
|
+
border-radius: var(--radius-card);
|
|
114
|
+
background: var(--bg-card);
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.account-row {
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
gap: 14px;
|
|
122
|
+
padding: 14px 16px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.account-row + .account-row {
|
|
126
|
+
border-top: 1px solid var(--hairline);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* A list with nothing in it says so, in the row's own inset. */
|
|
130
|
+
.empty-row {
|
|
131
|
+
padding: 14px 16px;
|
|
132
|
+
color: var(--text-secondary);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* The institution's initial on a round pill, as the console draws it. */
|
|
136
|
+
.account-glyph {
|
|
137
|
+
flex: none;
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
justify-content: center;
|
|
141
|
+
width: 32px;
|
|
142
|
+
height: 32px;
|
|
143
|
+
border-radius: 50%;
|
|
144
|
+
background: var(--bg-mute);
|
|
145
|
+
color: var(--text-secondary);
|
|
146
|
+
font-size: 16px;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.account-names {
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
flex: 1;
|
|
154
|
+
min-width: 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.account-name {
|
|
158
|
+
font-weight: 500;
|
|
159
|
+
overflow: hidden;
|
|
160
|
+
text-overflow: ellipsis;
|
|
161
|
+
white-space: nowrap;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.account-institution {
|
|
165
|
+
/* Holds its line even when empty, so every row measures the same as
|
|
166
|
+
the skeleton row it replaces. */
|
|
167
|
+
min-height: 21px;
|
|
168
|
+
color: var(--text-secondary);
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
text-overflow: ellipsis;
|
|
171
|
+
white-space: nowrap;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.account-balance {
|
|
175
|
+
text-align: right;
|
|
176
|
+
font-variant-numeric: tabular-nums;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* The skeleton ships in the HTML so the first paint is the final layout as
|
|
180
|
+
muted bars; the first render replaces it and fades in (see app.js). Each
|
|
181
|
+
bar's margins pad it to exactly the height of the text it stands in for,
|
|
182
|
+
and the columns holding stacked bars are flex so those margins never
|
|
183
|
+
collapse — the skeleton page and the data page measure identical, and
|
|
184
|
+
nothing shifts when data lands. */
|
|
185
|
+
.skeleton {
|
|
186
|
+
border-radius: var(--radius-bar);
|
|
187
|
+
background: var(--bg-mute);
|
|
188
|
+
animation: skeleton-pulse 1.6s ease-in-out infinite;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.skeleton-label {
|
|
192
|
+
width: 64px;
|
|
193
|
+
height: 12px;
|
|
194
|
+
margin: 4px 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.skeleton-value {
|
|
198
|
+
width: 96px;
|
|
199
|
+
height: 20px;
|
|
200
|
+
margin: 8px 0 4px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.skeleton-glyph {
|
|
204
|
+
flex: none;
|
|
205
|
+
width: 32px;
|
|
206
|
+
height: 32px;
|
|
207
|
+
border-radius: 50%;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.skeleton-name {
|
|
211
|
+
width: 150px;
|
|
212
|
+
height: 14px;
|
|
213
|
+
margin: 3.5px 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.skeleton-institution {
|
|
217
|
+
width: 110px;
|
|
218
|
+
height: 12px;
|
|
219
|
+
margin: 4.5px 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.skeleton-balance {
|
|
223
|
+
width: 72px;
|
|
224
|
+
height: 14px;
|
|
225
|
+
margin: 3.5px 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@keyframes skeleton-pulse {
|
|
229
|
+
0%, 100% { opacity: 1; }
|
|
230
|
+
50% { opacity: 0.55; }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.fade-in {
|
|
234
|
+
animation: fade-in 200ms ease-out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@keyframes fade-in {
|
|
238
|
+
from { opacity: 0; }
|
|
239
|
+
to { opacity: 1; }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@media (prefers-reduced-motion: reduce) {
|
|
243
|
+
.skeleton,
|
|
244
|
+
.fade-in {
|
|
245
|
+
animation: none;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
`;
|
package/dist/init/templates.js
CHANGED
|
@@ -21,8 +21,6 @@ export function indexHtml(slug) {
|
|
|
21
21
|
<main>
|
|
22
22
|
<header>
|
|
23
23
|
<h1>${slug}</h1>
|
|
24
|
-
<p class="sample-note" id="sample-note" hidden>Sample data — your real
|
|
25
|
-
accounts appear when this app runs inside Driggsby.</p>
|
|
26
24
|
</header>
|
|
27
25
|
<!-- The bars below are skeleton placeholders. app.js replaces them
|
|
28
26
|
with data on its first render; edit the render functions there,
|
|
@@ -53,6 +51,7 @@ export function indexHtml(slug) {
|
|
|
53
51
|
<h2>Accounts</h2>
|
|
54
52
|
<div id="accounts" aria-busy="true">
|
|
55
53
|
<div class="account-row">
|
|
54
|
+
<div class="skeleton skeleton-glyph"></div>
|
|
56
55
|
<div class="account-names">
|
|
57
56
|
<div class="skeleton skeleton-name"></div>
|
|
58
57
|
<div class="skeleton skeleton-institution"></div>
|
|
@@ -60,6 +59,7 @@ export function indexHtml(slug) {
|
|
|
60
59
|
<div class="skeleton skeleton-balance"></div>
|
|
61
60
|
</div>
|
|
62
61
|
<div class="account-row">
|
|
62
|
+
<div class="skeleton skeleton-glyph"></div>
|
|
63
63
|
<div class="account-names">
|
|
64
64
|
<div class="skeleton skeleton-name"></div>
|
|
65
65
|
<div class="skeleton skeleton-institution"></div>
|
|
@@ -67,6 +67,7 @@ export function indexHtml(slug) {
|
|
|
67
67
|
<div class="skeleton skeleton-balance"></div>
|
|
68
68
|
</div>
|
|
69
69
|
<div class="account-row">
|
|
70
|
+
<div class="skeleton skeleton-glyph"></div>
|
|
70
71
|
<div class="account-names">
|
|
71
72
|
<div class="skeleton skeleton-name"></div>
|
|
72
73
|
<div class="skeleton skeleton-institution"></div>
|
|
@@ -112,6 +113,11 @@ export const APP_JS = `// Your app's code. Edit anything — driggsby dev reload
|
|
|
112
113
|
// Params mirror Driggsby's public MCP tools of the same names, with one
|
|
113
114
|
// simplification: those tools require a "reason" string, but a watch can
|
|
114
115
|
// leave it out — Driggsby fills one in naming the dashboard.
|
|
116
|
+
//
|
|
117
|
+
// To see a tool's exact result shape before writing render code, run it
|
|
118
|
+
// once from the terminal — the JSON it prints is what the callback gets:
|
|
119
|
+
//
|
|
120
|
+
// npx driggsby@latest query <tool>
|
|
115
121
|
|
|
116
122
|
// ---------------------------------------------------------------------------
|
|
117
123
|
// Sample data. Every name and number here is invented. It paints only
|
|
@@ -159,10 +165,22 @@ function formatMoney(value) {
|
|
|
159
165
|
if (!value || typeof value.amount !== "string") return "—";
|
|
160
166
|
const number = Number(value.amount);
|
|
161
167
|
if (!Number.isFinite(number)) return "—";
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
168
|
+
const currency = value.currency_code === undefined ? "USD" : value.currency_code;
|
|
169
|
+
if (typeof currency !== "string") return "—";
|
|
170
|
+
try {
|
|
171
|
+
return new Intl.NumberFormat(undefined, {
|
|
172
|
+
style: "currency",
|
|
173
|
+
currency: currency
|
|
174
|
+
}).format(number);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
// An unknown currency code renders as absent, never as a guess in
|
|
177
|
+
// the wrong currency — and never takes the rest of the page down.
|
|
178
|
+
return "—";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function asText(value) {
|
|
183
|
+
return typeof value === "string" ? value : "";
|
|
166
184
|
}
|
|
167
185
|
|
|
168
186
|
function element(tag, className, text) {
|
|
@@ -207,22 +225,41 @@ function renderOverview(result) {
|
|
|
207
225
|
}
|
|
208
226
|
|
|
209
227
|
function renderAccounts(result) {
|
|
210
|
-
const
|
|
228
|
+
const list = result && Array.isArray(result.linked_accounts) ? result.linked_accounts : [];
|
|
229
|
+
const accounts = list.filter((account) => account && typeof account === "object");
|
|
211
230
|
const container = document.getElementById("accounts");
|
|
212
231
|
container.replaceChildren();
|
|
232
|
+
if (accounts.length === 0) {
|
|
233
|
+
container.append(element("div", "empty-row", "No linked accounts yet."));
|
|
234
|
+
revealOnce(container);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
213
237
|
for (const account of accounts) {
|
|
214
238
|
const row = element("div", "account-row");
|
|
215
239
|
const names = element("div", "account-names");
|
|
216
|
-
|
|
240
|
+
// Only strings paint; a malformed field renders as absent, never as
|
|
241
|
+
// "[object Object]".
|
|
242
|
+
const displayName = asText(account.account_display_name);
|
|
243
|
+
const institution = asText(account.institution_name);
|
|
244
|
+
const mask = asText(account.account_mask_last4);
|
|
245
|
+
// The institution's initial on a round pill, the way Driggsby's own
|
|
246
|
+
// account rows draw it. Array.from keeps an emoji-leading name from
|
|
247
|
+
// breaking the render (it splits by whole character, not UTF-16 half).
|
|
248
|
+
const initialSource = (institution || displayName).trim();
|
|
249
|
+
const initial = (Array.from(initialSource)[0] || "?").toUpperCase();
|
|
250
|
+
const institutionLine = [institution, mask ? "····" + mask : ""].filter(Boolean).join(" ");
|
|
217
251
|
names.append(
|
|
218
|
-
element("div", "account-name",
|
|
219
|
-
element(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
252
|
+
element("div", "account-name", displayName || "Account"),
|
|
253
|
+
element("div", "account-institution", institutionLine)
|
|
254
|
+
);
|
|
255
|
+
const glyph = element("div", "account-glyph", initial);
|
|
256
|
+
// Decorative: the institution's name is read out on the next line.
|
|
257
|
+
glyph.setAttribute("aria-hidden", "true");
|
|
258
|
+
row.append(
|
|
259
|
+
glyph,
|
|
260
|
+
names,
|
|
261
|
+
element("div", "account-balance", formatMoney(account.current_balance))
|
|
224
262
|
);
|
|
225
|
-
row.append(names, element("div", "account-balance", formatMoney(account.current_balance)));
|
|
226
263
|
container.append(row);
|
|
227
264
|
}
|
|
228
265
|
revealOnce(container);
|
|
@@ -232,25 +269,18 @@ function renderAccounts(result) {
|
|
|
232
269
|
// Live data. window.driggsby exists when the Driggsby SDK loaded — inside
|
|
233
270
|
// Driggsby, or under driggsby dev. Whether anything embeds this page is
|
|
234
271
|
// knowable synchronously: opened directly in a tab, no host will ever
|
|
235
|
-
// answer, so the sample data replaces the skeleton right away and the
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
272
|
+
// answer, so the sample data replaces the skeleton right away, and the SDK
|
|
273
|
+
// shows its own notice explaining that the page runs with real data only
|
|
274
|
+
// inside Driggsby. Embedded, sample numbers never paint at all — the
|
|
275
|
+
// shipped skeleton holds the layout until the first real results replace
|
|
276
|
+
// it and fade in.
|
|
240
277
|
// ---------------------------------------------------------------------------
|
|
241
278
|
|
|
242
279
|
const standalone = window.parent === window;
|
|
243
|
-
const note = document.getElementById("sample-note");
|
|
244
280
|
|
|
245
281
|
if (standalone) {
|
|
246
|
-
// The note and the sample values paint together (this whole branch is
|
|
247
|
-
// one synchronous task). The reveal is kept first so an edit that ever
|
|
248
|
-
// makes rendering async can't paint values before their label.
|
|
249
|
-
if (note) note.hidden = false;
|
|
250
282
|
renderOverview(SAMPLE_OVERVIEW);
|
|
251
283
|
renderAccounts(SAMPLE_ACCOUNTS);
|
|
252
|
-
} else if (note) {
|
|
253
|
-
note.remove();
|
|
254
284
|
}
|
|
255
285
|
|
|
256
286
|
if (window.driggsby) {
|
|
@@ -262,165 +292,10 @@ if (window.driggsby) {
|
|
|
262
292
|
});
|
|
263
293
|
}
|
|
264
294
|
`;
|
|
265
|
-
export const STYLES_CSS = `:root {
|
|
266
|
-
--background: #ffffff;
|
|
267
|
-
--text: #1f2430;
|
|
268
|
-
--text-muted: #6d7280;
|
|
269
|
-
--hairline: #e6e8ec;
|
|
270
|
-
--note-background: #fdf6e3;
|
|
271
|
-
--note-text: #7a5b1e;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
* {
|
|
275
|
-
box-sizing: border-box;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
body {
|
|
279
|
-
margin: 0;
|
|
280
|
-
background: var(--background);
|
|
281
|
-
color: var(--text);
|
|
282
|
-
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
283
|
-
font-size: 15px;
|
|
284
|
-
line-height: 1.5;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
main {
|
|
288
|
-
max-width: 720px;
|
|
289
|
-
margin: 0 auto;
|
|
290
|
-
padding: 48px 24px 64px;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
h1 {
|
|
294
|
-
margin: 0;
|
|
295
|
-
font-size: 22px;
|
|
296
|
-
font-weight: 600;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
h2 {
|
|
300
|
-
margin: 40px 0 4px;
|
|
301
|
-
font-size: 15px;
|
|
302
|
-
font-weight: 600;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
.sample-note {
|
|
306
|
-
display: inline-block;
|
|
307
|
-
margin: 12px 0 0;
|
|
308
|
-
padding: 4px 10px;
|
|
309
|
-
border-radius: 6px;
|
|
310
|
-
background: var(--note-background);
|
|
311
|
-
color: var(--note-text);
|
|
312
|
-
font-size: 13px;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/* The display rule above would defeat the hidden attribute without this. */
|
|
316
|
-
.sample-note[hidden] {
|
|
317
|
-
display: none;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/* The skeleton ships in the HTML so the first paint is the final layout as
|
|
321
|
-
muted bars; the first render replaces it and fades in (see app.js). The
|
|
322
|
-
bar margins make each skeleton block exactly the height of the text it
|
|
323
|
-
stands in for, so nothing shifts when data lands. */
|
|
324
|
-
.skeleton {
|
|
325
|
-
border-radius: 4px;
|
|
326
|
-
background: var(--hairline);
|
|
327
|
-
animation: skeleton-pulse 1.6s ease-in-out infinite;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
.skeleton-label {
|
|
331
|
-
width: 64px;
|
|
332
|
-
height: 12px;
|
|
333
|
-
margin: 3.75px 0 0;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
.skeleton-value {
|
|
337
|
-
width: 96px;
|
|
338
|
-
height: 20px;
|
|
339
|
-
margin: 8.75px 0 5px;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
.skeleton-name {
|
|
343
|
-
width: 150px;
|
|
344
|
-
height: 14px;
|
|
345
|
-
margin: 4px 0 8px;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
.skeleton-institution {
|
|
349
|
-
width: 110px;
|
|
350
|
-
height: 12px;
|
|
351
|
-
margin: 4px 0;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
.skeleton-balance {
|
|
355
|
-
width: 72px;
|
|
356
|
-
height: 14px;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
@keyframes skeleton-pulse {
|
|
360
|
-
0%, 100% { opacity: 1; }
|
|
361
|
-
50% { opacity: 0.55; }
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
.fade-in {
|
|
365
|
-
animation: fade-in 200ms ease-out;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
@keyframes fade-in {
|
|
369
|
-
from { opacity: 0; }
|
|
370
|
-
to { opacity: 1; }
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
@media (prefers-reduced-motion: reduce) {
|
|
374
|
-
.skeleton,
|
|
375
|
-
.fade-in {
|
|
376
|
-
animation: none;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
.stat-grid {
|
|
381
|
-
display: grid;
|
|
382
|
-
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
383
|
-
gap: 20px;
|
|
384
|
-
margin-top: 32px;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
.stat-label {
|
|
388
|
-
font-size: 13px;
|
|
389
|
-
color: var(--text-muted);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
.stat-value {
|
|
393
|
-
font-size: 20px;
|
|
394
|
-
font-weight: 600;
|
|
395
|
-
font-variant-numeric: tabular-nums;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
.account-row {
|
|
399
|
-
display: flex;
|
|
400
|
-
align-items: center;
|
|
401
|
-
justify-content: space-between;
|
|
402
|
-
gap: 16px;
|
|
403
|
-
padding: 12px 0;
|
|
404
|
-
border-top: 1px solid var(--hairline);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
.account-row:first-child {
|
|
408
|
-
border-top: none;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
.account-institution {
|
|
412
|
-
font-size: 13px;
|
|
413
|
-
color: var(--text-muted);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
.account-balance {
|
|
417
|
-
font-variant-numeric: tabular-nums;
|
|
418
|
-
}
|
|
419
|
-
`;
|
|
420
295
|
// "background" is the template's own page background (styles.css
|
|
421
|
-
// --
|
|
296
|
+
// --bg-app): Driggsby paints it while the app loads, so the loading
|
|
422
297
|
// surface matches the app from the first frame. An agent that re-themes
|
|
423
298
|
// the app should keep this equal to the page's real background color.
|
|
424
299
|
export function driggsbyJsonText(slug) {
|
|
425
|
-
return `{\n "slug": ${JSON.stringify(slug)},\n "serve": ".",\n "background": "#
|
|
300
|
+
return `{\n "slug": ${JSON.stringify(slug)},\n "serve": ".",\n "background": "#000000"\n}\n`;
|
|
426
301
|
}
|
package/dist/main.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// CLI entry point. The bin stub (bin/driggsby.js) imports this module; it is
|
|
2
2
|
// never imported as a library.
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { parseArgv } from "./args.js";
|
|
4
5
|
import { CliError } from "./cli-error.js";
|
|
5
6
|
import { runDeploy } from "./deploy/deploy-command.js";
|
|
6
7
|
import { runDev } from "./dev/dev-command.js";
|
|
8
|
+
import { runDevStop } from "./dev/dev-stop.js";
|
|
7
9
|
import { runInit } from "./init/init-command.js";
|
|
8
10
|
import { runRollback } from "./deploy/rollback-command.js";
|
|
9
11
|
import { runVersions } from "./deploy/versions-command.js";
|
|
12
|
+
import { runQuery } from "./query/query-command.js";
|
|
10
13
|
import { runLogin } from "./login/login.js";
|
|
11
14
|
import { runLogout } from "./login/logout.js";
|
|
12
15
|
import { runMcpSetup } from "./mcp-setup/setup.js";
|
|
@@ -33,13 +36,15 @@ async function run(argv) {
|
|
|
33
36
|
case "init":
|
|
34
37
|
return await runInit({ slug: command.slug });
|
|
35
38
|
case "dev":
|
|
36
|
-
return await runDev();
|
|
39
|
+
return command.stop ? await runDevStop(homedir()) : await runDev();
|
|
37
40
|
case "deploy":
|
|
38
41
|
return await runDeploy({ preview: command.preview });
|
|
39
42
|
case "rollback":
|
|
40
43
|
return await runRollback({ toVersion: command.toVersion });
|
|
41
44
|
case "versions":
|
|
42
45
|
return await runVersions({});
|
|
46
|
+
case "query":
|
|
47
|
+
return await runQuery({ tool: command.tool, params: command.params });
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
try {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// A file under ~/.driggsby that only its owner may read, written atomically:
|
|
2
|
+
// the directory is created owner-only if it does not exist, the body lands
|
|
3
|
+
// in a temp file with mode 0600, and a rename makes it appear whole. Used
|
|
4
|
+
// for the saved sign-in and for the record of a running `driggsby dev`.
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
export const DRIGGSBY_DIRECTORY_NAME = ".driggsby";
|
|
9
|
+
export function driggsbyDirectory(homeDirectory) {
|
|
10
|
+
return join(homeDirectory, DRIGGSBY_DIRECTORY_NAME);
|
|
11
|
+
}
|
|
12
|
+
export async function writeOwnerOnlyFile(homeDirectory, fileName, body) {
|
|
13
|
+
const directory = driggsbyDirectory(homeDirectory);
|
|
14
|
+
// mode applies only when the directory is created; an existing ~/.driggsby
|
|
15
|
+
// keeps whatever permissions the user already gave it.
|
|
16
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
17
|
+
const temporaryPath = join(directory, `${fileName}.${randomUUID()}.tmp`);
|
|
18
|
+
try {
|
|
19
|
+
await writeFile(temporaryPath, body, { mode: 0o600 });
|
|
20
|
+
await rename(temporaryPath, join(directory, fileName));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
await rm(temporaryPath, { force: true });
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|