create-geonosis 1.3.0 → 2.0.0

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/README.md CHANGED
@@ -68,6 +68,36 @@ verify. It is done by hand because neither package manager can do it: pnpm resol
68
68
  and bun refuses both `link:` and `file:` here, because it follows into the target and tries to
69
69
  resolve the `workspace:*` specifiers the kit's own packages use.
70
70
 
71
+ ## `--check` — what an existing repo never received
72
+
73
+ ```bash
74
+ npx create-geonosis . --check --runtime pnpm --backend medusa --ui tiered-shadcn
75
+ ```
76
+
77
+ A repo is scaffolded once and the scaffold keeps learning, so the divergence is permanent and
78
+ one-way. `--check` closes the loop in the only direction that is safe: it says what is missing and
79
+ **writes nothing** — no file created, no file opened for writing, no `writeScaffold` on the path at
80
+ all. Exit 0 when nothing drifted, 1 with a line per drift.
81
+
82
+ Two things are compared and two are deliberately not:
83
+
84
+ - **a file the scaffold writes that the repo has never had** — drift;
85
+ - **a key inside a JSON file the repo does have** — drift, named as `file → a.b.c`;
86
+ - a file's **contents** — never. A repo that has been worked in has rewritten them and was entitled to;
87
+ - a **value** — never. `doctor.corpus` pointing somewhere else is that repo's answer, not a drift.
88
+
89
+ **The workspaces are out of the question.** `packages/core/src/money.ts` is a sample that makes a
90
+ new repo compile, not a shape a repo must hold. Measured over dielime: including them turned 24 real
91
+ findings into 52, thirty of which were about a `packages/ui` that repo does not have.
92
+
93
+ Over dielime — a repo scaffolded before D-054 moved the law — it reads 24, among them `LAW.md`,
94
+ `geonosis.json → law.file`, three architecture rules the plugin has since added, and
95
+ `gate-baseline.json`. `proofs/024-W31a-create-check/` has the whole run and the before/after hash.
96
+
97
+ A JSON file the repo has that is not plain JSON — dielime's `tsconfig.json` carries `//` comments,
98
+ legal JSONC — is reported rather than skipped. A check that could not read a file has not passed
99
+ over it.
100
+
71
101
  ## Proven, not claimed
72
102
 
73
103
  A repo scaffolded on **both** runtimes passes its own `geonosis-verify full` in a temp directory —
