create-geonosis 2.12.0 → 3.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/CHANGELOG.md +30 -0
- package/bin/create-geonosis.mjs +7 -192
- package/dist/adopt.js +144 -0
- package/dist/answers.d.ts +42 -0
- package/dist/answers.js +111 -0
- package/dist/check.d.ts +24 -0
- package/dist/check.js +62 -0
- package/dist/composition.d.ts +46 -0
- package/dist/composition.js +137 -0
- package/dist/corpus.d.ts +16 -0
- package/dist/corpus.js +46 -0
- package/dist/create-geonosis-cli.js +41 -229
- package/dist/files.d.ts +18 -0
- package/dist/files.js +497 -0
- package/dist/floor-versions.d.ts +29 -0
- package/dist/floor-versions.js +35 -0
- package/dist/index.d.ts +7 -170
- package/dist/index.js +16 -19
- package/dist/layout.d.ts +18 -0
- package/dist/layout.js +164 -0
- package/dist/scaffold.d.ts +36 -0
- package/dist/scaffold.js +173 -0
- package/package.json +5 -4
- package/dist/chunk-4IX6PJ7R.js +0 -1200
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# create-geonosis
|
|
2
2
|
|
|
3
|
+
## 3.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 670f7c1: Every bin runs on bun: its shebang is `#!/usr/bin/env bun`, so a repo that runs the tools needs bun on `PATH`. On pnpm 12, `devEngines.runtime` names it and the install puts it in `node_modules/.bin`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
"devEngines": { "runtime": { "name": "bun", "version": "^1.4.2", "onFail": "download" } }
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The runtime floors (foundations, domains, kits) are unchanged: they declare no runtime of their own and run on Node, Workers or a browser bundle.
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 0f272e2: A tree says which bun the kit's bins start on. Every package with a bin declares `engines.bun`, at the range the kit itself runs. `create-geonosis` writes a pnpm repo's `devEngines.runtime` and `packageManager` as the kit's own manifest has them, so pnpm 12 installs that bun. `geonosis-doctor`'s drift check WARNs, on the `tool runtime` line, where an installed kit package asks for bun and the root neither runs on it nor declares it, and prints the block to add.
|
|
18
|
+
- 29a4820: TypeScript 7. A scaffold pins the tools at the ranges the kit itself runs, read from the kit's own manifest when a cut is made, so the two cannot drift: `typescript` ^7, `oxlint` ^1.85, `oxfmt` ^0.70.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- 1e38e4e: `geonosis-themekit`, `geonosis-rails` and `geonosis-evals` read their arguments with `node:util`'s `parseArgs`. A flag one does not know is refused by name, exit 2, where the hand-written readers ignored it and ran without it; a flag given twice keeps its last value in all three (evals kept the first). `geonosis-evals compare --tolerance x=0.1 a.json b.json` reads `a.json` and `b.json` as the scores: the old reader took the tolerance's value for the first score file. `geonosis-ratchet` and `geonosis-verify`, which already refused what they did not know, read their flags the same way; verify still refuses a flag that belongs to another of its commands. `geonosis-doctor` reads its flags with `parseArgs` too, its optional `--baseline-against` ref through the parser's tokens. `@geonosis/ledger`'s exported `parseArgs(argv, valued, switches)` is `node:util`'s underneath and takes the switches it accepts, refusing an undeclared one and a valued flag followed by another flag (`--note --plan 3` used to store `--plan` as the note); `rejectUnknownSwitches` is gone. `geonosis-walk`, `geonosis-review` and `geonosis-lint-parity` parse the same way; parity keeps its own words for a bare `--corpus` or `--expect-changed`. `geonosis-observability`, `geonosis-verify-arch`, `geonosis-testbed` and `geonosis-visual-diff` follow; the last two used to accept a flag they did not know (testbed ignored it, visual-diff read it as a switch) and now refuse it. `@geonosis/release`'s exported `parseArgs` keeps its `Parsed` shape over `node:util`'s. `@geonosis/cli`'s `parseArgv` and `create-geonosis`'s `parseArgs` are built on it too, keeping their own refusals; `parseArgv` now refuses a valued flag followed by another flag instead of taking that flag as its value.
|
|
23
|
+
- Updated dependencies [1e38e4e]
|
|
24
|
+
- @geonosis/cli@3.0.0
|
|
25
|
+
|
|
26
|
+
## 2.13.0
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- Updated dependencies [f422aea]
|
|
31
|
+
- @geonosis/cli@2.13.0
|
|
32
|
+
|
|
3
33
|
## 2.12.0
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
package/bin/create-geonosis.mjs
CHANGED
|
@@ -1,196 +1,11 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
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'
|
|
17
|
-
import { dirname, join } from 'node:path'
|
|
18
|
-
import { fileURLToPath } from 'node:url'
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Generated by pnpm generate:shims. In the kit's own tree src/ sits beside dist/, and a dist older
|
|
3
|
+
// than src once answered for a fix it did not carry (#62); a published package ships no src/.
|
|
4
|
+
import { existsSync } from 'node:fs'
|
|
19
5
|
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
// not gone stale (#399).
|
|
24
|
-
const NOT_SOURCE = (name) =>
|
|
25
|
-
name.endsWith('.test.ts') ||
|
|
26
|
-
name.endsWith('.test.tsx') ||
|
|
27
|
-
name === '__tests__' ||
|
|
28
|
-
name === '__fixtures__'
|
|
29
|
-
const newest = (dir, skip) =>
|
|
30
|
-
existsSync(dir)
|
|
31
|
-
? readdirSync(dir, { withFileTypes: true }).reduce((most, entry) => {
|
|
32
|
-
if (skip !== undefined && skip(entry.name)) return most
|
|
33
|
-
const at = join(dir, entry.name)
|
|
34
|
-
return Math.max(most, entry.isDirectory() ? newest(at, skip) : statSync(at).mtimeMs)
|
|
35
|
-
}, 0)
|
|
36
|
-
: 0
|
|
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
|
-
}
|
|
144
|
-
// `newest` answers 0 for a directory that is NOT THERE and for one that is merely old, so the
|
|
145
|
-
// two conditions were one comparison and one sentence — and the staleness sentence was printed
|
|
146
|
-
// over trees that had never been built at all, blocking a turn in a fresh git worktree, which
|
|
147
|
-
// has no dist because dist is not committed (#357). Only the second is evidence of a mistake.
|
|
148
|
-
if (existsSync(join(own, 'src'))) {
|
|
149
|
-
const built = newest(join(own, 'dist'))
|
|
150
|
-
if (built === 0) {
|
|
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.'
|
|
153
|
-
)
|
|
154
|
-
}
|
|
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.`
|
|
159
|
-
)
|
|
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
|
-
}
|
|
193
|
-
}
|
|
6
|
+
if (existsSync(new URL('../src', import.meta.url))) {
|
|
7
|
+
const { guardBuild } = await import('../../../tooling/bin-guard.mjs')
|
|
8
|
+
guardBuild(new URL('..', import.meta.url), 'create-geonosis')
|
|
194
9
|
}
|
|
195
10
|
|
|
196
11
|
await import('../dist/create-geonosis-cli.js')
|
package/dist/adopt.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// src/adopt.ts
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { countersFor, rulesFor, runtimeOf, tiersFor } from "@geonosis/cli";
|
|
5
|
+
import { BACKENDS, scopeOf, UIS } from "./answers.js";
|
|
6
|
+
import { checkScaffold } from "./check.js";
|
|
7
|
+
var ADOPT_USAGE = "create-geonosis --plan <dir> [--presets <a,b>] [--runtime pnpm|bun] [--backend medusa|sagaflow-cf|none] [--ui tiered-shadcn|atoms-only|none] — what adopting this repo would take, computed off the tree. Writes nothing.";
|
|
8
|
+
var LOCKFILES = ["bun.lock", "bun.lockb", "pnpm-lock.yaml"];
|
|
9
|
+
var MEDUSA = "@medusajs/";
|
|
10
|
+
var WORKERS = ["wrangler", "@cloudflare/workers-types", "sagaflow-js"];
|
|
11
|
+
var readJson = (at) => {
|
|
12
|
+
if (!existsSync(at))
|
|
13
|
+
return;
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(at, "utf8"));
|
|
16
|
+
} catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var manifestsIn = (dir) => {
|
|
21
|
+
const found = [readJson(join(dir, "package.json"))];
|
|
22
|
+
for (const group of ["apps", "packages"]) {
|
|
23
|
+
const at = join(dir, group);
|
|
24
|
+
if (!existsSync(at))
|
|
25
|
+
continue;
|
|
26
|
+
for (const entry of readdirSync(at, { withFileTypes: true })) {
|
|
27
|
+
if (entry.isDirectory())
|
|
28
|
+
found.push(readJson(join(at, entry.name, "package.json")));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return found.filter((one) => one !== undefined);
|
|
32
|
+
};
|
|
33
|
+
var dependencyNames = (manifests) => manifests.flatMap((one) => [
|
|
34
|
+
...Object.keys(one.dependencies ?? {}),
|
|
35
|
+
...Object.keys(one.devDependencies ?? {})
|
|
36
|
+
]);
|
|
37
|
+
var backendOf = (manifests) => {
|
|
38
|
+
const names = dependencyNames(manifests);
|
|
39
|
+
if (names.some((one) => one.startsWith(MEDUSA)))
|
|
40
|
+
return "medusa";
|
|
41
|
+
if (names.some((one) => WORKERS.includes(one)))
|
|
42
|
+
return "sagaflow-cf";
|
|
43
|
+
return "none";
|
|
44
|
+
};
|
|
45
|
+
var chosen = (flag, given, allowed) => {
|
|
46
|
+
if (given === undefined)
|
|
47
|
+
return;
|
|
48
|
+
if (!allowed.includes(given)) {
|
|
49
|
+
throw new Error(`${flag} "${given}" is not one of ${allowed.join(", ")}`);
|
|
50
|
+
}
|
|
51
|
+
return given;
|
|
52
|
+
};
|
|
53
|
+
var uiOf = (dir, given) => {
|
|
54
|
+
if (given !== undefined)
|
|
55
|
+
return given;
|
|
56
|
+
if (!existsSync(join(dir, "packages/ui")))
|
|
57
|
+
return "none";
|
|
58
|
+
throw new Error(`${dir} has a packages/ui and no --ui was given — the tier decides which rules the plan proposes, and a directory listing cannot say which tier it is. Name --ui ${UIS.join("|")}.`);
|
|
59
|
+
};
|
|
60
|
+
var doorFor = (block) => `@geonosis/${block}`;
|
|
61
|
+
var KIT_OWNED_BLOCKS = ["ledger", "review", "testbed", "verify", "walk"];
|
|
62
|
+
var planAdoption = ({ backend, dir, presets, runtime, ui }) => {
|
|
63
|
+
const at = resolve(dir);
|
|
64
|
+
if (!existsSync(at))
|
|
65
|
+
throw new Error(`${at} is not there — --plan reads a repo, it never makes one`);
|
|
66
|
+
const manifests = manifestsIn(at);
|
|
67
|
+
const chosenRuntime = chosen("--runtime", runtime, ["bun", "pnpm"]) ?? runtimeOf(at);
|
|
68
|
+
if (chosenRuntime === undefined) {
|
|
69
|
+
throw new Error(`${at} has none of ${LOCKFILES.join(", ")}, so nothing there says which package manager it runs — name --runtime pnpm|bun.`);
|
|
70
|
+
}
|
|
71
|
+
const answers = {
|
|
72
|
+
backend: chosen("--backend", backend, BACKENDS) ?? backendOf(manifests),
|
|
73
|
+
dir: at,
|
|
74
|
+
domains: [],
|
|
75
|
+
name: basename(at),
|
|
76
|
+
runtime: chosenRuntime,
|
|
77
|
+
scope: scopeOf(basename(at)),
|
|
78
|
+
ui: uiOf(at, chosen("--ui", ui, UIS))
|
|
79
|
+
};
|
|
80
|
+
const drift = checkScaffold(answers);
|
|
81
|
+
const enabled = new Set(Object.keys(readJson(join(at, ".oxlintrc.json"))?.rules ?? {}));
|
|
82
|
+
const config = readJson(join(at, "geonosis.json")) ?? {};
|
|
83
|
+
const declaredTiers = config["verify"] ?? {};
|
|
84
|
+
const declaredCounters = new Set((readJson(join(at, "geonosis.ratchet.json"))?.counters ?? []).map((one) => one.key ?? one.counter ?? ""));
|
|
85
|
+
const declaredPackages = new Set(dependencyNames(manifests));
|
|
86
|
+
const { unanswered } = rulesFor(presets);
|
|
87
|
+
const tiers = tiersFor({
|
|
88
|
+
rules: {},
|
|
89
|
+
runtime: chosenRuntime,
|
|
90
|
+
scripts: Object.keys(readJson(join(at, "package.json"))?.scripts ?? {})
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
backend: answers.backend,
|
|
94
|
+
counters: countersFor(chosenRuntime, "LAW.md").map((one) => {
|
|
95
|
+
const key = typeof one["key"] === "string" ? one["key"] : String(one["counter"]);
|
|
96
|
+
return { declared: declaredCounters.has(key), key };
|
|
97
|
+
}),
|
|
98
|
+
dir: at,
|
|
99
|
+
doors: KIT_OWNED_BLOCKS.map((block) => ({
|
|
100
|
+
block,
|
|
101
|
+
declared: declaredPackages.has(doorFor(block)),
|
|
102
|
+
package: doorFor(block)
|
|
103
|
+
})),
|
|
104
|
+
files: drift.filter((one) => one.kind === "missing"),
|
|
105
|
+
keys: drift.filter((one) => one.kind === "key"),
|
|
106
|
+
presets: presets.map((name) => {
|
|
107
|
+
const brings = Object.keys(rulesFor([name]).rules);
|
|
108
|
+
return {
|
|
109
|
+
brings: brings.length,
|
|
110
|
+
enabled: brings.filter((id) => enabled.has(id)),
|
|
111
|
+
name
|
|
112
|
+
};
|
|
113
|
+
}),
|
|
114
|
+
runtime: chosenRuntime,
|
|
115
|
+
tiers: Object.entries(tiers).map(([name, steps]) => ({
|
|
116
|
+
declared: Object.hasOwn(declaredTiers, name),
|
|
117
|
+
name,
|
|
118
|
+
steps
|
|
119
|
+
})),
|
|
120
|
+
ui: answers.ui,
|
|
121
|
+
unanswered,
|
|
122
|
+
unreadable: drift.filter((one) => one.kind === "unreadable")
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
var section = (title, lines) => lines.length === 0 ? [`${title} — none`, ""] : [`${title} (${lines.length})`, ...lines, ""];
|
|
126
|
+
var formatPlan = (plan) => [
|
|
127
|
+
`adoption plan for ${plan.dir}`,
|
|
128
|
+
` runtime ${plan.runtime} · backend ${plan.backend} · ui ${plan.ui}`,
|
|
129
|
+
"",
|
|
130
|
+
...section("FILES the scaffold writes and this repo has none of", plan.files.map((one) => ` ${one.file}`)),
|
|
131
|
+
...section("KEYS the scaffold declares inside a file this repo has", plan.keys.map((one) => ` ${one.file} → ${one.at}`)),
|
|
132
|
+
...section("RULES the presets bring", plan.presets.map((one) => ` ${one.name}: ${one.brings} rules, ${one.enabled.length} already enabled here`)),
|
|
133
|
+
...section("RULES left out until somebody answers their option", plan.unanswered.map(([rule, option]) => ` ${rule} needs \`${option}\``)),
|
|
134
|
+
...section("COUNTERS the ratchet would run", plan.counters.map((one) => ` ${one.key}${one.declared ? " — declared" : ""}`)),
|
|
135
|
+
...section("TIERS", plan.tiers.map((one) => ` ${one.name}${one.declared ? " — declared" : ""}`)),
|
|
136
|
+
...section("DOORS a block needs declared", plan.doors.map((one) => ` ${one.package} for "${one.block}"${one.declared ? " — declared" : ""}`)),
|
|
137
|
+
...plan.unreadable.length === 0 ? [] : section("FILES this check could not read as JSON", plan.unreadable.map((one) => ` ${one.file}`))
|
|
138
|
+
].join(`
|
|
139
|
+
`);
|
|
140
|
+
export {
|
|
141
|
+
ADOPT_USAGE,
|
|
142
|
+
formatPlan,
|
|
143
|
+
planAdoption
|
|
144
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare const RUNTIMES: readonly ['bun', 'pnpm'];
|
|
2
|
+
export declare const BACKENDS: readonly ['medusa', 'none', 'sagaflow-cf'];
|
|
3
|
+
export declare const UIS: readonly ['atoms-only', 'none', 'tiered-shadcn'];
|
|
4
|
+
export type Runtime = (typeof RUNTIMES)[number];
|
|
5
|
+
export type Backend = (typeof BACKENDS)[number];
|
|
6
|
+
export type Ui = (typeof UIS)[number];
|
|
7
|
+
/**
|
|
8
|
+
* The Medusa the `--backend medusa` scaffold declares.
|
|
9
|
+
*
|
|
10
|
+
* MEASURED 2026-09-02: the one Medusa consumer of this kit runs `^2.19.0`. The scaffold turns the
|
|
11
|
+
* Medusa rules on, and a tree that enables them while declaring no `@medusajs/*` earns nine
|
|
12
|
+
* `exercised` WARNs — rules with no possible finding, which read exactly like a clean tree (#228).
|
|
13
|
+
* A range nobody has measured against is worse than none, so this one is the default and
|
|
14
|
+
* `--medusa <range>` is how a repo on another one says so.
|
|
15
|
+
*/
|
|
16
|
+
export declare const MEDUSA_RANGE = "^2.19.0";
|
|
17
|
+
export declare const MEDUSA_PACKAGES: readonly ['@medusajs/framework', '@medusajs/medusa'];
|
|
18
|
+
export type Answers = {
|
|
19
|
+
backend: Backend;
|
|
20
|
+
brand?: string;
|
|
21
|
+
dir: string;
|
|
22
|
+
/** The domains this repo composes at its composition root. Empty until the flag names one. */
|
|
23
|
+
domains: string[];
|
|
24
|
+
/** The kit read from a workspace instead of the registry — a proof aid, never the default. */
|
|
25
|
+
kit?: string;
|
|
26
|
+
marketplace?: string;
|
|
27
|
+
/** The Medusa range a `--backend medusa` tree declares. `MEDUSA_RANGE` unless one is named. */
|
|
28
|
+
medusa?: string;
|
|
29
|
+
name: string;
|
|
30
|
+
runtime: Runtime;
|
|
31
|
+
scope: string;
|
|
32
|
+
themekit?: string;
|
|
33
|
+
ui: Ui;
|
|
34
|
+
};
|
|
35
|
+
export declare const parseArgs: (argv: readonly string[]) => {
|
|
36
|
+
flags: Record<string, string>;
|
|
37
|
+
dir?: string;
|
|
38
|
+
};
|
|
39
|
+
/** A package scope from the repo's own name, lowercased, so `@Pinecone/core` is never written. */
|
|
40
|
+
export declare const scopeOf: (name: string) => string;
|
|
41
|
+
export declare const parseAnswers: (argv: readonly string[], cwd: string) => Answers;
|
|
42
|
+
export declare const wantsInstall: (argv: readonly string[]) => boolean;
|
package/dist/answers.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// src/answers.ts
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import { parseArgs as readArgv } from "node:util";
|
|
4
|
+
import { domainsNamed } from "./composition.js";
|
|
5
|
+
var RUNTIMES = ["bun", "pnpm"];
|
|
6
|
+
var BACKENDS = ["medusa", "none", "sagaflow-cf"];
|
|
7
|
+
var UIS = ["atoms-only", "none", "tiered-shadcn"];
|
|
8
|
+
var MEDUSA_RANGE = "^2.19.0";
|
|
9
|
+
var MEDUSA_PACKAGES = ["@medusajs/framework", "@medusajs/medusa"];
|
|
10
|
+
var VALUED = new Set([
|
|
11
|
+
"--backend",
|
|
12
|
+
"--presets",
|
|
13
|
+
"--brand",
|
|
14
|
+
"--domains",
|
|
15
|
+
"--kit",
|
|
16
|
+
"--marketplace",
|
|
17
|
+
"--medusa",
|
|
18
|
+
"--runtime",
|
|
19
|
+
"--themekit",
|
|
20
|
+
"--ui"
|
|
21
|
+
]);
|
|
22
|
+
var SWITCHES = new Set(["--check", "--help", "-h", "--no-install", "--plan"]);
|
|
23
|
+
var OPTIONS = Object.fromEntries([
|
|
24
|
+
...[...VALUED].map((flag) => [flag.slice(2), { type: "string" }]),
|
|
25
|
+
...[...SWITCHES].filter((flag) => flag.startsWith("--")).map((flag) => [
|
|
26
|
+
flag.slice(2),
|
|
27
|
+
flag === "--help" ? { short: "h", type: "boolean" } : { type: "boolean" }
|
|
28
|
+
])
|
|
29
|
+
]);
|
|
30
|
+
var parseArgs = (argv) => {
|
|
31
|
+
const { tokens } = readArgv({
|
|
32
|
+
allowPositionals: true,
|
|
33
|
+
args: [...argv],
|
|
34
|
+
options: OPTIONS,
|
|
35
|
+
strict: false,
|
|
36
|
+
tokens: true
|
|
37
|
+
});
|
|
38
|
+
for (const token of tokens) {
|
|
39
|
+
if (token.kind === "option" && !(token.name in OPTIONS)) {
|
|
40
|
+
throw new Error(`${token.rawName} is not an option create-geonosis takes. Run create-geonosis --help for the ones it does.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const { positionals, values } = readArgv({
|
|
44
|
+
allowPositionals: true,
|
|
45
|
+
args: [...argv],
|
|
46
|
+
options: OPTIONS,
|
|
47
|
+
strict: true
|
|
48
|
+
});
|
|
49
|
+
const flags = {};
|
|
50
|
+
for (const [name, value] of Object.entries(values)) {
|
|
51
|
+
if (typeof value === "string")
|
|
52
|
+
flags[`--${name}`] = value;
|
|
53
|
+
if (value === true)
|
|
54
|
+
flags[`--${name}`] = "true";
|
|
55
|
+
}
|
|
56
|
+
const [dir] = positionals;
|
|
57
|
+
return { ...dir === undefined ? {} : { dir }, flags };
|
|
58
|
+
};
|
|
59
|
+
var choose = (flag, given, allowed) => {
|
|
60
|
+
if (given === undefined) {
|
|
61
|
+
throw new Error(`${flag} is required — it is one of ${allowed.join(", ")}, and never a default`);
|
|
62
|
+
}
|
|
63
|
+
const chosen = allowed.find((one) => one === given);
|
|
64
|
+
if (chosen === undefined) {
|
|
65
|
+
throw new Error(`${flag} "${given}" is not one of ${allowed.join(", ")}`);
|
|
66
|
+
}
|
|
67
|
+
return chosen;
|
|
68
|
+
};
|
|
69
|
+
var scopeOf = (name) => `@${name.toLowerCase().replaceAll(/[^a-z0-9-]/g, "-")}`;
|
|
70
|
+
var domainsFrom = (given) => {
|
|
71
|
+
const named = (given ?? "").split(",").map((one) => one.trim()).filter((one) => one !== "");
|
|
72
|
+
domainsNamed(named);
|
|
73
|
+
return named;
|
|
74
|
+
};
|
|
75
|
+
var parseAnswers = (argv, cwd) => {
|
|
76
|
+
const { dir, flags } = parseArgs(argv);
|
|
77
|
+
if (dir === undefined) {
|
|
78
|
+
throw new Error("create-geonosis needs a directory to write the repo into");
|
|
79
|
+
}
|
|
80
|
+
if (flags["--presets"] !== undefined) {
|
|
81
|
+
throw new Error("--presets is a --plan option: a scaffold takes its presets from --backend and --ui, and would ignore the ones named here");
|
|
82
|
+
}
|
|
83
|
+
const name = basename(resolve(cwd, dir));
|
|
84
|
+
const domains = domainsFrom(flags["--domains"]);
|
|
85
|
+
return {
|
|
86
|
+
backend: choose("--backend", flags["--backend"], BACKENDS),
|
|
87
|
+
...flags["--brand"] === undefined ? {} : { brand: flags["--brand"] },
|
|
88
|
+
dir: resolve(cwd, dir),
|
|
89
|
+
domains,
|
|
90
|
+
...flags["--kit"] === undefined ? {} : { kit: resolve(cwd, flags["--kit"]) },
|
|
91
|
+
...flags["--marketplace"] === undefined ? {} : { marketplace: flags["--marketplace"] },
|
|
92
|
+
...flags["--medusa"] === undefined ? {} : { medusa: flags["--medusa"] },
|
|
93
|
+
name,
|
|
94
|
+
runtime: choose("--runtime", flags["--runtime"], RUNTIMES),
|
|
95
|
+
scope: scopeOf(name),
|
|
96
|
+
...flags["--themekit"] === undefined ? {} : { themekit: flags["--themekit"] },
|
|
97
|
+
ui: choose("--ui", flags["--ui"], UIS)
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
var wantsInstall = (argv) => !argv.includes("--no-install");
|
|
101
|
+
export {
|
|
102
|
+
BACKENDS,
|
|
103
|
+
MEDUSA_PACKAGES,
|
|
104
|
+
MEDUSA_RANGE,
|
|
105
|
+
RUNTIMES,
|
|
106
|
+
UIS,
|
|
107
|
+
parseAnswers,
|
|
108
|
+
parseArgs,
|
|
109
|
+
scopeOf,
|
|
110
|
+
wantsInstall
|
|
111
|
+
};
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Answers } from './answers.js';
|
|
2
|
+
/**
|
|
3
|
+
* What an existing repo never receives: whatever the scaffold has learned since it was run.
|
|
4
|
+
*
|
|
5
|
+
* The comparison is over SHAPE and not over content, because a repo that has been worked in has
|
|
6
|
+
* legitimately rewritten every file the scaffold gave it. What it cannot have legitimately done is
|
|
7
|
+
* lose a file the scaffold now writes, or miss a declaration a new gate reads — so a missing path is
|
|
8
|
+
* drift and a changed value never is.
|
|
9
|
+
*/
|
|
10
|
+
export type DriftKind = 'key' | 'missing' | 'unreadable';
|
|
11
|
+
export type Drift = {
|
|
12
|
+
/** The key path inside the file, or `''` when the fact is about the whole file. */
|
|
13
|
+
at: string;
|
|
14
|
+
file: string;
|
|
15
|
+
kind: DriftKind;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The drift between a tree and what the scaffold would write into it today. Reads only.
|
|
19
|
+
*
|
|
20
|
+
* Nothing in this module opens a file for writing, and nothing calls `writeScaffold`. That is the
|
|
21
|
+
* whole contract of `--check`: it is run against a repo somebody is working in.
|
|
22
|
+
*/
|
|
23
|
+
export declare const checkScaffold: (answers: Answers) => Drift[];
|
|
24
|
+
export declare const describeDrift: (drift: readonly Drift[]) => string[];
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/check.ts
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { PLATFORM_FILE } from "./composition.js";
|
|
5
|
+
import { filesFor } from "./files.js";
|
|
6
|
+
import { workspacesOf } from "./layout.js";
|
|
7
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
var pathsIn = (value, prefix = "") => {
|
|
9
|
+
if (!isRecord(value))
|
|
10
|
+
return [];
|
|
11
|
+
return Object.entries(value).flatMap(([key, child]) => {
|
|
12
|
+
const here = prefix === "" ? key : `${prefix}.${key}`;
|
|
13
|
+
return [here, ...pathsIn(child, here)];
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var parsed = (body) => {
|
|
17
|
+
try {
|
|
18
|
+
const value = JSON.parse(body);
|
|
19
|
+
return isRecord(value) ? value : undefined;
|
|
20
|
+
} catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var missingPaths = (want, has) => {
|
|
25
|
+
const held = new Set(pathsIn(has));
|
|
26
|
+
return pathsIn(want).filter((path) => !held.has(path));
|
|
27
|
+
};
|
|
28
|
+
var isCompositionRoot = (file) => file.endsWith(`/${PLATFORM_FILE}`);
|
|
29
|
+
var checkScaffold = (answers) => {
|
|
30
|
+
const drift = [];
|
|
31
|
+
const seeded = workspacesOf(answers).map((one) => `${one.dir}/`);
|
|
32
|
+
for (const [file, body] of Object.entries(filesFor(answers))) {
|
|
33
|
+
if (!isCompositionRoot(file) && seeded.some((prefix) => file.startsWith(prefix)))
|
|
34
|
+
continue;
|
|
35
|
+
const at = join(answers.dir, file);
|
|
36
|
+
if (!existsSync(at)) {
|
|
37
|
+
drift.push({ at: "", file, kind: "missing" });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const want = parsed(body);
|
|
41
|
+
if (want === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
const has = parsed(readFileSync(at, "utf8"));
|
|
44
|
+
if (has === undefined) {
|
|
45
|
+
drift.push({ at: "", file, kind: "unreadable" });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const path of missingPaths(want, has))
|
|
49
|
+
drift.push({ at: path, file, kind: "key" });
|
|
50
|
+
}
|
|
51
|
+
return drift;
|
|
52
|
+
};
|
|
53
|
+
var SAID = {
|
|
54
|
+
key: (one) => `${one.file} → ${one.at} — the scaffold writes this key and your file has none`,
|
|
55
|
+
missing: (one) => `${one.file} — the scaffold writes this file and your repo has none`,
|
|
56
|
+
unreadable: (one) => `${one.file} — the scaffold writes plain JSON here and yours is not (comments? trailing commas?), so this check could not read its keys`
|
|
57
|
+
};
|
|
58
|
+
var describeDrift = (drift) => drift.map((one) => SAID[one.kind](one));
|
|
59
|
+
export {
|
|
60
|
+
checkScaffold,
|
|
61
|
+
describeDrift
|
|
62
|
+
};
|