@montytools/cli 0.1.1 → 0.1.3

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
@@ -63,6 +63,7 @@ async function login() {
63
63
  mkdirSync(MONTY_HOME, { recursive: true });
64
64
  console.log(`logged-in: ${host} (key saved to ~/.monty/config.json)`);
65
65
  console.log(`apps home: ${MONTY_HOME}`);
66
+ installSkills({ silent: false });
66
67
  }
67
68
 
68
69
  // Loopback auth: serve one callback on 127.0.0.1, send the browser to
@@ -128,6 +129,110 @@ function openInBrowser(url) {
128
129
  }
129
130
  }
130
131
 
132
+
133
+
134
+ // ── agent skills ─────────────────────────────────────────────────────────────
135
+ // The build skill ships inside this package and is stamped wherever agents
136
+ // look for it: ~/.claude/skills (user) and <app>/.claude + <app>/.agents
137
+ // (project — self-propagating contract, refreshed whenever the CLI runs).
138
+ const CLI_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
139
+ const CLI_VERSION = JSON.parse(readFileSync(join(CLI_ROOT, "package.json"), "utf8")).version;
140
+ const SKILLS_SRC = join(CLI_ROOT, "skills");
141
+
142
+ function installSkillsInto(dir, { silent = true } = {}) {
143
+ if (!existsSync(SKILLS_SRC)) return false;
144
+ let changed = false;
145
+ for (const name of readdirSync(SKILLS_SRC)) {
146
+ const dest = join(dir, name);
147
+ const versionFile = join(dest, "VERSION");
148
+ const installed = existsSync(versionFile) ? readFileSync(versionFile, "utf8").trim() : null;
149
+ if (installed === CLI_VERSION) continue;
150
+ cpSync(join(SKILLS_SRC, name), dest, { recursive: true });
151
+ writeFileSync(versionFile, CLI_VERSION + "\n");
152
+ changed = true;
153
+ }
154
+ if (changed && !silent) console.log(`skills: installed -> ${dir} (v${CLI_VERSION})`);
155
+ return changed;
156
+ }
157
+
158
+ function installSkills({ appDir = null, silent = true } = {}) {
159
+ if (process.env.MONTY_NO_SKILLS) return;
160
+ try {
161
+ let changed = installSkillsInto(join(homedir(), ".claude", "skills"), { silent });
162
+ for (const base of appDir ? [join(appDir, ".claude", "skills"), join(appDir, ".agents", "skills")] : []) {
163
+ changed = installSkillsInto(base, { silent }) || changed;
164
+ }
165
+ if (changed && silent) console.log(`skills: refreshed (v${CLI_VERSION})`);
166
+ } catch {
167
+ /* skills are best-effort — never block a command */
168
+ }
169
+ }
170
+
171
+ // ── monty current / select / apps ───────────────────────────────────────────
172
+ // Folder management users never think about: every app lives in ~/Monty,
173
+ // `current` says where you are, `select` prints the folder for cd $(...).
174
+ function findAppRoot(start) {
175
+ let d = start;
176
+ for (;;) {
177
+ if (existsSync(join(d, "monty.config.ts"))) return d;
178
+ const parent = dirname(d);
179
+ if (parent === d) return null;
180
+ d = parent;
181
+ }
182
+ }
183
+
184
+ function readSlugFromConfig(dir) {
185
+ try {
186
+ return /slug:\s*"([^"]+)"/.exec(readFileSync(join(dir, "monty.config.ts"), "utf8"))?.[1] ?? null;
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
191
+
192
+ function listLocalApps() {
193
+ if (!existsSync(MONTY_HOME)) return [];
194
+ return readdirSync(MONTY_HOME)
195
+ .map((name) => join(MONTY_HOME, name))
196
+ .filter((p) => existsSync(join(p, "monty.config.ts")))
197
+ .map((p) => ({ path: p, slug: readSlugFromConfig(p) ?? basename(p) }));
198
+ }
199
+
200
+ function current() {
201
+ const root = findAppRoot(process.cwd());
202
+ if (!root) {
203
+ fail("NOT_IN_APP", `You are not inside a Monty app. \`monty apps\` lists local apps; cd "$(monty select <slug>)" jumps to one.`);
204
+ }
205
+ console.log(`app: ${readSlugFromConfig(root) ?? "?"}`);
206
+ console.log(`path: ${root}`);
207
+ if (!root.startsWith(MONTY_HOME)) {
208
+ console.log(`note: outside ${MONTY_HOME} (fine, but apps normally live there)`);
209
+ }
210
+ }
211
+
212
+ function select() {
213
+ const slug = rest.find((a) => !a.startsWith("--"));
214
+ if (!slug) {
215
+ fail("MISSING_SLUG", `Usage: cd "$(monty select <slug>)" — prints the app's folder.`);
216
+ }
217
+ const apps = listLocalApps();
218
+ const hit = apps.find((a) => a.slug === slug || basename(a.path) === slug);
219
+ if (!hit) {
220
+ const known = apps.map((a) => a.slug).join(", ") || "(none)";
221
+ fail("APP_NOT_LOCAL", `No local source for "${slug}" in ${MONTY_HOME}. Local apps: ${known}. Create it with \`monty create ${slug}\`.`);
222
+ }
223
+ // Bare path on stdout so command substitution works: cd "$(monty select x)"
224
+ console.log(hit.path);
225
+ }
226
+
227
+ function apps() {
228
+ const local = listLocalApps();
229
+ if (!local.length) {
230
+ console.log(`no local apps in ${MONTY_HOME} — create one with \`monty create <slug>\``);
231
+ return;
232
+ }
233
+ for (const a of local) console.log(`${a.slug}\t${a.path}`);
234
+ }
235
+
131
236
  // ── monty create ───────────────────────────────────────────────────────────
