@vizualmodel/vmblu-cli 0.4.1 → 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 CHANGED
@@ -1,61 +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
- bin/
10
- vmblu.js # discovers and adds commands
11
- commands/
12
- init/
13
- profile/
14
- migrate/
15
- templates/
16
- x.y.z/ # a directory per version x.y.z
17
- blueprint.schema.json
18
- blueprint.annex.md
19
- vizual.schema.json
20
- system-prompt.md
21
- profile.schema.json
22
- package.json
23
- README.md
24
- LICENSE.txt
25
- ```
26
-
27
- ## Add more commands
28
-
29
- Create commands/migrate/index.js with the same export shape { command, describe, builder, handler }. The router auto-discovers it.
30
-
31
- ## Dev/test workflow
32
-
33
- ### from vmblu/cli
34
-
35
- ```bash
36
- npm link # exposes "vmblu" globally
37
- vmblu init my-app --schema 0.8.2
38
- vmblu --help
39
- vmblu init --help
40
- ```
41
- ### Publish & use
42
- ```bash
43
- npm publish --access public
44
- ```
45
- ## Usage
46
-
47
- ```bash
48
- npx @vizualmodel/vmblu-cli init my-app --schema 0.8.2
49
- ```
50
-
51
- **or, after global install:**
52
-
53
- ```bash
54
- vmblu init my-app
55
- ```
56
- ## Tips
57
-
58
- * Keep templates inside the package and list them in "files" so npx works offline.
59
- * If you later prefer a richer UX, you can swap the router to commander/yargs without changing your command folders.
60
- * If your main repo houses both runtime and CLI, publish the CLI from vmblu/cli (separate package.json). This keeps runtime installs lean.
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.
61
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} vmblu model (scaffolded)`
66
+ description: `${projectName} - vmblu model (scaffolded)`
67
67
  },
68
- models: [],
68
+ imports: [],
69
69
  factories: [],
70
+ types: {},
70
71
  root: {
71
- group: "Root",
72
- pins: [],
72
+ kind: "group",
73
+ name: "Root",
74
+ prompt: "Root group for the application.",
75
+ interfaces: [],
73
76
  nodes: [],
74
- routes: [],
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 fallbackSeed() {
128
- return `# Session Seed (System Prompt)
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.json`);
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 promptSrc = path.join(templatesDir, schemaVersion, 'system-prompt.md');
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 promptDst = path.join(llmDir, 'system-prompt.md');
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 ${promptSrc} -> ${promptDst}${force ? ' (force)' : ''}`);
227
- await copyOrWriteFallback(promptSrc, promptDst, fallbackSeed(), { force, dry: dryRun });
228
-
229
- /*
204
+ ui.info(`copy ${promptPrj} -> ${promptPrjDst}${force ? ' (force)' : ''}`);
205
+ await copyOrWriteFallback(promptPrj, promptPrjDst, fallbackPrompt('project'), { force, dry: dryRun });
230
206
 
231
- // 4) Build manifest with hashes
232
- const willWriteManifest = !(await exists(path.join(llmDir, 'manifest.json'))) || force;
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
- if (dryRun) {
236
- ui.info(`would write manifest.json in ${llmDir}`);
237
- } else {
210
+ ui.info(`copy ${promptTst} -> ${promptTstDst}${force ? ' (force)' : ''}`);
211
+ await copyOrWriteFallback(promptTst, promptTstDst, fallbackPrompt('test'), { force, dry: dryRun });
238
212
 
239
- // I don't need the hashes
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
+ }