create-geonosis 2.10.0 → 2.11.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # create-geonosis
2
2
 
3
+ ## 2.11.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [08611c4]
8
+ - @geonosis/cli@2.11.1
9
+
10
+ ## 2.11.0
11
+
12
+ ### Patch Changes
13
+
14
+ - 0f99e26: Every bin's stale-build guard now sees the workspace dependencies its build read. A `dist`
15
+ current against its own `src` could still have been built against an older dependency's `dist` —
16
+ literally, where tsup inlined a workspace devDependency into it — and a dependency whose own `src`
17
+ had moved past its `dist` was read stale by every bin above it; the guard answered "current" for
18
+ both. Now each shim holds every `workspace:` dependency, transitively, to the same reading as its
19
+ own package (never built, stale against its src, current), and refuses when this `dist` is older
20
+ than a dependency's — by content, so a dependency rebuilt to the same bytes with
21
+ `--force --filter` is the same build and does not poison the tree. Unknown is still stale: the
22
+ first run over a tree where a dependency was rebuilt alone, before any record, refuses once and
23
+ names `--force --filter=<this package>`. Only a checkout with `src/` beside `dist/` sees any of
24
+ this; the published package ships no `src/` and the guard is skipped. Measured: within noise for a
25
+ bin with one dependency, +8 ms for `geonosis`, which walks fourteen.
26
+ - 0f99e26: Every bin's stale-build guard decides, and never throws. The digest guard's refusal read
27
+ `package.json` to name `pnpm build --force --filter=<name>`; over a checkout with no manifest
28
+ beside the bin it ended with exit 1 and a `node:fs` frame instead of exit 2 and a sentence. The
29
+ name is now a courtesy the refusal does without. The contract, stated: a source newer by mtime whose
30
+ content the bin saw beside a current dist is not stale; one whose content differs is, and the
31
+ refusal names the fix; one the bin has no record for is stale — unknown is stale; a missing dist is
32
+ "never built". Each is exit 2 with a sentence. Only a checkout with `src/` beside `dist/` sees any
33
+ of this; the published package ships no `src/` and the guard is skipped.
34
+ - 0f99e26: The kit names no consumer. Docs, doc comments and messages describe the repos a pattern was measured in by their stack (a Medusa storefront, a Workers + D1 app, a Mercur v1 marketplace). Example configs are named the same way: `examples/medusa-storefront.*.json`, `examples/workers-app.*.json`. No behaviour changes.
35
+ - 0f99e26: `create-geonosis` refuses a flag it would have ignored. A misspelled flag written with `=`
36
+ (`--runtim=pnpm`) was taken and dropped, where the same flag written with a space was refused; and
37
+ `--presets` outside `--plan` was accepted and ignored, because a scaffold's presets follow from
38
+ `--backend` and `--ui`.
39
+ - 0f99e26: The `backend-sagaflow-cf` deprecation line says the old preset name resolves "until the next major", which is what the 2.3.0 changelog promised. It used to say "for one release", seven minor releases on. The retired-rule docs and RULES.md now use the same wording.
40
+ - 0f99e26: `--backend sagaflow-cf` now enables the `backend-workflows-cf` preset instead of the deprecated
41
+ `backend-sagaflow-cf` alias (#197 in the plugin) — a scaffolded repo's `.oxlintrc.json` no longer
42
+ goes stale the release the alias is dropped. Same rules today; no change for other backends.
43
+ - 0f99e26: A bun repo's scaffolded `LAW.md` says `bun run format` and `bun run format:check`; it said `pnpm`
44
+ whatever the runtime.
45
+
46
+ The README says the scaffold writes `LAW.md` with `CLAUDE.md` importing it, which it has since
47
+ D-054, and lists `--domains`, `--medusa`, `--check` and `--plan` in the flag table.
48
+ - 0f99e26: A scaffolded repo pins vitest `^4.1.11`, the major the kit itself runs; a test holds every catalog tool to the kit's own major.
49
+ - 0f99e26: Every order a tool writes, compares or applies — generated files, migration order, listings, cursors — sorts with the `en-US` collation, named rather than inherited from the machine. On an `en-US` machine nothing moves; on one set to another language (Estonian sorts `z` before `t`) the output is now the same one.
50
+ - Updated dependencies [0f99e26]
51
+ - Updated dependencies [0f99e26]
52
+ - Updated dependencies [0f99e26]
53
+ - Updated dependencies [0f99e26]
54
+ - Updated dependencies [0f99e26]
55
+ - Updated dependencies [0f99e26]
56
+ - Updated dependencies [0f99e26]
57
+ - Updated dependencies [0f99e26]
58
+ - Updated dependencies [0f99e26]
59
+ - Updated dependencies [0f99e26]
60
+ - Updated dependencies [0f99e26]
61
+ - Updated dependencies [0f99e26]
62
+ - Updated dependencies [0f99e26]
63
+ - Updated dependencies [0f99e26]
64
+ - @geonosis/cli@2.11.0
65
+
3
66
  ## 2.10.0
4
67
 
5
68
  ### Patch Changes
package/README.md CHANGED
@@ -15,11 +15,15 @@ and a default borrowed from another repo would be that repo's stack hardcoded in
15
15
  | `--runtime` | `pnpm` · `bun` |
16
16
  | `--backend` | `medusa` · `sagaflow-cf` · `none` |
17
17
  | `--ui` | `tiered-shadcn` · `atoms-only` · `none` |
18
+ | `--domains <a,b>` | the domains the app composes, written into its `src/platform.ts` |
19
+ | `--medusa <range>` | the Medusa a `--backend medusa` tree declares |
18
20
  | `--brand <name>` | turns on `no-brand-names` for that word |
19
21
  | `--themekit <name>` | consumes a themekit **by version**, never by copying it |
20
22
  | `--marketplace <path>` | install the plugin from a local marketplace instead of GitHub |
21
23
  | `--kit <path>` | read the kit from a workspace instead of the registry — a proof aid |
22
24
  | `--no-install` | write the files and stop |
25
+ | `--check` | say what an existing repo lacks of what the scaffold writes today; write nothing |
26
+ | `--plan` | what adopting an existing repo would take, computed off its tree; write nothing |
23
27
 
24
28
  ## What it writes
25
29
 
@@ -34,7 +38,8 @@ and a default borrowed from another repo would be that repo's stack hardcoded in
34
38
  `geonosis-ledger sync` generates turbo's `boundaries.tags` AND the linter's `layer-walls` layers
35
39
  from it, so a tag added there reaches both gates and neither can drift from the other. The
36
40
  scaffold runs both splices for you.
37
- - **`CLAUDE.md` under 60 lines**, plus a `docs/pipelines/` page template.
41
+ - **`LAW.md` under 60 lines**, with `CLAUDE.md` the one line that imports it, plus a
42
+ `docs/pipelines/` page template.
38
43
  - **The five config files**: `geonosis.json` (verify tiers, ledger, walk, testbed, doctor),
39
44
  `.oxlintrc.json` with the presets the answers imply, `geonosis.ratchet.json`, `gate-baseline.json`,
40
45
  `.oxfmtrc.json`.
@@ -45,16 +50,16 @@ and a default borrowed from another repo would be that repo's stack hardcoded in
45
50
  ## The baseline, and the one number that is not zero
46
51
 
47
52
  Every counter starts at **0**: a new repo has earned no debt. `lawLineCount` starts at the measured
48
- length of the `CLAUDE.md` that was just written, because it counts something that already exists and
49
- a 0 there would fail the first `ratchet` run over a clean tree.
53
+ length of the `LAW.md` that was just written, because it counts something that already exists and a
54
+ 0 there would fail the first `ratchet` run over a clean tree.
50
55
 
51
56
  ## `lint-corpus/` — the reach corpus a shipped one cannot be
52
57
 
53
58
  `geonosis-doctor` asks whether every enabled rule can fire at all, against the corpus the **plugin**
54
- ships. That answers for most rules and cannot answer for three: `layer-walls` fires on a repo's own
55
- layer names, `no-brand-names` on its own brand, `plugin-route-namespaced` on its own package root —
56
- and the plugin's corpus says `acme` and `layers/core`. So the scaffold writes a small corpus in
57
- **this** repo's vocabulary, with a `manifest.json` naming exactly the rules it is evidence about.
59
+ ships. That cannot answer for a rule whose reach is the repo's own vocabulary — its layer names, its
60
+ brand, its plugin package root, the `within` its sagas live in — and the plugin's corpus says `acme`
61
+ and `layers/core`. So the scaffold writes a small corpus in **this** repo's vocabulary, with a
62
+ `manifest.json` naming exactly the rules it is evidence about.
58
63
 
59
64
  It is deliberately broken code, so it is excluded from the repo's own lint, format and typecheck —
60
65
  the same treatment the plugin gives its own fixtures. The corpus run copies it to a temp root and
@@ -90,13 +95,12 @@ Two things are compared and two are deliberately not:
90
95
  new repo compile, not a shape a repo must hold. Measured over a consumer: including them turned 24 real
91
96
  findings into 52, thirty of them about a sample workspace that repo does not have.
92
97
 
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.
98
+ Over a repo scaffolded before D-054 moved the law it reads `LAW.md`, `geonosis.json → law.file`
99
+ and the rest of what the scaffold has learned since; `proofs/024-W31a-create-check/` has a whole run
100
+ and the before/after hash.
96
101
 
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.
102
+ A JSON file the repo has that is not plain JSON — a `tsconfig.json` with `//` comments, legal
103
+ JSONC — is reported rather than skipped. A check that could not read a file has not passed over it.
100
104
 
101
105
  ## Proven, not claimed
102
106
 
@@ -1,14 +1,24 @@
1
1
  #!/usr/bin/env node
2
- // Committed, so `pnpm install` can link the bin on a fresh clone — before `pnpm build` has
3
- // produced dist/. A bin that only exists after a build is a bin that is missing when you need it.
4
- //
5
- // In the repo, `src/` sits beside `dist/`, and a dist older than src answered for a fix it did not
6
- // carry once (#62). The published package ships no src, so there the check is skipped.
7
- import { existsSync, readdirSync, statSync } from 'node:fs'
2
+ // Every packages/*/bin shim is packages/cli/bin/geonosis.mjs with its own name and entry: edit that
3
+ // one, then `pnpm generate:shims`. Committed so `pnpm install` can link the bin on a fresh clone,
4
+ // before `pnpm build` has made dist/. In the repo `src/` sits beside `dist/`, and a dist older
5
+ // than src answered for a fix it did not carry once (#62); the published package ships no src, so
6
+ // there the check is skipped.
7
+ import { createHash } from 'node:crypto'
8
+ import {
9
+ existsSync,
10
+ mkdirSync,
11
+ readdirSync,
12
+ readFileSync,
13
+ realpathSync,
14
+ statSync,
15
+ writeFileSync
16
+ } from 'node:fs'
8
17
  import { dirname, join } from 'node:path'
9
18
  import { fileURLToPath } from 'node:url'
10
19
 
11
20
  const here = dirname(fileURLToPath(import.meta.url))
21
+ const own = join(here, '..')
12
22
  // tsup never reads a test file, so one counting as source made every bin refuse a build that had
13
23
  // not gone stale (#399).
14
24
  const NOT_SOURCE = (name) =>
@@ -24,25 +34,162 @@ const newest = (dir, skip) =>
24
34
  return Math.max(most, entry.isDirectory() ? newest(at, skip) : statSync(at).mtimeMs)
25
35
  }, 0)
