@zodmire/cli 0.1.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/LICENSE +21 -0
- package/bin/zodmire.js +2 -0
- package/mod.d.mts +1 -0
- package/mod.mjs +299 -0
- package/package.json +22 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Vercel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/zodmire.js
ADDED
package/mod.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/mod.mjs
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { VENDORED_FILE_MANIFEST, buildCompositionArtifacts, buildContextNormalizedSpecFromConfig, buildFileArtifactTags, buildNormalizedCompositionSpec, buildNormalizedSpecTags, buildV5ContextArtifacts, filterPerContextArtifactsForComposition, inferReconcileScopes, mergeGeneratedArtifacts, prepareAllVendoredFiles, resolveAbsoluteContextInputs, serializeCompositionSpec, serializeContextSpec } from "@zodmire/core";
|
|
5
|
+
import { materializeOwnedArtifacts } from "@zodmire/core/materialize";
|
|
6
|
+
import { statSync } from "node:fs";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
//#region packages/cli/version.ts
|
|
10
|
+
const GENERATOR_VERSION = "2026.03.26.1";
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region packages/cli/init.ts
|
|
14
|
+
function resolveSourcePath(relativePath) {
|
|
15
|
+
return `${`${dirname(fileURLToPath(import.meta.url))}/../..`}/${relativePath}`;
|
|
16
|
+
}
|
|
17
|
+
async function runInit(targetDir) {
|
|
18
|
+
const sourceContents = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const entry of VENDORED_FILE_MANIFEST) {
|
|
20
|
+
const path = resolveSourcePath(entry.sourceRelativePath);
|
|
21
|
+
sourceContents.set(entry.sourceRelativePath, await readFile(path, "utf-8"));
|
|
22
|
+
}
|
|
23
|
+
const prepared = prepareAllVendoredFiles(sourceContents, GENERATOR_VERSION);
|
|
24
|
+
await mkdir(targetDir, { recursive: true });
|
|
25
|
+
for (const file of prepared) {
|
|
26
|
+
const destPath = `${targetDir}/${file.outputName}`;
|
|
27
|
+
await writeFile(destPath, file.content, "utf-8");
|
|
28
|
+
console.log(` wrote ${destPath}`);
|
|
29
|
+
}
|
|
30
|
+
console.log(`\nInit complete. ${prepared.length} files written to ${targetDir}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region packages/cli/version_data.ts
|
|
35
|
+
function isSwampRepoAvailable(basePath = ".") {
|
|
36
|
+
try {
|
|
37
|
+
return statSync(resolve(basePath, ".swamp")).isDirectory();
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function spawnSwampDataPut(args, stdinContent) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const proc = spawn("swamp", args, { stdio: [
|
|
45
|
+
"pipe",
|
|
46
|
+
"ignore",
|
|
47
|
+
"pipe"
|
|
48
|
+
] });
|
|
49
|
+
let stderr = "";
|
|
50
|
+
proc.stderr.on("data", (chunk) => {
|
|
51
|
+
stderr += chunk.toString();
|
|
52
|
+
});
|
|
53
|
+
proc.on("close", (code) => {
|
|
54
|
+
if (code !== 0) reject(/* @__PURE__ */ new Error(`swamp data put failed: ${stderr}`));
|
|
55
|
+
else resolve();
|
|
56
|
+
});
|
|
57
|
+
proc.stdin.write(stdinContent);
|
|
58
|
+
proc.stdin.end();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function mirrorToSwampDataLayer(opts) {
|
|
62
|
+
const { modelName, artifacts, specJson, specDataName, generatorVersion, generationRunId, methodName, basis, contextName, compositionName, producer, producerName } = opts;
|
|
63
|
+
for (const artifact of artifacts) {
|
|
64
|
+
const tagArgs = Object.entries(buildFileArtifactTags({
|
|
65
|
+
logicalPath: artifact.logicalPath,
|
|
66
|
+
generatorVersion,
|
|
67
|
+
generationRunId,
|
|
68
|
+
methodName,
|
|
69
|
+
producer,
|
|
70
|
+
producerName,
|
|
71
|
+
basis,
|
|
72
|
+
contextName,
|
|
73
|
+
compositionName,
|
|
74
|
+
ownershipScope: artifact.ownershipScope,
|
|
75
|
+
scopeKey: artifact.scopeKey
|
|
76
|
+
})).map(([key, value]) => `${key}=${value}`).flatMap((t) => ["--tag", t]);
|
|
77
|
+
await spawnSwampDataPut([
|
|
78
|
+
"data",
|
|
79
|
+
"put",
|
|
80
|
+
modelName,
|
|
81
|
+
artifact.dataName,
|
|
82
|
+
"--content-type",
|
|
83
|
+
"text/plain",
|
|
84
|
+
...tagArgs
|
|
85
|
+
], artifact.content);
|
|
86
|
+
}
|
|
87
|
+
if (specJson !== void 0 && specDataName !== void 0) await spawnSwampDataPut([
|
|
88
|
+
"data",
|
|
89
|
+
"put",
|
|
90
|
+
modelName,
|
|
91
|
+
specDataName,
|
|
92
|
+
"--content-type",
|
|
93
|
+
"application/json",
|
|
94
|
+
...Object.entries(buildNormalizedSpecTags({
|
|
95
|
+
generatorVersion,
|
|
96
|
+
generationRunId,
|
|
97
|
+
methodName,
|
|
98
|
+
producer,
|
|
99
|
+
producerName,
|
|
100
|
+
basis,
|
|
101
|
+
contextName,
|
|
102
|
+
compositionName
|
|
103
|
+
})).map(([key, value]) => `${key}=${value}`).flatMap((t) => ["--tag", t])
|
|
104
|
+
], specJson);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region packages/cli/generate.ts
|
|
109
|
+
async function runGenerate(opts) {
|
|
110
|
+
const configPath = opts.configPath.startsWith("/") ? opts.configPath : `${process.cwd()}/${opts.configPath}`;
|
|
111
|
+
if (opts.composition) await runCompositionGenerate(configPath, opts.targetOverride, opts.versionData);
|
|
112
|
+
else await runContextGenerate(configPath, opts.targetOverride, opts.versionData);
|
|
113
|
+
}
|
|
114
|
+
async function runContextGenerate(configPath, targetOverride, versionData) {
|
|
115
|
+
const resolvedInputs = await resolveAbsoluteContextInputs(configPath);
|
|
116
|
+
const config = resolvedInputs.config;
|
|
117
|
+
const codegenSupportPath = resolvedInputs.codegenSupportPath;
|
|
118
|
+
console.log(`Generating context from ${configPath}`);
|
|
119
|
+
console.log(` codegen support: ${codegenSupportPath}`);
|
|
120
|
+
const artifacts = await buildV5ContextArtifacts({
|
|
121
|
+
contextConfigPath: configPath,
|
|
122
|
+
codegenSupportPath
|
|
123
|
+
});
|
|
124
|
+
const targetRoot = targetOverride ?? config.materialization?.targetRoot;
|
|
125
|
+
if (!targetRoot) throw new Error("No --target specified and context config has no materialization.targetRoot");
|
|
126
|
+
const resolvedTarget = targetRoot.startsWith("/") ? targetRoot : `${process.cwd()}/${targetRoot}`;
|
|
127
|
+
console.log(` target: ${resolvedTarget}`);
|
|
128
|
+
const result = await materializeOwnedArtifacts(resolvedTarget, artifacts, GENERATOR_VERSION, { reconcileScopes: inferReconcileScopes(artifacts) });
|
|
129
|
+
if (versionData) {
|
|
130
|
+
if (!isSwampRepoAvailable()) throw new Error("Swamp repo not initialized. Run `swamp init` or omit --version-data");
|
|
131
|
+
const contextSpec = await buildContextNormalizedSpecFromConfig({
|
|
132
|
+
contextConfigPath: configPath,
|
|
133
|
+
codegenSupportPath
|
|
134
|
+
});
|
|
135
|
+
const specJson = serializeContextSpec(contextSpec, GENERATOR_VERSION);
|
|
136
|
+
const generationRunId = `run-${(/* @__PURE__ */ new Date()).toISOString()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
137
|
+
console.log(" mirroring to Swamp data layer...");
|
|
138
|
+
await mirrorToSwampDataLayer({
|
|
139
|
+
modelName: "codegen",
|
|
140
|
+
artifacts: artifacts.map((a) => ({
|
|
141
|
+
logicalPath: a.logicalPath,
|
|
142
|
+
dataName: a.logicalPath.replace(/\//g, ":"),
|
|
143
|
+
content: a.content,
|
|
144
|
+
ownershipScope: a.ownershipScope,
|
|
145
|
+
scopeKey: a.scopeKey
|
|
146
|
+
})),
|
|
147
|
+
specJson,
|
|
148
|
+
specDataName: `context-normalized-spec:${contextSpec.context.modulePath}`,
|
|
149
|
+
generatorVersion: GENERATOR_VERSION,
|
|
150
|
+
generationRunId,
|
|
151
|
+
methodName: "generate",
|
|
152
|
+
basis: "context",
|
|
153
|
+
contextName: contextSpec.context.name,
|
|
154
|
+
producer: "cli",
|
|
155
|
+
producerName: "zodmire"
|
|
156
|
+
});
|
|
157
|
+
console.log(" mirrored to Swamp data layer");
|
|
158
|
+
}
|
|
159
|
+
console.log(`\nMaterialized ${result.files.length} files to ${result.targetRoot}`);
|
|
160
|
+
}
|
|
161
|
+
async function runCompositionGenerate(compositionConfigPath, targetOverride, versionData) {
|
|
162
|
+
const compositionConfig = (await import(new URL(compositionConfigPath, "file:///").href)).default;
|
|
163
|
+
const targetRoot = targetOverride ?? compositionConfig.materialization?.targetRoot;
|
|
164
|
+
if (!targetRoot) throw new Error("No --target specified and composition config has no materialization.targetRoot");
|
|
165
|
+
const resolvedTarget = targetRoot.startsWith("/") ? targetRoot : `${process.cwd()}/${targetRoot}`;
|
|
166
|
+
if (!compositionConfig.contextConfigs || compositionConfig.contextConfigs.length === 0) throw new Error("Composition config must include contextConfigs array with { contextConfigPath, codegenSupportPath } entries");
|
|
167
|
+
const compositionDir = compositionConfigPath.substring(0, compositionConfigPath.lastIndexOf("/") + 1);
|
|
168
|
+
const resolvedContextConfigs = compositionConfig.contextConfigs.map((cfg) => ({
|
|
169
|
+
contextConfigPath: cfg.contextConfigPath.startsWith("/") ? cfg.contextConfigPath : compositionDir + cfg.contextConfigPath.replace(/^\.\//, ""),
|
|
170
|
+
codegenSupportPath: cfg.codegenSupportPath.startsWith("/") ? cfg.codegenSupportPath : compositionDir + cfg.codegenSupportPath.replace(/^\.\//, "")
|
|
171
|
+
}));
|
|
172
|
+
console.log(`Generating composition from ${compositionConfigPath}`);
|
|
173
|
+
console.log(` contexts: ${resolvedContextConfigs.length}`);
|
|
174
|
+
const contextSpecs = await Promise.all(resolvedContextConfigs.map((cfg) => buildContextNormalizedSpecFromConfig(cfg)));
|
|
175
|
+
const perContextArtifacts = await Promise.all(resolvedContextConfigs.map((cfg) => buildV5ContextArtifacts(cfg)));
|
|
176
|
+
const compositionArtifacts = await buildCompositionArtifacts(compositionConfigPath, contextSpecs);
|
|
177
|
+
const allArtifacts = mergeGeneratedArtifacts(...perContextArtifacts.map(filterPerContextArtifactsForComposition), compositionArtifacts).sort((a, b) => a.logicalPath.localeCompare(b.logicalPath));
|
|
178
|
+
console.log(` target: ${resolvedTarget}`);
|
|
179
|
+
const result = await materializeOwnedArtifacts(resolvedTarget, allArtifacts, GENERATOR_VERSION, { reconcileScopes: inferReconcileScopes(allArtifacts) });
|
|
180
|
+
if (versionData) {
|
|
181
|
+
if (!isSwampRepoAvailable()) throw new Error("Swamp repo not initialized. Run `swamp init` or omit --version-data");
|
|
182
|
+
const compositionSpec = buildNormalizedCompositionSpec(compositionConfig, contextSpecs);
|
|
183
|
+
const specJson = serializeCompositionSpec(compositionSpec, GENERATOR_VERSION);
|
|
184
|
+
const generationRunId = `run-${(/* @__PURE__ */ new Date()).toISOString()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
185
|
+
console.log(" mirroring to Swamp data layer...");
|
|
186
|
+
await mirrorToSwampDataLayer({
|
|
187
|
+
modelName: "codegen",
|
|
188
|
+
artifacts: allArtifacts.map((a) => ({
|
|
189
|
+
logicalPath: a.logicalPath,
|
|
190
|
+
dataName: a.logicalPath.replace(/\//g, ":"),
|
|
191
|
+
content: a.content,
|
|
192
|
+
ownershipScope: a.ownershipScope,
|
|
193
|
+
scopeKey: a.scopeKey
|
|
194
|
+
})),
|
|
195
|
+
specJson,
|
|
196
|
+
specDataName: `composition-normalized-spec:${compositionSpec.name}`,
|
|
197
|
+
generatorVersion: GENERATOR_VERSION,
|
|
198
|
+
generationRunId,
|
|
199
|
+
methodName: "compose",
|
|
200
|
+
basis: "composition",
|
|
201
|
+
compositionName: compositionSpec.name,
|
|
202
|
+
producer: "cli",
|
|
203
|
+
producerName: "zodmire"
|
|
204
|
+
});
|
|
205
|
+
console.log(" mirrored to Swamp data layer");
|
|
206
|
+
}
|
|
207
|
+
console.log(`\nMaterialized ${result.files.length} files to ${result.targetRoot}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region packages/cli/mod.ts
|
|
212
|
+
function printUsage() {
|
|
213
|
+
console.log(`zodmire v${GENERATOR_VERSION}
|
|
214
|
+
|
|
215
|
+
Usage:
|
|
216
|
+
zodmire init --target <dir>
|
|
217
|
+
zodmire generate --config <path> [--composition] [--target <dir>] [--version-data]
|
|
218
|
+
zodmire --version
|
|
219
|
+
zodmire --help
|
|
220
|
+
|
|
221
|
+
Commands:
|
|
222
|
+
init Scaffold codegen helper files into your project
|
|
223
|
+
generate Generate a bounded context skeleton from Zod schemas
|
|
224
|
+
|
|
225
|
+
Options:
|
|
226
|
+
--target For init: directory to write helpers into
|
|
227
|
+
For generate: override materialization target directory
|
|
228
|
+
--config Path to context config (.context.ts) or composition config
|
|
229
|
+
--composition Treat --config as a composition config (multi-context)
|
|
230
|
+
--version-data Mirror artifacts to Swamp data layer (requires .swamp/ repo)
|
|
231
|
+
--version Print version
|
|
232
|
+
--help Print this help
|
|
233
|
+
`);
|
|
234
|
+
}
|
|
235
|
+
function parseArgs(args) {
|
|
236
|
+
const command = args[0] && !args[0].startsWith("--") ? args[0] : "";
|
|
237
|
+
const flags = {};
|
|
238
|
+
const startIdx = command ? 1 : 0;
|
|
239
|
+
for (let i = startIdx; i < args.length; i++) {
|
|
240
|
+
const arg = args[i];
|
|
241
|
+
if (arg === "--composition") flags.composition = true;
|
|
242
|
+
else if (arg === "--version-data") flags["version-data"] = true;
|
|
243
|
+
else if (arg.startsWith("--") && i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
244
|
+
flags[arg.slice(2)] = args[i + 1];
|
|
245
|
+
i++;
|
|
246
|
+
} else if (arg.startsWith("--")) flags[arg.slice(2)] = true;
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
command,
|
|
250
|
+
flags
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async function main() {
|
|
254
|
+
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
255
|
+
if (flags.version) {
|
|
256
|
+
console.log(GENERATOR_VERSION);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (flags.help || !command) {
|
|
260
|
+
printUsage();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
switch (command) {
|
|
264
|
+
case "init": {
|
|
265
|
+
const target = flags.target;
|
|
266
|
+
if (typeof target !== "string") {
|
|
267
|
+
console.error("Error: --target <dir> is required for init");
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
await runInit(target);
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
case "generate": {
|
|
274
|
+
const config = flags.config;
|
|
275
|
+
if (typeof config !== "string") {
|
|
276
|
+
console.error("Error: --config <path> is required for generate");
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
await runGenerate({
|
|
280
|
+
configPath: config,
|
|
281
|
+
composition: flags.composition === true,
|
|
282
|
+
targetOverride: typeof flags.target === "string" ? flags.target : void 0,
|
|
283
|
+
versionData: flags["version-data"] === true
|
|
284
|
+
});
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
default:
|
|
288
|
+
console.error(`Unknown command: ${command}`);
|
|
289
|
+
printUsage();
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
main().catch((err) => {
|
|
294
|
+
console.error(`Error: ${err.message}`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
//#endregion
|
|
299
|
+
export { };
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zodmire/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./mod.mjs",
|
|
7
|
+
"module": "./mod.mjs",
|
|
8
|
+
"types": "./mod.d.mts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./mod.d.mts",
|
|
12
|
+
"import": "./mod.mjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@zodmire/config": "^0.1.0",
|
|
17
|
+
"@zodmire/core": "^0.1.0"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"zodmire": "./bin/zodmire.js"
|
|
21
|
+
}
|
|
22
|
+
}
|