@vizualmodel/vmblu-cli 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -58
- package/bin/vmblu.js +1 -1
- package/commands/init/init-project.js +29 -82
- package/commands/make-app/index.js +108 -0
- package/commands/make-test/index.js +103 -0
- package/commands/profile/profile.bundle.js +19357 -18714
- package/package.json +22 -19
- package/templates/{0.9.1 → 0.9.2}/blueprint.annex.md +10 -17
- package/templates/{0.9.1 → 0.9.2}/blueprint.schema.json +364 -372
- package/templates/0.9.2/system-prompt.dev.md +129 -0
- package/templates/0.9.2/system-prompt.project.md +77 -0
- package/templates/0.9.2/system-prompt.test.md +163 -0
- package/templates/{0.9.1 → 0.9.2}/vizual.schema.json +169 -169
- package/templates/0.9.2/z-system-prompt.md +344 -0
- package/templates/0.9.2/zz-system-prompt.md +154 -0
- package/templates/0.9.2/zzz-system-prompt.md +143 -0
- package/templates/0.9.1/system-prompt.md +0 -72
- /package/templates/{0.9.1 → 0.9.2}/profile.schema.json +0 -0
package/README.md
CHANGED
|
@@ -1,59 +1,66 @@
|
|
|
1
|
-
# CLI for vmblu
|
|
2
|
-
This folder contains the CLI commands that are available for vmblu.
|
|
3
|
-
|
|
4
|
-
## Folder layout
|
|
5
|
-
|
|
6
|
-
```txt
|
|
7
|
-
vmblu/
|
|
8
|
-
cli/ # your CLI source
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
1
|
+
# CLI for vmblu
|
|
2
|
+
This folder contains the CLI commands that are available for vmblu.
|
|
3
|
+
|
|
4
|
+
## Folder layout
|
|
5
|
+
|
|
6
|
+
```txt
|
|
7
|
+
vmblu/
|
|
8
|
+
cli/ # your CLI source
|
|
9
|
+
bin/
|
|
10
|
+
vmblu.js # discovers and adds commands
|
|
11
|
+
commands/
|
|
12
|
+
init/
|
|
13
|
+
make-app/
|
|
14
|
+
make-test/
|
|
15
|
+
profile/
|
|
16
|
+
migrate/
|
|
17
|
+
templates/
|
|
18
|
+
x.y.z/ # a directory per version x.y.z
|
|
19
|
+
blueprint.schema.json
|
|
20
|
+
blueprint.annex.md
|
|
21
|
+
vizual.schema.json
|
|
22
|
+
profile.schema.json
|
|
23
|
+
system-prompt.project.md
|
|
24
|
+
system-prompt.dev.md
|
|
25
|
+
system-prompt.test.md
|
|
26
|
+
package.json
|
|
27
|
+
README.md
|
|
28
|
+
LICENSE.txt
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Add more commands
|
|
32
|
+
|
|
33
|
+
Create commands/migrate/index.js with the same export shape { command, describe, builder, handler }. The router auto-discovers it.
|
|
34
|
+
|
|
35
|
+
## Dev/test workflow
|
|
36
|
+
|
|
37
|
+
### from vmblu/cli
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm link # exposes "vmblu" globally
|
|
41
|
+
vmblu init my-app --schema 0.8.2
|
|
42
|
+
vmblu --help
|
|
43
|
+
vmblu init --help
|
|
44
|
+
```
|
|
45
|
+
### Publish & use
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm publish --access public
|
|
49
|
+
```
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx @vizualmodel/vmblu-cli init my-app --schema 0.8.2
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**or, after global install:**
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
vmblu init my-app
|
|
60
|
+
```
|
|
61
|
+
## Tips
|
|
62
|
+
|
|
63
|
+
* Keep templates inside the package and list them in "files" so npx works offline.
|
|
64
|
+
* If you later prefer a richer UX, you can swap the router to commander/yargs without changing your command folders.
|
|
65
|
+
* If your main repo houses both runtime and CLI, publish the CLI from vmblu/cli (separate package.json). This keeps runtime installs lean.
|
|
59
66
|
* This gives you one tidy package for all current and future commands, with zero drift and easy discoverability.
|
package/bin/vmblu.js
CHANGED
|
@@ -30,7 +30,7 @@ async function run() {
|
|
|
30
30
|
|
|
31
31
|
if (['-v', '--version', 'version'].includes(cmd)) {
|
|
32
32
|
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
33
|
-
console.log(pkg.version);
|
|
33
|
+
console.log('version: ' + pkg.version + ' schema: ' + pkg.schemaVersion);
|
|
34
34
|
process.exit(0);
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -63,29 +63,22 @@ function defaultModel(projectName) {
|
|
|
63
63
|
utc: now,
|
|
64
64
|
style: "#2c7be5",
|
|
65
65
|
runtime: "@vizualmodel/vmblu-runtime",
|
|
66
|
-
description: `${projectName}
|
|
66
|
+
description: `${projectName} - vmblu model (scaffolded)`
|
|
67
67
|
},
|
|
68
|
-
|
|
68
|
+
imports: [],
|
|
69
69
|
factories: [],
|
|
70
|
+
types: {},
|
|
70
71
|
root: {
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
kind: "group",
|
|
73
|
+
name: "Root",
|
|
74
|
+
prompt: "Root group for the application.",
|
|
75
|
+
interfaces: [],
|
|
73
76
|
nodes: [],
|
|
74
|
-
|
|
75
|
-
prompt: "Root group for the application."
|
|
77
|
+
connections: []
|
|
76
78
|
}
|
|
77
79
|
}, null, 2);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
function defaultDoc(projectName) {
|
|
81
|
-
const now = new Date().toISOString();
|
|
82
|
-
return JSON.stringify({
|
|
83
|
-
version: CLI_VERSION,
|
|
84
|
-
generatedAt: now,
|
|
85
|
-
entries: {}
|
|
86
|
-
}, null, 2);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
82
|
function fallbackAnnex() {
|
|
90
83
|
return `# vmblu Annex (placeholder)
|
|
91
84
|
This is a minimal scaffold. Replace with the official annex matching your pinned schema version.
|
|
@@ -124,23 +117,9 @@ function fallbackProfileSchema() {
|
|
|
124
117
|
}`;
|
|
125
118
|
}
|
|
126
119
|
|
|
127
|
-
function
|
|
128
|
-
return `#
|
|
129
|
-
|
|
130
|
-
vmblu (Vizual Model Blueprint) is a graphical editor that maintains a visual, runnable model of a software system.
|
|
131
|
-
vmblu models software as interconnected nodes that pass messages via pins.
|
|
132
|
-
The model has a well defined format described by a schema. An additional annex gives semantic background information about the schema.
|
|
133
|
-
The parameter profiles of messages and where in the actual source code messages are received and sent, is stored in a second file, the profile file.
|
|
134
|
-
The profile file is generated automatically by vmblu and is only to be consulted, not written.
|
|
135
|
-
|
|
136
|
-
You are an expert **architecture + code copilot** for **vmblu** .
|
|
137
|
-
You can find the location of the model file, the model schema, the model annex, the profile file and the profile schema in the 'manifest.json' file of this project.
|
|
138
|
-
The location of all other files in the project can be found via the model file.
|
|
139
|
-
|
|
140
|
-
Your job is to co-design the architecture and the software for the system.
|
|
141
|
-
For modifications of the model, always follow the schema.
|
|
142
|
-
If the profile does not contain profile information it could be that the code for a message has not been written yet, this should not stop you from continuing
|
|
143
|
-
`}
|
|
120
|
+
function fallbackPrompt(promptType) {
|
|
121
|
+
return `# Missing prompt for ${promptType}`
|
|
122
|
+
}
|
|
144
123
|
|
|
145
124
|
/**
|
|
146
125
|
* Initialize a vmblu project directory.
|
|
@@ -175,8 +154,7 @@ async function initProject(opts) {
|
|
|
175
154
|
if (!targetDir) throw new Error("initProject: targetDir is required");
|
|
176
155
|
|
|
177
156
|
const absTarget = path.resolve(targetDir);
|
|
178
|
-
const modelFile = path.join(absTarget, `${projectName}.blu
|
|
179
|
-
// const docFile = path.join(absTarget, `${projectName}.prf.json`);
|
|
157
|
+
const modelFile = path.join(absTarget, `${projectName}.mod.blu`);
|
|
180
158
|
|
|
181
159
|
const llmDir = path.join(absTarget, 'llm');
|
|
182
160
|
//const sessionDir = path.join(llmDir, 'session');
|
|
@@ -187,7 +165,9 @@ async function initProject(opts) {
|
|
|
187
165
|
const annexSrc = path.join(templatesDir, schemaVersion, 'blueprint.annex.md');
|
|
188
166
|
const vizualSrc = path.join(templatesDir, schemaVersion, 'vizual.schema.json');
|
|
189
167
|
const profileSchemaSrc = path.join(templatesDir, schemaVersion, 'profile.schema.json');
|
|
190
|
-
const
|
|
168
|
+
const promptPrj = path.join(templatesDir, schemaVersion, 'system-prompt.project.md');
|
|
169
|
+
const promptDev = path.join(templatesDir, schemaVersion, 'system-prompt.dev.md');
|
|
170
|
+
const promptTst = path.join(templatesDir, schemaVersion, 'system-prompt.test.md');
|
|
191
171
|
|
|
192
172
|
// 1) Create folders
|
|
193
173
|
//for (const dir of [absTarget, llmDir, sessionDir, nodesDir]) {
|
|
@@ -200,16 +180,14 @@ async function initProject(opts) {
|
|
|
200
180
|
ui.info(`create ${modelFile}${force ? ' (force)' : ''}`);
|
|
201
181
|
await writeFileSafe(modelFile, defaultModel(projectName), { force, dry: dryRun });
|
|
202
182
|
|
|
203
|
-
//ui.info(`create ${docFile}${force ? ' (force)' : ''}`);
|
|
204
|
-
//await writeFileSafe(docFile, defaultDoc(projectName), { force, dry: dryRun });
|
|
205
|
-
|
|
206
183
|
// 3) Copy schema + annex into llm/
|
|
207
184
|
const schemaDst = path.join(llmDir, 'blueprint.schema.json');
|
|
208
185
|
const annexDst = path.join(llmDir, 'blueprint.annex.md');
|
|
209
186
|
const vizualDst = path.join(llmDir, 'vizual.schema.json');
|
|
210
187
|
const profileSchemaDst = path.join(llmDir, 'profile.schema.json');
|
|
211
|
-
const
|
|
212
|
-
|
|
188
|
+
const promptPrjDst = path.join(llmDir, 'system-prompt.project.md');
|
|
189
|
+
const promptDevDst = path.join(llmDir, 'system-prompt.dev.md');
|
|
190
|
+
const promptTstDst = path.join(llmDir, 'system-prompt.test.md');
|
|
213
191
|
|
|
214
192
|
ui.info(`copy ${schemaSrc} -> ${schemaDst}${force ? ' (force)' : ''}`);
|
|
215
193
|
await copyOrWriteFallback(schemaSrc, schemaDst, fallbackSchema(), { force, dry: dryRun });
|
|
@@ -223,49 +201,16 @@ async function initProject(opts) {
|
|
|
223
201
|
ui.info(`copy ${profileSchemaSrc} -> ${profileSchemaDst}${force ? ' (force)' : ''}`);
|
|
224
202
|
await copyOrWriteFallback(profileSchemaSrc, profileSchemaDst, fallbackProfileSchema(), { force, dry: dryRun });
|
|
225
203
|
|
|
226
|
-
ui.info(`copy ${
|
|
227
|
-
await copyOrWriteFallback(
|
|
228
|
-
|
|
229
|
-
/*
|
|
204
|
+
ui.info(`copy ${promptPrj} -> ${promptPrjDst}${force ? ' (force)' : ''}`);
|
|
205
|
+
await copyOrWriteFallback(promptPrj, promptPrjDst, fallbackPrompt('project'), { force, dry: dryRun });
|
|
230
206
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
let manifest = null;
|
|
207
|
+
ui.info(`copy ${promptDev} -> ${promptDevDst}${force ? ' (force)' : ''}`);
|
|
208
|
+
await copyOrWriteFallback(promptDev, promptDevDst, fallbackPrompt('development'), { force, dry: dryRun });
|
|
234
209
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
} else {
|
|
210
|
+
ui.info(`copy ${promptTst} -> ${promptTstDst}${force ? ' (force)' : ''}`);
|
|
211
|
+
await copyOrWriteFallback(promptTst, promptTstDst, fallbackPrompt('test'), { force, dry: dryRun });
|
|
238
212
|
|
|
239
|
-
|
|
240
|
-
// const [schemaHash, annexHash, modelHash, docHash] = await Promise.all([
|
|
241
|
-
// sha256(schemaDst), sha256(annexDst), sha256(modelFile), sha256(docFile)
|
|
242
|
-
// ]);
|
|
243
|
-
|
|
244
|
-
// Paths in manifest should be relative to /llm to keep it portable
|
|
245
|
-
const llmPosix = llmDir; // absolute
|
|
246
|
-
manifest = {
|
|
247
|
-
version: schemaVersion,
|
|
248
|
-
model: {
|
|
249
|
-
path: rel(llmPosix, modelFile),
|
|
250
|
-
schema: 'vmblu.schema.json',
|
|
251
|
-
annex: 'vmblu.annex.md',
|
|
252
|
-
},
|
|
253
|
-
profile: {
|
|
254
|
-
path: rel(llmPosix, docFile),
|
|
255
|
-
schema: 'profile.schema.json',
|
|
256
|
-
},
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
if (willWriteManifest) {
|
|
260
|
-
const manifestPath = path.join(llmDir, 'manifest.json');
|
|
261
|
-
ui.info(`create ${manifestPath}${force ? ' (force)' : ''}`);
|
|
262
|
-
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
263
|
-
} else {
|
|
264
|
-
ui.warn(`manifest.json exists and --force not set. Skipped.`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
*/
|
|
213
|
+
// 4) Build manifest with hashes DELETED
|
|
269
214
|
|
|
270
215
|
// 5) Make the package file
|
|
271
216
|
makePackageJson({ absTarget, projectName, force, dryRun, addCliDep: true, cliVersion: "^" + CLI_VERSION }, ui);
|
|
@@ -276,7 +221,9 @@ async function initProject(opts) {
|
|
|
276
221
|
${path.basename(modelFile)}
|
|
277
222
|
package.json
|
|
278
223
|
llm/
|
|
279
|
-
system-prompt.md
|
|
224
|
+
system-prompt.project.md
|
|
225
|
+
system-prompt.dev.md
|
|
226
|
+
system-prompt.test.md
|
|
280
227
|
blueprint.schema.json
|
|
281
228
|
blueprint.annex.md
|
|
282
229
|
vizual.schema.json
|
|
@@ -289,9 +236,9 @@ async function initProject(opts) {
|
|
|
289
236
|
schemaVersion,
|
|
290
237
|
files: {
|
|
291
238
|
model: modelFile,
|
|
292
|
-
doc: docFile,
|
|
293
239
|
schema: schemaDst,
|
|
294
240
|
annex: annexDst,
|
|
241
|
+
vizualSchema: vizualDst,
|
|
295
242
|
profileSchema: profileSchemaDst,
|
|
296
243
|
// manifest: path.join(llmDir, 'manifest.json')
|
|
297
244
|
},
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// vmblu make-app <model-file> [--out <file>]
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
import { ModelBlueprint, ModelCompiler } from '../../../core/model/index.js';
|
|
6
|
+
import { ARL } from '../../../core/arl/arl-node.js';
|
|
7
|
+
import { normalizeSeparators } from '../../../core/arl/path.js';
|
|
8
|
+
import { UIDGenerator } from '../../../core/document/uid-generator.js';
|
|
9
|
+
|
|
10
|
+
export const command = 'make-app <model-file>';
|
|
11
|
+
export const describe = 'Generate an application JS file from a model';
|
|
12
|
+
|
|
13
|
+
export const builder = [
|
|
14
|
+
{ flag: '--out <file>', desc: 'specifies the output file' }
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const handler = async (argv) => {
|
|
18
|
+
// Parse the CLI arguments into a structured object.
|
|
19
|
+
const args = parseCliArgs(argv);
|
|
20
|
+
|
|
21
|
+
// Require a model file path to proceed.
|
|
22
|
+
if (!args.modelFile) {
|
|
23
|
+
console.error('Usage: vmblu make-app <model-file> [--out <file>]');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Resolve and validate the model file path.
|
|
28
|
+
const absoluteModelPath = path.resolve(args.modelFile);
|
|
29
|
+
if (!fs.existsSync(absoluteModelPath) || !fs.statSync(absoluteModelPath).isFile()) {
|
|
30
|
+
console.error(args.modelFile, 'is not a file');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Normalize to forward slashes so ARL resolution is consistent.
|
|
35
|
+
const modelPath = normalizeSeparators(absoluteModelPath);
|
|
36
|
+
|
|
37
|
+
// Compute the output path (default: <model>.app.js).
|
|
38
|
+
const outPath = args.outFile
|
|
39
|
+
? path.resolve(args.outFile)
|
|
40
|
+
: (() => {
|
|
41
|
+
const { dir, name, ext } = path.parse(absoluteModelPath);
|
|
42
|
+
const baseName = ext === '.blu' && name.endsWith('.mod')
|
|
43
|
+
? name.slice(0, -'.mod'.length)
|
|
44
|
+
: name;
|
|
45
|
+
return path.join(dir, `${baseName}.app.js`);
|
|
46
|
+
})();
|
|
47
|
+
|
|
48
|
+
// Normalize output path for the model app writer.
|
|
49
|
+
const appPath = normalizeSeparators(outPath);
|
|
50
|
+
|
|
51
|
+
// Build the model root via the compiler.
|
|
52
|
+
const arl = new ARL(modelPath);
|
|
53
|
+
const model = new ModelBlueprint(arl);
|
|
54
|
+
const compiler = new ModelCompiler(new UIDGenerator());
|
|
55
|
+
|
|
56
|
+
// Compile the model into a root node.
|
|
57
|
+
const root = await compiler.getRoot(model);
|
|
58
|
+
if (!root) {
|
|
59
|
+
console.error('Failed to compile model root.');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Build runtime connection tables from the compiled routes.
|
|
64
|
+
root.rxtxBuildTxTable();
|
|
65
|
+
|
|
66
|
+
// Generate and save the app file from the compiled model.
|
|
67
|
+
model.makeAndSaveApp(appPath, root);
|
|
68
|
+
console.log(`App written to ${outPath}`);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function parseCliArgs(argvInput) {
|
|
72
|
+
// Accept argv as-is when already an array.
|
|
73
|
+
const argv = Array.isArray(argvInput) ? argvInput : [];
|
|
74
|
+
const result = {
|
|
75
|
+
modelFile: null,
|
|
76
|
+
outFile: null,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < argv.length; i++) {
|
|
80
|
+
const token = argv[i];
|
|
81
|
+
// Handle the --out flag which expects a following path.
|
|
82
|
+
if (token === '--out') {
|
|
83
|
+
const next = argv[i + 1];
|
|
84
|
+
if (next && !next.startsWith('--')) {
|
|
85
|
+
result.outFile = next;
|
|
86
|
+
i += 1;
|
|
87
|
+
} else {
|
|
88
|
+
console.warn('Warning: --out requires a path argument; ignoring.');
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Warn on unknown options but keep parsing.
|
|
94
|
+
if (token?.startsWith('--')) {
|
|
95
|
+
console.warn(`Warning: unknown option "${token}" ignored.`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Treat the first positional token as the model file.
|
|
100
|
+
if (!result.modelFile) {
|
|
101
|
+
result.modelFile = token;
|
|
102
|
+
} else {
|
|
103
|
+
console.warn(`Warning: extra positional argument "${token}" ignored.`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// vmblu make-test <model-file> [--outDir <dir>]
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
import { ModelBlueprint, ModelCompiler } from '../../../core/model/index.js';
|
|
6
|
+
import { ARL } from '../../../core/arl/arl-node.js';
|
|
7
|
+
import { UIDGenerator } from '../../../core/document/uid-generator.js';
|
|
8
|
+
import { normalizeSeparators } from '../../../core/arl/path.js';
|
|
9
|
+
|
|
10
|
+
export const command = 'make-test <model-file>';
|
|
11
|
+
export const describe = 'Generate test app files from a model';
|
|
12
|
+
|
|
13
|
+
export const builder = [
|
|
14
|
+
{ flag: '--out-dir <dir>', desc: 'output directory for test files (default: ./test)' },
|
|
15
|
+
{ flag: '--out <dir>', desc: 'alias for --out-dir' },
|
|
16
|
+
{ flag: '-o <dir>', desc: 'alias for --out-dir' }
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export const handler = async (argv) => {
|
|
20
|
+
const args = parseCliArgs(argv);
|
|
21
|
+
|
|
22
|
+
// Require a model file path to proceed.
|
|
23
|
+
if (!args.modelFile) {
|
|
24
|
+
console.error('Usage: vmblu make-test <model-file> [--outDir <dir>]');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Resolve and validate the model file path.
|
|
29
|
+
const absoluteModelPath = path.resolve(args.modelFile);
|
|
30
|
+
if (!fs.existsSync(absoluteModelPath) || !fs.statSync(absoluteModelPath).isFile()) {
|
|
31
|
+
console.error(args.modelFile, 'is not a file');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Resolve the output directory (default: <model-dir>/test).
|
|
36
|
+
const outDir = args.outDir
|
|
37
|
+
? path.resolve(args.outDir)
|
|
38
|
+
: path.join(path.dirname(absoluteModelPath), 'test');
|
|
39
|
+
|
|
40
|
+
// Ensure the output directory exists.
|
|
41
|
+
if (!fs.existsSync(outDir)) {
|
|
42
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
43
|
+
const mirrorsDir = path.join(outDir, 'mirrors');
|
|
44
|
+
fs.mkdirSync(mirrorsDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Normalize to forward slashes so ARL resolution is consistent.
|
|
48
|
+
const modelPath = normalizeSeparators(absoluteModelPath);
|
|
49
|
+
|
|
50
|
+
// Build the model root via the compiler.
|
|
51
|
+
const arl = new ARL(modelPath);
|
|
52
|
+
const model = new ModelBlueprint(arl);
|
|
53
|
+
const compiler = new ModelCompiler(new UIDGenerator());
|
|
54
|
+
|
|
55
|
+
// Compile the model into a root node.
|
|
56
|
+
const root = await compiler.getRoot(model);
|
|
57
|
+
if (!root) {
|
|
58
|
+
console.error('Failed to compile model root.');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Build runtime connection tables from the compiled routes.
|
|
63
|
+
root.rxtxBuildTxTable();
|
|
64
|
+
|
|
65
|
+
// Generate and save the test app files from the compiled model.
|
|
66
|
+
model.makeTestApp(normalizeSeparators(outDir), root);
|
|
67
|
+
console.log(`Test app written to ${outDir}`);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function parseCliArgs(argvInput) {
|
|
71
|
+
const argv = Array.isArray(argvInput) ? argvInput : [];
|
|
72
|
+
const result = {
|
|
73
|
+
outDir: null,
|
|
74
|
+
modelFile: null,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < argv.length; i++) {
|
|
78
|
+
const token = argv[i];
|
|
79
|
+
if (token === '--out-dir' || token === '--out' || token === '-o') {
|
|
80
|
+
const next = argv[i + 1];
|
|
81
|
+
if (next && !next.startsWith('--')) {
|
|
82
|
+
result.outDir = next;
|
|
83
|
+
i += 1;
|
|
84
|
+
} else {
|
|
85
|
+
console.warn('Warning: --out-dir requires a path argument; ignoring.');
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (token?.startsWith('--')) {
|
|
91
|
+
console.warn(`Warning: unknown option "${token}" ignored.`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!result.modelFile) {
|
|
96
|
+
result.modelFile = token;
|
|
97
|
+
} else {
|
|
98
|
+
console.warn(`Warning: extra positional argument "${token}" ignored.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
}
|