@vizualmodel/vmblu-cli 0.3.4 → 0.3.5

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.
@@ -1,4 +1,4 @@
1
- // vmblu init [targetDir] --name <project> --schema <ver> --force --dry-run
1
+ // vmblu init [targetDir] --name <project> --schema <ver> --force --dry-run
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { initProject } from './init-project.js';
@@ -9,46 +9,46 @@ const pckg = require('../../package.json');
9
9
 
10
10
 
11
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
-
13
- export const command = 'init <folder name>';
14
- export const describe = 'Scaffold an empty vmblu project';
15
- export const builder = [
16
- { flag: '--name <project>', desc: 'Project name (default: folder name)' },
17
- { flag: '--schema <ver>', desc: 'Schema version (default: latest version)' },
18
- { flag: '--force', desc: 'Overwrite existing files' },
19
- { flag: '--dry-run', desc: 'Show actions without writing' }
20
- ];
21
-
22
- export const handler = async (argv) => {
23
- // tiny arg parse (no deps)
24
- const args = { _: [] };
25
- for (let i = 0; i < argv.length; i++) {
26
- const a = argv[i];
27
- if (a === '--force') args.force = true;
28
- else if (a === '--dry-run') args.dryRun = true;
29
- else if (a === '--name') args.name = argv[++i];
30
- else if (a === '--schema') args.schema = argv[++i];
31
- else if (!a.startsWith('-')) args._.push(a);
32
- }
33
-
34
- const targetDir = path.resolve(args._[0] || '.');
35
- const projectName = args.name || path.basename(targetDir);
36
- const schemaVersion = args.schema || pckg.schemaVersion;
37
-
38
- await initProject({
39
- targetDir,
40
- projectName,
41
- schemaVersion,
42
- force: Boolean(args.force),
43
- dryRun: Boolean(args.dryRun),
44
- templatesDir: path.join(__dirname, '..', '..', 'templates'),
45
- ui: {
46
- info: (m) => console.log(m),
47
- warn: (m) => console.warn(m),
48
- error: (m) => console.error(m)
49
- }
50
- });
51
-
52
- console.log(`vmblu project scaffolded in ${targetDir}`);
53
- };
54
-
12
+
13
+ export const command = 'init <folder name>';
14
+ export const describe = 'Scaffold an empty vmblu project';
15
+ export const builder = [
16
+ { flag: '--name <project>', desc: 'Project name (default: folder name)' },
17
+ { flag: '--schema <ver>', desc: 'Schema version (default: latest version)' },
18
+ { flag: '--force', desc: 'Overwrite existing files' },
19
+ { flag: '--dry-run', desc: 'Show actions without writing' }
20
+ ];
21
+
22
+ export const handler = async (argv) => {
23
+ // tiny arg parse (no deps)
24
+ const args = { _: [] };
25
+ for (let i = 0; i < argv.length; i++) {
26
+ const a = argv[i];
27
+ if (a === '--force') args.force = true;
28
+ else if (a === '--dry-run') args.dryRun = true;
29
+ else if (a === '--name') args.name = argv[++i];
30
+ else if (a === '--schema') args.schema = argv[++i];
31
+ else if (!a.startsWith('-')) args._.push(a);
32
+ }
33
+
34
+ const targetDir = path.resolve(args._[0] || '.');
35
+ const projectName = args.name || path.basename(targetDir);
36
+ const schemaVersion = args.schema || pckg.schemaVersion;
37
+
38
+ await initProject({
39
+ targetDir,
40
+ projectName,
41
+ schemaVersion,
42
+ force: Boolean(args.force),
43
+ dryRun: Boolean(args.dryRun),
44
+ templatesDir: path.join(__dirname, '..', '..', 'templates'),
45
+ ui: {
46
+ info: (m) => console.log(m),
47
+ warn: (m) => console.warn(m),
48
+ error: (m) => console.error(m)
49
+ }
50
+ });
51
+
52
+ console.log(`vmblu project scaffolded in ${targetDir}`);
53
+ };
54
+
@@ -1,5 +1,5 @@
1
- // core/initProject.js
2
- // Node 18+ (fs/promises, crypto). No external deps.
1
+ // core/initProject.js
2
+ // Node 18+ (fs/promises, crypto). No external deps.
3
3
  import * as fs from 'fs/promises';
4
4
  import * as fssync from 'fs';
5
5
  import path from 'path';
@@ -12,279 +12,279 @@ const require = createRequire(import.meta.url);
12
12
  const pckg = require('../../package.json');