26
36
  : 0
27
- const src = join(here, '..', 'src')
28
- const dist = join(here, '..', 'dist')
37
+ const digestOf = (dir, hash) => {
38
+ for (const name of readdirSync(dir).toSorted()) {
39
+ if (NOT_SOURCE(name)) continue
40
+ const at = join(dir, name)
41
+ if (statSync(at).isDirectory()) {
42
+ digestOf(at, hash)
43
+ continue
44
+ }
45
+ hash.update(name).update('\0').update(readFileSync(at)).update('\0')
46
+ }
47
+ return hash
48
+ }
49
+ const digest = (...dirs) => {
50
+ const hash = createHash('sha256')
51
+ for (const dir of dirs) if (existsSync(dir)) digestOf(dir, hash)
52
+ return hash.digest('hex')
53
+ }
54
+ // mtime says a source file was WRITTEN, not that it CHANGED. A checkout rewrites it with the same
55
+ // content, and the next `pnpm build` is a cache hit that leaves the current dist exactly as it
56
+ // was — so every bin refused with "run pnpm build", the action that had just changed nothing and
57
+ // would change nothing again. So a digest of the sources is kept from the last run that found
58
+ // dist current: the same digest later is the same sources, whatever their timestamps say, and a
59
+ // different one is the staleness this guard exists for. The newest mtime is kept beside it so a
60
+ // run over untouched sources need not hash them again. The second line is the same record for
61
+ // the dists of the workspace dependencies this build read (row 584, below).
62
+ const recordOf = (pkg) => join(pkg, '.turbo', 'geonosis-src-digest')
63
+ const recorded = (pkg) => {
64
+ try {
65
+ const [first = '', second = ''] = readFileSync(recordOf(pkg), 'utf8').split('\n')
66
+ const [seen = '', sum = ''] = first.trim().split(' ')
67
+ const [depsSeen = '', depsSum = ''] = second.trim().split(' ')
68
+ return { depsSeen: Number(depsSeen), depsSum, seen: Number(seen), sum }
69
+ } catch {
70
+ return { depsSeen: 0, depsSum: '', seen: 0, sum: '' }
71
+ }
72
+ }
73
+ const remember = (pkg, known) => {
74
+ try {
75
+ mkdirSync(dirname(recordOf(pkg)), { recursive: true })
76
+ writeFileSync(recordOf(pkg), `${known.seen} ${known.sum}\n${known.depsSeen} ${known.depsSum}\n`)
77
+ } catch {
78
+ // A record nothing could write costs one more hash next time, never a wrong answer.
79
+ }
80
+ }
81
+ // The refusal names a package for turbo's --filter. A manifest that cannot be read costs the
82
+ // name, never the decision: the first digest shim threw here — exit 1 and a node:fs frame — over a
83
+ // tree with no package.json, and a guard that throws neither answers nor refuses.
84
+ const manifestOf = (pkg) => {
85
+ try {
86
+ return JSON.parse(readFileSync(join(pkg, 'package.json'), 'utf8'))
87
+ } catch {
88
+ return {}
89
+ }
90
+ }
91
+ const refuse = (sentence) => {
92
+ process.stderr.write(`create-geonosis: ${sentence}\n`)
93
+ process.exit(2)
94
+ }
95
+ // Whether one package's dist reflects its src: 'unbuilt', 'stale' or 'current'. The same reading
96
+ // for this package and for each workspace dependency, off that package's own record.
97
+ const standing = (pkg) => {
98
+ const src = join(pkg, 'src')
99
+ const dist = join(pkg, 'dist')
100
+ if (!existsSync(src)) return 'current'
101
+ const built = newest(dist)
102
+ if (built === 0) return 'unbuilt'
103
+ const written = newest(src, NOT_SOURCE)
104
+ const known = recorded(pkg)
105
+ if (written > built) {
106
+ const sum = known.seen === written ? known.sum : digest(src)
107
+ if (sum !== known.sum) return 'stale'
108
+ if (known.seen !== written) remember(pkg, { ...known, seen: written, sum })
109
+ } else if (known.seen !== written) {
110
+ remember(pkg, { ...known, seen: written, sum: digest(src) })
111
+ }
112
+ return 'current'
113
+ }
114
+ // Row 584: a dist current against its own src can still have been built against an OLDER
115
+ // dependency's dist — literally, when tsup inlined a workspace devDependency into it — and a
116
+ // dependency whose src has moved past its dist is read by this dist stale, inlined or imported.
117
+ // So every workspace dependency this manifest names (`workspace:`, resolved through the link pnpm
118
+ // made, transitively) is held to the same reading as this package, and this dist must not be
119
+ // older than any of theirs — by content: a dependency rebuilt to the same bytes is the same build,
120
+ // so the `--force --filter` escape below cannot poison the tree it was run in.
121
+ const workspaceDependencies = (pkg, found = new Map()) => {
122
+ const manifest = manifestOf(pkg)
123
+ for (const block of [
124
+ manifest.dependencies,
125
+ manifest.devDependencies,
126
+ manifest.optionalDependencies,
127
+ manifest.peerDependencies
128
+ ]) {
129
+ for (const [name, range] of Object.entries(block ?? {})) {
130
+ if (typeof range !== 'string' || !range.startsWith('workspace:')) continue
131
+ let dir
132
+ try {
133
+ dir = realpathSync(join(pkg, 'node_modules', name))
134
+ } catch {
135
+ continue // Not linked: the import will say so far more loudly than this guard can.
136
+ }
137
+ if (found.has(dir)) continue
138
+ found.set(dir, name)
139
+ workspaceDependencies(dir, found)
140
+ }
141
+ }
142
+ return found
143
+ }
29
144
  // `newest` answers 0 for a directory that is NOT THERE and for one that is merely old, so the
