@montytools/cli 0.1.0 → 0.1.2

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 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,138 @@ 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
+ }
129
+ }
130
+
131
+
132
+ // ── monty current / select / apps ───────────────────────────────────────────
133
+ // Folder management users never think about: every app lives in ~/Monty,
134
+ // `current` says where you are, `select` prints the folder for cd $(...).
135
+ function findAppRoot(start) {
136
+ let d = start;
137
+ for (;;) {
138
+ if (existsSync(join(d, "monty.config.ts"))) return d;
139
+ const parent = dirname(d);
140
+ if (parent === d) return null;
141
+ d = parent;
142
+ }
143
+ }
144
+
145
+ function readSlugFromConfig(dir) {
146
+ try {
147
+ return /slug:\s*"([^"]+)"/.exec(readFileSync(join(dir, "monty.config.ts"), "utf8"))?.[1] ?? null;
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ function listLocalApps() {
154
+ if (!existsSync(MONTY_HOME)) return [];
155
+ return readdirSync(MONTY_HOME)
156
+ .map((name) => join(MONTY_HOME, name))
157
+ .filter((p) => existsSync(join(p, "monty.config.ts")))
158
+ .map((p) => ({ path: p, slug: readSlugFromConfig(p) ?? basename(p) }));
159
+ }
160
+
161
+ function current() {
162
+ const root = findAppRoot(process.cwd());
163
+ if (!root) {
164
+ fail("NOT_IN_APP", `You are not inside a Monty app. \`monty apps\` lists local apps; cd "$(monty select <slug>)" jumps to one.`);
165
+ }
166
+ console.log(`app: ${readSlugFromConfig(root) ?? "?"}`);
167
+ console.log(`path: ${root}`);
168
+ if (!root.startsWith(MONTY_HOME)) {
169
+ console.log(`note: outside ${MONTY_HOME} (fine, but apps normally live there)`);
170
+ }
171
+ }
172
+
173
+ function select() {
174
+ const slug = rest.find((a) => !a.startsWith("--"));
175
+ if (!slug) {
176
+ fail("MISSING_SLUG", `Usage: cd "$(monty select <slug>)" — prints the app's folder.`);
177
+ }
178
+ const apps = listLocalApps();
179
+ const hit = apps.find((a) => a.slug === slug || basename(a.path) === slug);
180
+ if (!hit) {
181
+ const known = apps.map((a) => a.slug).join(", ") || "(none)";
182
+ fail("APP_NOT_LOCAL", `No local source for "${slug}" in ${MONTY_HOME}. Local apps: ${known}. Create it with \`monty create ${slug}\`.`);
183
+ }
184
+ // Bare path on stdout so command substitution works: cd "$(monty select x)"
185
+ console.log(hit.path);
186
+ }
187
+
188
+ function apps() {
189
+ const local = listLocalApps();
190
+ if (!local.length) {
191
+ console.log(`no local apps in ${MONTY_HOME} — create one with \`monty create <slug>\``);
192
+ return;
193
+ }
194
+ for (const a of local) console.log(`${a.slug}\t${a.path}`);
54
195
  }
55
196
 
56
197
  // ── monty create ───────────────────────────────────────────────────────────
@@ -63,10 +204,14 @@ async function create() {
63
204
  flag("name") ??
64
205
  slug.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
65
206
  const icon = flag("icon") ?? "layout-grid";
66
- const target = join(process.cwd(), flag("dir") ?? slug);
207
+ // Apps live in ~/Monty/<slug> unless --dir points elsewhere.
208
+ const target = flag("dir")
209
+ ? join(process.cwd(), flag("dir"))
210
+ : join(MONTY_HOME, slug);
67
211
  if (existsSync(target)) {
68
212
  fail("DIR_EXISTS", `${target} already exists. Pick another slug or remove the directory.`);
69
213
  }
214
+ mkdirSync(dirname(target), { recursive: true });
70
215
 
71
216
  // Template is bundled into the published package (../template). Fall back to
72
217
  // the monorepo path when running the CLI in-place during development.
@@ -121,7 +266,7 @@ async function create() {
121
266
  }
122
267
 
123
268
  console.log(`created: ${target}`);
124
- console.log(`next: cd ${relative(process.cwd(), target)} && pnpm install && monty dev`);
269
+ console.log(`next: cd ${target} && pnpm install && monty dev`);
125
270
  }
126
271
 
127
272
  // ── monty dev ──────────────────────────────────────────────────────────────
@@ -381,17 +526,29 @@ switch (command) {
381
526
  case "docs":
382
527
  await docs();
383
528
  break;
529
+ case "current":
530
+ current();
531
+ break;
532
+ case "select":
533
+ select();
534
+ break;
535
+ case "apps":
536
+ apps();
537
+ break;
384
538
  case "deploy":
385
539
  await deploy();
386
540
  break;
387
541
  default:
388
- 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");
542
+ console.log("usage: monty <login|create|current|select|apps|dev|add|components|docs|deploy>");
543
+ console.log(" login [--host <url>] [--key <mk_...>] sign in (opens your browser to authorize)");
544
+ console.log(" create <slug> [--name N] [--icon I] stamp a new app into ~/Monty/<slug>");
391
545
  console.log(" dev [--port 5173] run the app locally (sandboxed data)");
392
546
  console.log(" add <name...> install curated UI components (see `monty components`)");
393
547
  console.log(" components [query] list the curated component catalog");
394
548
  console.log(" docs <name> view a component's source before installing");
549
+ console.log(" current which app folder am I in?");
550
+ console.log(" select <slug> print an app's folder — cd \"$(monty select x)\"");
551
+ console.log(" apps list local apps in ~/Monty");
395
552
  console.log(" deploy build + upload this app");
396
553
  process.exit(command ? 1 : 0);
397
554
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@montytools/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "monty": "./bin/monty.mjs"
@@ -4,6 +4,8 @@ Monty is a work OS: your app runs inside a team's workspace, on shared reactive
4
4
  data, with auth and deployment handled by the platform. **You only write product
5
5
  logic.** Everything below is the complete contract.
6
6
 
7
+ Folders are managed for you: this app lives in `~/Monty/<slug>`. `monty current` confirms where you are; `cd "$(monty select <slug>)"` jumps to any app; never create app folders by hand.
8
+
7
9
  ## The three files that matter
8
10
 
9
11
  | File | What it is |