@montytools/cli 0.5.4 → 0.5.6

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/lib/compile.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // The ONE config-compile pipeline: esbuild-bundle a temp entry that runs the
2
2
  // app's OWN compileApp/zod/sdk instances on monty.config.ts, execute it in a
3
- // subprocess, parse the emitted metadata. Used by `monty dev`/`monty deploy`
3
+ // subprocess, parse the emitted metadata. Used by `monty dev`/`monty save`
4
4
  // (bin/monty.mjs) and the demo harness (scripts/demo.mjs) — one pipeline, so
5
5
  // what a demo installs is byte-for-byte what a deploy would send.
6
6
  import { spawnSync } from "node:child_process";
@@ -19,7 +19,7 @@ export class CompileError extends Error {
19
19
  // Throws CompileError:
20
20
  // - CONFIG_BUNDLE_FAILED — esbuild could not bundle (usually unlinked deps)
21
21
  // - CONFIG_COMPILE_FAILED — the config threw while loading
22
- export async function compileAppConfig(appDir, { forceManifest = false } = {}) {
22
+ export async function compileAppConfig(appDir) {
23
23
  appDir = resolve(appDir); // esbuild requires an absolute absWorkingDir
24
24
  // esbuild is a dependency of THIS package (@montytools/cli), so resolution
25
25
  // from here works for any caller — no per-script resolution dance.
@@ -31,7 +31,7 @@ export async function compileAppConfig(appDir, { forceManifest = false } = {}) {
31
31
  writeFileSync(entry, [
32
32
  `import { app } from "../monty.config";`,
33
33
  `import { compileApp } from "@montytools/sdk/compile";`,
34
- `process.stdout.write(JSON.stringify(compileApp(app, { forceManifest: ${forceManifest} })));`,
34
+ `process.stdout.write(JSON.stringify(compileApp(app)));`,
35
35
  ].join("\n"));
36
36
  try {
37
37
  await build({
@@ -220,9 +220,13 @@ export function manifestToConfig(manifest, { name, icon } = {}) {
220
220
  ...sdkImports.map((n) => ` ${n},`),
221
221
  `} from "@montytools/sdk";`,
222
222
  ``,
223
- `// Regenerated by \`monty schema pull\` from the app's stored manifest.`,
224
- `// Edit freely \`monty dev\` / \`monty deploy\` push changes back; another`,
225
- `// editor's remote changes surface as MANIFEST_DRIFT (then pull again).`,
223
+ `// GENERATED by Monty from the app's config stored in the workspace.`,
224
+ `// DO NOT EDIT: edits here never land anywhere, and this file is`,
225
+ `// overwritten on every sync (a running \`monty dev\` regenerates it within`,
226
+ `// a heartbeat of a remote change; \`monty save\` and \`monty schema pull\``,
227
+ `// refresh it too). Change the app through the doors instead:`,
228
+ `// \`monty schema set\`, the MCP schema_update tool, or Configuration.`,
229
+ `// Import { app } from it for typed SDK hooks — that part is yours.`,
226
230
  `export const app = defineApp({`,
227
231
  ` slug: ${JSON.stringify(manifest.slug)},`,
228
232
  ...(name ? [` name: ${JSON.stringify(name)},`] : []),
@@ -1,16 +1,17 @@
1
- // `monty schema pull` — regenerate monty.config.ts from the app's stored
2
- // Live manifest (the schema-as-data flow: another agent may have edited the
3
- // schema via the API/MCP; this brings the code checkout back in sync).
4
- // Distinct from `monty pull`, which restores the whole SOURCE SNAPSHOT.
1
+ // `monty schema pull` — regenerate src/monty.gen.ts from the app's stored
2
+ // manifest (the schema-as-data flow: the workspace owns the config; this
3
+ // file is its generated, never-hand-edited mirror, giving app code typed
4
+ // SDK hooks and the runtime zod schemas). Distinct from `monty pull`,
5
+ // which restores the whole SOURCE SNAPSHOT.
5
6
  //
6
- // Safety: refuses when the local config has schema changes that never
7
- // reached the registry (compile-and-compare against the base hash recorded
8
- // in .monty/schema.json) "deploy or discard", like git with a dirty tree.
9
- // The old file is backed up beside the new one on every overwrite.
7
+ // No dirty check, no backup: the gen module is not an editor — nothing in
8
+ // it can be "local changes" so regeneration is a plain overwrite. The
9
+ // same call runs from the dev-session heartbeat (a remote schema edit
10
+ // lands here within ~30s) and from `monty save`/`monty connect`.
10
11
 
11
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
13
  import { join } from "node:path";
13
- import { manifestHash, manifestToConfig } from "./schemaCodegen.mjs";
14
+ import { manifestToConfig } from "./schemaCodegen.mjs";
14
15
 
15
16
  const STATE_FILE = ["schema.json"]; // .monty/schema.json
16
17
 
@@ -31,7 +32,20 @@ export function writeSchemaState(appDir, hash) {
31
32
  writeFileSync(join(dir, ...STATE_FILE), JSON.stringify({ hash, syncedAt: Date.now() }) + "\n");
32
33
  }
33
34
 
34
- export async function schemaPull({ appDir, host, key, slug, force, compileAppConfig, fail }) {
35
+ /** Write src/monty.gen.ts from a manifest. Only apps with code get the
36
+ * module (a config-only folder has nothing that could import it). Returns
37
+ * whether a file was written. */
38
+ export function writeGenModule(appDir, manifest, { name, icon } = {}) {
39
+ const srcDir = join(appDir, "src");
40
+ if (!existsSync(srcDir)) return false;
41
+ writeFileSync(
42
+ join(srcDir, "monty.gen.ts"),
43
+ manifestToConfig(manifest, { name, icon: icon ?? undefined }),
44
+ );
45
+ return true;
46
+ }
47
+
48
+ export async function schemaPull({ appDir, host, key, slug, quiet, fail }) {
35
49
  const res = await fetch(`${host}/api/schema?slug=${encodeURIComponent(slug)}`, {
36
50
  headers: { authorization: `Bearer ${key}` },
37
51
  });
@@ -39,45 +53,17 @@ export async function schemaPull({ appDir, host, key, slug, force, compileAppCon
39
53
  if (!res.ok || !body?.ok) {
40
54
  fail(body?.code ?? `HTTP_${res.status}`, body?.fix ?? "Could not fetch the app's schema — check the connection and `monty login`.");
41
55
  }
42
- if (!body.manifest) {
43
- fail(
44
- "NO_MANIFEST",
45
- `"${slug}" has no stored App Manifest (it is a V1 app or has never pushed one). Author monty.config.ts with V2 features and run \`monty dev\` or \`monty deploy\` first.`,
56
+ // An empty registry config is still a config — generate from it.
57
+ const manifest = body.manifest ?? { slug, tables: {} };
58
+ const wrote = writeGenModule(appDir, manifest, { name: body.name, icon: body.icon });
59
+ if (body.hash) writeSchemaState(appDir, body.hash);
60
+ if (!quiet) {
61
+ console.log(
62
+ wrote
63
+ ? `schema: pulled "${slug}" (${Object.keys(manifest.tables ?? {}).length} tables) -> src/monty.gen.ts`
64
+ : `schema: "${slug}" synced (config-only app — no code, no gen module; read it with \`monty schema\`)`,
46
65
  );
66
+ if (body.hash) console.log(`base: ${body.hash.slice(0, 12)} (.monty/schema.json — the CAS base for the next push)`);
47
67
  }
48
-
49
- const configPath = join(appDir, "monty.config.ts");
50
- if (existsSync(configPath) && !force) {
51
- // Dirty check: does the local config compile to the manifest this
52
- // checkout last synced? If not, pulling would clobber local edits.
53
- const state = readSchemaState(appDir);
54
- let localHash = null;
55
- try {
56
- const compiled = await compileAppConfig(appDir);
57
- localHash = compiled.manifest ? manifestHash(compiled.manifest) : null;
58
- } catch {
59
- // A config that doesn't compile can't be proven clean — refuse without
60
- // --force rather than silently discarding whatever it holds.
61
- fail(
62
- "SCHEMA_DIRTY",
63
- "monty.config.ts does not compile, so local schema edits cannot be verified against the registry. Fix it and deploy, or re-run with --force to REPLACE it (a .bak is kept).",
64
- );
65
- }
66
- const cleanAgainst = state?.hash ?? body.hash;
67
- if (localHash !== null && localHash !== cleanAgainst && localHash !== body.hash) {
68
- fail(
69
- "SCHEMA_DIRTY",
70
- "monty.config.ts has schema changes that never reached the registry. Push them first (`monty dev` save or `monty deploy`), or discard them with --force (a .bak is kept).",
71
- );
72
- }
73
- }
74
-
75
- if (existsSync(configPath)) {
76
- renameSync(configPath, `${configPath}.bak`);
77
- console.log(`backup: monty.config.ts.bak`);
78
- }
79
- writeFileSync(configPath, manifestToConfig(body.manifest, { name: body.name, icon: body.icon ?? undefined }));
80
- writeSchemaState(appDir, body.hash);
81
- console.log(`schema: pulled "${slug}" (${Object.keys(body.manifest.tables).length} tables) -> monty.config.ts`);
82
- console.log(`base: ${body.hash.slice(0, 12)} (.monty/schema.json — the CAS base for the next push)`);
68
+ return { wrote, hash: body.hash ?? null, manifest };
83
69
  }
@@ -0,0 +1,175 @@
1
+ // The Monty style lint — the platform's stylelint-polaris equivalent for
2
+ // Tailwind class strings. Rules are disallow-lists grouped by category with
3
+ // an instruction message per finding (Polaris's coverage-plugin shape); the
4
+ // allowed vocabularies are IMPORTED from @monty/tokens/manifest.mjs so the
5
+ // lint can never drift from the tokens. It runs blocking inside `monty save`
6
+ // (beside the typecheck), advisory at `monty dev`, and standalone as
7
+ // `monty style`.
8
+ //
9
+ // Scope: agent-authored app code only — src/**/*.{ts,tsx,jsx} minus the
10
+ // vendored kit (src/components/ui/**), generated files, and wiring. A line
11
+ // carrying `monty-style-ignore` is skipped (the stylelint-disable of this
12
+ // system; use it for the rare sanctioned exception, never to silence a page).
13
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
14
+ import { dirname, join, relative } from "node:path";
15
+ import { fileURLToPath, pathToFileURL } from "node:url";
16
+
17
+ const HERE = dirname(fileURLToPath(import.meta.url));
18
+
19
+ let manifestPromise;
20
+ function loadManifest() {
21
+ // Published tarball: prepack copies the manifest beside this file.
22
+ // Repo checkout: import straight from packages/tokens — the dependency
23
+ // direction that keeps rules derived from tokens.
24
+ manifestPromise ??= (async () => {
25
+ for (const p of [
26
+ join(HERE, "tokens-manifest.mjs"),
27
+ join(HERE, "../../tokens/manifest.mjs"),
28
+ ]) {
29
+ if (existsSync(p)) return import(pathToFileURL(p).href);
30
+ }
31
+ return null;
32
+ })();
33
+ return manifestPromise;
34
+ }
35
+
36
+ const SIDE = "(?:-(?:t|b|l|r|tl|tr|bl|br|s|e|ss|se|es|ee))?";
37
+ const COLOR_PROPS = "(?:bg|text|border|ring|outline|fill|stroke|from|via|to|divide|decoration|caret|accent)";
38
+
39
+ function buildRules(m) {
40
+ const hues = m.PALETTE_HUES.join("|");
41
+ return [
42
+ {
43
+ category: "type",
44
+ // Arbitrary sizes only — arbitrary COLOR values on text- belong to color.
45
+ re: /\btext-\[(?!#|rgb|hsl|oklch|var\(|color-mix)[^\]]+\]/g,
46
+ fix: () => "use the ladder (text-body is the default)",
47
+ },
48
+ {
49
+ category: "type",
50
+ re: /\btext-(?:xs|sm|base|lg|xl|[2-9]xl)\b/g,
51
+ fix: (got) => `use ${m.TEXT_SUGGESTIONS[got] ?? "the ladder"}`,
52
+ },
53
+ {
54
+ category: "weight",
55
+ re: /\bfont-(?:thin|extralight|light|bold|extrabold|black)\b/g,
56
+ fix: () => "use font-medium (or the weight the ladder style carries)",
57
+ },
58
+ {
59
+ category: "color",
60
+ re: new RegExp(`\\b${COLOR_PROPS}-\\[(?:#|rgb|hsl|oklch|var\\(|color-mix)[^\\]]*\\]`, "g"),
61
+ fix: () => "use a semantic token class",
62
+ },
63
+ {
64
+ category: "color",
65
+ re: new RegExp(`\\b${COLOR_PROPS}-(?:${hues})-\\d{2,3}(?:/\\d{1,3})?\\b`, "g"),
66
+ fix: (got) => {
67
+ const hue = got.match(new RegExp(`-(${hues})-`))?.[1];
68
+ return `use ${m.HUE_SUGGESTIONS[hue] ?? "a semantic token"}`;
69
+ },
70
+ },
71
+ {
72
+ category: "color",
73
+ re: /["'`]#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?["'`]/g,
74
+ fix: () => "use var(--chart-1…5) or a semantic class",
75
+ },
76
+ {
77
+ category: "radius",
78
+ re: new RegExp(`\\brounded${SIDE}-\\[[^\\]]+\\]`, "g"),
79
+ fix: () => "corners are sharp — delete it",
80
+ },
81
+ {
82
+ category: "radius",
83
+ re: new RegExp(`\\brounded${SIDE}-(?:xs|sm|md|lg|xl|[234]xl)\\b`, "g"),
84
+ fix: () => "corners are sharp — delete it (rounded-full only for dots/avatars)",
85
+ },
86
+ {
87
+ category: "elevation",
88
+ re: /\bshadow-\[[^\]]+\]/g,
89
+ fix: () => `use ${m.SHADOWS_ALLOWED.slice(0, 3).join("/")}`,
90
+ },
91
+ {
92
+ category: "elevation",
93
+ re: /\bshadow-(?:2xs|xs|sm|md|lg|xl|2xl|inner)\b/g,
94
+ fix: () => `use ${m.SHADOWS_ALLOWED.slice(0, 3).join("/")}`,
95
+ },
96
+ ];
97
+ }
98
+
99
+ const SKIP_FILES = new Set(["main.tsx", "vite-env.d.ts", "monty.gen.ts", "routeTree.gen.ts"]);
100
+
101
+ function* sourceFiles(dir, base = dir) {
102
+ let entries;
103
+ try {
104
+ entries = readdirSync(dir, { withFileTypes: true });
105
+ } catch {
106
+ return;
107
+ }
108
+ for (const entry of entries) {
109
+ const p = join(dir, entry.name);
110
+ const rel = relative(base, p);
111
+ if (entry.isDirectory()) {
112
+ // The vendored kit is template/registry-owned — its internals may
113
+ // legitimately carry classes agents must not write.
114
+ if (rel === join("components", "ui") || entry.name === "node_modules") continue;
115
+ yield* sourceFiles(p, base);
116
+ } else if (/\.(?:tsx|jsx|ts)$/.test(entry.name)) {
117
+ if (SKIP_FILES.has(entry.name) || entry.name.endsWith(".gen.ts")) continue;
118
+ yield p;
119
+ }
120
+ }
121
+ }
122
+
123
+ /** Scan an app folder. Returns [{ file, line, category, got, fix }] sorted
124
+ * by file then line; [] when clean (or when src/ doesn't exist —
125
+ * config-only apps have nothing to lint). */
126
+ export async function lintStyles(appDir) {
127
+ const manifest = await loadManifest();
128
+ if (!manifest) return []; // never block a save on a broken lint install
129
+ const rules = buildRules(manifest);
130
+ const src = join(appDir, "src");
131
+ const violations = [];
132
+ for (const file of sourceFiles(src)) {
133
+ const rel = join("src", relative(src, file));
134
+ const lines = readFileSync(file, "utf8").split("\n");
135
+ lines.forEach((text, i) => {
136
+ if (text.includes("monty-style-ignore")) return;
137
+ for (const rule of rules) {
138
+ for (const match of text.matchAll(rule.re)) {
139
+ violations.push({
140
+ file: rel,
141
+ line: i + 1,
142
+ category: rule.category,
143
+ got: match[0],
144
+ fix: rule.fix(match[0]),
145
+ });
146
+ }
147
+ }
148
+ });
149
+ }
150
+ return violations.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
151
+ }
152
+
153
+ const MAX_LINES = 40;
154
+
155
+ /** Findings grouped by category, each under its token-vocabulary lede. */
156
+ export async function formatViolations(violations) {
157
+ const manifest = await loadManifest();
158
+ const byCategory = new Map();
159
+ for (const v of violations) {
160
+ if (!byCategory.has(v.category)) byCategory.set(v.category, []);
161
+ byCategory.get(v.category).push(v);
162
+ }
163
+ const out = [];
164
+ let shown = 0;
165
+ for (const [category, list] of byCategory) {
166
+ out.push(`${category} — ${manifest?.CATEGORY_MESSAGES?.[category] ?? ""}`);
167
+ for (const v of list) {
168
+ if (shown >= MAX_LINES) break;
169
+ out.push(` ${v.file}:${v.line} ${v.got} → ${v.fix}`);
170
+ shown++;
171
+ }
172
+ }
173
+ if (violations.length > shown) out.push(` … and ${violations.length - shown} more`);
174
+ return out.join("\n");
175
+ }
@@ -0,0 +1,90 @@
1
+ // The machine-readable half of the Monty design tokens (tokens.css is the
2
+ // stylesheet). The style lint in the CLI derives its allowed lists and
3
+ // suggestions from HERE — Polaris's pattern (stylelint-polaris imports
4
+ // polaris-tokens) — so the rules can never drift from the tokens.
5
+ // Keep this file in lockstep with tokens.css.
6
+
7
+ /** The six text styles a Monty page uses. */
8
+ export const TEXT_LADDER = [
9
+ "text-tick",
10
+ "text-meta",
11
+ "text-body",
12
+ "text-title",
13
+ "text-heading",
14
+ "text-stat",
15
+ ];
16
+
17
+ /** What each off-ladder Tailwind size maps to. */
18
+ export const TEXT_SUGGESTIONS = {
19
+ "text-xs": "text-meta",
20
+ "text-sm": "text-body (content) or text-title (titles)",
21
+ "text-base": "text-body",
22
+ "text-lg": "text-heading",
23
+ "text-xl": "text-heading",
24
+ "text-2xl": "text-stat",
25
+ "text-3xl": "text-stat",
26
+ "text-4xl": "text-stat",
27
+ "text-5xl": "text-stat",
28
+ "text-6xl": "text-stat",
29
+ "text-7xl": "text-stat",
30
+ "text-8xl": "text-stat",
31
+ "text-9xl": "text-stat",
32
+ };
33
+
34
+ /** Allowed font weights (400 default / 500 titled / 600 via heading+stat). */
35
+ export const WEIGHTS_ALLOWED = ["font-normal", "font-medium", "font-semibold"];
36
+
37
+ /** The radius policy: sharp everywhere; these are the only radius classes. */
38
+ export const RADIUS_ALLOWED = [
39
+ "rounded-none",
40
+ "rounded-full",
41
+ "rounded-control",
42
+ "rounded-control-sm",
43
+ "rounded-overlay",
44
+ ];
45
+
46
+ /** The only three shadows (plus shadow-none). */
47
+ export const SHADOWS_ALLOWED = ["shadow-control", "shadow-btn-primary", "shadow-menu", "shadow-none"];
48
+
49
+ /** Tailwind default palette hues — banned; the palette is the platform's. */
50
+ export const PALETTE_HUES = [
51
+ "slate", "gray", "zinc", "neutral", "stone",
52
+ "red", "orange", "amber", "yellow", "lime",
53
+ "green", "emerald", "teal", "cyan", "sky",
54
+ "blue", "indigo", "violet", "purple", "fuchsia", "pink", "rose",
55
+ ];
56
+
57
+ /** Where a banned hue should point instead. */
58
+ export const HUE_SUGGESTIONS = {
59
+ red: "text-destructive / bg-destructive",
60
+ orange: "text-warning",
61
+ amber: "text-warning",
62
+ yellow: "text-warning",
63
+ lime: "text-success",
64
+ green: "text-success",
65
+ emerald: "text-success",
66
+ teal: "text-success",
67
+ blue: "bg-primary / text-link",
68
+ sky: "text-link",
69
+ indigo: "bg-primary",
70
+ cyan: "text-link",
71
+ violet: "var(--chart-3)",
72
+ purple: "var(--chart-3)",
73
+ fuchsia: "var(--chart-3)",
74
+ pink: "var(--chart-3)",
75
+ rose: "text-destructive",
76
+ slate: "text-muted-foreground / bg-accent",
77
+ gray: "text-muted-foreground / bg-accent",
78
+ zinc: "text-muted-foreground / bg-accent",
79
+ neutral: "text-muted-foreground / bg-accent",
80
+ stone: "text-muted-foreground / bg-accent",
81
+ };
82
+
83
+ /** Category ledes — the default instruction on each violation class. */
84
+ export const CATEGORY_MESSAGES = {
85
+ type: "The type ladder is text-tick/meta/body/title/heading/stat; most text needs no size class at all (text-body is on <body>).",
86
+ weight: "Weights are 400/500/600 — font-medium for titled text; 600 arrives via text-heading/text-stat.",
87
+ color: "Colors come from the semantic tokens (bg-background, text-muted-foreground, var(--chart-2), …) — the palette is the platform's.",
88
+ radius: "Corners are sharp by policy; rounded-full (dots, avatars) is the one exception.",
89
+ elevation: "Elevation is shadow-control (rest controls), shadow-btn-primary (the one primary action), or shadow-menu (overlays).",
90
+ };
package/lib/views.mjs CHANGED
@@ -107,8 +107,25 @@ export function parseHiddenColumns(input) {
107
107
  return [...new Set(input.split(",").map((field) => field.trim()).filter(Boolean))];
108
108
  }
109
109
 
110
+ /** `--kanban <field>` makes the view a kanban laned by that select field's
111
+ * values; `--kanban none` makes it a table. */
112
+ export function parseKanbanFlag(input) {
113
+ if (input === undefined) return undefined;
114
+ if (input === "none") return null;
115
+ const groupBy = input.trim();
116
+ if (groupBy === "") {
117
+ badViewConfig('--kanban needs a select field name, or "none" to make the view a table.');
118
+ }
119
+ return { type: "kanban", groupBy };
120
+ }
121
+
110
122
  export function mergeViewConfig(existing, patch) {
111
123
  const base = isObject(existing) ? existing : {};
124
+ const kanban = Object.hasOwn(patch, "kanban")
125
+ ? patch.kanban
126
+ : base.type === "kanban" && typeof base.groupBy === "string"
127
+ ? { type: "kanban", groupBy: base.groupBy }
128
+ : null;
112
129
  return {
113
130
  filters: Object.hasOwn(patch, "filters")
114
131
  ? patch.filters
@@ -117,6 +134,7 @@ export function mergeViewConfig(existing, patch) {
117
134
  hidden: Object.hasOwn(patch, "hidden")
118
135
  ? patch.hidden
119
136
  : (Array.isArray(base.hidden) ? base.hidden : []),
137
+ ...(kanban ? { type: "kanban", groupBy: kanban.groupBy } : {}),
120
138
  };
121
139
  }
122
140
 
@@ -126,6 +144,7 @@ export function validateViewColumns(config, fieldNames) {
126
144
  ...Object.keys(config.filters),
127
145
  ...(config.sort ? [config.sort.column] : []),
128
146
  ...config.hidden,
147
+ ...(config.type === "kanban" && config.groupBy ? [config.groupBy] : []),
129
148
  ];
130
149
  const unknown = [...new Set(used.filter((field) => !known.has(field)))];
131
150
  if (unknown.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@montytools/cli",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/TomasMonty/monty-v2.git",
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "scripts": {
23
23
  "prepack": "node scripts/bundle-template.mjs",
24
- "typecheck": "node --check bin/monty.mjs && node --check lib/compile.mjs && node --check lib/schemaCodegen.mjs && node --check lib/schemaPull.mjs && node --check lib/views.mjs && node --check scripts/schema-roundtrip.mjs",
25
- "test": "node --test test/views.test.mjs",
24
+ "typecheck": "node --check bin/monty.mjs && node --check lib/compile.mjs && node --check lib/schemaCodegen.mjs && node --check lib/schemaPull.mjs && node --check lib/views.mjs && node --check scripts/schema-roundtrip.mjs && node --check lib/styleLint.mjs",
25
+ "test": "node --test \"test/*.test.mjs\"",
26
26
  "postinstall": "node bin/postinstall.mjs",
27
27
  "test:roundtrip": "node scripts/schema-roundtrip.mjs"
28
28
  },