create-geonosis 2.13.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 +23 -0
- package/bin/create-geonosis.mjs +1 -1
- 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-LUUI74BB.js +0 -1224
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
3
26
|
## 2.13.0
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/bin/create-geonosis.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
// Generated by pnpm generate:shims. In the kit's own tree src/ sits beside dist/, and a dist older
|
|
3
3
|
// than src once answered for a fix it did not carry (#62); a published package ships no src/.
|
|
4
4
|
import { existsSync } from 'node:fs'
|
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
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Where the composition root lives, relative to the workspace that composes: the app. */
|
|
2
|
+
export declare const PLATFORM_FILE = "src/platform.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A domain the kit ships, and the two lines composing it takes.
|
|
5
|
+
*
|
|
6
|
+
* A domain is added here and nowhere else: the flag's vocabulary, the app's dependencies and the
|
|
7
|
+
* generated file all read this table, so a domain that ships without a way to compose it cannot be
|
|
8
|
+
* offered by name. `version` is the floor package's OWN line — the method's number says nothing
|
|
9
|
+
* about it (D-053) — and `tooling/floor-scaffold-versions.test.ts` holds each against the workspace.
|
|
10
|
+
*/
|
|
11
|
+
export type DomainScaffold = {
|
|
12
|
+
/** Where the package sits in the kit's tree, for `--kit`'s workspace links. */
|
|
13
|
+
at: string;
|
|
14
|
+
/** What the scope field is called, and the type it is annotated with. */
|
|
15
|
+
field: string;
|
|
16
|
+
/** Everything the generated file needs above the composition itself. */
|
|
17
|
+
declarations: readonly string[];
|
|
18
|
+
fieldType: string;
|
|
19
|
+
imports: readonly string[];
|
|
20
|
+
name: string;
|
|
21
|
+
/** How the store is built, once the tenant's transaction is already open. */
|
|
22
|
+
built: readonly string[];
|
|
23
|
+
version: string;
|
|
24
|
+
};
|
|
25
|
+
export declare const DOMAINS: readonly DomainScaffold[];
|
|
26
|
+
export declare const DOMAIN_NAMES: readonly string[];
|
|
27
|
+
/** The two foundations a composition root is built from, whichever domains it composes. */
|
|
28
|
+
export declare const FOUNDATIONS: readonly {
|
|
29
|
+
at: string;
|
|
30
|
+
name: string;
|
|
31
|
+
version: string;
|
|
32
|
+
}[];
|
|
33
|
+
export declare const domainsNamed: (named: readonly string[]) => DomainScaffold[];
|
|
34
|
+
/** What the composing workspace declares once a domain is named. */
|
|
35
|
+
export declare const floorDependencies: (named: readonly string[]) => Record<string, string>;
|
|
36
|
+
/** Every kit directory a `--kit` link has to reach for the domains this repo named. */
|
|
37
|
+
export declare const floorDirectories: (named: readonly string[]) => string[];
|
|
38
|
+
/**
|
|
39
|
+
* The composition root, in the two lifetimes it has.
|
|
40
|
+
*
|
|
41
|
+
* DEFINITIONS are what this deployment IS — wired once per isolate, before anybody has asked
|
|
42
|
+
* anything. SCOPE is who is asking, bound per request. Keeping them apart is what lets the session
|
|
43
|
+
* come first: the stores are built INSIDE the tenant's transaction, so a caller cannot hold one
|
|
44
|
+
* that was made without a session to run in (D-014 — structural, never an assertion).
|
|
45
|
+
*/
|
|
46
|
+
export declare const platformSource: (named: readonly string[]) => string;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/composition.ts
|
|
2
|
+
import { FLOOR_VERSIONS } from "./floor-versions.js";
|
|
3
|
+
var PLATFORM_FILE = "src/platform.ts";
|
|
4
|
+
var SEARCH = {
|
|
5
|
+
at: "domains/search",
|
|
6
|
+
built: [
|
|
7
|
+
" search: pgFtsSearchIndex({",
|
|
8
|
+
" executor: tx,",
|
|
9
|
+
" nowMs: definitions.nowMs,",
|
|
10
|
+
" projections: SEARCH_PROJECTIONS,",
|
|
11
|
+
" }),"
|
|
12
|
+
],
|
|
13
|
+
declarations: [
|
|
14
|
+
"/** How each kind this repo indexes becomes searchable text — declared once, never per call. */",
|
|
15
|
+
"const SEARCH_PROJECTIONS: SearchProjections<'note'> = {",
|
|
16
|
+
" note: {",
|
|
17
|
+
" body: (fields) => fields.notes ?? '',",
|
|
18
|
+
" title: (fields) => fields.heading ?? '',",
|
|
19
|
+
" },",
|
|
20
|
+
"}"
|
|
21
|
+
],
|
|
22
|
+
field: "search",
|
|
23
|
+
fieldType: "SearchIndex<'note'>",
|
|
24
|
+
imports: [
|
|
25
|
+
"import { pgFtsSearchIndex, type SearchIndex, type SearchProjections } from '@geonosis/search'"
|
|
26
|
+
],
|
|
27
|
+
name: "@geonosis/search",
|
|
28
|
+
version: FLOOR_VERSIONS["@geonosis/search"]
|
|
29
|
+
};
|
|
30
|
+
var DOCUMENT = {
|
|
31
|
+
at: "domains/document",
|
|
32
|
+
built: [" document: DOCUMENT_ENTITIES,"],
|
|
33
|
+
declarations: [
|
|
34
|
+
"/** Every `Document.extend` this repo declares, by the name its rows carry — grow this map. */",
|
|
35
|
+
"const DOCUMENT_ENTITIES = documentEntities({})"
|
|
36
|
+
],
|
|
37
|
+
field: "document",
|
|
38
|
+
fieldType: "typeof DOCUMENT_ENTITIES",
|
|
39
|
+
imports: ["import { documentEntities } from '@geonosis/document'"],
|
|
40
|
+
name: "@geonosis/document",
|
|
41
|
+
version: FLOOR_VERSIONS["@geonosis/document"]
|
|
42
|
+
};
|
|
43
|
+
var DOMAINS = [SEARCH, DOCUMENT];
|
|
44
|
+
var DOMAIN_NAMES = DOMAINS.map((one) => one.field);
|
|
45
|
+
var FOUNDATIONS = [
|
|
46
|
+
{ at: "foundations/db", name: "@geonosis/db", version: FLOOR_VERSIONS["@geonosis/db"] },
|
|
47
|
+
{
|
|
48
|
+
at: "foundations/events",
|
|
49
|
+
name: "@geonosis/events",
|
|
50
|
+
version: FLOOR_VERSIONS["@geonosis/events"]
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
var domainsNamed = (named) => named.map((one) => {
|
|
54
|
+
const found = DOMAINS.find((domain) => domain.field === one);
|
|
55
|
+
if (found === undefined) {
|
|
56
|
+
throw new Error(`--domains "${one}" is not a domain this kit ships — 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.`);
|
|
57
|
+
}
|
|
58
|
+
return found;
|
|
59
|
+
});
|
|
60
|
+
var floorDependencies = (named) => named.length === 0 ? {} : Object.fromEntries([...FOUNDATIONS, ...domainsNamed(named)].map((one) => [one.name, `^${one.version}`]).toSorted(([a], [b]) => String(a).localeCompare(String(b), "en-US")));
|
|
61
|
+
var floorDirectories = (named) => named.length === 0 ? [] : [...FOUNDATIONS, ...domainsNamed(named)].map((one) => one.at);
|
|
62
|
+
var platformSource = (named) => {
|
|
63
|
+
const domains = domainsNamed(named);
|
|
64
|
+
return [
|
|
65
|
+
"import { createSessionSeam, type Executor, type SessionSeam } from '@geonosis/db'",
|
|
66
|
+
"import { createEmitDoor, type EmitDoor, type Extensions } from '@geonosis/events'",
|
|
67
|
+
...domains.flatMap((one) => one.imports),
|
|
68
|
+
"",
|
|
69
|
+
"/** Every name a foundation cannot pick for this repo, answered here and only here. */",
|
|
70
|
+
"export type PlatformConfig = {",
|
|
71
|
+
" newId: () => string",
|
|
72
|
+
" nowMs: () => number",
|
|
73
|
+
" /** What maintenance sets to see every tenant, through a lever no request path reaches. */",
|
|
74
|
+
" opsSetting: string",
|
|
75
|
+
" /** The producer every envelope names (D-057) — spelled here, never at an emit call. */",
|
|
76
|
+
" source: string",
|
|
77
|
+
" /** What a transaction sets to name its tenant, and what the RLS policies read. */",
|
|
78
|
+
" tenantSetting: string",
|
|
79
|
+
"}",
|
|
80
|
+
"",
|
|
81
|
+
"export type Definitions = {",
|
|
82
|
+
" emit: EmitDoor<Extensions>",
|
|
83
|
+
" nowMs: () => number",
|
|
84
|
+
" session: SessionSeam",
|
|
85
|
+
"}",
|
|
86
|
+
"",
|
|
87
|
+
"/** Once per isolate: what this platform is, before anybody has asked it anything. */",
|
|
88
|
+
"export const definitionsOf = (config: PlatformConfig): Definitions => ({",
|
|
89
|
+
" emit: createEmitDoor({",
|
|
90
|
+
" extensions: [],",
|
|
91
|
+
" newId: config.newId,",
|
|
92
|
+
" now: config.nowMs,",
|
|
93
|
+
" source: config.source,",
|
|
94
|
+
" }),",
|
|
95
|
+
" nowMs: config.nowMs,",
|
|
96
|
+
" session: createSessionSeam({",
|
|
97
|
+
" settings: { opsSetting: config.opsSetting, tenantSetting: config.tenantSetting },",
|
|
98
|
+
" }),",
|
|
99
|
+
"})",
|
|
100
|
+
"",
|
|
101
|
+
...domains.flatMap((one) => [...one.declarations, ""]),
|
|
102
|
+
"/** Who is asking, and the connection they may ask on. */",
|
|
103
|
+
"export type Caller = { connection: Executor; tenantId: string }",
|
|
104
|
+
"",
|
|
105
|
+
"/** Every domain this repo composed, built for one caller. */",
|
|
106
|
+
"export type Scope = {",
|
|
107
|
+
...domains.map((one) => ` ${one.field}: ${one.fieldType}`),
|
|
108
|
+
"}",
|
|
109
|
+
"",
|
|
110
|
+
"/**",
|
|
111
|
+
" * Per request. The tenant session opens FIRST and the stores are built inside it, so a store",
|
|
112
|
+
" * that could run outside the transaction that named its tenant does not exist to be held.",
|
|
113
|
+
" */",
|
|
114
|
+
"export const inScope = async <Result>(",
|
|
115
|
+
" definitions: Definitions,",
|
|
116
|
+
" caller: Caller,",
|
|
117
|
+
" run: (scope: Scope) => Promise<Result>,",
|
|
118
|
+
"): Promise<Result> =>",
|
|
119
|
+
" definitions.session.inTenant(caller.connection, caller.tenantId, async (tx) =>",
|
|
120
|
+
" run({",
|
|
121
|
+
...domains.flatMap((one) => [...one.built]),
|
|
122
|
+
" }),",
|
|
123
|
+
" )",
|
|
124
|
+
""
|
|
125
|
+
].join(`
|
|
126
|
+
`);
|
|
127
|
+
};
|
|
128
|
+
export {
|
|
129
|
+
DOMAINS,
|
|
130
|
+
DOMAIN_NAMES,
|
|
131
|
+
FOUNDATIONS,
|
|
132
|
+
PLATFORM_FILE,
|
|
133
|
+
domainsNamed,
|
|
134
|
+
floorDependencies,
|
|
135
|
+
floorDirectories,
|
|
136
|
+
platformSource
|
|
137
|
+
};
|
package/dist/corpus.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Answers } from './answers.js';
|
|
2
|
+
import type { Workspace } from './layout.js';
|
|
3
|
+
export declare const CORPUS_DIR = "lint-corpus";
|
|
4
|
+
/**
|
|
5
|
+
* A reach corpus a shipped one cannot be.
|
|
6
|
+
*
|
|
7
|
+
* `geonosis-doctor` asks whether every enabled rule can fire at all, and it asks the corpus the
|
|
8
|
+
* PLUGIN ships. That cannot answer for a rule whose reach is the repo's own vocabulary: its layer
|
|
9
|
+
* names, its brand, its plugin package root, the `within` its sagas live in. The plugin's corpus
|
|
10
|
+
* says "acme" and `layers/core`, so it is no evidence either way about a repo that says anything
|
|
11
|
+
* else — and those rules would read as firing nowhere.
|
|
12
|
+
*
|
|
13
|
+
* So the repo ships its own, in ITS vocabulary. The corpus is copied to a temp root and linted with
|
|
14
|
+
* `--no-ignore`, so the fixture paths mirror the repo's real ones and the layer patterns match.
|
|
15
|
+
*/
|
|
16
|
+
export declare const corpusFor: (answers: Answers, workspaces: Workspace[]) => Record<string, string>;
|
package/dist/corpus.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/corpus.ts
|
|
2
|
+
var CORPUS_DIR = "lint-corpus";
|
|
3
|
+
var PREFIX = "biological-architecture";
|
|
4
|
+
var corpusFor = (answers, workspaces) => {
|
|
5
|
+
const files = {};
|
|
6
|
+
const rules = [];
|
|
7
|
+
const walled = workspaces.find((one) => one.mayDependOn.length === 0);
|
|
8
|
+
const forbidden = workspaces.find((one) => one.tag === "app");
|
|
9
|
+
if (walled !== undefined && forbidden !== undefined) {
|
|
10
|
+
rules.push(`${PREFIX}/layer-walls`);
|
|
11
|
+
files[`${CORPUS_DIR}/${walled.dir}/src/bad-reaches-up.ts`] = `// BREACH: the ${walled.tag} layer may import nothing, and this reaches ${forbidden.tag}.
|
|
12
|
+
import { boot } from '${forbidden.name}/boot'
|
|
13
|
+
|
|
14
|
+
export const reachesUp = () => boot
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
17
|
+
if (answers.brand !== undefined) {
|
|
18
|
+
rules.push(`${PREFIX}/no-brand-names`);
|
|
19
|
+
files[`${CORPUS_DIR}/brand/bad-brand.ts`] = `// BREACH: the brand name spelled into source, which is what the rule is configured to find.
|
|
20
|
+
export const FOOTER_NOTE = 'Powered by ${answers.brand}'
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
if (answers.backend === "medusa") {
|
|
24
|
+
rules.push(`${PREFIX}/plugin-route-namespaced`);
|
|
25
|
+
files[`${CORPUS_DIR}/packages/medusa-plugins/example/src/api/store/route.ts`] = `// BREACH: a store route with no namespace under it, in a plugin package.
|
|
26
|
+
export async function GET(req: unknown, res: { json: (body: unknown) => void }) {
|
|
27
|
+
res.json({ ok: true, req })
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
if (answers.backend === "sagaflow-cf") {
|
|
32
|
+
rules.push(`${PREFIX}/step-opens-its-own-cell`);
|
|
33
|
+
files[`${CORPUS_DIR}/packages/backend/src/workflows/bad-engine-step.ts`] = `// BREACH: a saga inside the configured \`within\` reaching the engine's own step.
|
|
34
|
+
import { step } from 'sagaflow-js'
|
|
35
|
+
|
|
36
|
+
export const issue = step('issue', async () => ({ ok: true }))
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
files[`${CORPUS_DIR}/manifest.json`] = `${JSON.stringify({ plugin: PREFIX, rules }, undefined, 2)}
|
|
40
|
+
`;
|
|
41
|
+
return files;
|
|
42
|
+
};
|
|
43
|
+
export {
|
|
44
|
+
CORPUS_DIR,
|
|
45
|
+
corpusFor
|
|
46
|
+
};
|