@vizualmodel/vmblu-cli 0.1.0 → 0.3.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/bin/vmblu.js +64 -46
- package/commands/init/index.js +15 -11
- package/commands/init/init-project.js +24 -29
- package/commands/init/make-package-json.js +10 -11
- package/commands/profile/find-handlers.js +351 -0
- package/commands/profile/find-transmissions.js +54 -0
- package/commands/profile/index.js +16 -0
- package/commands/profile/profile.js +271 -0
- package/commands/profile/rollup.config.js +22 -0
- package/package.json +18 -5
- package/templates/0.8.2/{srcdoc.schema.json → profile.schema.json} +1 -1
- package/templates/0.8.2/seed.md +4 -4
package/bin/vmblu.js
CHANGED
|
@@ -1,47 +1,65 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* Minimal subcommand router: vmblu <command> [args] */
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (['-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
2
|
+
/* Minimal subcommand router: vmblu <command> [args] */
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const root = path.join(__dirname, '..');
|
|
10
|
+
const commandsDir = path.join(root, 'commands');
|
|
11
|
+
|
|
12
|
+
function printGlobalHelp() {
|
|
13
|
+
const cmds = fs.readdirSync(commandsDir)
|
|
14
|
+
.filter((name) => fs.existsSync(path.join(commandsDir, name, 'index.js')));
|
|
15
|
+
console.log(`vmblu <command> [options]
|
|
16
|
+
|
|
17
|
+
Commands:
|
|
18
|
+
${cmds.map((c) => `- ${c}`).join('\n ')}
|
|
19
|
+
|
|
20
|
+
Run "vmblu <command> --help" for details.`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function run() {
|
|
24
|
+
const [, , cmd, ...rest] = process.argv;
|
|
25
|
+
|
|
26
|
+
if (!cmd || ['-h', '--help', 'help'].includes(cmd)) {
|
|
27
|
+
printGlobalHelp();
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (['-v', '--version', 'version'].includes(cmd)) {
|
|
32
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
33
|
+
console.log(pkg.version);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const entry = path.join(commandsDir, cmd, 'index.js');
|
|
38
|
+
if (!fs.existsSync(entry)) {
|
|
39
|
+
console.error(`Unknown command: ${cmd}\n`);
|
|
40
|
+
printGlobalHelp();
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const mod = await import(pathToFileURL(entry));
|
|
45
|
+
const handler = mod.handler ?? mod.default;
|
|
46
|
+
if (typeof handler !== 'function') {
|
|
47
|
+
console.error(`Command "${cmd}" does not export a runnable handler.`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (rest.includes('--help') || rest.includes('-h')) {
|
|
52
|
+
const command = mod.command ?? cmd;
|
|
53
|
+
const describe = mod.describe ?? '';
|
|
54
|
+
const builder = Array.isArray(mod.builder) ? mod.builder : [];
|
|
55
|
+
console.log(`vmblu ${command}\n\n${describe}\n\nOptions:\n${builder.map((o) => ` ${o.flag}\t${o.desc}`).join('\n')}`);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await handler(rest);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
run().catch((err) => {
|
|
63
|
+
console.error(err?.stack || String(err));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
package/commands/init/index.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
// vmblu init [targetDir] --name <project> --schema <ver> --force --dry-run
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// vmblu init [targetDir] --name <project> --schema <ver> --force --dry-run
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { initProject } from './init-project.js';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export const command = 'init <folder name>';
|
|
9
|
+
export const describe = 'Scaffold an empty vmblu project';
|
|
10
|
+
export const builder = [
|
|
8
11
|
{ flag: '--name <project>', desc: 'Project name (default: folder name)' },
|
|
9
12
|
{ flag: '--schema <ver>', desc: 'Schema version (default: 0.8.2)' },
|
|
10
13
|
{ flag: '--force', desc: 'Overwrite existing files' },
|
|
11
14
|
{ flag: '--dry-run', desc: 'Show actions without writing' }
|
|
12
15
|
];
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
export const handler = async (argv) => {
|
|
15
18
|
// tiny arg parse (no deps)
|
|
16
19
|
const args = { _: [] };
|
|
17
|
-
for (let i=0;i<argv.length;i++) {
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
21
|
const a = argv[i];
|
|
19
22
|
if (a === '--force') args.force = true;
|
|
20
23
|
else if (a === '--dry-run') args.dryRun = true;
|
|
@@ -31,8 +34,8 @@ exports.handler = async (argv) => {
|
|
|
31
34
|
targetDir,
|
|
32
35
|
projectName,
|
|
33
36
|
schemaVersion,
|
|
34
|
-
force:
|
|
35
|
-
dryRun:
|
|
37
|
+
force: Boolean(args.force),
|
|
38
|
+
dryRun: Boolean(args.dryRun),
|
|
36
39
|
templatesDir: path.join(__dirname, '..', '..', 'templates'),
|
|
37
40
|
ui: {
|
|
38
41
|
info: (m) => console.log(m),
|
|
@@ -41,5 +44,6 @@ exports.handler = async (argv) => {
|
|
|
41
44
|
}
|
|
42
45
|
});
|
|
43
46
|
|
|
44
|
-
console.log(
|
|
47
|
+
console.log(`vmblu project scaffolded in ${targetDir}`);
|
|
45
48
|
};
|
|
49
|
+
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// core/initProject.js
|
|
2
2
|
// Node 18+ (fs/promises, crypto). No external deps.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as fssync from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
//import crypto from 'crypto';
|
|
7
|
+
import { makePackageJson } from './make-package-json.js';
|
|
8
8
|
|
|
9
9
|
function rel(from, to) {
|
|
10
10
|
return path.posix.join(...path.relative(from, to).split(path.sep));
|
|
@@ -73,14 +73,9 @@ function defaultModel(projectName) {
|
|
|
73
73
|
function defaultDoc(projectName) {
|
|
74
74
|
const now = new Date().toISOString();
|
|
75
75
|
return JSON.stringify({
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
created: now,
|
|
80
|
-
files: [],
|
|
81
|
-
nodes: [],
|
|
82
|
-
pins: [],
|
|
83
|
-
handlers: []
|
|
76
|
+
version: "0.0.0",
|
|
77
|
+
generatedAt: now,
|
|
78
|
+
entries: {}
|
|
84
79
|
}, null, 2);
|
|
85
80
|
}
|
|
86
81
|
|
|
@@ -104,10 +99,10 @@ function fallbackSchema() {
|
|
|
104
99
|
}`;
|
|
105
100
|
}
|
|
106
101
|
|
|
107
|
-
function
|
|
102
|
+
function fallbackProfileSchema() {
|
|
108
103
|
return `{
|
|
109
104
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
110
|
-
"title": "
|
|
105
|
+
"title": "profile.schema (placeholder)",
|
|
111
106
|
"type": "object",
|
|
112
107
|
"description": "Placeholder schema. Replace with official version."
|
|
113
108
|
}`;
|
|
@@ -119,16 +114,16 @@ function fallbackSeed() {
|
|
|
119
114
|
vmblu (Vizual Model Blueprint) is a graphical editor that maintains a visual, runnable model of a software system.
|
|
120
115
|
vmblu models software as interconnected nodes that pass messages via pins.
|
|
121
116
|
The model has a well defined format described by a schema. An additional annex gives semantic background information about the schema.
|
|
122
|
-
The parameter profiles of messages and where in the actual source code messages are received and sent, is stored in a second file, the
|
|
123
|
-
The
|
|
117
|
+
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.
|
|
118
|
+
The profile file is generated automatically by vmblu and is only to be consulted, not written.
|
|
124
119
|
|
|
125
120
|
You are an expert **architecture + code copilot** for **vmblu** .
|
|
126
|
-
You can find the location of the model file, the model schema, the model annex, the
|
|
121
|
+
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.
|
|
127
122
|
The location of all other files in the project can be found via the model file.
|
|
128
123
|
|
|
129
124
|
Your job is to co-design the architecture and the software for the system.
|
|
130
125
|
For modifications of the model, always follow the schema.
|
|
131
|
-
If the
|
|
126
|
+
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
|
|
132
127
|
`}
|
|
133
128
|
|
|
134
129
|
/**
|
|
@@ -178,7 +173,7 @@ async function initProject(opts) {
|
|
|
178
173
|
// Template sources
|
|
179
174
|
const schemaSrc = path.join(templatesDir, schemaVersion, 'vmblu.schema.json');
|
|
180
175
|
const annexSrc = path.join(templatesDir, schemaVersion, 'vmblu.annex.md');
|
|
181
|
-
const
|
|
176
|
+
const profileSchemaSrc = path.join(templatesDir, schemaVersion, 'profile.schema.json');
|
|
182
177
|
const seedSrc = path.join(templatesDir, schemaVersion, 'seed.md');
|
|
183
178
|
|
|
184
179
|
// 1) Create folders
|
|
@@ -197,7 +192,7 @@ async function initProject(opts) {
|
|
|
197
192
|
// 3) Copy schema + annex into llm/
|
|
198
193
|
const schemaDst = path.join(llmDir, 'vmblu.schema.json');
|
|
199
194
|
const annexDst = path.join(llmDir, 'vmblu.annex.md');
|
|
200
|
-
const
|
|
195
|
+
const profileSchemaDst = path.join(llmDir, 'profile.schema.json');
|
|
201
196
|
const seedDst = path.join(llmDir, 'seed.md');
|
|
202
197
|
|
|
203
198
|
|
|
@@ -207,8 +202,8 @@ async function initProject(opts) {
|
|
|
207
202
|
ui.info(`copy ${annexSrc} -> ${annexDst}${force ? ' (force)' : ''}`);
|
|
208
203
|
await copyOrWriteFallback(annexSrc, annexDst, fallbackAnnex(), { force, dry: dryRun });
|
|
209
204
|
|
|
210
|
-
ui.info(`copy ${
|
|
211
|
-
await copyOrWriteFallback(
|
|
205
|
+
ui.info(`copy ${profileSchemaSrc} -> ${profileSchemaDst}${force ? ' (force)' : ''}`);
|
|
206
|
+
await copyOrWriteFallback(profileSchemaSrc, profileSchemaDst, fallbackProfileSchema(), { force, dry: dryRun });
|
|
212
207
|
|
|
213
208
|
ui.info(`copy ${seedSrc} -> ${seedDst}${force ? ' (force)' : ''}`);
|
|
214
209
|
await copyOrWriteFallback(seedSrc, seedDst, fallbackSeed(), { force, dry: dryRun });
|
|
@@ -235,9 +230,9 @@ async function initProject(opts) {
|
|
|
235
230
|
schema: 'vmblu.schema.json',
|
|
236
231
|
annex: 'vmblu.annex.md',
|
|
237
232
|
},
|
|
238
|
-
|
|
233
|
+
profile: {
|
|
239
234
|
path: rel(llmPosix, docFile),
|
|
240
|
-
schema: '
|
|
235
|
+
schema: 'profile.schema.json',
|
|
241
236
|
},
|
|
242
237
|
};
|
|
243
238
|
|
|
@@ -251,7 +246,7 @@ async function initProject(opts) {
|
|
|
251
246
|
}
|
|
252
247
|
|
|
253
248
|
// 5) Make the package file
|
|
254
|
-
|
|
249
|
+
makePackageJson({ absTarget, projectName, force, dryRun, addCliDep: true, cliVersion: "^0.1.0" }, ui);
|
|
255
250
|
|
|
256
251
|
// 6) Final tree hint
|
|
257
252
|
ui.info(`\nScaffold complete${dryRun ? ' (dry run)' : ''}:\n` +
|
|
@@ -264,7 +259,7 @@ async function initProject(opts) {
|
|
|
264
259
|
manifest.json
|
|
265
260
|
vmblu.schema.json
|
|
266
261
|
vmblu.annex.md
|
|
267
|
-
|
|
262
|
+
profile.schema.json
|
|
268
263
|
session/
|
|
269
264
|
nodes/\n`);
|
|
270
265
|
|
|
@@ -277,7 +272,7 @@ async function initProject(opts) {
|
|
|
277
272
|
doc: docFile,
|
|
278
273
|
schema: schemaDst,
|
|
279
274
|
annex: annexDst,
|
|
280
|
-
|
|
275
|
+
profileSchema: profileSchemaDst,
|
|
281
276
|
manifest: path.join(llmDir, 'manifest.json')
|
|
282
277
|
},
|
|
283
278
|
dryRun,
|
|
@@ -285,4 +280,4 @@ async function initProject(opts) {
|
|
|
285
280
|
};
|
|
286
281
|
}
|
|
287
282
|
|
|
288
|
-
|
|
283
|
+
export { initProject };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
async function readJsonIfExists(file) {
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
async function readJsonIfExists(file) {
|
|
5
5
|
try { return JSON.parse(await fs.readFile(file, 'utf8')); } catch { return null; }
|
|
6
6
|
}
|
|
7
7
|
|
|
@@ -9,10 +9,10 @@ function sortKeys(obj) {
|
|
|
9
9
|
return Object.fromEntries(Object.entries(obj).sort(([a],[b]) => a.localeCompare(b)));
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
async function makePackageJson({
|
|
13
|
-
absTarget, projectName, force, dryRun,
|
|
14
|
-
addCliDep = true, cliVersion = "^0.1.0"
|
|
15
|
-
}, ui) {
|
|
12
|
+
export async function makePackageJson({
|
|
13
|
+
absTarget, projectName, force, dryRun,
|
|
14
|
+
addCliDep = true, cliVersion = "^0.1.0"
|
|
15
|
+
}, ui) {
|
|
16
16
|
const pkgPath = path.join(absTarget, 'package.json');
|
|
17
17
|
const existing = await readJsonIfExists(pkgPath);
|
|
18
18
|
|
|
@@ -54,6 +54,5 @@ async function makePackageJson({
|
|
|
54
54
|
await fs.writeFile(pkgPath, JSON.stringify(ordered, null, 2) + '\n', 'utf8');
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
return pkgPath;
|
|
58
|
-
}
|
|
59
|
-
module.exports = { makePackageJson };
|
|
57
|
+
return pkgPath;
|
|
58
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// extractHandlersFromFile.js
|
|
2
|
+
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
|
|
5
|
+
export let currentNode = null;
|
|
6
|
+
export let topLevelClass = null
|
|
7
|
+
let nodeMap = null
|
|
8
|
+
let filePath = null
|
|
9
|
+
|
|
10
|
+
export function findHandlers(sourceFile, _filePath, _nodeMap) {
|
|
11
|
+
|
|
12
|
+
// Reset any node context carried over from previous files.
|
|
13
|
+
currentNode = null;
|
|
14
|
+
|
|
15
|
+
// The fallback name is the top-level class
|
|
16
|
+
topLevelClass = sourceFile.getClasses()[0]?.getName?.() || null;
|
|
17
|
+
nodeMap = _nodeMap
|
|
18
|
+
filePath = _filePath
|
|
19
|
+
|
|
20
|
+
// Check all the functions in the sourcefile - typically generator functions
|
|
21
|
+
sourceFile.getFunctions().forEach(fn => {
|
|
22
|
+
|
|
23
|
+
// Capture node annotations on generator-style functions and harvest handlers returned from them.
|
|
24
|
+
const jsdoc = getFullJsDoc(fn);
|
|
25
|
+
updateNodeFromJsdoc(jsdoc);
|
|
26
|
+
|
|
27
|
+
const name = fn.getName();
|
|
28
|
+
|
|
29
|
+
if (isHandler(name)) {
|
|
30
|
+
|
|
31
|
+
const line = fn.getNameNode()?.getStartLineNumber() ?? fn.getStartLineNumber();
|
|
32
|
+
const docTags = getParamDocs(fn);
|
|
33
|
+
const params = fn.getParameters().flatMap(p => describeParam(p, docTags));
|
|
34
|
+
|
|
35
|
+
collect(name, params, line, jsdoc);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
collectHandlersFromFunctionReturns(fn);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Check the variable declarations in the sourcefile
|
|
42
|
+
sourceFile.getVariableDeclarations().forEach(decl => {
|
|
43
|
+
|
|
44
|
+
// check the name
|
|
45
|
+
const name = decl.getName();
|
|
46
|
+
const init = decl.getInitializer();
|
|
47
|
+
const line = decl.getStartLineNumber();
|
|
48
|
+
const jsdoc = getFullJsDoc(decl);
|
|
49
|
+
updateNodeFromJsdoc(jsdoc);
|
|
50
|
+
|
|
51
|
+
// check if the name is a handler and initialised with a function
|
|
52
|
+
if (isHandler(name) && init && init.getKindName().includes('Function')) {
|
|
53
|
+
|
|
54
|
+
const docTags = getParamDocs(decl);
|
|
55
|
+
const params = init.getParameters().flatMap(p => describeParam(p, docTags));
|
|
56
|
+
|
|
57
|
+
collect(name, params, line, jsdoc);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (init && init.getKind() === ts.SyntaxKind.ObjectLiteralExpression) {
|
|
61
|
+
collectObjectLiteralHandlers(init);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// check all the classes in the file
|
|
66
|
+
sourceFile.getClasses().forEach(cls => {
|
|
67
|
+
|
|
68
|
+
// get the name of the node
|
|
69
|
+
const nodeName = cls.getName?.() || topLevelClass;
|
|
70
|
+
|
|
71
|
+
// check all the methods
|
|
72
|
+
cls.getMethods().forEach(method => {
|
|
73
|
+
|
|
74
|
+
// check the name
|
|
75
|
+
const name = method.getName();
|
|
76
|
+
if (!isHandler(name)) return;
|
|
77
|
+
|
|
78
|
+
// extract
|
|
79
|
+
const line = method.getNameNode()?.getStartLineNumber() ?? method.getStartLineNumber();
|
|
80
|
+
const jsdoc = getFullJsDoc(method);
|
|
81
|
+
const docTags = getParamDocs(method);
|
|
82
|
+
const params = method.getParameters().flatMap(p => describeParam(p, docTags));
|
|
83
|
+
|
|
84
|
+
// and collect
|
|
85
|
+
collect(name, params, line, jsdoc, nodeName);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// check all the statements
|
|
90
|
+
sourceFile.getStatements().forEach(stmt => {
|
|
91
|
+
|
|
92
|
+
// only binary expressions
|
|
93
|
+
if (!stmt.isKind(ts.SyntaxKind.ExpressionStatement)) return;
|
|
94
|
+
const expr = stmt.getExpression();
|
|
95
|
+
if (!expr.isKind(ts.SyntaxKind.BinaryExpression)) return;
|
|
96
|
+
|
|
97
|
+
// get the two parts of the statement
|
|
98
|
+
const left = expr.getLeft().getText();
|
|
99
|
+
const right = expr.getRight();
|
|
100
|
+
|
|
101
|
+
// check for protype
|
|
102
|
+
if (left.includes('.prototype.') && right.isKind(ts.SyntaxKind.FunctionExpression)) {
|
|
103
|
+
|
|
104
|
+
// get the name and check
|
|
105
|
+
const parts = left.split('.');
|
|
106
|
+
const name = parts[parts.length - 1];
|
|
107
|
+
if (!isHandler(name)) return;
|
|
108
|
+
|
|
109
|
+
// extract
|
|
110
|
+
const line = expr.getStartLineNumber();
|
|
111
|
+
const params = right.getParameters().flatMap(p => describeParam(p));
|
|
112
|
+
const jsdoc = getFullJsDoc(expr);
|
|
113
|
+
|
|
114
|
+
// and save in nodemap
|
|
115
|
+
collect(name, params, line, jsdoc);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (left.endsWith('.prototype') && right.isKind(ts.SyntaxKind.ObjectLiteralExpression)) {
|
|
119
|
+
collectObjectLiteralHandlers(right);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
function collectHandlersFromFunctionReturns(fn) {
|
|
126
|
+
|
|
127
|
+
// Look for factory-style returns that expose handlers via object literals.
|
|
128
|
+
fn.getDescendantsOfKind(ts.SyntaxKind.ReturnStatement).forEach(ret => {
|
|
129
|
+
const expr = ret.getExpression();
|
|
130
|
+
if (!expr || expr.getKind() !== ts.SyntaxKind.ObjectLiteralExpression) return;
|
|
131
|
+
|
|
132
|
+
collectObjectLiteralHandlers(expr);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function collectObjectLiteralHandlers(objectLiteral) {
|
|
137
|
+
|
|
138
|
+
// Reuse the same extraction logic for any handler stored on an object literal shape.
|
|
139
|
+
objectLiteral.getProperties().forEach(prop => {
|
|
140
|
+
|
|
141
|
+
const propName = prop.getName?.();
|
|
142
|
+
if (!isHandler(propName)) return;
|
|
143
|
+
|
|
144
|
+
let params = [];
|
|
145
|
+
if (prop.getKind() === ts.SyntaxKind.MethodDeclaration) {
|
|
146
|
+
const docTags = getParamDocs(prop);
|
|
147
|
+
params = prop.getParameters().flatMap(p => describeParam(p, docTags));
|
|
148
|
+
} else if (prop.getKind() === ts.SyntaxKind.PropertyAssignment) {
|
|
149
|
+
const fn = prop.getInitializerIfKind(ts.SyntaxKind.FunctionExpression) || prop.getInitializerIfKind(ts.SyntaxKind.ArrowFunction);
|
|
150
|
+
if (fn) {
|
|
151
|
+
const docTags = getParamDocs(fn);
|
|
152
|
+
params = fn.getParameters().flatMap(p => describeParam(p, docTags));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const jsdoc = getFullJsDoc(prop);
|
|
157
|
+
const line = prop.getStartLineNumber();
|
|
158
|
+
|
|
159
|
+
collect(propName, params, line, jsdoc);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function updateNodeFromJsdoc(jsdoc = {}) {
|
|
164
|
+
|
|
165
|
+
const nodeTag = jsdoc.tags?.find(t => t.tagName === 'node')?.comment;
|
|
166
|
+
if (nodeTag) currentNode = nodeTag.trim();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function collect(rawName, params, line, jsdoc = {}) {
|
|
170
|
+
|
|
171
|
+
//if (!isHandler(rawName)) return;
|
|
172
|
+
const cleanHandler = rawName.replace(/^['"]|['"]$/g, '');
|
|
173
|
+
|
|
174
|
+
let pin = null;
|
|
175
|
+
let node = null;
|
|
176
|
+
|
|
177
|
+
const pinTag = jsdoc.tags?.find(t => t.tagName === 'pin')?.comment;
|
|
178
|
+
const nodeTag = jsdoc.tags?.find(t => t.tagName === 'node')?.comment;
|
|
179
|
+
const mcpTag = jsdoc.tags?.find(t => t.tagName === 'mcp')?.comment ?? null;
|
|
180
|
+
|
|
181
|
+
// if there is a node tag, change the name of the current node
|
|
182
|
+
if (nodeTag) currentNode = nodeTag.trim();
|
|
183
|
+
|
|
184
|
+
// check the pin tag to get a pin name and node name
|
|
185
|
+
if (pinTag) {
|
|
186
|
+
|
|
187
|
+
if (pinTag.includes('@')) {
|
|
188
|
+
const [p, n] = pinTag.split('@').map(s => s.trim());
|
|
189
|
+
pin = p;
|
|
190
|
+
node = n;
|
|
191
|
+
}
|
|
192
|
+
else pin = pinTag.trim();
|
|
193
|
+
|
|
194
|
+
// Use the current context when the pin tag does not specify a node.
|
|
195
|
+
if (!node) node = currentNode || topLevelClass || null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// check the pin tag to get a pin name and node name
|
|
199
|
+
// if (pinTag && pinTag.includes('@')) {
|
|
200
|
+
// const [p, n] = pinTag.split('@').map(s => s.trim());
|
|
201
|
+
// pin = p;
|
|
202
|
+
// node = n;
|
|
203
|
+
// }
|
|
204
|
+
else {
|
|
205
|
+
|
|
206
|
+
// no explicit tag - try these...
|
|
207
|
+
node = currentNode || topLevelClass || null;
|
|
208
|
+
|
|
209
|
+
// deduct the pin name from the handler name
|
|
210
|
+
if (cleanHandler.startsWith('on')) {
|
|
211
|
+
pin = cleanHandler.slice(2).replace(/([A-Z])/g, ' $1').trim().toLowerCase();
|
|
212
|
+
} else if (cleanHandler.startsWith('->')) {
|
|
213
|
+
pin = cleanHandler.slice(2).trim();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// if there is no node we just don't save the data
|
|
218
|
+
if (!node) return
|
|
219
|
+
|
|
220
|
+
// check if we have an entry for the node
|
|
221
|
+
if (!nodeMap.has(node)) nodeMap.set(node, { handles: [], transmits: [] });
|
|
222
|
+
|
|
223
|
+
// The handler data to save
|
|
224
|
+
const handlerData = {
|
|
225
|
+
pin,
|
|
226
|
+
handler: cleanHandler,
|
|
227
|
+
file: filePath,
|
|
228
|
+
line,
|
|
229
|
+
summary: jsdoc.summary || '',
|
|
230
|
+
returns: jsdoc.returns || '',
|
|
231
|
+
examples: jsdoc.examples || [],
|
|
232
|
+
params
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// extract the data from an mcp tag if present
|
|
236
|
+
if (mcpTag !== null) {
|
|
237
|
+
handlerData.mcp = true;
|
|
238
|
+
if (mcpTag.includes('name:') || mcpTag.includes('description:')) {
|
|
239
|
+
const nameMatch = /name:\s*\"?([^\"]+)\"?/.exec(mcpTag);
|
|
240
|
+
const descMatch = /description:\s*\"?([^\"]+)\"?/.exec(mcpTag);
|
|
241
|
+
if (nameMatch) handlerData.mcpName = nameMatch[1];
|
|
242
|
+
if (descMatch) handlerData.mcpDescription = descMatch[1];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// and put it in the nodemap
|
|
247
|
+
nodeMap.get(node).handles.push(handlerData);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// determines if a name is the name for a handler
|
|
251
|
+
function isHandler(name) {
|
|
252
|
+
// must be a string
|
|
253
|
+
if (typeof name !== 'string') return false;
|
|
254
|
+
|
|
255
|
+
// get rid of " and '
|
|
256
|
+
const clean = name.replace(/^['"]|['"]$/g, '');
|
|
257
|
+
|
|
258
|
+
// check that it starts with the right symbols...
|
|
259
|
+
return clean.startsWith('on') || clean.startsWith('->');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Get the parameter description from the function or method
|
|
263
|
+
function getParamDocs(fnOrMethod) {
|
|
264
|
+
|
|
265
|
+
// extract
|
|
266
|
+
const docs = fnOrMethod.getJsDocs?.() ?? [];
|
|
267
|
+
const tags = docs.flatMap(d => d.getTags());
|
|
268
|
+
const paramDocs = {};
|
|
269
|
+
|
|
270
|
+
// check the tags
|
|
271
|
+
for (const tag of tags) {
|
|
272
|
+
if (tag.getTagName() === 'param') {
|
|
273
|
+
const name = tag.getNameNode()?.getText?.() || tag.getName();
|
|
274
|
+
const desc = tag.getComment() ?? '';
|
|
275
|
+
const type = tag.getTypeNode?.()?.getText?.() || tag.getTypeExpression()?.getTypeNode()?.getText();
|
|
276
|
+
paramDocs[name] = { description: desc, type };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return paramDocs;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Get the jsdoc
|
|
283
|
+
function getFullJsDoc(node) {
|
|
284
|
+
|
|
285
|
+
const docs = node.getJsDocs?.() ?? [];
|
|
286
|
+
const summary = docs.map(d => d.getComment()).filter(Boolean).join('\n');
|
|
287
|
+
const tags = docs.flatMap(d => d.getTags()).map(t => ({
|
|
288
|
+
tagName: t.getTagName(),
|
|
289
|
+
comment: t.getComment() || ''
|
|
290
|
+
}));
|
|
291
|
+
|
|
292
|
+
const returns = tags.find(t => t.tagName === 'returns')?.comment || '';
|
|
293
|
+
const examples = tags.filter(t => t.tagName === 'example').map(t => t.comment);
|
|
294
|
+
|
|
295
|
+
return { summary, returns, examples, tags };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// make a parameter description
|
|
299
|
+
function describeParam(p, docTags = {}) {
|
|
300
|
+
|
|
301
|
+
const nameNode = p.getNameNode();
|
|
302
|
+
|
|
303
|
+
// const func = p.getParent();
|
|
304
|
+
// const funcName = func.getName?.() || '<anonymous>';
|
|
305
|
+
// console.log(funcName)
|
|
306
|
+
|
|
307
|
+
if (nameNode.getKindName() === 'ObjectBindingPattern') {
|
|
308
|
+
|
|
309
|
+
const objType = p.getType();
|
|
310
|
+
const properties = objType.getProperties();
|
|
311
|
+
const isTSFallback = objType.getText() === 'any' || objType.getText() === 'string' || properties.length === 0;
|
|
312
|
+
|
|
313
|
+
return nameNode.getElements().map(el => {
|
|
314
|
+
|
|
315
|
+
const subName = el.getName();
|
|
316
|
+
const doc = docTags[subName] ?? {};
|
|
317
|
+
let tsType = null;
|
|
318
|
+
|
|
319
|
+
if (!isTSFallback) {
|
|
320
|
+
const symbol = objType.getProperty(subName);
|
|
321
|
+
if (symbol) {
|
|
322
|
+
const resolvedType = symbol.getTypeAtLocation(el);
|
|
323
|
+
const text = resolvedType.getText();
|
|
324
|
+
if (text && text !== 'any') {
|
|
325
|
+
tsType = text;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const type = tsType || doc.type || 'string';
|
|
331
|
+
const description = doc.description || '';
|
|
332
|
+
return { name: subName, type, description };
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const name = p.getName();
|
|
337
|
+
const doc = docTags[name] ?? {};
|
|
338
|
+
const tsType = p.getType().getText();
|
|
339
|
+
|
|
340
|
+
// const isTSFallback = tsType === 'any' || tsType === 'string';
|
|
341
|
+
// if (isTSFallback && !doc.type) {
|
|
342
|
+
// console.warn(`⚠️ No type info for param "${name}" in function "${funcName}"`);
|
|
343
|
+
// }
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
name,
|
|
347
|
+
type: doc.type || tsType || 'string',
|
|
348
|
+
description: doc.description || '',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { SyntaxKind } from 'ts-morph';
|
|
2
|
+
import {currentNode, topLevelClass} from './find-handlers.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Finds tx.send or this.tx.send calls and maps them to their node context.
|
|
6
|
+
*
|
|
7
|
+
* @param {import('ts-morph').SourceFile} sourceFile - The source file being analyzed
|
|
8
|
+
* @param {string} filePath - The (relative) path of the source file
|
|
9
|
+
* @param {Map} nodeMap - Map from node name to metadata
|
|
10
|
+
* @param {string|null} currentNode - Explicitly set node name (takes priority)
|
|
11
|
+
*/
|
|
12
|
+
export function findTransmissions(sourceFile, filePath, nodeMap) {
|
|
13
|
+
|
|
14
|
+
// Search all call expressions
|
|
15
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).forEach(node => {
|
|
16
|
+
const expr = node.getExpression();
|
|
17
|
+
|
|
18
|
+
// check
|
|
19
|
+
if (expr.getKind() !== SyntaxKind.PropertyAccessExpression) return
|
|
20
|
+
|
|
21
|
+
// Match tx.send or this.tx.send - regular expression could be : expr.getText().match(/\w+\.tx\.send/)
|
|
22
|
+
const text = expr.getText()
|
|
23
|
+
|
|
24
|
+
// check
|
|
25
|
+
if (! (text === 'tx.send' || text === 'this.tx.send' || text.endsWith('.tx.send'))) return;
|
|
26
|
+
|
|
27
|
+
const args = node.getArguments();
|
|
28
|
+
if (args.length === 0 || !args[0].isKind(SyntaxKind.StringLiteral)) return;
|
|
29
|
+
|
|
30
|
+
const pin = args[0].getLiteralText();
|
|
31
|
+
|
|
32
|
+
// Try to infer the class context of the tx.send call
|
|
33
|
+
const method = node.getFirstAncestorByKind(SyntaxKind.MethodDeclaration);
|
|
34
|
+
const classDecl = method?.getFirstAncestorByKind(SyntaxKind.ClassDeclaration) ?? node.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
|
|
35
|
+
const className = classDecl?.getName?.();
|
|
36
|
+
|
|
37
|
+
// Priority order: currentNode > className > topLevelClass > 'global'
|
|
38
|
+
const nodeName = currentNode || className || topLevelClass || null;
|
|
39
|
+
|
|
40
|
+
// check
|
|
41
|
+
if (!nodeName) return
|
|
42
|
+
|
|
43
|
+
// check if there is an entry for the node or create it
|
|
44
|
+
nodeMap.has(nodeName) || nodeMap.set(nodeName, { handles: [], transmits: [] });
|
|
45
|
+
|
|
46
|
+
// add the entry to the transmits array
|
|
47
|
+
nodeMap.get(nodeName).transmits.push({
|
|
48
|
+
pin,
|
|
49
|
+
file: filePath,
|
|
50
|
+
line: node.getStartLineNumber()
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { profile } from './profile.js';
|
|
2
|
+
|
|
3
|
+
export const command = 'profile <model-file>';
|
|
4
|
+
export const describe = 'Find message handlers and message transmissions.';
|
|
5
|
+
|
|
6
|
+
export const builder = [
|
|
7
|
+
{ flag: '--out <file>', desc: 'specifies the output file' },
|
|
8
|
+
{ flag: '--full', desc: 'check all source files in the model' },
|
|
9
|
+
{ flag: '--changed <files...>', desc: 'only check changed files' },
|
|
10
|
+
{ flag: '--deleted <files...>', desc: 'remove data from deleted files' },
|
|
11
|
+
{ flag: '--delta-file <path>', desc: 'write the delta to a file' },
|
|
12
|
+
{ flag: '--reason <text>', desc: 'information' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export const handler = profile;
|
|
16
|
+
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Native
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import ts from 'typescript';
|
|
8
|
+
|
|
9
|
+
// ts-morph
|
|
10
|
+
import { Project } from 'ts-morph';
|
|
11
|
+
|
|
12
|
+
// vmblu
|
|
13
|
+
import { ModelBlueprint, ModelCompiler } from '../../../core/model/index.js';
|
|
14
|
+
import { ARL } from '../../../core/arl/arl-node.js'
|
|
15
|
+
|
|
16
|
+
// profile tool
|
|
17
|
+
import {findHandlers} from './find-handlers.js'
|
|
18
|
+
import {findTransmissions} from './find-transmissions.js'
|
|
19
|
+
|
|
20
|
+
const SRC_DOC_VERSION = '0.2';
|
|
21
|
+
|
|
22
|
+
// The main function for the profile tool
|
|
23
|
+
export async function profile(argv = process.argv.slice(2)) {
|
|
24
|
+
|
|
25
|
+
const cli = parseCliArgs(argv);
|
|
26
|
+
|
|
27
|
+
if (!cli.modelFile) {
|
|
28
|
+
console.error('Usage: vmblu profile <model-file> [--out <file>] [--full] [--changed <files...>] [--deleted <files...>] [--delta-file <path>] [--reason <text>]');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const absoluteModelPath = path.resolve(cli.modelFile);
|
|
33
|
+
const modelPath = absoluteModelPath.replace(/\\/g, '/');
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(absoluteModelPath) || !fs.statSync(absoluteModelPath).isFile()) {
|
|
36
|
+
console.error(cli.modelFile, 'is not a file');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const outPath = cli.outFile
|
|
41
|
+
? path.resolve(cli.outFile)
|
|
42
|
+
: (() => {
|
|
43
|
+
const { dir, name } = path.parse(absoluteModelPath);
|
|
44
|
+
return path.join(dir, `${name}-doc.json`);
|
|
45
|
+
})();
|
|
46
|
+
|
|
47
|
+
if (cli.deltaFile) cli.deltaFile = path.resolve(cli.deltaFile);
|
|
48
|
+
if (cli.reason) console.log('[profile] reason:', cli.reason);
|
|
49
|
+
|
|
50
|
+
if (!cli.full && (cli.changed.length || cli.deleted.length || cli.deltaFile)) {
|
|
51
|
+
console.log('[profile] incremental updates not yet supported; performing full rescan.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Make an Application Resource Locator // Make an Application Resource Locator
|
|
55
|
+
const arl = new ARL(modelPath);
|
|
56
|
+
|
|
57
|
+
// Create model object
|
|
58
|
+
const model = new ModelBlueprint(arl);
|
|
59
|
+
|
|
60
|
+
// create a model compile object - we do not need a uid generator
|
|
61
|
+
const compiler = new ModelCompiler(null);
|
|
62
|
+
|
|
63
|
+
// get all the factories that are refernced in the model and submodels
|
|
64
|
+
await compiler.getFactoriesAndModels(model);
|
|
65
|
+
|
|
66
|
+
// extract the factories
|
|
67
|
+
const factories = compiler.factories.map.values();
|
|
68
|
+
|
|
69
|
+
// setup the ts-morph project with the factory files
|
|
70
|
+
const project = setupProject(factories)
|
|
71
|
+
|
|
72
|
+
// Extract the source files
|
|
73
|
+
const sourceFiles = project.getSourceFiles()
|
|
74
|
+
|
|
75
|
+
// get all the handlers and transmissions of all the source files into the rxtx array
|
|
76
|
+
const rxtx = []
|
|
77
|
+
const generatedAt = new Date().toISOString()
|
|
78
|
+
for (const sourceFile of sourceFiles) {
|
|
79
|
+
|
|
80
|
+
// A file reference is always relative to the model file
|
|
81
|
+
const filePath = path.relative(path.dirname(modelPath), sourceFile.getFilePath()).replace(/\\/g, '/');
|
|
82
|
+
|
|
83
|
+
// the node map to collect the data for the file
|
|
84
|
+
const nodeMap = new Map();
|
|
85
|
+
|
|
86
|
+
// find the handlers in the file
|
|
87
|
+
findHandlers(sourceFile, filePath, nodeMap)
|
|
88
|
+
|
|
89
|
+
// find the transmissions in the file
|
|
90
|
+
findTransmissions(sourceFile, filePath, nodeMap)
|
|
91
|
+
|
|
92
|
+
// map the nodemap to an array
|
|
93
|
+
const nodeArray = Array.from(nodeMap.entries()).map(([node, { handles, transmits }]) => ({node,handles,transmits}))
|
|
94
|
+
|
|
95
|
+
// add these to the overall rxtx array
|
|
96
|
+
rxtx.push(...nodeArray)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Assemble the output file path
|
|
100
|
+
// (outPath was resolved earlier based on CLI arguments)
|
|
101
|
+
|
|
102
|
+
// and write the output to that file
|
|
103
|
+
const output = {
|
|
104
|
+
version: SRC_DOC_VERSION,
|
|
105
|
+
generatedAt,
|
|
106
|
+
entries: rxtx
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Persist the structured documentation with its header so downstream tools can validate against the schema.
|
|
110
|
+
fs.writeFileSync(outPath, JSON.stringify(output, null, 2));
|
|
111
|
+
console.log(`Documentation written to ${outPath}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseCliArgs(argv) {
|
|
115
|
+
|
|
116
|
+
const result = {
|
|
117
|
+
modelFile: null,
|
|
118
|
+
outFile: null,
|
|
119
|
+
full: false,
|
|
120
|
+
reason: null,
|
|
121
|
+
changed: [],
|
|
122
|
+
deleted: [],
|
|
123
|
+
deltaFile: null,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
let i = 0;
|
|
127
|
+
while (i < argv.length) {
|
|
128
|
+
const token = argv[i];
|
|
129
|
+
|
|
130
|
+
if (token === '--out') {
|
|
131
|
+
const next = argv[i + 1];
|
|
132
|
+
if (next && !next.startsWith('--')) {
|
|
133
|
+
result.outFile = next;
|
|
134
|
+
i += 2;
|
|
135
|
+
} else {
|
|
136
|
+
console.warn('Warning: --out requires a path argument; ignoring.');
|
|
137
|
+
i += 1;
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (token === '--full') {
|
|
143
|
+
result.full = true;
|
|
144
|
+
i += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (token === '--reason') {
|
|
149
|
+
const next = argv[i + 1];
|
|
150
|
+
if (next && !next.startsWith('--')) {
|
|
151
|
+
result.reason = next;
|
|
152
|
+
i += 2;
|
|
153
|
+
} else {
|
|
154
|
+
result.reason = '';
|
|
155
|
+
i += 1;
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (token === '--delta-file') {
|
|
161
|
+
const next = argv[i + 1];
|
|
162
|
+
if (next && !next.startsWith('--')) {
|
|
163
|
+
result.deltaFile = next;
|
|
164
|
+
i += 2;
|
|
165
|
+
} else {
|
|
166
|
+
console.warn('Warning: --delta-file requires a path argument; ignoring.');
|
|
167
|
+
i += 1;
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (token === '--changed') {
|
|
173
|
+
const values = [];
|
|
174
|
+
i += 1;
|
|
175
|
+
while (i < argv.length && !argv[i].startsWith('--')) {
|
|
176
|
+
values.push(argv[i]);
|
|
177
|
+
i += 1;
|
|
178
|
+
}
|
|
179
|
+
if (values.length === 0) {
|
|
180
|
+
console.warn('Warning: --changed provided without any paths.');
|
|
181
|
+
} else {
|
|
182
|
+
result.changed.push(...values);
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (token === '--deleted') {
|
|
188
|
+
const values = [];
|
|
189
|
+
i += 1;
|
|
190
|
+
while (i < argv.length && !argv[i].startsWith('--')) {
|
|
191
|
+
values.push(argv[i]);
|
|
192
|
+
i += 1;
|
|
193
|
+
}
|
|
194
|
+
if (values.length === 0) {
|
|
195
|
+
console.warn('Warning: --deleted provided without any paths.');
|
|
196
|
+
} else {
|
|
197
|
+
result.deleted.push(...values);
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (typeof token === 'string' && token.startsWith('--')) {
|
|
203
|
+
console.warn('Warning: unknown option "' + token + '" ignored.');
|
|
204
|
+
i += 1;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!result.modelFile) {
|
|
209
|
+
result.modelFile = token;
|
|
210
|
+
} else {
|
|
211
|
+
console.warn('Warning: extra positional argument "' + token + '" ignored.');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
i += 1;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
// Gets all the source files that are part of this project
|
|
223
|
+
function setupProject(factories) {
|
|
224
|
+
|
|
225
|
+
// Initialize ts-morph without tsconfig
|
|
226
|
+
const project = new Project({
|
|
227
|
+
compilerOptions: {
|
|
228
|
+
allowJs: true,
|
|
229
|
+
checkJs: true,
|
|
230
|
+
module: ts.ModuleKind.ESNext,
|
|
231
|
+
target: ts.ScriptTarget.ESNext,
|
|
232
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
|
233
|
+
esModuleInterop: true,
|
|
234
|
+
noEmit: true,
|
|
235
|
+
},
|
|
236
|
+
skipAddingFilesFromTsConfig: true,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Add factory entry files
|
|
240
|
+
for (const factory of factories) {
|
|
241
|
+
|
|
242
|
+
// get the file path
|
|
243
|
+
const filePath = factory.arl.url;
|
|
244
|
+
|
|
245
|
+
// user feedback
|
|
246
|
+
console.log('Adding factory entry:', filePath);
|
|
247
|
+
|
|
248
|
+
// add to the project
|
|
249
|
+
try {
|
|
250
|
+
project.addSourceFileAtPath(factory.arl.url);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.warn(`Could not load ${filePath}: ${err.message}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Resolve all imports recursively
|
|
257
|
+
project.resolveSourceFileDependencies();
|
|
258
|
+
|
|
259
|
+
// done
|
|
260
|
+
return project
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
265
|
+
|
|
266
|
+
if (isDirectRun) {
|
|
267
|
+
profile().catch(err => {
|
|
268
|
+
console.error('Failed to generate source documentation:', err);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import resolve from '@rollup/plugin-node-resolve';
|
|
2
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
3
|
+
import json from '@rollup/plugin-json';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
input: './profile.js',
|
|
7
|
+
output: {
|
|
8
|
+
file: './profile.cjs', // ⬅️ Use .cjs extension and CommonJS format
|
|
9
|
+
format: 'cjs',
|
|
10
|
+
sourcemap: true
|
|
11
|
+
},
|
|
12
|
+
external: [
|
|
13
|
+
'ts-morph',
|
|
14
|
+
'typescript', // Exclude heavy dependencies from bundle
|
|
15
|
+
],
|
|
16
|
+
plugins: [
|
|
17
|
+
commonjs(),
|
|
18
|
+
resolve({ preferBuiltins: true }),
|
|
19
|
+
json()
|
|
20
|
+
]
|
|
21
|
+
};
|
|
22
|
+
|
package/package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizualmodel/vmblu-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"type": "
|
|
5
|
-
"bin": {
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"vmblu": "bin/vmblu.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"commands",
|
|
11
|
+
"templates",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"vmblu": "vmblu"
|
|
20
|
+
}
|
|
8
21
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://vmblu.dev/schema/
|
|
3
|
+
"$id": "https://vmblu.dev/schema/profile/0.2/profile.schema.json",
|
|
4
4
|
"title": "vmblu Source Documentation (grouped by node)",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["version", "generatedAt", "entries"],
|
package/templates/0.8.2/seed.md
CHANGED
|
@@ -4,14 +4,14 @@ vmblu (Vizual Model Blueprint) is a graphical editor that maintains a visual, ru
|
|
|
4
4
|
vmblu models software as interconnected nodes that pass messages via pins.
|
|
5
5
|
|
|
6
6
|
The model has a well defined format described by a schema. An additional annex gives semantic background information about the schema.
|
|
7
|
-
The parameter profiles of messages and where messages are received and sent in the actual source code, are stored in a second file, the
|
|
8
|
-
The
|
|
7
|
+
The parameter profiles of messages and where messages are received and sent in the actual source code, are stored in a second file, the profile file.
|
|
8
|
+
The profile file is generated automatically by vmblu and is only to be consulted, not written, at the start of a project it does not yet exist
|
|
9
9
|
|
|
10
10
|
You are an expert **architecture + code copilot** for **vmblu** .
|
|
11
|
-
You can find the location of the model file, the model schema, the model annex, the
|
|
11
|
+
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. Read these files.
|
|
12
12
|
|
|
13
13
|
The location of all other files in the project can be found via the model file.
|
|
14
14
|
|
|
15
15
|
Your job is to co-design the architecture and the software for the system.
|
|
16
16
|
For modifications of the model, always follow the schema.
|
|
17
|
-
If the
|
|
17
|
+
If the profile does not exist yet or does notcontain profile information it could be that the code for the node has not been written yet, this should not stop you from continuing.
|