@montytools/cli 0.4.0 → 0.4.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/bin/monty.mjs +292 -5
- package/package.json +1 -1
- package/template/gitignore +5 -0
- package/template/index.html +12 -1
- package/template/package.json +1 -1
- package/template/src/main.tsx +4 -2
package/bin/monty.mjs
CHANGED
|
@@ -11,13 +11,20 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
11
11
|
import { createServer } from "node:http";
|
|
12
12
|
import { connect as netConnect } from "node:net";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
|
-
import { basename, dirname, join, relative } from "node:path";
|
|
14
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { createInterface } from "node:readline/promises";
|
|
17
17
|
import { CATALOG, REGISTRIES } from "./catalog.mjs";
|
|
18
18
|
import { CompileError, compileAppConfig } from "../lib/compile.mjs";
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// MONTY_HOME overrides the state root (default ~/.monty): config.json,
|
|
21
|
+
// apps/, and desktop.json all live under it. This is how a second, isolated
|
|
22
|
+
// Monty state coexists on one machine — the desktop's dev channel points it
|
|
23
|
+
// at ~/.monty-dev so platform development never touches the real state. A
|
|
24
|
+
// custom root is a sandbox: the legacy visible home (~/Monty) is not scanned.
|
|
25
|
+
const CONFIG_DIR = process.env.MONTY_HOME
|
|
26
|
+
? resolve(process.env.MONTY_HOME)
|
|
27
|
+
: join(homedir(), ".monty");
|
|
21
28
|
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
22
29
|
const DEFAULT_HOST = "https://usemonty.dev";
|
|
23
30
|
const DEV_SESSION_HEARTBEAT_MS = 30_000;
|
|
@@ -336,7 +343,11 @@ function scanAppsHome(root) {
|
|
|
336
343
|
}
|
|
337
344
|
|
|
338
345
|
function listLocalApps() {
|
|
339
|
-
|
|
346
|
+
// A MONTY_HOME sandbox lists only its own apps — leaking the legacy home
|
|
347
|
+
// into an isolated root would defeat the isolation.
|
|
348
|
+
return process.env.MONTY_HOME
|
|
349
|
+
? scanAppsHome(MONTY_HOME)
|
|
350
|
+
: [...scanAppsHome(MONTY_HOME), ...scanAppsHome(LEGACY_MONTY_HOME)];
|
|
340
351
|
}
|
|
341
352
|
|
|
342
353
|
function current() {
|
|
@@ -661,6 +672,19 @@ async function create() {
|
|
|
661
672
|
},
|
|
662
673
|
});
|
|
663
674
|
|
|
675
|
+
// The bundled template ships `.gitignore` as `gitignore` (npm-packlist
|
|
676
|
+
// hard-excludes the real name from tarballs); the in-repo template has the
|
|
677
|
+
// real file. Normalize, and backfill for bundles that carried neither —
|
|
678
|
+
// without a .gitignore, tailwind v4's content scan includes .monty/ and
|
|
679
|
+
// full-reloads Studio on every dev.json touch.
|
|
680
|
+
const gitignorePath = join(target, ".gitignore");
|
|
681
|
+
if (existsSync(join(target, "gitignore"))) {
|
|
682
|
+
renameSync(join(target, "gitignore"), gitignorePath);
|
|
683
|
+
}
|
|
684
|
+
if (!existsSync(gitignorePath)) {
|
|
685
|
+
writeFileSync(gitignorePath, "node_modules/\ndist/\n.monty/\n.env.local\n.env\n");
|
|
686
|
+
}
|
|
687
|
+
|
|
664
688
|
// Stamp identity into the copied files. The id line is INSERTED (the
|
|
665
689
|
// template ships without one — only real creates have a server id).
|
|
666
690
|
const configPath = join(target, "monty.config.ts");
|
|
@@ -795,7 +819,7 @@ async function freePort(start) {
|
|
|
795
819
|
// Apps pin @montytools/sdk at scaffold time and go stale — the CLI knows the
|
|
796
820
|
// minimum SDK its workflows need (e.g. tunnel-host allowlisting lives in the
|
|
797
821
|
// SDK's vite plugin) and upgrades the app automatically before dev/deploy.
|
|
798
|
-
const MIN_SDK = "0.1.
|
|
822
|
+
const MIN_SDK = "0.1.5";
|
|
799
823
|
const SDK_VITE_CACHE_STAMP = "sdk-vite-cache-version";
|
|
800
824
|
|
|
801
825
|
function installedSdkVersion(appDir) {
|
|
@@ -2260,6 +2284,265 @@ function cronFieldMatches(field, value, [lo, hi], idx) {
|
|
|
2260
2284
|
return false;
|
|
2261
2285
|
}
|
|
2262
2286
|
|
|
2287
|
+
// ── monty data ─────────────────────────────────────────────────────────────
|
|
2288
|
+
// The agent verbs for OPERATING an app: read and write its records from any
|
|
2289
|
+
// terminal — no browser, no dev session. Auth is the mk_ key exchanged at
|
|
2290
|
+
// /api/dev-token for a 5-minute workspace token (member lane, org_id from
|
|
2291
|
+
// the verified JWT), then the 6 public records functions over Convex's HTTP
|
|
2292
|
+
// API. `app` picks the records namespace: the plain slug is Live; --studio
|
|
2293
|
+
// targets "{slug}#dev" (the Studio sandbox — same wire literal the dev
|
|
2294
|
+
// shell uses). Results are ONE JSON document on stdout so agents can pipe.
|
|
2295
|
+
|
|
2296
|
+
const DATA_VALUE_FLAGS = new Set(["app", "filter", "order", "limit", "cursor", "data", "key", "unset", "host"]);
|
|
2297
|
+
|
|
2298
|
+
// rest, minus flags AND their values — `monty data list leads --app crm`
|
|
2299
|
+
// must not read "crm" as a positional. Boolean flags (--studio) have no
|
|
2300
|
+
// value and are skipped alone.
|
|
2301
|
+
function dataPositionals() {
|
|
2302
|
+
const out = [];
|
|
2303
|
+
for (let i = 0; i < rest.length; i++) {
|
|
2304
|
+
const a = rest[i];
|
|
2305
|
+
if (a.startsWith("--")) {
|
|
2306
|
+
if (DATA_VALUE_FLAGS.has(a.slice(2))) i++;
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
out.push(a);
|
|
2310
|
+
}
|
|
2311
|
+
return out;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
function parseJsonFlag(name) {
|
|
2315
|
+
const raw = flag(name);
|
|
2316
|
+
if (raw === undefined) return undefined;
|
|
2317
|
+
try {
|
|
2318
|
+
return JSON.parse(raw);
|
|
2319
|
+
} catch {
|
|
2320
|
+
fail("BAD_JSON", `--${name} is not valid JSON. Quote the whole value in single quotes, e.g. --${name} '{"field":"value"}'. Got: ${raw.slice(0, 120)}`);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// Which records namespace a data verb targets. Slug from --app or the
|
|
2325
|
+
// surrounding app folder; existence is decided server-side (an unknown slug
|
|
2326
|
+
// simply has zero records — list makes that visible immediately).
|
|
2327
|
+
function resolveDataApp() {
|
|
2328
|
+
const explicit = flag("app");
|
|
2329
|
+
const root = findAppRoot(process.cwd());
|
|
2330
|
+
const slug = explicit ?? (root ? readSlugFromConfig(root) : null);
|
|
2331
|
+
if (!slug) {
|
|
2332
|
+
fail("NO_APP", "Say which app: pass --app <slug>, or run inside an app folder. `monty apps` lists local apps.");
|
|
2333
|
+
}
|
|
2334
|
+
return rest.includes("--studio") ? `${slug}#dev` : slug;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// mk_ key → { convexUrl, token }. The token is workspace-scoped and expires
|
|
2338
|
+
// in 5 minutes — minted fresh per invocation, never stored.
|
|
2339
|
+
async function dataAuth() {
|
|
2340
|
+
const { host, key } = loadConfig();
|
|
2341
|
+
if (!key) fail("NOT_LOGGED_IN", `Reading and writing app data needs your workspace (${host}). Run \`monty login\` first.`);
|
|
2342
|
+
let convexUrl = null;
|
|
2343
|
+
try {
|
|
2344
|
+
convexUrl = (await fetch(`${host}/api/config`).then((r) => r.json()))?.convexUrl;
|
|
2345
|
+
} catch { /* handled below */ }
|
|
2346
|
+
if (!convexUrl) fail("HOST_UNREACHABLE", `Could not read ${host}/api/config — is the host up and the network reachable?`);
|
|
2347
|
+
const r = await fetch(`${host}/api/dev-token`, {
|
|
2348
|
+
method: "POST",
|
|
2349
|
+
headers: { authorization: `Bearer ${key}` },
|
|
2350
|
+
});
|
|
2351
|
+
const body = await r.json().catch(() => null);
|
|
2352
|
+
if (!r.ok || !body?.token) {
|
|
2353
|
+
fail(body?.code ?? `HTTP_${r.status}`, body?.fix ?? "Minting a workspace token failed. Run `monty login`, then retry.");
|
|
2354
|
+
}
|
|
2355
|
+
return { convexUrl, token: body.token };
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// One records function over Convex's public HTTP API (plain-JSON format —
|
|
2359
|
+
// app data is JSON by construction, no convex encoding needed). ConvexError
|
|
2360
|
+
// payloads carry { code, fix } and surface verbatim: errors stay
|
|
2361
|
+
// instructions in the terminal the agent is watching.
|
|
2362
|
+
async function callRecords(kind, fn, args, auth) {
|
|
2363
|
+
let body = null;
|
|
2364
|
+
try {
|
|
2365
|
+
const r = await fetch(`${auth.convexUrl}/api/${kind}`, {
|
|
2366
|
+
method: "POST",
|
|
2367
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${auth.token}` },
|
|
2368
|
+
body: JSON.stringify({ path: `records:${fn}`, args, format: "json" }),
|
|
2369
|
+
});
|
|
2370
|
+
body = await r.json().catch(() => null);
|
|
2371
|
+
} catch { /* handled below */ }
|
|
2372
|
+
if (!body) fail("CONVEX_UNREACHABLE", `The data backend at ${auth.convexUrl} did not answer — check the network and retry.`);
|
|
2373
|
+
if (body.status !== "success") {
|
|
2374
|
+
const d = body.errorData;
|
|
2375
|
+
if (d && typeof d === "object" && d.code) fail(d.code, d.fix ?? body.errorMessage ?? "See the error code.");
|
|
2376
|
+
fail("CONVEX_ERROR", body.errorMessage ?? "Unknown data-layer error — retry; if it persists, report it.");
|
|
2377
|
+
}
|
|
2378
|
+
return body.value;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
function printJson(value) {
|
|
2382
|
+
console.log(JSON.stringify(value, null, 2));
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// The documented row shape everywhere on the platform (SDK hooks, server
|
|
2386
|
+
// functions) is FLATTENED: app fields at the top level + the four system
|
|
2387
|
+
// fields. Mirror it here — and don't echo workspaceId/appId/table back;
|
|
2388
|
+
// scope is the caller's own arguments, not row payload.
|
|
2389
|
+
function flattenRow(doc) {
|
|
2390
|
+
if (!doc) return null;
|
|
2391
|
+
return {
|
|
2392
|
+
...doc.data,
|
|
2393
|
+
_id: doc._id,
|
|
2394
|
+
_creationTime: doc._creationTime,
|
|
2395
|
+
updatedAt: doc.updatedAt,
|
|
2396
|
+
createdBy: doc.createdBy,
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
function dataUsage() {
|
|
2401
|
+
console.log("usage: monty data <verb> [table] [flags] read/write an app's Live records (add --studio for the Studio sandbox)");
|
|
2402
|
+
console.log(" schema [table] the app's table shapes (from local monty.config.ts — `monty pull` first if needed)");
|
|
2403
|
+
console.log(" list <table> [--filter '{\"k\":\"v\"}'] [--order asc|desc] [--limit N] [--cursor C]");
|
|
2404
|
+
console.log(" get <table> <id>");
|
|
2405
|
+
console.log(" insert <table> --data '<json|[json,…]>'");
|
|
2406
|
+
console.log(" update <table> <id> --data '<json>' [--unset field,field]");
|
|
2407
|
+
console.log(" upsert <table> --key <field[,field]> --data '<json|[json,…]>' find-or-create matched on the key fields (idempotent)");
|
|
2408
|
+
console.log(" remove <table> <id>");
|
|
2409
|
+
console.log("target: --app <slug> (or run inside the app folder); default is LIVE data — --studio targets the sandbox");
|
|
2410
|
+
process.exit(1);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
async function data() {
|
|
2414
|
+
const [verb, table, id] = dataPositionals();
|
|
2415
|
+
|
|
2416
|
+
if (verb === "schema") {
|
|
2417
|
+
// Shape comes from the LOCAL source checkout (compiled through the real
|
|
2418
|
+
// pipeline) — the same monty.config.ts that defines what the app stores.
|
|
2419
|
+
const explicit = flag("app");
|
|
2420
|
+
const root = explicit
|
|
2421
|
+
? (listLocalApps().find((a) => a.slug === explicit || a.id === explicit)?.path ?? null)
|
|
2422
|
+
: findAppRoot(process.cwd());
|
|
2423
|
+
if (!root) {
|
|
2424
|
+
fail("APP_NOT_LOCAL", explicit
|
|
2425
|
+
? `No local source for "${explicit}" on this machine — run \`monty pull ${explicit}\` first, or cd into the app folder.`
|
|
2426
|
+
: "Not inside an app folder. Pass --app <slug> (needs the source pulled locally) or cd into the app.");
|
|
2427
|
+
}
|
|
2428
|
+
let meta;
|
|
2429
|
+
try {
|
|
2430
|
+
meta = await compileAppConfig(root);
|
|
2431
|
+
} catch (e) {
|
|
2432
|
+
if (e instanceof CompileError) fail(e.code, e.fix);
|
|
2433
|
+
throw e;
|
|
2434
|
+
}
|
|
2435
|
+
const tables = meta.schemaJson?.tables ?? {};
|
|
2436
|
+
if (table !== undefined) {
|
|
2437
|
+
if (!tables[table]) {
|
|
2438
|
+
fail("NO_SUCH_TABLE", `App "${meta.slug}" has no table "${table}". Tables: ${Object.keys(tables).join(", ") || "(none)"}.`);
|
|
2439
|
+
}
|
|
2440
|
+
printJson({ app: meta.slug, table, schema: tables[table] });
|
|
2441
|
+
} else {
|
|
2442
|
+
printJson({ app: meta.slug, tables });
|
|
2443
|
+
}
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
const VERBS = new Set(["list", "get", "insert", "update", "upsert", "remove"]);
|
|
2448
|
+
if (!verb || !VERBS.has(verb)) dataUsage();
|
|
2449
|
+
if (!table) fail("MISSING_TABLE", `\`monty data ${verb}\` needs a table name: monty data ${verb} <table> … (\`monty data schema\` lists tables).`);
|
|
2450
|
+
|
|
2451
|
+
const app = resolveDataApp();
|
|
2452
|
+
const auth = await dataAuth();
|
|
2453
|
+
|
|
2454
|
+
switch (verb) {
|
|
2455
|
+
case "list": {
|
|
2456
|
+
const filter = parseJsonFlag("filter");
|
|
2457
|
+
const order = flag("order");
|
|
2458
|
+
if (order !== undefined && order !== "asc" && order !== "desc") {
|
|
2459
|
+
fail("BAD_ORDER", `--order must be "asc" or "desc" (default desc, newest first). Got: ${order}`);
|
|
2460
|
+
}
|
|
2461
|
+
const limit = Math.min(Math.max(parseInt(flag("limit") ?? "100", 10) || 100, 1), 1024);
|
|
2462
|
+
const value = await callRecords("query", "list", {
|
|
2463
|
+
app,
|
|
2464
|
+
table,
|
|
2465
|
+
...(filter !== undefined ? { filter } : {}),
|
|
2466
|
+
...(order !== undefined ? { order } : {}),
|
|
2467
|
+
paginationOpts: { numItems: limit, cursor: flag("cursor") ?? null },
|
|
2468
|
+
}, auth);
|
|
2469
|
+
printJson({
|
|
2470
|
+
rows: value.page.map(flattenRow),
|
|
2471
|
+
count: value.page.length,
|
|
2472
|
+
// A non-null cursor means MORE rows exist: repeat with --cursor <c>.
|
|
2473
|
+
cursor: value.isDone ? null : value.continueCursor,
|
|
2474
|
+
});
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
case "get": {
|
|
2478
|
+
if (!id) fail("MISSING_ID", "Usage: monty data get <table> <id> — ids come from list on the same table.");
|
|
2479
|
+
printJson(flattenRow(await callRecords("query", "get", { app, table, id }, auth)));
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
case "insert": {
|
|
2483
|
+
const input = parseJsonFlag("data");
|
|
2484
|
+
if (input === undefined) fail("MISSING_DATA", `Usage: monty data insert ${table} --data '{"field":"value"}' — an array inserts each element.`);
|
|
2485
|
+
const rows = Array.isArray(input) ? input : [input];
|
|
2486
|
+
const ids = [];
|
|
2487
|
+
for (const row of rows) {
|
|
2488
|
+
ids.push(await callRecords("mutation", "insert", { app, table, data: row }, auth));
|
|
2489
|
+
}
|
|
2490
|
+
printJson(Array.isArray(input) ? { ids } : { id: ids[0] });
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
case "update": {
|
|
2494
|
+
if (!id) fail("MISSING_ID", "Usage: monty data update <table> <id> --data '{…}' — ids come from list/get on the same table.");
|
|
2495
|
+
const patch = parseJsonFlag("data");
|
|
2496
|
+
if (patch === undefined) fail("MISSING_DATA", `Usage: monty data update ${table} ${id} --data '{"field":"newValue"}' (shallow-merged; --unset a,b removes fields).`);
|
|
2497
|
+
const unsetRaw = flag("unset");
|
|
2498
|
+
await callRecords("mutation", "update", {
|
|
2499
|
+
app,
|
|
2500
|
+
table,
|
|
2501
|
+
id,
|
|
2502
|
+
data: patch,
|
|
2503
|
+
...(unsetRaw ? { unset: unsetRaw.split(",").map((s) => s.trim()).filter(Boolean) } : {}),
|
|
2504
|
+
}, auth);
|
|
2505
|
+
printJson({ ok: true, id });
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
case "upsert": {
|
|
2509
|
+
// --key names the FIELDS that identify a row (e.g. --key linkedinUrl);
|
|
2510
|
+
// values come from each data row, so one flag serves single and batch
|
|
2511
|
+
// writes alike — and re-running an import never duplicates.
|
|
2512
|
+
const keyFields = (flag("key") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
2513
|
+
if (keyFields.length === 0) {
|
|
2514
|
+
fail("MISSING_KEY", `Usage: monty data upsert ${table} --key <field[,field]> --data '{…}' — key fields identify the row to match on.`);
|
|
2515
|
+
}
|
|
2516
|
+
const input = parseJsonFlag("data");
|
|
2517
|
+
if (input === undefined) fail("MISSING_DATA", `Usage: monty data upsert ${table} --key ${keyFields.join(",")} --data '{"${keyFields[0]}":"…", …}' — an array upserts each element.`);
|
|
2518
|
+
const rows = Array.isArray(input) ? input : [input];
|
|
2519
|
+
const ids = [];
|
|
2520
|
+
for (const row of rows) {
|
|
2521
|
+
const key = {};
|
|
2522
|
+
for (const f of keyFields) {
|
|
2523
|
+
if (row[f] === undefined) {
|
|
2524
|
+
fail("MISSING_KEY_FIELD", `A data row is missing key field "${f}" — every row must carry its own key values. Row: ${JSON.stringify(row).slice(0, 120)}`);
|
|
2525
|
+
}
|
|
2526
|
+
key[f] = row[f];
|
|
2527
|
+
}
|
|
2528
|
+
// patch = the caller's fields (merged over an existing row); full =
|
|
2529
|
+
// the stored row on insert. The CLI applies no zod defaults, so they
|
|
2530
|
+
// are the same object here — `monty data schema` shows what a
|
|
2531
|
+
// complete row needs.
|
|
2532
|
+
ids.push(await callRecords("mutation", "upsert", { app, table, key, patch: row, full: row }, auth));
|
|
2533
|
+
}
|
|
2534
|
+
printJson(Array.isArray(input) ? { ids } : { id: ids[0] });
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
case "remove": {
|
|
2538
|
+
if (!id) fail("MISSING_ID", "Usage: monty data remove <table> <id> — ids come from list/get on the same table.");
|
|
2539
|
+
await callRecords("mutation", "remove", { app, table, id }, auth);
|
|
2540
|
+
printJson({ ok: true, id, removed: true });
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2263
2546
|
// ── dispatch ───────────────────────────────────────────────────────────────
|
|
2264
2547
|
// Keep agent skills fresh on every invocation (user level + current app).
|
|
2265
2548
|
// `dev` and `logs` skip the refresh here: attach and log reads must stay
|
|
@@ -2327,11 +2610,14 @@ switch (command) {
|
|
|
2327
2610
|
case "deploy":
|
|
2328
2611
|
await deploy();
|
|
2329
2612
|
break;
|
|
2613
|
+
case "data":
|
|
2614
|
+
await data();
|
|
2615
|
+
break;
|
|
2330
2616
|
case "secret":
|
|
2331
2617
|
await secret();
|
|
2332
2618
|
break;
|
|
2333
2619
|
default:
|
|
2334
|
-
console.log("usage: monty <login|create|pull|commit|log|current|select|apps|install|dev|logs|build|typecheck|add|components|docs|deploy|skills>");
|
|
2620
|
+
console.log("usage: monty <login|create|pull|commit|log|current|select|apps|install|dev|logs|build|typecheck|add|components|docs|deploy|data|skills>");
|
|
2335
2621
|
console.log(" login [--host <url>] [--key <mk_...>] sign in (opens your browser to authorize)");
|
|
2336
2622
|
console.log(" create <slug> [--name N] [--icon I] [--build ID] register + stamp a new app into ~/.monty/apps/<id> (needs login)");
|
|
2337
2623
|
console.log(" pull <slug> [--version H] [--force] restore the app's source snapshot (latest, or one from `monty log`)");
|
|
@@ -2349,6 +2635,7 @@ switch (command) {
|
|
|
2349
2635
|
console.log(" build production build (vite, via monty)");
|
|
2350
2636
|
console.log(" typecheck typecheck (builds first if needed)");
|
|
2351
2637
|
console.log(" deploy build + upload this app straight to Live");
|
|
2638
|
+
console.log(" data <verb> [table] [flags] read/write an app's records from the terminal (`monty data` for verbs)");
|
|
2352
2639
|
console.log(" skills install/refresh the agent build skill");
|
|
2353
2640
|
process.exit(command ? 1 : 0);
|
|
2354
2641
|
}
|
package/package.json
CHANGED
package/template/index.html
CHANGED
|
@@ -6,7 +6,18 @@
|
|
|
6
6
|
<title>New app</title>
|
|
7
7
|
</head>
|
|
8
8
|
<body>
|
|
9
|
-
<div id="root"
|
|
9
|
+
<div id="root">
|
|
10
|
+
<!-- Boot splash: the first paint, shown while the JS loads. React
|
|
11
|
+
replaces it with the SDK's identical MontySplash, so a cold open
|
|
12
|
+
is one continuous visual. If you theme the app dark-only, change
|
|
13
|
+
the background below to match. -->
|
|
14
|
+
<style>
|
|
15
|
+
@keyframes monty-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .25 } }
|
|
16
|
+
</style>
|
|
17
|
+
<div style="display: grid; place-items: center; min-height: 100vh; background: oklch(1 0 0)">
|
|
18
|
+
<div style="width: 10px; height: 10px; border-radius: 999px; background: #999; animation: monty-pulse 1.2s ease-in-out infinite"></div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
10
21
|
<script type="module" src="/src/main.tsx"></script>
|
|
11
22
|
</body>
|
|
12
23
|
</html>
|
package/template/package.json
CHANGED
package/template/src/main.tsx
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { StrictMode } from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
3
|
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
|
4
|
-
import { MontyProvider } from "@montytools/sdk/react";
|
|
4
|
+
import { MontyProvider, MontySplash } from "@montytools/sdk/react";
|
|
5
5
|
|
|
6
6
|
import "./index.css";
|
|
7
7
|
import { app } from "../monty.config";
|
|
8
8
|
import { routeTree } from "./routeTree.gen";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// defaultPendingComponent: lazy route chunks show the same splash the rest
|
|
11
|
+
// of the boot uses instead of flashing blank.
|
|
12
|
+
const router = createRouter({ routeTree, defaultPendingComponent: MontySplash });
|
|
11
13
|
|
|
12
14
|
declare module "@tanstack/react-router" {
|
|
13
15
|
interface Register {
|