mikoshi-construct 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.
Files changed (3) hide show
  1. package/README.md +52 -3
  2. package/dist/cli.js +103 -59
  3. package/package.json +18 -20
package/README.md CHANGED
@@ -3,8 +3,10 @@
3
3
  Bootstrap for AI-native software projects. Start with a proven engineering workflow instead of an
4
4
  empty repository.
5
5
 
6
- ```
7
- npx mikoshi-construct init
6
+ ```bash
7
+ mkdir my-service && cd my-service
8
+ npx mikoshi-construct init # interactive: preset, agent, project name
9
+ pnpm install && pnpm run quality # green before you write a line
8
10
  ```
9
11
 
10
12
  > **v0.1.** Presets `node-backend`, `node-frontend`, `node-library` and `monorepo`. Claude Code gets
@@ -34,6 +36,52 @@ An existing repository gets the policy, the harness tooling and the agent files,
34
36
  code; its own `AGENTS.md`, `CLAUDE.md`, `package.json` and configs are merged or left alone, and
35
37
  `init` says what still has to be wired by hand.
36
38
 
39
+ ## Usage
40
+
41
+ **A new project.** Name a preset and it writes everything:
42
+
43
+ ```bash
44
+ mkdir my-service && cd my-service
45
+ npx mikoshi-construct init --yes --preset node-backend
46
+ pnpm install && pnpm run quality
47
+ ```
48
+
49
+ **An existing repository.** Look before you write:
50
+
51
+ ```bash
52
+ npx mikoshi-construct init --preset monorepo --dry-run
53
+ ```
54
+
55
+ ```
56
+ ~ package.json (merge)
57
+ = eslint.config.mjs — exists, review manually
58
+ + architecture/principles.md
59
+ + .claude/commands/construct-discover.md
60
+
61
+ Sample sources omitted: the directory is not empty. Discovery maps what is already here.
62
+ ```
63
+
64
+ `+` creates a file that is not there. `~` merges into one that is, inside a
65
+ `construct:begin … construct:end` block or as a JSON merge where your values win. `=` leaves the file
66
+ alone and reports it. No example code lands in a repository that already has some, and there is no
67
+ `--force`.
68
+
69
+ **Cursor, or both agents.** The rules are written once and rendered for each:
70
+
71
+ ```bash
72
+ npx mikoshi-construct init --yes --preset node-frontend --ai both
73
+ ```
74
+
75
+ **Check a repository at any time.**
76
+
77
+ ```bash
78
+ npx mikoshi-construct doctor # baseline intact, harness intact, markers filled
79
+ npx mikoshi-construct soulkill # what the detector sees; writes nothing
80
+ npx mikoshi-construct cost --last
81
+ ```
82
+
83
+ Every command, flag and exit code: [docs/cli.md](https://github.com/E1i/mikoshi-construct/blob/main/docs/cli.md).
84
+
37
85
  ## Three principles
38
86
 
39
87
  1. **CLI detects facts. Agent interprets the system.** Everything that needs understanding of the
@@ -82,7 +130,8 @@ with a named check, not a second fix.
82
130
  | `construct soulkill` | Print what the detector sees, write nothing (`--json`; aliases `inspect`, `capture`) |
83
131
  | `construct cost` | Token usage of the `/implement` runs in this directory, per agent, billable and price-weighted (`--last`, `--json`) |
84
132
 
85
- `--plain` turns off colours and lore for CI. `--johnny` — wake up, Netrunner.
133
+ `--plain` turns off colours and lore for CI. `--johnny` — wake up, Netrunner. The full reference,
134
+ with examples and exit codes, is in [docs/cli.md](https://github.com/E1i/mikoshi-construct/blob/main/docs/cli.md).
86
135
 
87
136
  > If you already keep a user-level `implement` skill in `~/.claude/skills/`, it shadows the one the
88
137
  > construct puts in `.claude/skills/implement/`; move yours aside to run the repository's ladder.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import path13 from "path";
4
+ import path14 from "path";
5
5
  import process4 from "process";
6
6
  import { isTTY } from "@clack/prompts";
7
7
  import { defineCommand, runMain } from "citty";
@@ -269,11 +269,11 @@ function printDoctor(ui2, result) {
269
269
 
270
270
  // src/commands/init.ts
271
271
  import { mkdirSync as mkdirSync2 } from "fs";
272
- import path12 from "path";
272
+ import path13 from "path";
273
273
 
274
274
  // src/detect/index.ts
275
- import { existsSync as existsSync7 } from "fs";
276
- import path7 from "path";
275
+ import { existsSync as existsSync8 } from "fs";
276
+ import path8 from "path";
277
277
  import process from "process";
278
278
 
279
279
  // src/detect/existing.ts
@@ -314,65 +314,109 @@ function detectExisting(dir) {
314
314
  }
315
315
 
316
316
  // src/detect/layout.ts
317
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
317
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
318
+ import path6 from "path";
319
+
320
+ // src/detect/workspaces.ts
321
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
318
322
  import path5 from "path";
319
- var IGNORED_ENTRIES = /* @__PURE__ */ new Set([".git", ".DS_Store", ".gitignore", ".gitattributes", "LICENSE", "README.md", ".idea", ".vscode"]);
320
- function isEmptyDir(dir) {
321
- if (!existsSync5(dir))
322
- return true;
323
- return readdirSync4(dir).every((entry) => IGNORED_ENTRIES.has(entry));
323
+ var TOP_LEVEL_PACKAGES_KEY = /^packages:(.*)$/m;
324
+ var EMPTY_FLOW_SEQUENCE = /^\[\s*\]$/;
325
+ var BLOCK_SEQUENCE_ENTRY = /^[ \t]+-[ \t]*\S/;
326
+ function readIfPresent(file) {
327
+ if (!existsSync5(file))
328
+ return null;
329
+ try {
330
+ return readFileSync4(file, "utf8");
331
+ } catch {
332
+ return null;
333
+ }
334
+ }
335
+ function startsBlockSequence(rest) {
336
+ for (const line of rest.split("\n")) {
337
+ if (line.trim() === "" || line.trimStart().startsWith("#"))
338
+ continue;
339
+ return BLOCK_SEQUENCE_ENTRY.test(line);
340
+ }
341
+ return false;
342
+ }
343
+ function declaresPnpmPackages(dir) {
344
+ const content = readIfPresent(path5.join(dir, "pnpm-workspace.yaml"));
345
+ if (content == null)
346
+ return false;
347
+ const match = TOP_LEVEL_PACKAGES_KEY.exec(content);
348
+ if (match == null)
349
+ return false;
350
+ const inline = match[1].trim();
351
+ if (inline.startsWith("["))
352
+ return !EMPTY_FLOW_SEQUENCE.test(inline);
353
+ if (inline !== "")
354
+ return false;
355
+ return startsBlockSequence(content.slice(match.index + match[0].length));
324
356
  }
325
- function hasWorkspacesField(dir) {
326
- const manifest = path5.join(dir, "package.json");
327
- if (!existsSync5(manifest))
357
+ function declaresNpmWorkspaces(dir) {
358
+ const content = readIfPresent(path5.join(dir, "package.json"));
359
+ if (content == null)
328
360
  return false;
361
+ let workspaces;
329
362
  try {
330
- const parsed = JSON.parse(readFileSync4(manifest, "utf8"));
331
- return parsed.workspaces != null;
363
+ ({ workspaces } = JSON.parse(content));
332
364
  } catch {
333
365
  return false;
334
366
  }
367
+ if (Array.isArray(workspaces))
368
+ return workspaces.length > 0;
369
+ const packages = workspaces?.packages;
370
+ return Array.isArray(packages) && packages.length > 0;
371
+ }
372
+
373
+ // src/detect/layout.ts
374
+ var IGNORED_ENTRIES = /* @__PURE__ */ new Set([".git", ".DS_Store", ".gitignore", ".gitattributes", "LICENSE", "README.md", ".idea", ".vscode"]);
375
+ function isEmptyDir(dir) {
376
+ if (!existsSync6(dir))
377
+ return true;
378
+ return readdirSync4(dir).every((entry) => IGNORED_ENTRIES.has(entry));
335
379
  }
336
380
  function detectMonorepoTools(dir) {
337
381
  const tools = [];
338
- if (existsSync5(path5.join(dir, "pnpm-workspace.yaml")))
382
+ if (declaresPnpmPackages(dir))
339
383
  tools.push("pnpm-workspace");
340
- if (hasWorkspacesField(dir))
384
+ if (declaresNpmWorkspaces(dir))
341
385
  tools.push("npm-workspaces");
342
- if (existsSync5(path5.join(dir, "turbo.json")))
386
+ if (existsSync6(path6.join(dir, "turbo.json")))
343
387
  tools.push("turbo");
344
- if (existsSync5(path5.join(dir, "nx.json")))
388
+ if (existsSync6(path6.join(dir, "nx.json")))
345
389
  tools.push("nx");
346
390
  return tools;
347
391
  }
348
392
  function detectWorkspaceDirs(dir) {
349
- return ["apps", "packages", "libs", "services"].filter((name) => existsSync5(path5.join(dir, name)) && statSync2(path5.join(dir, name)).isDirectory());
393
+ return ["apps", "packages", "libs", "services"].filter((name) => existsSync6(path6.join(dir, name)) && statSync2(path6.join(dir, name)).isDirectory());
350
394
  }
351
395
  function packageName(dir) {
352
396
  try {
353
- const parsed = JSON.parse(readFileSync4(path5.join(dir, "package.json"), "utf8"));
397
+ const parsed = JSON.parse(readFileSync5(path6.join(dir, "package.json"), "utf8"));
354
398
  return typeof parsed.name === "string" && parsed.name !== "" ? parsed.name : null;
355
399
  } catch {
356
400
  return null;
357
401
  }
358
402
  }
359
403
  function detectWorkspacePackages(root, workspaceDirs) {
360
- return workspaceDirs.flatMap((parent) => readdirSync4(path5.join(root, parent)).sort().map((entry) => `${parent}/${entry}`).filter((dir) => existsSync5(path5.join(root, dir, "package.json"))).map((dir) => ({ dir, name: packageName(path5.join(root, dir)) ?? dir.split("/").at(-1) ?? dir })));
404
+ return workspaceDirs.flatMap((parent) => readdirSync4(path6.join(root, parent)).sort().map((entry) => `${parent}/${entry}`).filter((dir) => existsSync6(path6.join(root, dir, "package.json"))).map((dir) => ({ dir, name: packageName(path6.join(root, dir)) ?? dir.split("/").at(-1) ?? dir })));
361
405
  }
362
406
  function detectLayout(dir, monorepoTools, workspaceDirs, hasSrc) {
363
407
  if (isEmptyDir(dir))
364
408
  return "empty";
365
409
  if (monorepoTools.length > 0 || workspaceDirs.length > 0)
366
410
  return "monorepo";
367
- if (hasSrc || existsSync5(path5.join(dir, "package.json")))
411
+ if (hasSrc || existsSync6(path6.join(dir, "package.json")))
368
412
  return "single";
369
413
  return "unknown";
370
414
  }
371
415
 
372
416
  // src/detect/package-manager.ts
373
417
  import { execFileSync } from "child_process";
374
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
375
- import path6 from "path";
418
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
419
+ import path7 from "path";
376
420
  var LOCKFILES = [
377
421
  ["pnpm-lock.yaml", "pnpm"],
378
422
  ["bun.lockb", "bun"],
@@ -381,11 +425,11 @@ var LOCKFILES = [
381
425
  ["package-lock.json", "npm"]
382
426
  ];
383
427
  function fromPackageManagerField(dir) {
384
- const manifest = path6.join(dir, "package.json");
385
- if (!existsSync6(manifest))
428
+ const manifest = path7.join(dir, "package.json");
429
+ if (!existsSync7(manifest))
386
430
  return null;
387
431
  try {
388
- const parsed = JSON.parse(readFileSync5(manifest, "utf8"));
432
+ const parsed = JSON.parse(readFileSync6(manifest, "utf8"));
389
433
  const name = parsed.packageManager?.split("@")[0];
390
434
  return name === "pnpm" || name === "npm" || name === "yarn" || name === "bun" ? name : null;
391
435
  } catch {
@@ -397,10 +441,10 @@ function detectPackageManager(dir) {
397
441
  if (declared != null)
398
442
  return declared;
399
443
  for (const [lockfile, manager] of LOCKFILES) {
400
- if (existsSync6(path6.join(dir, lockfile)))
444
+ if (existsSync7(path7.join(dir, lockfile)))
401
445
  return manager;
402
446
  }
403
- return existsSync6(path6.join(dir, "package.json")) ? "npm" : "none";
447
+ return existsSync7(path7.join(dir, "package.json")) ? "npm" : "none";
404
448
  }
405
449
  var pnpmVersionCache;
406
450
  function detectPnpmVersion() {
@@ -416,10 +460,10 @@ function detectPnpmVersion() {
416
460
 
417
461
  // src/detect/index.ts
418
462
  function detect(dir) {
419
- const root = path7.resolve(dir);
463
+ const root = path8.resolve(dir);
420
464
  const monorepoTools = detectMonorepoTools(root);
421
465
  const workspaceDirs = detectWorkspaceDirs(root);
422
- const hasSrc = existsSync7(path7.join(root, "src"));
466
+ const hasSrc = existsSync8(path8.join(root, "src"));
423
467
  return {
424
468
  dir: root,
425
469
  packageManager: detectPackageManager(root),
@@ -436,14 +480,14 @@ function detect(dir) {
436
480
 
437
481
  // src/materialize/apply.ts
438
482
  import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
439
- import path8 from "path";
483
+ import path9 from "path";
440
484
  function applyPlan(root, ops) {
441
485
  const written = [];
442
486
  for (const op of ops) {
443
487
  if (op.action === "skip")
444
488
  continue;
445
- const absolute = path8.join(root, op.target);
446
- mkdirSync(path8.dirname(absolute), { recursive: true });
489
+ const absolute = path9.join(root, op.target);
490
+ mkdirSync(path9.dirname(absolute), { recursive: true });
447
491
  writeFileSync2(absolute, op.content);
448
492
  written.push(op);
449
493
  }
@@ -451,8 +495,8 @@ function applyPlan(root, ops) {
451
495
  }
452
496
 
453
497
  // src/materialize/plan.ts
454
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
455
- import path10 from "path";
498
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
499
+ import path11 from "path";
456
500
 
457
501
  // src/materialize/rules.ts
458
502
  var CLAUDE_RULES_DIR = ".claude/rules/";
@@ -614,20 +658,20 @@ ${end}
614
658
  }
615
659
 
616
660
  // src/materialize/templates.ts
617
- import { existsSync as existsSync8, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
618
- import path9 from "path";
661
+ import { existsSync as existsSync9, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
662
+ import path10 from "path";
619
663
  import { fileURLToPath } from "url";
620
- var HERE = path9.dirname(fileURLToPath(import.meta.url));
664
+ var HERE = path10.dirname(fileURLToPath(import.meta.url));
621
665
  function templatesRoot() {
622
- const candidates = [path9.resolve(HERE, "../templates"), path9.resolve(HERE, "../../templates")];
623
- const found = candidates.find((candidate) => existsSync8(candidate));
666
+ const candidates = [path10.resolve(HERE, "../templates"), path10.resolve(HERE, "../../templates")];
667
+ const found = candidates.find((candidate) => existsSync9(candidate));
624
668
  if (found == null)
625
669
  throw new Error(`templates directory not found next to ${HERE}`);
626
670
  return found;
627
671
  }
628
672
  var EXISTING_SUFFIX = ".existing.eta";
629
673
  function toTargetPath(relative) {
630
- const segments = relative.split(path9.sep).map((segment) => segment.startsWith("_") ? `.${segment.slice(1)}` : segment);
674
+ const segments = relative.split(path10.sep).map((segment) => segment.startsWith("_") ? `.${segment.slice(1)}` : segment);
631
675
  const joined = segments.join("/");
632
676
  if (joined.endsWith(EXISTING_SUFFIX))
633
677
  return { target: joined.slice(0, -EXISTING_SUFFIX.length), rendered: true, variant: "existing" };
@@ -635,22 +679,22 @@ function toTargetPath(relative) {
635
679
  }
636
680
  function walk(root, current, files) {
637
681
  for (const entry of readdirSync5(current).sort()) {
638
- const absolute = path9.join(current, entry);
682
+ const absolute = path10.join(current, entry);
639
683
  if (statSync3(absolute).isDirectory())
640
684
  walk(root, absolute, files);
641
685
  else if (entry !== ".DS_Store")
642
- files.push(path9.relative(root, absolute));
686
+ files.push(path10.relative(root, absolute));
643
687
  }
644
688
  }
645
689
  function listTemplateFiles(group) {
646
- const root = path9.join(templatesRoot(), group);
647
- if (!existsSync8(root))
690
+ const root = path10.join(templatesRoot(), group);
691
+ if (!existsSync9(root))
648
692
  throw new Error(`template group "${group}" does not exist`);
649
693
  const files = [];
650
694
  walk(root, root, files);
651
695
  return files.map((relative) => {
652
696
  const { target, rendered, variant } = toTargetPath(relative);
653
- return { group, source: path9.join(root, relative), target, rendered, variant };
697
+ return { group, source: path10.join(root, relative), target, rendered, variant };
654
698
  });
655
699
  }
656
700
  var BLOCK = /^[ \t]*\{\{#(if|unless) (\w+)\}\}[ \t]*\n([\s\S]*?)^[ \t]*\{\{\/\1\}\}[ \t]*\n/gm;
@@ -677,7 +721,7 @@ function mountTarget(mount, target) {
677
721
  return mount.into == null || mount.into === "." ? target : `${mount.into.replace(/\/$/, "")}/${target}`;
678
722
  }
679
723
  function readTemplate(source, rendered, vars) {
680
- const raw = readFileSync6(source, "utf8");
724
+ const raw = readFileSync7(source, "utf8");
681
725
  return rendered ? render(raw, vars) : raw;
682
726
  }
683
727
  var SORTED_SECTIONS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
@@ -735,12 +779,12 @@ function layerJson(earlier, later) {
735
779
  var NOT_ADDED_TO_EXISTING_MANIFEST = ["version"];
736
780
  function planOne(root, target, content, conflicts, existingVariant) {
737
781
  const strategy = strategyFor(target);
738
- const absolute = path10.join(root, target);
739
- const exists = existsSync9(absolute);
782
+ const absolute = path11.join(root, target);
783
+ const exists = existsSync10(absolute);
740
784
  if (!exists)
741
785
  return { target, strategy, action: "create", content: strategy === "append-block" ? appendBlock("", content, target) : content };
742
786
  if (strategy === "merge-json") {
743
- const existing = JSON.parse(readFileSync6(absolute, "utf8"));
787
+ const existing = JSON.parse(readFileSync7(absolute, "utf8"));
744
788
  const incoming = JSON.parse(content);
745
789
  for (const key of NOT_ADDED_TO_EXISTING_MANIFEST)
746
790
  delete incoming[key];
@@ -751,7 +795,7 @@ function planOne(root, target, content, conflicts, existingVariant) {
751
795
  ` };
