@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.
Files changed (2) hide show
  1. package/bin/monty.mjs +83 -4
  2. 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
- const target = join(process.cwd(), flag("dir") ?? slug);
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 ${relative(process.cwd(), target)} && pnpm install && monty dev`);
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_...> save CLI credentials");
390
- console.log(" create <slug> [--name N] [--icon I] stamp a new app from the template");
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@montytools/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "monty": "./bin/monty.mjs"