132
237
  async function create() {
133
238
  const slug = rest.find((a) => !a.startsWith("--"));
@@ -199,6 +304,7 @@ async function create() {
199
304
  console.log(`config: WARNING — could not reach ${host}/api/config; copy .env.example to .env.local manually`);
200
305
  }
201
306
 
307
+ installSkills({ appDir: target });
202
308
  console.log(`created: ${target}`);
203
309
  console.log(`next: cd ${target} && pnpm install && monty dev`);
204
310
  }
@@ -440,6 +546,9 @@ function walk(dir) {
440
546
  }
441
547
 
442
548
  // ── dispatch ───────────────────────────────────────────────────────────────
549
+ // Keep agent skills fresh on every invocation (user level + current app).
550
+ installSkills({ appDir: findAppRoot(process.cwd()) });
551
+
443
552
  switch (command) {
444
553
  case "login":
445
554
  await login();
@@ -460,17 +569,34 @@ switch (command) {
460
569
  case "docs":
461
570
  await docs();
462
571
  break;
572
+ case "current":
573
+ current();
574
+ break;
575
+ case "select":
576
+ select();
577
+ break;
578
+ case "apps":
579
+ apps();
580
+ break;
581
+ case "skills":
582
+ installSkills({ appDir: findAppRoot(process.cwd()), silent: false });
583
+ console.log("skills: up to date");
584
+ break;
463
585
  case "deploy":
464
586
  await deploy();
465
587
  break;
466
588
  default:
467
- console.log("usage: monty <login|create|dev|add|components|docs|deploy>");
589
+ console.log("usage: monty <login|create|current|select|apps|dev|add|components|docs|deploy|skills>");
468
590
  console.log(" login [--host <url>] [--key <mk_...>] sign in (opens your browser to authorize)");
469
591
  console.log(" create <slug> [--name N] [--icon I] stamp a new app into ~/Monty/<slug>");
470
592
  console.log(" dev [--port 5173] run the app locally (sandboxed data)");
471
593
  console.log(" add <name...> install curated UI components (see `monty components`)");
472
594
  console.log(" components [query] list the curated component catalog");
473
595
  console.log(" docs <name> view a component's source before installing");
596
+ console.log(" current which app folder am I in?");
597
+ console.log(" select <slug> print an app's folder — cd \"$(monty select x)\"");
598
+ console.log(" apps list local apps in ~/Monty");
474
599
  console.log(" deploy build + upload this app");
600
+ console.log(" skills install/refresh the agent build skill");
475
601
  process.exit(command ? 1 : 0);
476
602
  }
@@ -0,0 +1,24 @@
1
+ // Best-effort: put the agent build skill where Claude Code looks for it
2
+ // (~/.claude/skills) as soon as the package lands. Never fails the install;
3
+ // set MONTY_NO_SKILLS=1 to opt out. `monty skills` re-runs this any time.
4
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ try {
10
+ if (process.env.MONTY_NO_SKILLS || process.env.CI) process.exit(0);
11
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
12
+ const src = join(root, "skills");
13
+ if (!existsSync(src)) process.exit(0);
14
+ const version = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version;
15
+ const target = join(homedir(), ".claude", "skills");
16
+ mkdirSync(target, { recursive: true });
17
+ for (const name of readdirSync(src)) {
18
+ cpSync(join(src, name), join(target, name), { recursive: true });
19
+ writeFileSync(join(target, name, "VERSION"), version + "\n");
20
+ }
21
+ console.log(`monty: agent skill installed (~/.claude/skills, v${version})`);
22
+ } catch {
23
+ /* silent — skills are a convenience, not a dependency */
24
+ }
package/package.json CHANGED
@@ -1,20 +1,22 @@
1
1
  {
2
2
  "name": "@montytools/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "monty": "./bin/monty.mjs"
7
7
  },
8
8
  "files": [
9
9
  "bin",
10
- "template"
10
+ "template",
11
+ "skills"
11
12
  ],
12
13
  "engines": {
13
14
  "node": ">=22"
14
15
  },
15
16
  "scripts": {
16
17
  "prepack": "node scripts/bundle-template.mjs",
17
- "typecheck": "node --check bin/monty.mjs"
18
+ "typecheck": "node --check bin/monty.mjs",
19
+ "postinstall": "node bin/postinstall.mjs"
18
20
  },
19
21
  "dependencies": {
20
22
  "esbuild": "^0.28.1"
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: monty-build
3
+ description: Build, run, and deploy Monty workspace apps. Use whenever the task involves a Monty app, monty.config.ts, the monty CLI (create/dev/deploy/add), the @montytools/sdk, or a prompt mentioning usemonty.dev. Covers folder discipline, the build loop, data/auth rules, and error handling.
4
+ ---
5
+
6
+ # Building Monty apps
7
+
8
+ Monty is a work OS: teams get internal apps built by coding agents. You write
9
+ product logic only — data, auth, tenancy, deployment, and embedding are the
10
+ platform's job. The complete contract lives in the app's own `AGENTS.md`
11
+ (nearest-file-wins — read it before writing code). This skill is the map, not
12
+ the territory.
13
+
14
+ ## Rules
15
+
16
+ 1. **Folders are managed.** Apps live in `~/Monty/<slug>`. `monty current`
17
+ tells you where you are; `cd "$(monty select <slug>)"` jumps to an app;
18
+ `monty apps` lists local ones. Never mkdir app folders by hand.
19
+ 2. **The loop:** `monty create <slug> --name "Name" --icon <lucide-icon>` →
20
+ edit `monty.config.ts` (zod tables) + `src/routes/` → verify with
21
+ `monty dev` (localhost:5173, already authenticated, sandboxed data) →
22
+ `monty deploy`. You are done when deploy prints the URL.
23
+ 3. **One import surface:** `@montytools/sdk` (`defineApp`, zod) and
24
+ `@montytools/sdk/react` (hooks: `useList`, `useInsert`, …). Never import
25
+ Clerk or Convex directly; never fetch external APIs from app code — the
26
+ platform CSP blocks them.
27
+ 4. **Schema is zod in `monty.config.ts`.** Field names `_*`, `updatedAt`,
28
+ `createdBy` are reserved. Push happens automatically on dev/deploy.
29
+ 5. **UI is stock shadcn** (preset already wired). Add curated components with
30
+ `monty add <name>`; browse with `monty components` / `monty docs <name>`.
31
+ 6. **Errors are instructions.** Every failure prints
32
+ `[MontyError CODE] Fix: …` — do exactly what the Fix says; don't guess.
33
+ Typecheck failures block deploy by design.
34
+ 7. **Verify before deploy.** `monty dev` writes to a `#dev` sandbox — live
35
+ team records are never touched, so exercise the app for real.
36
+
37
+ ## CLI reference
38
+
39
+ | command | purpose |
40
+ |---|---|
41
+ | `monty login` | browser sign-in (loopback authorize), once per machine |
42
+ | `monty create <slug>` | stamp a new app into `~/Monty/<slug>` |
43
+ | `monty current` / `select` / `apps` | where am I / jump to app / list local |
44
+ | `monty dev` | run locally on :5173, sandboxed data, auto-auth |
45
+ | `monty add <name…>` | install curated shadcn components |
46
+ | `monty deploy` | build + typecheck + upload; app appears in the workspace |
47
+ | `monty skills` | (re)install this skill for your agent |
@@ -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 |