752
796
  }
753
797
  if (strategy === "append-block") {
754
- const existing = readFileSync6(absolute, "utf8");
798
+ const existing = readFileSync7(absolute, "utf8");
755
799
  return { target, strategy, action: "append", content: appendBlock(existing, existingVariant ?? content, target) };
756
800
  }
757
801
  return { target, strategy, action: "skip", content, note: "exists, review manually" };
@@ -987,14 +1031,14 @@ function createClackPrompter(lore, streams = {}) {
987
1031
  }
988
1032
 
989
1033
  // src/version.ts
990
- import { readFileSync as readFileSync7 } from "fs";
991
- import path11 from "path";
1034
+ import { readFileSync as readFileSync8 } from "fs";
1035
+ import path12 from "path";
992
1036
  import { fileURLToPath as fileURLToPath2 } from "url";
993
- var HERE2 = path11.dirname(fileURLToPath2(import.meta.url));
1037
+ var HERE2 = path12.dirname(fileURLToPath2(import.meta.url));
994
1038
  function readVersion() {
995
1039
  for (const candidate of ["../package.json", "../../package.json"]) {
996
1040
  try {
997
- const parsed = JSON.parse(readFileSync7(path11.resolve(HERE2, candidate), "utf8"));
1041
+ const parsed = JSON.parse(readFileSync8(path12.resolve(HERE2, candidate), "utf8"));
998
1042
  if (parsed.name === "mikoshi-construct" && parsed.version != null)
999
1043
  return parsed.version;
1000
1044
  } catch {
@@ -1094,7 +1138,7 @@ async function askChoices(ui2, options, report, root, prompter) {
1094
1138
  return { presetId, ai, projectName, review };
1095
1139
  }
1096
1140
  async function runInit(ui2, options, prompter) {
1097
- const root = path12.resolve(options.dir);
1141
+ const root = path13.resolve(options.dir);
1098
1142
  mkdirSync2(root, { recursive: true });
1099
1143
  if (!options.yes && prompter == null) {
1100
1144
  ui2.glitch(ui2.lore.needsTerminal);
@@ -1462,7 +1506,7 @@ var cost = defineCommand({
1462
1506
  json: { type: "boolean", description: "Machine-readable report", default: false }
1463
1507
  },
1464
1508
  run({ args }) {
1465
- const runs = collectWorkflowRuns(path13.resolve(args.dir));
1509
+ const runs = collectWorkflowRuns(path14.resolve(args.dir));
1466
1510
  if (args.json) {
1467
1511
  const selected = runs == null ? null : args.last ? runs.slice(-1) : runs;
1468
1512
  process4.stdout.write(`${JSON.stringify(selected, null, 2)}
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "mikoshi-construct",
3
3
  "type": "module",
4
- "version": "0.1.1",
5
- "packageManager": "pnpm@12.4.2",
4
+ "version": "0.1.3",
6
5
  "description": "Bootstrap for AI-native software projects. Start with a proven engineering workflow instead of an empty repository.",
7
6
  "author": "Eli Tabrisov",
8
7
  "license": "MIT",
@@ -36,23 +35,6 @@
36
35
  "engines": {
37
36
  "node": ">=20"
38
37
  },
39
- "scripts": {
40
- "build": "tsup",
41
- "dev": "tsx src/cli.ts",
42
- "lint": "eslint .",
43
- "lint:fix": "eslint --fix .",
44
- "typecheck": "tsc --noEmit",
45
- "test": "vitest run",
46
- "test:watch": "vitest",
47
- "quality": "pnpm composition:check && pnpm lint && pnpm typecheck && pnpm test",
48
- "prepublishOnly": "pnpm build",
49
- "composition:render": "tsx scripts/composition/sync-docs.ts",
50
- "composition:check": "tsx scripts/composition/check.ts",
51
- "ci": "pnpm run quality",
52
- "changeset": "changeset",
53
- "version-packages": "changeset version",
54
- "release": "pnpm build && npm publish --provenance --access public"
55
- },
56
38
  "dependencies": {
57
39
  "@clack/prompts": "^1.8.0",
58
40
  "citty": "^0.2.2",
@@ -69,5 +51,21 @@
69
51
  "typescript": "^5.9.3",
70
52
  "vitest": "^5.0.0",
71
53
  "yaml": "^2.9.1"
54
+ },
55
+ "scripts": {
56
+ "build": "tsup",
57
+ "dev": "tsx src/cli.ts",
58
+ "lint": "eslint .",
59
+ "lint:fix": "eslint --fix .",
60
+ "typecheck": "tsc --noEmit",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest",
63
+ "quality": "pnpm composition:check && pnpm lint && pnpm typecheck && pnpm test",
64
+ "composition:render": "tsx scripts/composition/sync-docs.ts",
65
+ "composition:check": "tsx scripts/composition/check.ts",
66
+ "ci": "pnpm run quality",
67
+ "changeset": "changeset",
68
+ "version-packages": "changeset version",
69
+ "release": "pnpm build && changeset publish"
72
70
  }
73
- }
71
+ }