@@ -1,3 +1,116 @@
1
+ // src/composition.ts
2
+ var PLATFORM_FILE = "src/platform.ts";
3
+ var SEARCH = {
4
+ at: "domains/search",
5
+ built: [
6
+ " search: pgFtsSearchIndex({",
7
+ " executor: tx,",
8
+ " nowMs: definitions.nowMs,",
9
+ " projections: SEARCH_PROJECTIONS,",
10
+ " }),"
11
+ ],
12
+ declarations: [
13
+ "/** How each kind this repo indexes becomes searchable text \u2014 declared once, never per call. */",
14
+ "const SEARCH_PROJECTIONS: SearchProjections<'note'> = {",
15
+ " note: {",
16
+ " body: (fields) => fields.notes ?? '',",
17
+ " title: (fields) => fields.heading ?? '',",
18
+ " },",
19
+ "}"
20
+ ],
21
+ field: "search",
22
+ fieldType: "SearchIndex<'note'>",
23
+ imports: [
24
+ "import { pgFtsSearchIndex, type SearchIndex, type SearchProjections } from '@geonosis/search'"
25
+ ],
26
+ name: "@geonosis/search",
27
+ version: "0.1.1"
28
+ };
29
+ var DOMAINS = [SEARCH];
30
+ var DOMAIN_NAMES = DOMAINS.map((one) => one.field);
31
+ var FOUNDATIONS = [
32
+ { at: "foundations/db", name: "@geonosis/db", version: "0.2.0" },
33
+ { at: "foundations/events", name: "@geonosis/events", version: "1.5.0" }
34
+ ];
35
+ var domainsNamed = (named) => named.map((one) => {
36
+ const found = DOMAINS.find((domain) => domain.field === one);
37
+ if (found === void 0) {
38
+ throw new Error(
39
+ `--domains "${one}" is not a domain this kit ships \u2014 it ships ${DOMAIN_NAMES.join(", ")}. A domain nothing publishes cannot be composed, and a scaffold that wrote the import anyway would leave a repo that does not install.`
40
+ );
41
+ }
42
+ return found;
43
+ });
44
+ var floorDependencies = (named) => named.length === 0 ? {} : Object.fromEntries(
45
+ [...FOUNDATIONS, ...domainsNamed(named)].map((one) => [one.name, `^${one.version}`]).toSorted(([a], [b]) => String(a).localeCompare(String(b)))
46
+ );
47
+ var floorDirectories = (named) => named.length === 0 ? [] : [...FOUNDATIONS, ...domainsNamed(named)].map((one) => one.at);
48
+ var platformSource = (named) => {
49
+ const domains = domainsNamed(named);
50
+ return [
51
+ "import { createSessionSeam, type Executor, type SessionSeam } from '@geonosis/db'",
52
+ "import { createEmitDoor, type EmitDoor, type Extensions } from '@geonosis/events'",
53
+ ...domains.flatMap((one) => one.imports),
54
+ "",
55
+ "/** Every name a foundation cannot pick for this repo, answered here and only here. */",
56
+ "export type PlatformConfig = {",
57
+ " newId: () => string",
58
+ " nowMs: () => number",
59
+ " /** What maintenance sets to see every tenant, through a lever no request path reaches. */",
60
+ " opsSetting: string",
61
+ " /** The producer every envelope names (D-057) \u2014 spelled here, never at an emit call. */",
62
+ " source: string",
63
+ " /** What a transaction sets to name its tenant, and what the RLS policies read. */",
64
+ " tenantSetting: string",
65
+ "}",
66
+ "",
67
+ "export type Definitions = {",
68
+ " emit: EmitDoor<Extensions>",
69
+ " nowMs: () => number",
70
+ " session: SessionSeam",
71
+ "}",
72
+ "",
73
+ "/** Once per isolate: what this platform is, before anybody has asked it anything. */",
74
+ "export const definitionsOf = (config: PlatformConfig): Definitions => ({",
75
+ " emit: createEmitDoor({",
76
+ " extensions: [],",
77
+ " newId: config.newId,",
78
+ " now: config.nowMs,",
79
+ " source: config.source,",
80
+ " }),",
81
+ " nowMs: config.nowMs,",
82
+ " session: createSessionSeam({",
83
+ " settings: { opsSetting: config.opsSetting, tenantSetting: config.tenantSetting },",
84
+ " }),",
85
+ "})",
86
+ "",
87
+ ...domains.flatMap((one) => [...one.declarations, ""]),
88
+ "/** Who is asking, and the connection they may ask on. */",
89
+ "export type Caller = { connection: Executor; tenantId: string }",
90
+ "",
91
+ "/** Every domain this repo composed, built for one caller. */",
92
+ "export type Scope = {",
93
+ ...domains.map((one) => ` ${one.field}: ${one.fieldType}`),
94
+ "}",
95
+ "",
96
+ "/**",
97
+ " * Per request. The tenant session opens FIRST and the stores are built inside it, so a store",
98
+ " * that could run outside the transaction that named its tenant does not exist to be held.",
99
+ " */",
100
+ "export const inScope = async <Result>(",
101
+ " definitions: Definitions,",
102
+ " caller: Caller,",
103
+ " run: (scope: Scope) => Promise<Result>,",
104
+ "): Promise<Result> =>",
105
+ " definitions.session.inTenant(caller.connection, caller.tenantId, async (tx) =>",
106
+ " run({",
107
+ ...domains.flatMap((one) => [...one.built]),
108
+ " }),",
109
+ " )",
110
+ ""
111
+ ].join("\n");
112
+ };
113
+
1
114
  // src/answers.ts
2
115
  import { basename, resolve } from "path";
3
116
  var RUNTIMES = ["bun", "pnpm"];
@@ -6,13 +119,14 @@ var UIS = ["atoms-only", "none", "tiered-shadcn"];
6
119
  var VALUED = /* @__PURE__ */ new Set([
7
120
  "--backend",
8
121
  "--brand",
122
+ "--domains",
9
123
  "--kit",
10
124
  "--marketplace",
11
125
  "--runtime",
12
126
  "--themekit",
13
127
  "--ui"
14
128
  ]);