30
145
  // two conditions were one comparison and one sentence — and the staleness sentence was printed
31
146
  // over trees that had never been built at all, blocking a turn in a fresh git worktree, which
32
147
  // has no dist because dist is not committed (#357). Only the second is evidence of a mistake.
33
- if (existsSync(src)) {
34
- const built = newest(dist)
148
+ if (existsSync(join(own, 'src'))) {
149
+ const built = newest(join(own, 'dist'))
35
150
  if (built === 0) {
36
- process.stderr.write(
37
- 'create-geonosis: this tree has never been built — run pnpm build. No dist here at all, which is what a fresh clone or a linked git worktree starts with; nothing is stale.\n'
151
+ refuse(
152
+ 'this tree has never been built — run pnpm build. No dist here at all, which is what a fresh clone or a linked git worktree starts with; nothing is stale.'
38
153
  )
39
- process.exit(2)
40
154
  }
41
- if (newest(src, NOT_SOURCE) > built) {
42
- process.stderr.write(
43
- 'create-geonosis: dist is older than src — run pnpm build before trusting this bin.\n'
155
+ const name = manifestOf(own).name ?? '<this package>'
156
+ if (standing(own) === 'stale') {
157
+ refuse(
158
+ `dist is older than src — run pnpm build before trusting this bin. If that build just said FULL TURBO, the cache hit left a current dist with its old timestamp and this bin had not yet recorded the sources it was current for: pnpm build --force --filter=${name} rebuilds this one package and clears it.`
44
159
  )
45
- process.exit(2)
160
+ }
161
+ const dependencies = workspaceDependencies(own)
162
+ const dists = new Map()
163
+ for (const [dir, dependency] of dependencies) {
164
+ const state = standing(dir)
165
+ if (state === 'unbuilt') {
166
+ refuse(
167
+ `${dependency}, which this dist reads, has never been built — run pnpm build. No dist there at all; nothing is stale.`
168
+ )
169
+ }
170
+ if (state === 'stale') {
171
+ refuse(
172
+ `${dependency}'s dist is older than its src, and this dist reads that dist — run pnpm build before trusting this bin. If that build just said FULL TURBO, the cache hit left that dist current with its old timestamp and nothing had yet recorded the sources it was current for: pnpm build --force --filter=${dependency} rebuilds that one package and clears it.`
173
+ )
174
+ }
175
+ dists.set(join(dir, 'dist'), dependency)
176
+ }
177
+ if (dists.size > 0) {
178
+ const known = recorded(own)
179
+ const theirs = [...dists.keys()].toSorted()
180
+ const depsNewest = theirs.reduce((most, dist) => Math.max(most, newest(dist)), 0)
181
+ if (depsNewest > built) {
182
+ const depsSum = known.depsSeen === depsNewest ? known.depsSum : digest(...theirs)
183
+ if (depsSum !== known.depsSum) {
184
+ const newer = theirs.filter((dist) => newest(dist) > built).map((dist) => dists.get(dist))
185
+ refuse(
186
+ `dist was built before the current dist of ${newer.join(', ')}, which it reads — run pnpm build before trusting this bin. If that build just said FULL TURBO, the dependency was rebuilt alone and this bin had not yet recorded the build it was current against: pnpm build --force --filter=${name} rebuilds this one package against it.`
187
+ )
188
+ }
189
+ if (known.depsSeen !== depsNewest) remember(own, { ...known, depsSeen: depsNewest, depsSum })
190
+ } else if (known.depsSeen !== depsNewest) {
191
+ remember(own, { ...known, depsSeen: depsNewest, depsSum: digest(...theirs) })
192
+ }
46
193
  }
47
194
  }
48
195
 
@@ -24,7 +24,7 @@ var SEARCH = {
24
24
  "import { pgFtsSearchIndex, type SearchIndex, type SearchProjections } from '@geonosis/search'"
25
25
  ],
26
26
  name: "@geonosis/search",
27
- version: "0.3.11"
27
+ version: "0.4.1"
28
28
  };
29
29
  var DOCUMENT = {
30
30
  at: "domains/document",
@@ -37,13 +37,13 @@ var DOCUMENT = {
37
37
  fieldType: "typeof DOCUMENT_ENTITIES",
38
38
  imports: ["import { documentEntities } from '@geonosis/document'"],
39
39
  name: "@geonosis/document",
40
- version: "0.7.0"
40
+ version: "0.9.0"
41
41
  };
42
42
  var DOMAINS = [SEARCH, DOCUMENT];
43
43
  var DOMAIN_NAMES = DOMAINS.map((one) => one.field);
44
44
  var FOUNDATIONS = [
45
- { at: "foundations/db", name: "@geonosis/db", version: "0.12.0" },
46
- { at: "foundations/events", name: "@geonosis/events", version: "2.1.0" }
45
+ { at: "foundations/db", name: "@geonosis/db", version: "0.14.0" },
46
+ { at: "foundations/events", name: "@geonosis/events", version: "2.2.0" }
47
47
  ];
48
48
  var domainsNamed = (named) => named.map((one) => {
49
49
  const found = DOMAINS.find((domain) => domain.field === one);
@@ -55,7 +55,7 @@ var domainsNamed = (named) => named.map((one) => {
55
55
  return found;
56
56
  });
57
57
  var floorDependencies = (named) => named.length === 0 ? {} : Object.fromEntries(
58
- [...FOUNDATIONS, ...domainsNamed(named)].map((one) => [one.name, `^${one.version}`]).toSorted(([a], [b]) => String(a).localeCompare(String(b)))
58
+ [...FOUNDATIONS, ...domainsNamed(named)].map((one) => [one.name, `^${one.version}`]).toSorted(([a], [b]) => String(a).localeCompare(String(b), "en-US"))
59
59
  );
60
60
  var floorDirectories = (named) => named.length === 0 ? [] : [...FOUNDATIONS, ...domainsNamed(named)].map((one) => one.at);
61
61
  var platformSource = (named) => {
@@ -144,8 +144,7 @@ var VALUED = /* @__PURE__ */ new Set([
144
144
  "--ui"
145
145
  ]);
146
146
  var SWITCHES = /* @__PURE__ */ new Set(["--check", "--help", "-h", "--no-install", "--plan"]);
147
- var parseArgs = (argv) => read(argv);
148
- var read = (argv) => {
147
+ var parseArgs = (argv) => {
149
148
  const flags = {};
150
149
  let dir;
151
150
  for (let at = 0; at < argv.length; at += 1) {
@@ -155,16 +154,17 @@ var read = (argv) => {
155
154
  continue;
156
155
  }
157
156
  const equals = token.indexOf("=");
157
+ const flag = equals === -1 ? token : token.slice(0, equals);
158
+ if (!VALUED.has(flag) && !SWITCHES.has(flag)) {
159
+ throw new Error(
160
+ `${flag} is not an option create-geonosis takes. Run create-geonosis --help for the ones it does.`
161
+ );
162
+ }
158
163
  if (equals !== -1) {
159
- flags[token.slice(0, equals)] = token.slice(equals + 1);
164
+ flags[flag] = token.slice(equals + 1);
160
165
  continue;
161
166
  }
162
- if (!VALUED.has(token)) {
163
- if (!SWITCHES.has(token)) {
164
- throw new Error(
165
- `${token} is not an option create-geonosis takes. Run create-geonosis --help for the ones it does.`
166
- );
167
- }
167
+ if (SWITCHES.has(token)) {
168
168
  flags[token] = "true";
169
169
  continue;
170
170
  }
@@ -191,10 +191,15 @@ var domainsFrom = (given) => {
191
191
  return named;
192
192
  };
193
193
  var parseAnswers = (argv, cwd) => {
194
- const { dir, flags } = read(argv);
194
+ const { dir, flags } = parseArgs(argv);
195
195
  if (dir === void 0) {
196
196
  throw new Error("create-geonosis needs a directory to write the repo into");
197
197
  }
198
+ if (flags["--presets"] !== void 0) {
199
+ throw new Error(
200
+ "--presets is a --plan option: a scaffold takes its presets from --backend and --ui, and would ignore the ones named here"
201
+ );
202
+ }
198
203
  const name = basename(resolve(cwd, dir));
199
204
  const domains = domainsFrom(flags["--domains"]);
200
205
  return {
@@ -420,27 +425,37 @@ var workspacesOf = (answers) => {
420
425
  };
421
426
 
422
427
  // src/files.ts
423
- import { countersFor, LAW_SOURCE, rulesFor, tiersFor } from "@geonosis/cli";
424
- var VERSION = "2.10.0";
425
- var SCHEMA = "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json";
428
+ import {
429
+ countersFor,
430
+ LAW_SOURCE,
431
+ LOADED_VERSION,
432
+ oxlintConfigFor,
433
+ rulesFor,
434
+ tiersFor
435
+ } from "@geonosis/cli";
436
+ var VERSION = LOADED_VERSION;
426
437
  var CATALOG = {
427
438
  "@types/node": "^24.3.0",
428
439
  oxfmt: "^0.62.0",
429
440
  oxlint: "^1.80.0",
430
441
  typescript: "^5.9.2",
431
- vitest: "^3.2.4"
442
+ vitest: "^4.1.11"
432
443
  };
433
444
  var REACT = { "@types/react": "^19.0.0", react: "^19.0.0" };
434
445
  var json = (value) => `${JSON.stringify(value, void 0, 2)}
435
446
  `;
436
447
  var exec = (runtime) => runtime === "bun" ? "bunx" : "npx";
448
+ var run = (runtime) => runtime === "bun" ? "bun run" : "pnpm";
437
449
  var everyWorkspace = (runtime, script) => runtime === "bun" ? `bun run --filter '*' ${script}` : `pnpm -r ${script}`;
438
450
  var presetsFor = (answers) => [
439
451
  "tooling",
440
452
  ...answers.ui === "none" ? [] : ["frontend"],
441
453
  ...answers.backend === "none" ? [] : ["tenant-scoping"],
442
454
  ...answers.backend === "medusa" ? ["backend-medusa"] : [],
443
- ...answers.backend === "sagaflow-cf" ? ["backend-sagaflow-cf"] : [],
455
+ // #197 (plugin): `backend-sagaflow-cf` is a deprecated alias for `backend-workflows-cf` that
456
+ // resolves to the same rules until the next major, then goes — ask for the current name, never
457
+ // the alias, so a scaffolded repo's config does not go stale the day the alias is dropped.
458
+ ...answers.backend === "sagaflow-cf" ? ["backend-workflows-cf"] : [],
444
459
  ...answers.themekit === void 0 ? [] : ["themekit"]
445
460
  ];
446
461
  var tokenPrefix = (name) => `--${name.toLowerCase().replaceAll(/[^a-z]/g, "").slice(0, 3)}-`;
@@ -645,7 +660,7 @@ var workspaceYaml = (answers) => [
645
660
  "# One version of a shared dependency, named once. A repo whose workspaces each pin their own",
646
661
  "# typescript is a repo where one of them typechecks against a compiler the others do not run.",
647
662
  "catalog:",
648
- ...Object.entries({ ...CATALOG, ...answers.ui === "none" ? {} : REACT }).toSorted(([a], [b]) => a.localeCompare(b)).map(([name, spec]) => ` '${name}': '${spec}'`),
663
+ ...Object.entries({ ...CATALOG, ...answers.ui === "none" ? {} : REACT }).toSorted(([a], [b]) => a.localeCompare(b, "en-US")).map(([name, spec]) => ` '${name}': '${spec}'`),
649
664
  "",
650
665
  "# pnpm 11 runs no dependency's install script unless it is named here. esbuild fetches its",
651
666
  "# platform binary in one, and without it vitest cannot start \u2014 a test suite that cannot run",
@@ -713,8 +728,8 @@ var law = (answers) => [
713
728
  "",
714
729
  "**3. No suppression.** No `@ts-ignore`, no `@ts-expect-error`. Fix the type, or ask.",
715
730
  "",
716
- "**4. oxfmt only.** `pnpm format` writes, `pnpm format:check` verifies. Never prettier: it is",
717
- "not a dependency here, so it downloads itself and reformats with its own defaults.",
731
+ `**4. oxfmt only.** \`${run(answers.runtime)} format\` writes, \`${run(answers.runtime)} format:check\` verifies. Never`,
732
+ "prettier: it is not a dependency here, so it downloads itself and reformats with its own defaults.",
718
733
  "",
719
734
  "**5. Nothing is hardcoded to a repo.** Every list a rule needs is an option.",
720
735
  "",
@@ -845,14 +860,7 @@ var filesFor = (answers) => {
845
860
  CORPUS_DIR
846
861
  ]
847
862
  }),
848
- ".oxlintrc.json": json({
849
- $schema: SCHEMA,
850
- plugins: ["typescript", "unicorn"],
851
- jsPlugins: ["@geonosis/oxlint-plugin-biological-architecture"],
852
- categories: { correctness: "error", suspicious: "error" },
853
- rules,
854
- ignorePatterns: ["node_modules", "dist", "coverage", CORPUS_DIR]
855
- }),
863
+ ".oxlintrc.json": json(oxlintConfigFor(rules, [CORPUS_DIR])),
856
864
  "geonosis.json": json({
857
865
  verify: tiersFor({
858
866
  rules,
@@ -996,7 +1004,15 @@ var describeDrift = (drift) => drift.map((one) => SAID[one.kind](one));
996
1004
 
997
1005
  // src/scaffold.ts
998
1006
  import { spawnSync } from "child_process";
999
- import { chmodSync, existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1007
+ import {
1008
+ chmodSync,
1009
+ existsSync as existsSync2,
1010
+ mkdirSync,
1011
+ readdirSync,
1012
+ readFileSync as readFileSync2,
1013
+ symlinkSync,
1014
+ writeFileSync
1015
+ } from "fs";
1000
1016
  import { dirname, join as join2 } from "path";
1001
1017
  var KIT_PACKAGES = [
1002
1018
  "cli",
@@ -1023,7 +1039,7 @@ var writeScaffold = (answers) => {
1023
1039
  for (const [file, body] of Object.entries(files)) write(answers.dir, file, body);
1024
1040
  return Object.keys(files).toSorted();
1025
1041
  };
1026
- var run = (what, dir, command, args) => {
1042
+ var run2 = (what, dir, command, args) => {
1027
1043
  const done = spawnSync(command, args, { cwd: dir, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
1028
1044
  return {
1029
1045
  code: done.error === void 0 ? done.status ?? 2 : 2,
@@ -1081,7 +1097,7 @@ var linkKit = (answers, kit) => {
1081
1097
  const declared = readJson(join2(from, "package.json"));
1082
1098
  if (declared.name === void 0) continue;
1083
1099
  const to = join2(scope, declared.name.replace("@geonosis/", ""));
1084
- if (!existsSync2(to)) spawnSync("ln", ["-s", from, to]);
1100
+ if (!existsSync2(to)) symlinkSync(from, to);
1085
1101
  for (const [called, entry] of binsOf(from)) {
1086
1102
  const shim = join2(bin, called);
1087
1103
  writeFileSync(shim, `#!/bin/sh
@@ -1117,10 +1133,10 @@ var finish = ({ answers, install }) => {
1117
1133
  if (install) {
1118
1134
  const [command, args] = installer(answers);
1119
1135
  const offline = answers.runtime === "pnpm" ? [...args, "--offline"] : args;
1120
- const first = run(`${command} install (offline)`, answers.dir, command, offline);
1136
+ const first = run2(`${command} install (offline)`, answers.dir, command, offline);
1121
1137
  steps.push(first);
1122
1138
  if (first.code !== 0) {
1123
- steps.push(run(`${command} install (network)`, answers.dir, command, args));
1139
+ steps.push(run2(`${command} install (network)`, answers.dir, command, args));
1124
1140
  }
1125
1141
  }
1126
1142
  if (answers.kit !== void 0) {
@@ -1136,7 +1152,7 @@ var finish = ({ answers, install }) => {
1136
1152
  if (existsSync2(ledger)) {
1137
1153
  for (const { target, write: into } of GRAPH_SYNCS) {
1138
1154
  steps.push(
1139
- run(`sync the edges table into ${into}`, answers.dir, ledger, [
1155
+ run2(`sync the edges table into ${into}`, answers.dir, ledger, [
1140
1156
  "sync",
1141
1157
  "--target",
1142
1158
  target,
@@ -1148,7 +1164,7 @@ var finish = ({ answers, install }) => {
1148
1164
  }
1149
1165
  const oxfmt = join2(answers.dir, "node_modules/.bin/oxfmt");
1150
1166
  if (existsSync2(oxfmt)) {
1151
- steps.push(run("format", answers.dir, oxfmt, ["--config", ".oxfmtrc.json", "."]));
1167
+ steps.push(run2("format", answers.dir, oxfmt, ["--config", ".oxfmtrc.json", "."]));
1152
1168
  }
1153
1169
  return steps;
1154
1170
  };
@@ -1167,6 +1183,7 @@ export {
1167
1183
  UIS,
1168
1184
  MEDUSA_RANGE,
1169
1185
  parseArgs,
1186
+ scopeOf,
1170
1187
  parseAnswers,
1171
1188
  wantsInstall,
1172
1189
  CORPUS_DIR,
@@ -9,9 +9,10 @@ import {
9
9
  finish,
10
10
  parseAnswers,
11
11
  parseArgs,
12
+ scopeOf,
12
13
  wantsInstall,
13
14
  writeScaffold
14
- } from "./chunk-5AQ755XJ.js";
15
+ } from "./chunk-YF4AX76D.js";
15
16
 
16
17
  // src/create-geonosis-cli.ts
17
18
  import { existsSync as existsSync2 } from "fs";
@@ -86,7 +87,7 @@ var planAdoption = ({ backend, dir, presets, runtime, ui }) => {
86
87
  domains: [],
87
88
  name: basename(at),
88
89
  runtime: chosenRuntime,
89
- scope: `@${basename(at).toLowerCase()}`,
90
+ scope: scopeOf(basename(at)),
90
91
  ui: uiOf(at, chosen("--ui", ui, UIS))
91
92
  };
92
93
  const drift = checkScaffold(answers);
@@ -118,7 +119,7 @@ var planAdoption = ({ backend, dir, presets, runtime, ui }) => {
118
119
  return { declared: declaredCounters.has(key), key };
119
120
  }),
120
121
  dir: at,
121
- doors: [.../* @__PURE__ */ new Set([...KIT_OWNED_BLOCKS, ...Object.keys(config)])].filter((block) => KIT_OWNED_BLOCKS.includes(block)).toSorted().map((block) => ({
122
+ doors: KIT_OWNED_BLOCKS.map((block) => ({
122
123
  block,
123
124
  declared: declaredPackages.has(doorFor(block)),
124
125
  package: doorFor(block)
@@ -189,7 +190,7 @@ var formatPlan = (plan) => [
189
190
 
190
191
  // src/create-geonosis-cli.ts
191
192
  var USAGE = `create-geonosis <dir> --runtime pnpm|bun --backend medusa|sagaflow-cf|none --ui tiered-shadcn|atoms-only|none
192
- [--medusa <range>]
193
+ [--medusa <range>]
193
194
  [--domains <a,b>] [--brand <name>] [--themekit <name>] [--marketplace <path>] [--kit <path>] [--no-install]
194
195
  [--check]
195
196
  create-geonosis --plan <dir> [--presets <a,b>] [--runtime \u2026] [--backend \u2026] [--ui \u2026]
package/dist/index.d.ts CHANGED
@@ -118,10 +118,10 @@ declare const CORPUS_DIR = "lint-corpus";
118
118
  * A reach corpus a shipped one cannot be.
119
119
  *
120
120
  * `geonosis-doctor` asks whether every enabled rule can fire at all, and it asks the corpus the
121
- * PLUGIN ships. That answers for most rules and cannot answer for three: `layer-walls` fires on a
122
- * repo's own layer names, `no-brand-names` on its own brand, `plugin-route-namespaced` on its own
123
- * package root. The plugin's corpus says "acme" and `layers/core`, so it is no evidence either way
124
- * about a repo that says anything else — and today those rules read as firing nowhere.
121
+ * PLUGIN ships. That cannot answer for a rule whose reach is the repo's own vocabulary: its layer
122
+ * names, its brand, its plugin package root, the `within` its sagas live in. The plugin's corpus
123
+ * says "acme" and `layers/core`, so it is no evidence either way about a repo that says anything
124
+ * else — and those rules would read as firing nowhere.
125
125
  *
126
126
  * So the repo ships its own, in ITS vocabulary. The corpus is copied to a temp root and linted with
127
127
  * `--no-ignore`, so the fixture paths mirror the repo's real ones and the layer patterns match.
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  wantsInstall,
24
24
  workspacesOf,
25
25
  writeScaffold
26
- } from "./chunk-5AQ755XJ.js";
26
+ } from "./chunk-YF4AX76D.js";
27
27
  export {
28
28
  BACKENDS,
29
29
  CORPUS_DIR,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-geonosis",
3
- "version": "2.10.0",
3
+ "version": "2.11.1",
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": [
@@ -35,7 +35,7 @@
35
35
  "dist"
36
36
  ],
37
37
  "dependencies": {
38
- "@geonosis/cli": "2.10.0"
38
+ "@geonosis/cli": "2.11.1"
39
39
  },
40
40
  "engines": {
41
41
  "node": ">=22"
@@ -44,7 +44,7 @@
44
44
  "access": "public"
45
45
  },
46
46
  "scripts": {
47
- "build": "tsup",
47
+ "build": "tsx ../../tooling/build-step.ts tsup",
48
48
  "typecheck": "tsc --noEmit",
49
49
  "lint": "oxlint --config ../../.oxlintrc.json .",
50
50
  "test:unit": "vitest run --project unit --root ../.. --passWithNoTests \"$PWD\"",