@montytools/cli 0.1.0 → 0.1.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 +83 -4
- package/package.json +1 -1
package/bin/monty.mjs
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// deterministic final line (`deployed: …` / `error: …`).
|
|
5
5
|
|
|
6
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
7
8
|
import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
9
|
+
import { createServer } from "node:http";
|
|
8
10
|
import { homedir } from "node:os";
|
|
9
11
|
import { basename, dirname, join, relative } from "node:path";
|
|
10
12
|
import { fileURLToPath } from "node:url";
|
|
@@ -14,6 +16,9 @@ import { CATALOG, REGISTRIES } from "./catalog.mjs";
|
|
|
14
16
|
const CONFIG_DIR = join(homedir(), ".monty");
|
|
15
17
|
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
16
18
|
const DEFAULT_HOST = "https://usemonty.dev";
|
|
19
|
+
// Every app's source lives in one predictable place. `monty create` stamps
|
|
20
|
+
// here by default (override with --dir) and `monty login` provisions it.
|
|
21
|
+
const MONTY_HOME = join(homedir(), "Monty");
|
|
17
22
|
|
|
18
23
|
const [, , command, ...rest] = process.argv;
|
|
19
24
|
|
|
@@ -39,6 +44,11 @@ function loadConfig() {
|
|
|
39
44
|
async function login() {
|
|
40
45
|
const host = flag("host") ?? DEFAULT_HOST;
|
|
41
46
|
let key = flag("key");
|
|
47
|
+
if (!key) {
|
|
48
|
+
// Browser flow: loopback callback + explicit Authorize click in the host.
|
|
49
|
+
// Falls back to paste when the browser can't be opened (SSH, CI).
|
|
50
|
+
key = await browserLogin(host).catch(() => null);
|
|
51
|
+
}
|
|
42
52
|
if (!key) {
|
|
43
53
|
console.log(`open: ${host}/cli-auth (sign in, create a CLI key)`);
|
|
44
54
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -50,7 +60,72 @@ async function login() {
|
|
|
50
60
|
}
|
|
51
61
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
52
62
|
writeFileSync(CONFIG_PATH, JSON.stringify({ host, key }, null, 2) + "\n");
|
|
63
|
+
mkdirSync(MONTY_HOME, { recursive: true });
|
|
53
64
|
console.log(`logged-in: ${host} (key saved to ~/.monty/config.json)`);
|
|
65
|
+
console.log(`apps home: ${MONTY_HOME}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Loopback auth: serve one callback on 127.0.0.1, send the browser to
|
|
69
|
+
// /cli-auth with our address + a state nonce, and wait for the host to
|
|
70
|
+
// redirect back with a freshly minted key. The nonce ties the callback to
|
|
71
|
+
// THIS invocation; the host only ever redirects to 127.0.0.1/localhost.
|
|
72
|
+
function browserLogin(host) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const state = randomBytes(16).toString("hex");
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
server.close();
|
|
77
|
+
console.log("timeout: no browser authorization after 5 minutes — falling back to paste.");
|
|
78
|
+
reject(new Error("LOGIN_TIMEOUT"));
|
|
79
|
+
}, 300_000);
|
|
80
|
+
|
|
81
|
+
const server = createServer((req, res) => {
|
|
82
|
+
const url = new URL(req.url, "http://127.0.0.1");
|
|
83
|
+
if (url.pathname !== "/callback") {
|
|
84
|
+
res.writeHead(404).end();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const got = url.searchParams.get("key");
|
|
88
|
+
if (url.searchParams.get("state") !== state || !got) {
|
|
89
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
90
|
+
res.end("Stale or invalid authorization — run `monty login` again.");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
94
|
+
res.end("<title>Monty CLI</title><body style=\"font-family:system-ui;display:grid;place-items:center;height:100vh\"><p><b>Monty CLI authorized.</b> You can close this tab.</p></body>");
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
server.close();
|
|
97
|
+
resolve(got);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
server.on("error", (err) => {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
reject(err);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
server.listen(0, "127.0.0.1", () => {
|
|
106
|
+
const { port } = server.address();
|
|
107
|
+
const authUrl = `${host}/cli-auth?redirect=${encodeURIComponent(
|
|
108
|
+
`http://127.0.0.1:${port}/callback`,
|
|
109
|
+
)}&state=${state}`;
|
|
110
|
+
console.log(`auth: opening ${host}/cli-auth in your browser (sign in, click Authorize)`);
|
|
111
|
+
console.log(`auth: if nothing opens, visit ${authUrl}`);
|
|
112
|
+
openInBrowser(authUrl);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function openInBrowser(url) {
|
|
118
|
+
const [cmd, args] =
|
|
119
|
+
process.platform === "darwin"
|
|
120
|
+
? ["open", [url]]
|
|
121
|
+
: process.platform === "win32"
|
|
122
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
123
|
+
: ["xdg-open", [url]];
|
|
124
|
+
try {
|
|
125
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
126
|
+
} catch {
|
|
127
|
+
/* URL is already printed; paste flow remains available */
|
|
128
|
+
}
|
|
54
129
|
}
|
|
55
130
|
|
|
56
131
|
// ── monty create ───────────────────────────────────────────────────────────
|
|
@@ -63,10 +138,14 @@ async function create() {
|
|
|
63
138
|
flag("name") ??
|
|
64
139
|
slug.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
65
140
|
const icon = flag("icon") ?? "layout-grid";
|
|
66
|
-
|
|
141
|
+
// Apps live in ~/Monty/<slug> unless --dir points elsewhere.
|
|
142
|
+
const target = flag("dir")
|
|
143
|
+
? join(process.cwd(), flag("dir"))
|
|
144
|
+
: join(MONTY_HOME, slug);
|
|
67
145
|
if (existsSync(target)) {
|
|
68
146
|
fail("DIR_EXISTS", `${target} already exists. Pick another slug or remove the directory.`);
|
|
69
147
|
}
|
|
148
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
70
149
|
|
|
71
150
|
// Template is bundled into the published package (../template). Fall back to
|
|
72
151
|
// the monorepo path when running the CLI in-place during development.
|
|
@@ -121,7 +200,7 @@ async function create() {
|
|
|
121
200
|
}
|
|
122
201
|
|
|
123
202
|
console.log(`created: ${target}`);
|
|
124
|
-
console.log(`next: cd ${
|
|
203
|
+
console.log(`next: cd ${target} && pnpm install && monty dev`);
|
|
125
204
|
}
|
|
126
205
|
|
|
127
206
|
// ── monty dev ──────────────────────────────────────────────────────────────
|
|
@@ -386,8 +465,8 @@ switch (command) {
|
|
|
386
465
|
break;
|
|
387
466
|
default:
|
|
388
467
|
console.log("usage: monty <login|create|dev|add|components|docs|deploy>");
|
|
389
|
-
console.log(" login --host <url> --key <mk_...>
|
|
390
|
-
console.log(" create <slug> [--name N] [--icon I] stamp a new app
|
|
468
|
+
console.log(" login [--host <url>] [--key <mk_...>] sign in (opens your browser to authorize)");
|
|
469
|
+
console.log(" create <slug> [--name N] [--icon I] stamp a new app into ~/Monty/<slug>");
|
|
391
470
|
console.log(" dev [--port 5173] run the app locally (sandboxed data)");
|
|
392
471
|
console.log(" add <name...> install curated UI components (see `monty components`)");
|
|
393
472
|
console.log(" components [query] list the curated component catalog");
|