15
- var SWITCHES = /* @__PURE__ */ new Set(["--help", "-h", "--no-install"]);
129
+ var SWITCHES = /* @__PURE__ */ new Set(["--check", "--help", "-h", "--no-install"]);
16
130
  var read = (argv) => {
17
131
  const flags = {};
18
132
  let dir;
@@ -53,16 +167,23 @@ var choose = (flag, given, allowed) => {
53
167
  return given;
54
168
  };
55
169
  var scopeOf = (name) => `@${name.toLowerCase().replaceAll(/[^a-z0-9-]/g, "-")}`;
170
+ var domainsFrom = (given) => {
171
+ const named = (given ?? "").split(",").map((one) => one.trim()).filter((one) => one !== "");
172
+ domainsNamed(named);
173
+ return named;
174
+ };
56
175
  var parseAnswers = (argv, cwd) => {
57
176
  const { dir, flags } = read(argv);
58
177
  if (dir === void 0) {
59
178
  throw new Error("create-geonosis needs a directory to write the repo into");
60
179
  }
61
180
  const name = basename(resolve(cwd, dir));
181
+ const domains = domainsFrom(flags["--domains"]);
62
182
  return {
63
183
  backend: choose("--backend", flags["--backend"], BACKENDS),
64
184
  ...flags["--brand"] === void 0 ? {} : { brand: flags["--brand"] },
65
185
  dir: resolve(cwd, dir),
186
+ domains,
66
187
  ...flags["--kit"] === void 0 ? {} : { kit: resolve(cwd, flags["--kit"]) },
67
188
  ...flags["--marketplace"] === void 0 ? {} : { marketplace: flags["--marketplace"] },
68
189
  name,
@@ -263,15 +384,20 @@ var workspacesOf = (answers) => {
263
384
  dir: "apps/web",
264
385
  mayDependOn: list.map((one) => one.tag),
265
386
  name: `${scope}/web`,
266
- sources: { "src/boot.ts": APP(scope, ui !== "none"), "src/boot.test.ts": APP_TEST },
387
+ packages: floorDependencies(answers.domains),
388
+ sources: {
389
+ "src/boot.ts": APP(scope, ui !== "none"),
390
+ "src/boot.test.ts": APP_TEST,
391
+ ...answers.domains.length === 0 ? {} : { [PLATFORM_FILE]: platformSource(answers.domains) }
392
+ },
267
393
  tag: "app"
268
394
  });
269
395
  return list;
270
396
  };
271
397
 
272
398
  // src/files.ts
273
- import { countersFor, rulesFor } from "@geonosis/cli";
274
- var VERSION = "1.3.0";
399
+ import { countersFor, LAW_SOURCE, rulesFor, tiersFor } from "@geonosis/cli";
400
+ var VERSION = "2.0.0";
275
401
  var SCHEMA = "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json";
276
402
  var CATALOG = {
277
403
  "@types/node": "^24.3.0",
@@ -364,12 +490,17 @@ var workspaceManifest = (workspace, answers, workspaces) => {
364
490
  typescript: "catalog:",
365
491
  vitest: "catalog:"
366
492
  },
367
- dependencies: Object.fromEntries(
368
- workspace.mayDependOn.flatMap((tag) => {
369
- const found = workspaces.find((one) => one.tag === tag);
370
- return found === void 0 || found.name === workspace.name ? [] : [[found.name, "workspace:*"]];
371
- })
372
- )
493
+ dependencies: {
494
+ ...Object.fromEntries(
495
+ workspace.mayDependOn.flatMap((tag) => {
496
+ const found = workspaces.find((one) => one.tag === tag);
497
+ return found === void 0 || found.name === workspace.name ? [] : [[found.name, "workspace:*"]];
498
+ })
499
+ ),
500
+ // With --kit the floors are linked from a workspace AFTER the install, and the manifest is
501
+ // rewritten to say so — the same reason the root's kit versions are left out.
502
+ ...answers.kit === void 0 ? workspace.packages : {}
503
+ }
373
504
  });
374
505
  };