13
13
  const SCHEMA_VERSION = pckg.schemaVersion
14
14
  const CLI_VERSION = pckg.version
15
-
16
- function rel(from, to) {
17
- return path.posix.join(...path.relative(from, to).split(path.sep));
18
- }
19
-
20
- async function exists(p) {
21
- try { await fs.access(p); return true; } catch { return false; }
22
- }
23
-
24
- async function ensureDir(dir, dry) {
25
- if (dry) return;
26
- await fs.mkdir(dir, { recursive: true });
27
- }
28
-
29
- async function writeFileSafe(file, contents, { force = false, dry = false } = {}) {
30
- const already = await exists(file);
31
- if (already && !force) return false;
32
- if (dry) return true;
33
- await fs.writeFile(file, contents);
34
- return true;
35
- }
36
-
37
- async function copyOrWriteFallback(src, dst, fallback, { force = false, dry = false } = {}) {
38
- const already = await exists(dst);
39
- if (already && !force) return false;
40
-
41
- if (dry) return true;
42
-
43
- if (src && fssync.existsSync(src)) {
44
- await fs.copyFile(src, dst);
45
- } else {
46
- await fs.writeFile(dst, fallback);
47
- }
48
- return true;
49
- }
50
-
51
- // async function sha256(file) {
52
- // const buf = await fs.readFile(file);
53
- // return crypto.createHash('sha256').update(buf).digest('hex');
54
- // }
55
-
56
- function defaultModel(projectName) {
57
- const now = new Date().toISOString();
58
- return JSON.stringify({
59
- header: {
60
- version: SCHEMA_VERSION,
61
- created: now,
62
- saved: now,
63
- utc: now,
64
- style: "#2c7be5",
65
- runtime: "@vizualmodel/vmblu-runtime",
66
- description: `${projectName} — vmblu model (scaffolded)`
67
- },
68
- models: [],
69
- factories: [],
70
- root: {
71
- group: "Root",
72
- pins: [],
73
- nodes: [],
74
- routes: [],
75
- prompt: "Root group for the application."
76
- }
77
- }, null, 2);
78
- }
79
-
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
- function fallbackAnnex() {
90
- return `# vmblu Annex (placeholder)
91
- This is a minimal scaffold. Replace with the official annex matching your pinned schema version.
92
-
93
- - Nodes: Source, Group, Dock
94
- - Pins: input/output/channel, 'profile' describes payloads
95
- - Routes: "output@NodeA" -> "input@NodeB" (or via group pads)
96
- - Keep names normalized; avoid ambiguous magic.
97
- `;
98
- }
99
-
100
- function fallbackSchema() {
101
- return `{
102
- "$schema": "https://json-schema.org/draft/2020-12/schema",
103
- "title": "vmblu.schema (placeholder)",
104
- "type": "object",
105
- "description": "Placeholder schema. Replace with official version."
106
- }`;
107
- }
108
-
109
- function fallbackProfileSchema() {
110
- return `{
111
- "$schema": "https://json-schema.org/draft/2020-12/schema",
112
- "title": "profile.schema (placeholder)",
113
- "type": "object",
114
- "description": "Placeholder schema. Replace with official version."
115
- }`;
116
- }
117
-
118
- function fallbackSeed() {
119
- return `# Session Seed (System Prompt)
120
-
121
- vmblu (Vizual Model Blueprint) is a graphical editor that maintains a visual, runnable model of a software system.
122
- vmblu models software as interconnected nodes that pass messages via pins.
123
- The model has a well defined format described by a schema. An additional annex gives semantic background information about the schema.
124
- 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.
125
- The profile file is generated automatically by vmblu and is only to be consulted, not written.
126
-
127
- You are an expert **architecture + code copilot** for **vmblu** .
128
- 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.
129
- The location of all other files in the project can be found via the model file.
130
-
131
- Your job is to co-design the architecture and the software for the system.
132
- For modifications of the model, always follow the schema.
133
- 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
134
- `}
135
-
136
- /**
137
- * Initialize a vmblu project directory.
138
- *
139
- * @param {Object} opts
140
- * @param {string} opts.targetDir Absolute path to project dir (created if missing)
141
- * @param {string} [opts.projectName] Defaults to basename(targetDir)
142
- * @param {string} [opts.schemaVersion] e.g. "0.8.2"
143
- * @param {boolean}[opts.force] Overwrite existing files
144
- * @param {boolean}[opts.dryRun] Print actions, do not write
145
- * @param {string} [opts.templatesDir] Root where templates live (defaults to package templates)
146
- * expects:
147
- * templates/schemas/<ver>/vmblu.schema.json
148
- * templates/annex/<ver>/vmblu.annex.md
149
- * @param {Object} [opts.ui] { info, warn, error } callbacks (optional)
150
- */
151
- async function initProject(opts) {
152
- const {
153
- targetDir,
154
- projectName = path.basename(opts.targetDir),
155
- schemaVersion = SCHEMA_VERSION,
156
- force = false,
157
- dryRun = false,
158
- templatesDir = path.join(__dirname, '..', 'templates'),
159
- ui = {
160
- info: console.log,
161
- warn: console.warn,
162
- error: console.error
163
- }
164
- } = opts || {};
165
-
166
- if (!targetDir) throw new Error("initProject: targetDir is required");
167
-
168
- const absTarget = path.resolve(targetDir);
169
- const modelFile = path.join(absTarget, `${projectName}.vmblu`);
170
- const docFile = path.join(absTarget, `${projectName}.prf.json`);
171
-
172
- const llmDir = path.join(absTarget, 'llm');
173
- const sessionDir = path.join(llmDir, 'session');
174
- const nodesDir = path.join(absTarget, 'nodes');
175
-
176
- // Template sources
177
- // const schemaSrc = path.join(templatesDir, 'schemas', schemaVersion, 'vmblu.schema.json');
178
- // const annexSrc = path.join(templatesDir, 'annex', schemaVersion, 'vmblu.annex.md');
179
-
180
- // Template sources
181
- const schemaSrc = path.join(templatesDir, schemaVersion, 'vmblu.schema.json');
182
- const annexSrc = path.join(templatesDir, schemaVersion, 'vmblu.annex.md');
183
- const profileSchemaSrc = path.join(templatesDir, schemaVersion, 'profile.schema.json');
184
- const seedSrc = path.join(templatesDir, schemaVersion, 'seed.md');
185
-
186
- // 1) Create folders
187
- for (const dir of [absTarget, llmDir, sessionDir, nodesDir]) {
188
- ui.info(`mkdir -p ${dir}`);
189
- await ensureDir(dir, dryRun);
190
- }
191
-
192
- // 2) Create root files
193
- ui.info(`create ${modelFile}${force ? ' (force)' : ''}`);
194
- await writeFileSafe(modelFile, defaultModel(projectName), { force, dry: dryRun });
195
-
196
- ui.info(`create ${docFile}${force ? ' (force)' : ''}`);
197
- await writeFileSafe(docFile, defaultDoc(projectName), { force, dry: dryRun });
198
-
199
- // 3) Copy schema + annex into llm/
200
- const schemaDst = path.join(llmDir, 'vmblu.schema.json');
201
- const annexDst = path.join(llmDir, 'vmblu.annex.md');
202
- const profileSchemaDst = path.join(llmDir, 'profile.schema.json');
203
- const seedDst = path.join(llmDir, 'seed.md');
204
-
205
-
206
- ui.info(`copy ${schemaSrc} -> ${schemaDst}${force ? ' (force)' : ''}`);
207
- await copyOrWriteFallback(schemaSrc, schemaDst, fallbackSchema(), { force, dry: dryRun });
208
-
209
- ui.info(`copy ${annexSrc} -> ${annexDst}${force ? ' (force)' : ''}`);
210
- await copyOrWriteFallback(annexSrc, annexDst, fallbackAnnex(), { force, dry: dryRun });
211
-
212
- ui.info(`copy ${profileSchemaSrc} -> ${profileSchemaDst}${force ? ' (force)' : ''}`);
213
- await copyOrWriteFallback(profileSchemaSrc, profileSchemaDst, fallbackProfileSchema(), { force, dry: dryRun });
214
-
215
- ui.info(`copy ${seedSrc} -> ${seedDst}${force ? ' (force)' : ''}`);
216
- await copyOrWriteFallback(seedSrc, seedDst, fallbackSeed(), { force, dry: dryRun });
217
-
218
- // 4) Build manifest with hashes
219
- const willWriteManifest = !(await exists(path.join(llmDir, 'manifest.json'))) || force;
220
- let manifest = null;
221
-
222
- if (dryRun) {
223
- ui.info(`would write manifest.json in ${llmDir}`);
224
- } else {
225
-
226
- // I don't need the hashes
227
- // const [schemaHash, annexHash, modelHash, docHash] = await Promise.all([
228
- // sha256(schemaDst), sha256(annexDst), sha256(modelFile), sha256(docFile)
229
- // ]);
230
-
231
- // Paths in manifest should be relative to /llm to keep it portable
232
- const llmPosix = llmDir; // absolute
233
- manifest = {
234
- version: schemaVersion,
235
- model: {
236
- path: rel(llmPosix, modelFile),
237
- schema: 'vmblu.schema.json',
238
- annex: 'vmblu.annex.md',
239
- },
240
- profile: {
241
- path: rel(llmPosix, docFile),
242
- schema: 'profile.schema.json',
243
- },
244
- };
245
-
246
- if (willWriteManifest) {
247
- const manifestPath = path.join(llmDir, 'manifest.json');
248
- ui.info(`create ${manifestPath}${force ? ' (force)' : ''}`);
249
- await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
250
- } else {
251
- ui.warn(`manifest.json exists and --force not set. Skipped.`);
252
- }
253
- }
254
-
255
- // 5) Make the package file
256
- makePackageJson({ absTarget, projectName, force, dryRun, addCliDep: true, cliVersion: "^" + CLI_VERSION }, ui);
257
-
258
- // 6) Final tree hint
259
- ui.info(`\nScaffold complete${dryRun ? ' (dry run)' : ''}:\n` +
260
- ` ${absTarget}/
261
- ${path.basename(modelFile)}
262
- ${path.basename(docFile)}
263
- package.json
264
- llm/
265
- seed.md
266
- manifest.json
267
- vmblu.schema.json
268
- vmblu.annex.md
269
- profile.schema.json
270
- session/
271
- nodes/\n`);
272
-
273
- return {
274
- targetDir: absTarget,
275
- projectName,
276
- schemaVersion,
277
- files: {
278
- model: modelFile,
279
- doc: docFile,
280
- schema: schemaDst,
281
- annex: annexDst,
282
- profileSchema: profileSchemaDst,
283
- manifest: path.join(llmDir, 'manifest.json')
284
- },
285
- dryRun,
286
- manifest
287
- };
288
- }
289
-
290
- export { initProject };
15
+
16
+ function rel(from, to) {
17
+ return path.posix.join(...path.relative(from, to).split(path.sep));
18
+ }
19
+
20
+ async function exists(p) {
21
+ try { await fs.access(p); return true; } catch { return false; }
22
+ }
23
+
24
+ async function ensureDir(dir, dry) {
25
+ if (dry) return;
26
+ await fs.mkdir(dir, { recursive: true });
27
+ }
28
+
29
+ async function writeFileSafe(file, contents, { force = false, dry = false } = {}) {
30
+ const already = await exists(file);
31
+ if (already && !force) return false;
32
+ if (dry) return true;
33
+ await fs.writeFile(file, contents);
34
+ return true;
35
+ }
36
+
37
+ async function copyOrWriteFallback(src, dst, fallback, { force = false, dry = false } = {}) {
38
+ const already = await exists(dst);
39
+ if (already && !force) return false;
40
+
41
+ if (dry) return true;
42
+
43
+ if (src && fssync.existsSync(src)) {
44
+ await fs.copyFile(src, dst);
45
+ } else {
46
+ await fs.writeFile(dst, fallback);
47
+ }
48
+ return true;
49
+ }
50
+
51
+ // async function sha256(file) {
52
+ // const buf = await fs.readFile(file);
53
+ // return crypto.createHash('sha256').update(buf).digest('hex');
54
+ // }
55
+
56
+ function defaultModel(projectName) {
57
+ const now = new Date().toISOString();
58
+ return JSON.stringify({
59
+ header: {
60
+ version: SCHEMA_VERSION,
61
+ created: now,
62
+ saved: now,
63
+ utc: now,
64
+ style: "#2c7be5",
65
+ runtime: "@vizualmodel/vmblu-runtime",
66
+ description: `${projectName} — vmblu model (scaffolded)`
67
+ },
68
+ models: [],
69
+ factories: [],
70
+ root: {
71
+ group: "Root",
72
+ pins: [],
73
+ nodes: [],
74
+ routes: [],
75
+ prompt: "Root group for the application."
76
+ }
77
+ }, null, 2);
78
+ }
79
+
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
+ function fallbackAnnex() {
90
+ return `# vmblu Annex (placeholder)
91
+ This is a minimal scaffold. Replace with the official annex matching your pinned schema version.
92
+
93
+ - Nodes: Source, Group, Dock
94
+ - Pins: input/output/channel, 'profile' describes payloads
95
+ - Routes: "output@NodeA" -> "input@NodeB" (or via group pads)
96
+ - Keep names normalized; avoid ambiguous magic.
97
+ `;
98
+ }
99
+
100
+ function fallbackSchema() {
101
+ return `{
102
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
103
+ "title": "vmblu.schema (placeholder)",
104
+ "type": "object",
105
+ "description": "Placeholder schema. Replace with official version."
106
+ }`;
107
+ }
108
+
109
+ function fallbackProfileSchema() {
110
+ return `{
111
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
112
+ "title": "profile.schema (placeholder)",
113
+ "type": "object",
114
+ "description": "Placeholder schema. Replace with official version."
115
+ }`;
116
+ }
117
+
118
+ function fallbackSeed() {
119
+ return `# Session Seed (System Prompt)
120
+
121
+ vmblu (Vizual Model Blueprint) is a graphical editor that maintains a visual, runnable model of a software system.
122
+ vmblu models software as interconnected nodes that pass messages via pins.
123
+ The model has a well defined format described by a schema. An additional annex gives semantic background information about the schema.
124
+ 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.
125
+ The profile file is generated automatically by vmblu and is only to be consulted, not written.
126
+
127
+ You are an expert **architecture + code copilot** for **vmblu** .
128
+ 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.
129
+ The location of all other files in the project can be found via the model file.
130
+
131
+ Your job is to co-design the architecture and the software for the system.
132
+ For modifications of the model, always follow the schema.
133
+ 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
134
+ `}
135
+
136
+ /**
137
+ * Initialize a vmblu project directory.
138
+ *
139
+ * @param {Object} opts
140
+ * @param {string} opts.targetDir Absolute path to project dir (created if missing)
141
+ * @param {string} [opts.projectName] Defaults to basename(targetDir)
142
+ * @param {string} [opts.schemaVersion] e.g. "0.8.2"
143
+ * @param {boolean}[opts.force] Overwrite existing files
144
+ * @param {boolean}[opts.dryRun] Print actions, do not write
145
+ * @param {string} [opts.templatesDir] Root where templates live (defaults to package templates)
146
+ * expects:
147
+ * templates/schemas/<ver>/vmblu.schema.json
148
+ * templates/annex/<ver>/vmblu.annex.md
149
+ * @param {Object} [opts.ui] { info, warn, error } callbacks (optional)
150
+ */
151
+ async function initProject(opts) {
152
+ const {
153
+ targetDir,
154
+ projectName = path.basename(opts.targetDir),
155
+ schemaVersion = SCHEMA_VERSION,
156
+ force = false,
157
+ dryRun = false,
158
+ templatesDir = path.join(__dirname, '..', 'templates'),
159
+ ui = {
160
+ info: console.log,
161
+ warn: console.warn,
162
+ error: console.error
163
+ }
164
+ } = opts || {};
165
+
166
+ if (!targetDir) throw new Error("initProject: targetDir is required");
167
+
168
+ const absTarget = path.resolve(targetDir);
169
+ const modelFile = path.join(absTarget, `${projectName}.vmblu`);
170
+ const docFile = path.join(absTarget, `${projectName}.prf.json`);
171
+
172
+ const llmDir = path.join(absTarget, 'llm');
173
+ const sessionDir = path.join(llmDir, 'session');
174
+ const nodesDir = path.join(absTarget, 'nodes');
175
+
176
+ // Template sources
177
+ // const schemaSrc = path.join(templatesDir, 'schemas', schemaVersion, 'vmblu.schema.json');
178
+ // const annexSrc = path.join(templatesDir, 'annex', schemaVersion, 'vmblu.annex.md');
179
+
180
+ // Template sources
181
+ const schemaSrc = path.join(templatesDir, schemaVersion, 'vmblu.schema.json');
182
+ const annexSrc = path.join(templatesDir, schemaVersion, 'vmblu.annex.md');
183
+ const profileSchemaSrc = path.join(templatesDir, schemaVersion, 'profile.schema.json');
184
+ const seedSrc = path.join(templatesDir, schemaVersion, 'seed.md');
185
+
186
+ // 1) Create folders
187
+ for (const dir of [absTarget, llmDir, sessionDir, nodesDir]) {
188
+ ui.info(`mkdir -p ${dir}`);
189
+ await ensureDir(dir, dryRun);
190
+ }
191
+
192
+ // 2) Create root files
193
+ ui.info(`create ${modelFile}${force ? ' (force)' : ''}`);
194
+ await writeFileSafe(modelFile, defaultModel(projectName), { force, dry: dryRun });
195
+
196
+ ui.info(`create ${docFile}${force ? ' (force)' : ''}`);
197
+ await writeFileSafe(docFile, defaultDoc(projectName), { force, dry: dryRun });
198
+
199
+ // 3) Copy schema + annex into llm/
200
+ const schemaDst = path.join(llmDir, 'vmblu.schema.json');
201
+ const annexDst = path.join(llmDir, 'vmblu.annex.md');
202
+ const profileSchemaDst = path.join(llmDir, 'profile.schema.json');
203
+ const seedDst = path.join(llmDir, 'seed.md');
204
+
205
+
206
+ ui.info(`copy ${schemaSrc} -> ${schemaDst}${force ? ' (force)' : ''}`);
207
+ await copyOrWriteFallback(schemaSrc, schemaDst, fallbackSchema(), { force, dry: dryRun });
208
+
209
+ ui.info(`copy ${annexSrc} -> ${annexDst}${force ? ' (force)' : ''}`);
210
+ await copyOrWriteFallback(annexSrc, annexDst, fallbackAnnex(), { force, dry: dryRun });
211
+
212
+ ui.info(`copy ${profileSchemaSrc} -> ${profileSchemaDst}${force ? ' (force)' : ''}`);
213
+ await copyOrWriteFallback(profileSchemaSrc, profileSchemaDst, fallbackProfileSchema(), { force, dry: dryRun });
214
+
215
+ ui.info(`copy ${seedSrc} -> ${seedDst}${force ? ' (force)' : ''}`);
216
+ await copyOrWriteFallback(seedSrc, seedDst, fallbackSeed(), { force, dry: dryRun });
217
+
218
+ // 4) Build manifest with hashes
219
+ const willWriteManifest = !(await exists(path.join(llmDir, 'manifest.json'))) || force;
220
+ let manifest = null;
221
+
222
+ if (dryRun) {
223
+ ui.info(`would write manifest.json in ${llmDir}`);
224
+ } else {
225
+
226
+ // I don't need the hashes
227
+ // const [schemaHash, annexHash, modelHash, docHash] = await Promise.all([
228
+ // sha256(schemaDst), sha256(annexDst), sha256(modelFile), sha256(docFile)
229
+ // ]);
230
+
231
+ // Paths in manifest should be relative to /llm to keep it portable
232
+ const llmPosix = llmDir; // absolute
233
+ manifest = {
234
+ version: schemaVersion,
235
+ model: {
236
+ path: rel(llmPosix, modelFile),
237
+ schema: 'vmblu.schema.json',
238
+ annex: 'vmblu.annex.md',
239
+ },
240
+ profile: {
241
+ path: rel(llmPosix, docFile),
242
+ schema: 'profile.schema.json',
243
+ },
244
+ };
245
+
246
+ if (willWriteManifest) {
247
+ const manifestPath = path.join(llmDir, 'manifest.json');
248
+ ui.info(`create ${manifestPath}${force ? ' (force)' : ''}`);
249
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
250
+ } else {
251
+ ui.warn(`manifest.json exists and --force not set. Skipped.`);
252
+ }
253
+ }
254
+
255
+ // 5) Make the package file
256
+ makePackageJson({ absTarget, projectName, force, dryRun, addCliDep: true, cliVersion: "^" + CLI_VERSION }, ui);
257
+
258
+ // 6) Final tree hint
259
+ ui.info(`\nScaffold complete${dryRun ? ' (dry run)' : ''}:\n` +
260
+ ` ${absTarget}/
261
+ ${path.basename(modelFile)}
262
+ ${path.basename(docFile)}
263
+ package.json
264
+ llm/
265
+ seed.md
266
+ manifest.json
267
+ vmblu.schema.json
268
+ vmblu.annex.md
269
+ profile.schema.json
270
+ session/
271
+ nodes/\n`);
272
+
273
+ return {
274
+ targetDir: absTarget,
275
+ projectName,
276
+ schemaVersion,
277
+ files: {
278
+ model: modelFile,
279
+ doc: docFile,
280
+ schema: schemaDst,
281
+ annex: annexDst,
282
+ profileSchema: profileSchemaDst,
283
+ manifest: path.join(llmDir, 'manifest.json')
284
+ },
285
+ dryRun,
286
+ manifest
287
+ };
288
+ }
289
+
290
+ export { initProject };