375
506
  var kitDependencies = (answers) => ({
@@ -381,6 +512,17 @@ var kitDependencies = (answers) => ({
381
512
  "@geonosis/verify": `^${VERSION}`,
382
513
  ...answers.themekit === void 0 ? {} : { "@geonosis/themekit": `^${VERSION}` }
383
514
  });
515
+ var rootScripts = ({ runtime }) => ({
516
+ doctor: "geonosis-doctor",
517
+ "format:check": "oxfmt --config .oxfmtrc.json --check .",
518
+ format: "oxfmt --config .oxfmtrc.json .",
519
+ lint: "oxlint --config .oxlintrc.json .",
520
+ ratchet: "geonosis-ratchet",
521
+ sync: "geonosis sync",
522
+ test: everyWorkspace(runtime, "test"),
523
+ typecheck: everyWorkspace(runtime, "typecheck"),
524
+ verify: "geonosis-verify"
525
+ });
384
526
  var rootManifest = (answers) => {
385
527
  const { runtime } = answers;
386
528
  const globs = ["apps/*", "packages/*"];
@@ -395,17 +537,7 @@ var rootManifest = (answers) => {
395
537
  packages: globs
396
538
  }
397
539
  } : {},
398
- scripts: {
399
- doctor: "geonosis-doctor",
400
- "format:check": "oxfmt --config .oxfmtrc.json --check .",
401
- format: "oxfmt --config .oxfmtrc.json .",
402
- lint: "oxlint --config .oxlintrc.json .",
403
- ratchet: "geonosis-ratchet",
404
- sync: "geonosis sync",
405
- test: everyWorkspace(runtime, "test"),
406
- typecheck: everyWorkspace(runtime, "typecheck"),
407
- verify: "geonosis-verify"
408
- },
540
+ scripts: rootScripts(answers),
409
541
  devDependencies: {
410
542
  // With --kit the kit is linked from a workspace AFTER the install, and the manifest is
411
543
  // rewritten to say so. Declaring published versions here would make the install go looking
@@ -540,6 +672,9 @@ var ci = (answers) => [
540
672
  "jobs:",
541
673
  " verify:",
542
674
  " runs-on: ubuntu-latest",
675
+ // #161: a job with no ceiling ran three hours before failing in a consumer. The runner's
676
+ // timeout is not the repo's budget — it is what stops a gate nobody can see from billing.
677
+ " timeout-minutes: 30",
543
678
  " steps:",
544
679
  " - uses: actions/checkout@v4",
545
680
  ...answers.runtime === "bun" ? [" - uses: oven-sh/setup-bun@v2", " - run: bun install --frozen-lockfile"] : [
@@ -587,12 +722,16 @@ var tsconfigBase = (answers) => json({
587
722
  });
588
723
  var filesFor = (answers) => {
589
724
  const workspaces = workspacesOf(answers);
590
- const counters = countersFor(answers.runtime);
725
+ const counters = countersFor(answers.runtime, LAW_SOURCE);
591
726
  const { rules } = rulesFor(presetsFor(answers), answersForRules(answers));
592
727
  const files = {
593
728
  "package.json": rootManifest(answers),
594
729
  ...answers.runtime === "pnpm" ? { "pnpm-workspace.yaml": workspaceYaml(answers) } : {},
595
- "CLAUDE.md": law(answers),
730
+ // D-054 from day one: the law is written once, CLAUDE.md imports it, and the three
731
+ // declarations below point at the same file — a repo with nothing to migrate later.
732
+ [LAW_SOURCE]: law(answers),
733
+ "CLAUDE.md": `@${LAW_SOURCE}
734
+ `,
596
735
  "docs/architecture.md": architecture(answers, workspaces),
597
736
  "docs/pipelines/order-placed.md": pipeline(answers),
598
737
  ".github/workflows/ci.yml": ci(answers),
@@ -628,19 +767,16 @@ var filesFor = (answers) => {
628
767
  ignorePatterns: ["node_modules", "dist", "coverage", CORPUS_DIR]
629
768
  }),
630
769
  "geonosis.json": json({
631
- verify: {
632
- fast: ["pnpm typecheck", "pnpm lint", "pnpm test"].map(
633
- (one) => answers.runtime === "bun" ? one.replace("pnpm ", "bun run ") : one
634
- ),
635
- full: [
636
- "fast",
637
- answers.runtime === "bun" ? "bun run format:check" : "pnpm format:check",
638
- answers.runtime === "bun" ? "bun run ratchet" : "pnpm ratchet"
639
- ]
640
- },
770
+ verify: tiersFor({
771
+ rules,
772
+ runtime: answers.runtime,
773
+ scripts: Object.keys(rootScripts(answers))
774
+ }),
641
775
  doctor: { corpus: CORPUS_DIR },
776
+ law: { file: LAW_SOURCE },
642
777
  ledger: {
643
778
  architecture: "docs/architecture.md",
779
+ law: LAW_SOURCE,
644
780
  plans: "plans",
645
781
  progress: "plans/PROGRESS.md",
646
782
  proofs: "proofs"
@@ -718,10 +854,62 @@ var filesFor = (answers) => {
718
854
  };
719
855
  var presetsChosenBy = presetsFor;
720
856
 
857
+ // src/check.ts
858
+ import { existsSync, readFileSync } from "fs";
859
+ import { join } from "path";
860
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
861
+ var pathsIn = (value, prefix = "") => {
862
+ if (!isRecord(value)) return [];
863
+ return Object.entries(value).flatMap(([key, child]) => {
864
+ const here = prefix === "" ? key : `${prefix}.${key}`;
865
+ return [here, ...pathsIn(child, here)];
866
+ });
867
+ };
868
+ var parsed = (body) => {
869
+ try {
870
+ const value = JSON.parse(body);
871
+ return isRecord(value) ? value : void 0;
872
+ } catch {
873
+ return void 0;
874
+ }
875
+ };
876
+ var missingPaths = (want, has) => {
877
+ const held = new Set(pathsIn(has));
878
+ return pathsIn(want).filter((path) => !held.has(path));
879
+ };
880
+ var isCompositionRoot = (file) => file.endsWith(`/${PLATFORM_FILE}`);
881
+ var checkScaffold = (answers) => {
882
+ const drift = [];
883
+ const seeded = workspacesOf(answers).map((one) => `${one.dir}/`);
884
+ for (const [file, body] of Object.entries(filesFor(answers))) {
885
+ if (!isCompositionRoot(file) && seeded.some((prefix) => file.startsWith(prefix))) continue;
886
+ const at = join(answers.dir, file);
887
+ if (!existsSync(at)) {
888
+ drift.push({ at: "", file, kind: "missing" });
889
+ continue;
890
+ }
891
+ const want = parsed(body);
892
+ if (want === void 0) continue;
893
+ const has = parsed(readFileSync(at, "utf8"));
894
+ if (has === void 0) {
895
+ drift.push({ at: "", file, kind: "unreadable" });
896
+ continue;
897
+ }
898
+ for (const path of missingPaths(want, has)) drift.push({ at: path, file, kind: "key" });
899
+ }
900
+ return drift;
901
+ };
902
+ var SAID = {
903
+ key: (one) => `${one.file} \u2192 ${one.at} \u2014 the scaffold writes this key and your file has none`,
904
+ missing: (one) => `${one.file} \u2014 the scaffold writes this file and your repo has none`,
905
+ unreadable: (one) => `${one.file} \u2014 the scaffold writes plain JSON here and yours is not (comments? trailing commas?), so this check could not read its keys`
906
+ };
907
+ var describeDrift = (drift) => drift.map((one) => SAID[one.kind](one));
908
+
721
909
  // src/scaffold.ts
722
910
  import { spawnSync } from "child_process";
723
- import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
724
- import { dirname, join } from "path";
911
+ import { chmodSync, existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
912
+ import { dirname, join as join2 } from "path";
725
913
  var KIT_PACKAGES = [
726
914
  "cli",
727
915
  "doctor",
@@ -733,12 +921,12 @@ var KIT_PACKAGES = [
733
921
  "verify"
734
922
  ];
735
923
  var write = (dir, file, body) => {
736
- const path = join(dir, file);
924
+ const path = join2(dir, file);
737
925
  mkdirSync(dirname(path), { recursive: true });
738
926
  writeFileSync(path, body);
739
927
  };
740
928
  var writeScaffold = (answers) => {
741
- if (existsSync(answers.dir) && readdirSync(answers.dir).length > 0) {
929
+ if (existsSync2(answers.dir) && readdirSync(answers.dir).length > 0) {
742
930
  throw new Error(`${answers.dir} is not empty \u2014 nothing was written`);
743
931
  }
744
932
  const files = filesFor(answers);
@@ -755,38 +943,77 @@ var run = (what, dir, command, args) => {
755
943
  };
756
944
  };
757
945
  var binsOf = (dir) => {
758
- const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
946
+ const manifest = JSON.parse(readFileSync2(join2(dir, "package.json"), "utf8"));
759
947
  if (typeof manifest.bin === "string") return [[manifest.name ?? "", manifest.bin]];
760
948
  return Object.entries(manifest.bin ?? {});
761
949
  };
950
+ var repoint = (path, at, owed) => {
951
+ if (!existsSync2(path)) return [];
952
+ const manifest = JSON.parse(readFileSync2(path, "utf8"));
953
+ const moved = [];
954
+ for (const block of ["dependencies", "devDependencies"]) {
955
+ const declared = manifest[block];
956
+ if (declared === void 0) continue;
957
+ for (const name of Object.keys(declared)) {
958
+ const from = at.get(name);
959
+ if (from === void 0) continue;
960
+ declared[name] = `link:${from}`;
961
+ moved.push(name);
962
+ }
963
+ }
964
+ for (const name of owed) {
965
+ const from = at.get(name);
966
+ if (from === void 0 || moved.includes(name)) continue;
967
+ manifest.dependencies = { ...manifest.dependencies, [name]: `link:${from}` };
968
+ moved.push(name);
969
+ }
970
+ if (moved.length > 0) writeFileSync(path, `${JSON.stringify(manifest, void 0, 2)}
971
+ `);
972
+ return moved;
973
+ };
762
974
  var linkKit = (answers, kit) => {
763
- const modules = join(answers.dir, "node_modules");
764
- const scope = join(modules, "@geonosis");
765
- const bin = join(modules, ".bin");
975
+ const modules = join2(answers.dir, "node_modules");
976
+ const scope = join2(modules, "@geonosis");
977
+ const bin = join2(modules, ".bin");
766
978
  mkdirSync(scope, { recursive: true });
767
979
  mkdirSync(bin, { recursive: true });
768
980
  const linked = [];
769
- const manifestPath = join(answers.dir, "package.json");
770
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
771
- for (const name of KIT_PACKAGES) {
772
- const from = join(kit, "packages", name);
773
- if (!existsSync(from)) continue;
774
- const at = join(scope, name);
775
- if (!existsSync(at)) {
776
- spawnSync("ln", ["-s", from, at]);
777
- }
778
- for (const [called, relative] of binsOf(from)) {
779
- const shim = join(bin, called);
981
+ const at = /* @__PURE__ */ new Map();
982
+ const manifestPath = join2(answers.dir, "package.json");
983
+ const manifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
984
+ for (const relative of [
985
+ ...KIT_PACKAGES.map((name) => `packages/${name}`),
986
+ ...floorDirectories(answers.domains)
987
+ ]) {
988
+ const from = join2(kit, relative);
989
+ if (!existsSync2(from)) continue;
990
+ const declared = JSON.parse(readFileSync2(join2(from, "package.json"), "utf8"));
991
+ if (declared.name === void 0) continue;
992
+ const to = join2(scope, declared.name.replace("@geonosis/", ""));
993
+ if (!existsSync2(to)) spawnSync("ln", ["-s", from, to]);
994
+ for (const [called, entry] of binsOf(from)) {
995
+ const shim = join2(bin, called);
780
996
  writeFileSync(shim, `#!/bin/sh
781
- exec node "${join(from, relative)}" "$@"
997
+ exec node "${join2(from, entry)}" "$@"
782
998
  `);
783
999
  chmodSync(shim, 493);
784
1000
  }
785
- const declared = JSON.parse(readFileSync(join(from, "package.json"), "utf8"));
786
- if (declared.name !== void 0) {
787
- manifest.devDependencies = { ...manifest.devDependencies, [declared.name]: `link:${from}` };
1001
+ at.set(declared.name, from);
1002
+ linked.push(relative);
1003
+ }
1004
+ const owned = new Set(
1005
+ workspacesOf(answers).flatMap(
1006
+ (workspace) => repoint(
1007
+ join2(answers.dir, workspace.dir, "package.json"),
1008
+ at,
1009
+ Object.keys(workspace.packages ?? {})
1010
+ )
1011
+ )
1012
+ );
1013
+ for (const [name, from] of at) {
1014
+ if (!owned.has(name)) {
1015
+ manifest.devDependencies = { ...manifest.devDependencies, [name]: `link:${from}` };
788
1016
  }
789
- linked.push(name);
790
1017
  }
791
1018
  writeFileSync(manifestPath, `${JSON.stringify(manifest, void 0, 2)}
792
1019
  `);
@@ -813,8 +1040,8 @@ var finish = ({ answers, install }) => {
813
1040
  what: `link the kit from ${answers.kit}`
814
1041
  });
815
1042
  }
816
- const ledger = join(answers.dir, "node_modules/.bin/geonosis-ledger");
817
- if (existsSync(ledger)) {
1043
+ const ledger = join2(answers.dir, "node_modules/.bin/geonosis-ledger");
1044
+ if (existsSync2(ledger)) {
818
1045
  steps.push(
819
1046
  run("sync the edges table into turbo.json", answers.dir, ledger, [
820
1047
  "sync",
@@ -832,14 +1059,22 @@ var finish = ({ answers, install }) => {
832
1059
  ])
833
1060
  );
834
1061
  }
835
- const oxfmt = join(answers.dir, "node_modules/.bin/oxfmt");
836
- if (existsSync(oxfmt)) {
1062
+ const oxfmt = join2(answers.dir, "node_modules/.bin/oxfmt");
1063
+ if (existsSync2(oxfmt)) {
837
1064
  steps.push(run("format", answers.dir, oxfmt, ["--config", ".oxfmtrc.json", "."]));
838
1065
  }
839
1066
  return steps;
840
1067
  };
841
1068
 
842
1069
  export {
1070
+ PLATFORM_FILE,
1071
+ DOMAINS,
1072
+ DOMAIN_NAMES,
1073
+ FOUNDATIONS,
1074
+ domainsNamed,
1075
+ floorDependencies,
1076
+ floorDirectories,
1077
+ platformSource,
843
1078
  RUNTIMES,
844
1079
  BACKENDS,
845
1080
  UIS,
@@ -850,6 +1085,8 @@ export {
850
1085
  workspacesOf,
851
1086
  filesFor,
852
1087
  presetsChosenBy,
1088
+ checkScaffold,
1089
+ describeDrift,
853
1090
  KIT_PACKAGES,
854
1091
  writeScaffold,
855
1092
  linkKit,
@@ -1,13 +1,19 @@
1
1
  import {
2
+ DOMAIN_NAMES,
3
+ PLATFORM_FILE,
4
+ checkScaffold,
5
+ describeDrift,
2
6
  finish,
3
7
  parseAnswers,
4
8
  wantsInstall,
5
9
  writeScaffold
6
- } from "./chunk-3FX4T27X.js";
10
+ } from "./chunk-PRX7MKFZ.js";
7
11
 
8
12
  // src/create-geonosis-cli.ts
13
+ import { existsSync } from "fs";
9
14
  var USAGE = `create-geonosis <dir> --runtime pnpm|bun --backend medusa|sagaflow-cf|none --ui tiered-shadcn|atoms-only|none
10
- [--brand <name>] [--themekit <name>] [--marketplace <path>] [--kit <path>] [--no-install]
15
+ [--domains <a,b>] [--brand <name>] [--themekit <name>] [--marketplace <path>] [--kit <path>] [--no-install]
16
+ [--check]
11
17
 
12
18
  Writes a repo that passes its own gates on day one: a workspace with one catalog of shared versions,
13
19
  an exports map and no barrel in every package, typecheck + lint + test scripts in every workspace,
@@ -18,13 +24,20 @@ skills lock, and a reach corpus in this repo's own vocabulary so the doctor can
18
24
  There is no default for --runtime, --backend or --ui. Each one decides the shape of the tree, and a
19
25
  default borrowed from another repo would be that repo's stack hardcoded into this one.
20
26
 
27
+ --domains the domains this repo composes \u2014 ${DOMAIN_NAMES.join(", ")}. Each one writes its
28
+ store into the app's ${PLATFORM_FILE}: the composition root, in two lifetimes, with
29
+ the tenant session opened before any store is built. A name nothing ships is refused.
21
30
  --kit <path> read the kit from a workspace instead of the registry. A proof aid: it symlinks the
22
31
  packages after the install and rewrites the manifest to say so.
23
32
  --no-install write the files and stop. The gates cannot run until the two "geonosis sync
24
33
  --target" commands below have been run, and they are printed for you.
34
+ --check say what an EXISTING repo is missing of what the scaffold writes today, and WRITE
35
+ NOTHING. Files it has never had, and keys inside the JSON it has. Contents and
36
+ values are never compared: a repo that has been worked in has rewritten them, and
37
+ it was entitled to. Exit 1 when anything drifted.
25
38
 
26
- Exit: 0 written \xB7 1 refused (a directory with something in it, an answer that is not one) \xB7 2 a run
27
- that could not be made at all.`;
39
+ Exit: 0 written (or, under --check, no drift) \xB7 1 refused (a directory with something in it, an
40
+ answer that is not one) or drift found \xB7 2 a run that could not be made at all.`;
28
41
  var CONFIG_SHAPE = `create-geonosis reads no configuration file.
29
42
 
30
43
  It WRITES them: geonosis.json, .oxlintrc.json, geonosis.ratchet.json and gate-baseline.json, from
@@ -45,6 +58,24 @@ var main = () => {
45
58
  return argv.length === 0 ? 2 : 0;
46
59
  }
47
60
  const answers = parseAnswers(argv, process.cwd());
61
+ if (argv.includes("--check")) {
62
+ if (!existsSync(answers.dir)) {
63
+ throw new Error(`${answers.dir} is not there \u2014 --check reads a repo, it never makes one`);
64
+ }
65
+ const drift = checkScaffold(answers);
66
+ if (drift.length === 0) {
67
+ process.stdout.write(`create-geonosis --check ${answers.dir} \u2014 no drift
68
+ `);
69
+ return 0;
70
+ }
71
+ for (const line of describeDrift(drift)) process.stderr.write(` ${line}
72
+ `);
73
+ process.stderr.write(
74
+ `create-geonosis --check ${answers.dir} \u2014 ${drift.length} drift, nothing written
75
+ `
76
+ );
77
+ return 1;
78
+ }
48
79
  const written = writeScaffold(answers);
49
80
  process.stdout.write(`create-geonosis \u2014 ${written.length} file(s) into ${answers.dir}
50
81
  `);
package/dist/index.d.ts CHANGED
@@ -8,6 +8,8 @@ type Answers = {
8
8
  backend: Backend;
9
9
  brand?: string;
10
10
  dir: string;
11
+ /** The domains this repo composes at its composition root. Empty until the flag names one. */
12
+ domains: string[];
11
13
  /** The kit read from a workspace instead of the registry — a proof aid, never the default. */
12
14
  kit?: string;
13
15
  marketplace?: string;
@@ -20,11 +22,84 @@ type Answers = {
20
22
  declare const parseAnswers: (argv: readonly string[], cwd: string) => Answers;
21
23
  declare const wantsInstall: (argv: readonly string[]) => boolean;
22
24
 
25
+ /**
26
+ * What an existing repo never receives: whatever the scaffold has learned since it was run.
27
+ *
28
+ * The comparison is over SHAPE and not over content, because a repo that has been worked in has
29
+ * legitimately rewritten every file the scaffold gave it. What it cannot have legitimately done is
30
+ * lose a file the scaffold now writes, or miss a declaration a new gate reads — so a missing path is
31
+ * drift and a changed value never is.
32
+ */
33
+ type DriftKind = 'key' | 'missing' | 'unreadable';
34
+ type Drift = {
35
+ /** The key path inside the file, or `''` when the fact is about the whole file. */
36
+ at: string;
37
+ file: string;
38
+ kind: DriftKind;
39
+ };
40
+ /**
41
+ * The drift between a tree and what the scaffold would write into it today. Reads only.
42
+ *
43
+ * Nothing in this module opens a file for writing, and nothing calls `writeScaffold`. That is the
44
+ * whole contract of `--check`: it is run against a repo somebody is working in.
45
+ */
46
+ declare const checkScaffold: (answers: Answers) => Drift[];
47
+ declare const describeDrift: (drift: readonly Drift[]) => string[];
48
+
49
+ /** Where the composition root lives, relative to the workspace that composes: the app. */
50
+ declare const PLATFORM_FILE = "src/platform.ts";
51
+ /**
52
+ * A domain the kit ships, and the two lines composing it takes.
53
+ *
54
+ * A domain is added here and nowhere else: the flag's vocabulary, the app's dependencies and the
55
+ * generated file all read this table, so a domain that ships without a way to compose it cannot be
56
+ * offered by name. `version` is the floor package's OWN line — the method's number says nothing
57
+ * about it (D-053) — and `tooling/floor-scaffold-versions.test.ts` holds each against the workspace.
58
+ */
59
+ type DomainScaffold = {
60
+ /** Where the package sits in the kit's tree, for `--kit`'s workspace links. */
61
+ at: string;
62
+ /** What the scope field is called, and the type it is annotated with. */
63
+ field: string;
64
+ /** Everything the generated file needs above the composition itself. */
65
+ declarations: readonly string[];
66
+ fieldType: string;
67
+ imports: readonly string[];
68
+ name: string;
69
+ /** How the store is built, once the tenant's transaction is already open. */
70
+ built: readonly string[];
71
+ version: string;
72
+ };
73
+ declare const DOMAINS: readonly DomainScaffold[];
74
+ declare const DOMAIN_NAMES: readonly string[];
75
+ /** The two foundations a composition root is built from, whichever domains it composes. */
76
+ declare const FOUNDATIONS: readonly {
77
+ at: string;
78
+ name: string;
79
+ version: string;
80
+ }[];
81
+ declare const domainsNamed: (named: readonly string[]) => DomainScaffold[];
82
+ /** What the composing workspace declares once a domain is named. */
83
+ declare const floorDependencies: (named: readonly string[]) => Record<string, string>;
84
+ /** Every kit directory a `--kit` link has to reach for the domains this repo named. */
85
+ declare const floorDirectories: (named: readonly string[]) => string[];
86
+ /**
87
+ * The composition root, in the two lifetimes it has.
88
+ *
89
+ * DEFINITIONS are what this deployment IS — wired once per isolate, before anybody has asked
90
+ * anything. SCOPE is who is asking, bound per request. Keeping them apart is what lets the session
91
+ * come first: the stores are built INSIDE the tenant's transaction, so a caller cannot hold one
92
+ * that was made without a session to run in (D-014 — structural, never an assertion).
93
+ */
94
+ declare const platformSource: (named: readonly string[]) => string;
95
+
23
96
  type Workspace = {
24
97
  dir: string;
25
98
  /** The files under this workspace, keyed by path relative to the workspace. */
26
99
  sources: Record<string, string>;
27
100
  name: string;
101
+ /** Packages outside the workspace this one declares, spec and all. */
102
+ packages?: Record<string, string>;
28
103
  tag: string;
29
104
  mayDependOn: string[];
30
105
  };
@@ -74,7 +149,7 @@ type Step = {
74
149
  * and tries to resolve the `workspace:*` specifiers the kit's own packages use. Symlinking the
75
150
  * directories and writing the bin shims is the one form of the answer that is true on both.
76
151
  *
77
- * The manifest is then told what is on disk, because `geonosis-doctor` FAILS a config whose plugin
152
+ * The manifests are then told what is on disk, because `geonosis-doctor` FAILS a config whose plugin
78
153
  * no manifest declares — and it should. A link is a declaration it accepts and can verify.
79
154
  */
80
155
  declare const linkKit: (answers: Answers, kit: string) => string[];
@@ -90,4 +165,4 @@ declare const finish: ({ answers, install }: {
90
165
  install: boolean;
91
166
  }) => Step[];
92
167
 
93
- export { type Answers, BACKENDS, type Backend, CORPUS_DIR, KIT_PACKAGES, RUNTIMES, type Runtime, type Step, UIS, type Ui, type Workspace, corpusFor, filesFor, finish, linkKit, parseAnswers, presetsChosenBy, wantsInstall, workspacesOf, writeScaffold };
168
+ export { type Answers, BACKENDS, type Backend, CORPUS_DIR, DOMAINS, DOMAIN_NAMES, type DomainScaffold, type Drift, type DriftKind, FOUNDATIONS, KIT_PACKAGES, PLATFORM_FILE, RUNTIMES, type Runtime, type Step, UIS, type Ui, type Workspace, checkScaffold, corpusFor, describeDrift, domainsNamed, filesFor, finish, floorDependencies, floorDirectories, linkKit, parseAnswers, platformSource, presetsChosenBy, wantsInstall, workspacesOf, writeScaffold };
package/dist/index.js CHANGED
@@ -1,30 +1,50 @@
1
1
  import {
2
2
  BACKENDS,
3
3
  CORPUS_DIR,
4
+ DOMAINS,
5
+ DOMAIN_NAMES,
6
+ FOUNDATIONS,
4
7
  KIT_PACKAGES,
8
+ PLATFORM_FILE,
5
9
  RUNTIMES,
6
10
  UIS,
11
+ checkScaffold,
7
12
  corpusFor,
13
+ describeDrift,
14
+ domainsNamed,
8
15
  filesFor,
9
16
  finish,
17
+ floorDependencies,
18
+ floorDirectories,
10
19
  linkKit,
11
20
  parseAnswers,
21
+ platformSource,
12
22
  presetsChosenBy,
13
23
  wantsInstall,
14
24
  workspacesOf,
15
25
  writeScaffold
16
- } from "./chunk-3FX4T27X.js";
26
+ } from "./chunk-PRX7MKFZ.js";
17
27
  export {
18
28
  BACKENDS,
19
29
  CORPUS_DIR,
30
+ DOMAINS,
31
+ DOMAIN_NAMES,
32
+ FOUNDATIONS,
20
33
  KIT_PACKAGES,
34
+ PLATFORM_FILE,
21
35
  RUNTIMES,
22
36
  UIS,
37
+ checkScaffold,
23
38
  corpusFor,
39
+ describeDrift,
40
+ domainsNamed,
24
41
  filesFor,
25
42
  finish,
43
+ floorDependencies,
44
+ floorDirectories,
26
45
  linkKit,
27
46
  parseAnswers,
47
+ platformSource,
28
48
  presetsChosenBy,
29
49
  wantsInstall,
30
50
  workspacesOf,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-geonosis",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "types": "./dist/index.d.ts",
5
5
  "description": "Scaffold a microcompany repo that passes its own geonosis gates on day one.",
6
6
  "keywords": [
@@ -34,7 +34,7 @@
34
34
  "dist"
35
35
  ],
36
36
  "dependencies": {
37
- "@geonosis/cli": "1.3.0"
37
+ "@geonosis/cli": "2.0.0"
38
38
  },
39
39
  "engines": {
40
40
  "node": ">=22"