@vitld/meld-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1423 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command as Command7 } from "commander";
5
+
6
+ // src/cli/gen.ts
7
+ import { Command } from "commander";
8
+
9
+ // src/generate.ts
10
+ import { mkdirSync as mkdirSync2 } from "fs";
11
+ import { join as join5 } from "path";
12
+
13
+ // src/config/loader.ts
14
+ import { readFileSync, existsSync } from "fs";
15
+ import { join } from "path";
16
+ import { parse as parseJsonc } from "jsonc-parser";
17
+
18
+ // src/config/schema.ts
19
+ var VALID_AGENTS = ["claude-code", "codex-cli", "gemini-cli"];
20
+ function validateConfig(input) {
21
+ const errors = [];
22
+ if (typeof input !== "object" || input === null) {
23
+ return { ok: false, errors: ["Config must be an object"] };
24
+ }
25
+ const obj = input;
26
+ const requiredKeys = ["projects", "agents", "mcp", "ide"];
27
+ for (const key of requiredKeys) {
28
+ if (!(key in obj)) {
29
+ errors.push(`Missing required key: ${key}`);
30
+ }
31
+ }
32
+ if (errors.length > 0) {
33
+ return { ok: false, errors };
34
+ }
35
+ const agents = obj.agents;
36
+ for (const name of Object.keys(agents)) {
37
+ if (!VALID_AGENTS.includes(name)) {
38
+ errors.push(`Invalid agent name: ${name}. Must be one of: ${VALID_AGENTS.join(", ")}`);
39
+ }
40
+ const agent = agents[name];
41
+ if ("overrides" in agent && agent.overrides != null && (typeof agent.overrides !== "object" || Array.isArray(agent.overrides))) {
42
+ errors.push(`Agent "${name}" overrides must be an object`);
43
+ }
44
+ }
45
+ const mcp = obj.mcp;
46
+ for (const [serverName, server] of Object.entries(mcp)) {
47
+ const s = server;
48
+ if (s.type === "http") {
49
+ if (typeof s.url !== "string" || !s.url) {
50
+ errors.push(`MCP server "${serverName}" (http) must have a "url" string`);
51
+ }
52
+ } else {
53
+ if (typeof s.command !== "string" || !s.command) {
54
+ errors.push(`MCP server "${serverName}" (stdio) must have a "command" string`);
55
+ }
56
+ if (!Array.isArray(s.args)) {
57
+ errors.push(`MCP server "${serverName}" (stdio) must have an "args" array`);
58
+ }
59
+ }
60
+ if (s.agents && Array.isArray(s.agents)) {
61
+ for (const agent of s.agents) {
62
+ if (!VALID_AGENTS.includes(agent)) {
63
+ errors.push(`MCP server "${serverName}" has invalid agent scope: ${agent}`);
64
+ }
65
+ }
66
+ }
67
+ }
68
+ if ("context" in obj && obj.context != null) {
69
+ if (typeof obj.context !== "string") {
70
+ errors.push("context must be a string path");
71
+ }
72
+ }
73
+ if (errors.length > 0) {
74
+ return { ok: false, errors };
75
+ }
76
+ return { ok: true, config: input };
77
+ }
78
+
79
+ // src/config/loader.ts
80
+ function loadConfig(hubDir) {
81
+ const configPath = join(hubDir, "meld.jsonc");
82
+ if (!existsSync(configPath)) {
83
+ return { ok: false, errors: [`Config file not found: ${configPath}`] };
84
+ }
85
+ let raw;
86
+ try {
87
+ raw = readFileSync(configPath, "utf-8");
88
+ } catch (err) {
89
+ return {
90
+ ok: false,
91
+ errors: [`Failed to read config: ${err.message}`]
92
+ };
93
+ }
94
+ let parsed;
95
+ const parseErrors = [];
96
+ parsed = parseJsonc(raw, parseErrors);
97
+ if (parseErrors.length > 0) {
98
+ return {
99
+ ok: false,
100
+ errors: [`Invalid JSONC in meld.jsonc: ${parseErrors.length} parse error(s)`]
101
+ };
102
+ }
103
+ return validateConfig(parsed);
104
+ }
105
+
106
+ // src/config/interpolate.ts
107
+ var ENV_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)}/g;
108
+ function interpolateEnv(config) {
109
+ const warnings = [];
110
+ const resolved = deepInterpolate(config, warnings);
111
+ return { config: resolved, warnings };
112
+ }
113
+ function deepInterpolate(value, warnings) {
114
+ if (typeof value === "string") {
115
+ return value.replace(ENV_PATTERN, (match, varName) => {
116
+ const envValue = process.env[varName];
117
+ if (envValue === void 0) {
118
+ warnings.push(`Environment variable not set: ${varName}`);
119
+ return match;
120
+ }
121
+ return envValue;
122
+ });
123
+ }
124
+ if (Array.isArray(value)) {
125
+ return value.map((item) => deepInterpolate(item, warnings));
126
+ }
127
+ if (typeof value === "object" && value !== null) {
128
+ const result = {};
129
+ for (const [key, val] of Object.entries(value)) {
130
+ result[key] = deepInterpolate(val, warnings);
131
+ }
132
+ return result;
133
+ }
134
+ return value;
135
+ }
136
+
137
+ // src/context/composer.ts
138
+ import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
139
+ import { join as join2, basename, extname, relative } from "path";
140
+
141
+ // src/config/types.ts
142
+ var AGENTS_DIR = "agents";
143
+ var DEFAULT_AGENT_DIRS = {
144
+ "claude-code": "claude-code",
145
+ "codex-cli": "codex",
146
+ "gemini-cli": "gemini"
147
+ };
148
+ function resolveAgentDir(name, agentConfig) {
149
+ return agentConfig.dir ?? DEFAULT_AGENT_DIRS[name];
150
+ }
151
+ function isHttpMcp(server) {
152
+ return server.type === "http";
153
+ }
154
+ var DEFAULT_CONTEXT_PATH = "./context/";
155
+ function resolveContextPath(contextPath) {
156
+ return contextPath ?? DEFAULT_CONTEXT_PATH;
157
+ }
158
+
159
+ // src/context/composer.ts
160
+ function composeContext(hubDir, config) {
161
+ const { inline, contextFiles } = readContext(hubDir, config);
162
+ return {
163
+ hubDir,
164
+ hubPreamble: buildHubPreamble(config),
165
+ projectTable: buildProjectTable(config),
166
+ artifactsSection: buildArtifactsSection(),
167
+ context: inline,
168
+ contextFiles,
169
+ commands: readCommands(hubDir),
170
+ skills: readSkills(hubDir)
171
+ };
172
+ }
173
+ function buildHubPreamble(config) {
174
+ return [
175
+ `# ${config.ide.workspaceName}`,
176
+ "",
177
+ "This is a **meld hub** \u2014 a multi-project workspace with shared configuration generated by [meld](https://github.com/vitld/meld-cli).",
178
+ "",
179
+ "You are running from a subfolder of the hub (`agents/<name>/`). The hub root is at `../../`.",
180
+ "",
181
+ "## Hub Structure",
182
+ "",
183
+ "| Path | Purpose |",
184
+ "|------|---------|",
185
+ "| `meld.jsonc` | Central config \u2014 projects, agents, MCP servers |",
186
+ "| `context/` | Agent instructions \u2014 root `.md` files are inlined, subfolders are copied |",
187
+ "| `commands/` | Slash commands available to agents |",
188
+ "| `skills/` | Reusable skills with frontmatter metadata |",
189
+ "| `artifacts/` | Persistent research, plans, and notes |",
190
+ "| `scratch/` | Temporary work (gitignored) |",
191
+ "| `agents/` | Generated output \u2014 **do not edit**, regenerated by `meld gen` |",
192
+ "",
193
+ "> To change workspace settings, edit `meld.jsonc` at the hub root and run `meld gen` to regenerate."
194
+ ].join("\n");
195
+ }
196
+ function buildProjectTable(config) {
197
+ const projects = buildProjectIndex(config);
198
+ if (projects.length === 0) return "";
199
+ const lines = [
200
+ "## Projects",
201
+ "",
202
+ "| Project | Aliases | Path | Repo |",
203
+ "|---------|---------|------|------|"
204
+ ];
205
+ for (const project of projects) {
206
+ lines.push(
207
+ `| ${project.name} | ${project.aliases.join(", ")} | ${project.path} | ${project.repo ?? ""} |`
208
+ );
209
+ }
210
+ return lines.join("\n");
211
+ }
212
+ function buildArtifactsSection() {
213
+ return [
214
+ "## Artifacts & Scratch",
215
+ "",
216
+ "- Hub-scoped artifacts: `../../artifacts/hub/`",
217
+ "- Per-project artifacts: `../../artifacts/projects/{project-name}/`",
218
+ "- Scratch (temporary work, gitignored): `../../scratch/`",
219
+ "",
220
+ "> Save research, plans, and notes to the appropriate artifacts folder."
221
+ ].join("\n");
222
+ }
223
+ function buildProjectIndex(config) {
224
+ return Object.entries(config.projects).map(([name, project]) => ({
225
+ name,
226
+ aliases: project.aliases,
227
+ path: project.path,
228
+ ...project.repo && { repo: project.repo }
229
+ }));
230
+ }
231
+ function readContext(hubDir, config) {
232
+ const contextPath = resolveContextPath(config.context);
233
+ const dir = join2(hubDir, contextPath);
234
+ if (!existsSync2(dir)) return { inline: "", contextFiles: [] };
235
+ const entries = readdirSync(dir, { withFileTypes: true });
236
+ const mdFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name).sort();
237
+ const inline = mdFiles.length > 0 ? mdFiles.map((f) => readFileSync2(join2(dir, f), "utf-8").trim()).join("\n\n") : "";
238
+ const contextFiles = [];
239
+ const subdirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
240
+ for (const subdir of subdirs) {
241
+ collectFiles(join2(dir, subdir), dir, contextFiles);
242
+ }
243
+ return { inline, contextFiles };
244
+ }
245
+ function collectFiles(dirPath, relativeTo, out) {
246
+ const entries = readdirSync(dirPath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
247
+ for (const entry of entries) {
248
+ const fullPath = join2(dirPath, entry.name);
249
+ if (entry.isDirectory()) {
250
+ collectFiles(fullPath, relativeTo, out);
251
+ } else if (entry.isFile()) {
252
+ out.push({
253
+ path: relative(relativeTo, fullPath),
254
+ content: readFileSync2(fullPath, "utf-8")
255
+ });
256
+ }
257
+ }
258
+ }
259
+ function readCommands(hubDir) {
260
+ const commandsDir = join2(hubDir, "commands");
261
+ if (!existsSync2(commandsDir)) return [];
262
+ return readdirSync(commandsDir).filter((f) => f.endsWith(".md")).sort().map((f) => ({
263
+ name: basename(f, extname(f)),
264
+ content: readFileSync2(join2(commandsDir, f), "utf-8")
265
+ }));
266
+ }
267
+ function readSkills(hubDir) {
268
+ const skillsDir = join2(hubDir, "skills");
269
+ if (!existsSync2(skillsDir)) return [];
270
+ return readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).sort((a, b) => a.name.localeCompare(b.name)).map((d) => {
271
+ const skillFile = join2(skillsDir, d.name, "SKILL.md");
272
+ if (!existsSync2(skillFile)) return null;
273
+ const raw = readFileSync2(skillFile, "utf-8");
274
+ const { frontmatter, body } = parseFrontmatter(raw);
275
+ return { name: d.name, frontmatter, body };
276
+ }).filter((s) => s !== null);
277
+ }
278
+ function parseFrontmatter(content) {
279
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
280
+ if (!match) {
281
+ return { frontmatter: {}, body: content };
282
+ }
283
+ const yamlStr = match[1];
284
+ const body = match[2].trim();
285
+ const frontmatter = parseSimpleYaml(yamlStr);
286
+ return { frontmatter, body };
287
+ }
288
+ function parseSimpleYaml(yaml) {
289
+ const result = {};
290
+ const lines = yaml.split("\n");
291
+ let currentKey = "";
292
+ let currentMap = null;
293
+ for (const line of lines) {
294
+ if (line.trim() === "") continue;
295
+ const indentedMatch = line.match(/^ (\w[\w-]*): (.+)$/);
296
+ if (indentedMatch && currentKey) {
297
+ if (!currentMap) currentMap = {};
298
+ currentMap[indentedMatch[1]] = indentedMatch[2].trim();
299
+ continue;
300
+ }
301
+ if (currentMap && currentKey) {
302
+ result[currentKey] = currentMap;
303
+ currentMap = null;
304
+ }
305
+ const topMatch = line.match(/^(\w[\w-]*): (.+)$/);
306
+ if (topMatch) {
307
+ currentKey = topMatch[1];
308
+ const value = topMatch[2].trim();
309
+ if (value.startsWith("[") && value.endsWith("]")) {
310
+ result[currentKey] = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, ""));
311
+ } else if (value === "true") {
312
+ result[currentKey] = true;
313
+ } else if (value === "false") {
314
+ result[currentKey] = false;
315
+ } else {
316
+ result[currentKey] = value.replace(/^["']|["']$/g, "");
317
+ }
318
+ continue;
319
+ }
320
+ const mapKeyMatch = line.match(/^(\w[\w-]*):$/);
321
+ if (mapKeyMatch) {
322
+ currentKey = mapKeyMatch[1];
323
+ currentMap = {};
324
+ continue;
325
+ }
326
+ }
327
+ if (currentMap && currentKey) {
328
+ result[currentKey] = currentMap;
329
+ }
330
+ return result;
331
+ }
332
+
333
+ // src/generators/writer.ts
334
+ import { writeFileSync, mkdirSync } from "fs";
335
+ import { join as join3, dirname } from "path";
336
+ function writeGeneratedFiles(hubDir, files, options = {}) {
337
+ if (!options.dryRun) {
338
+ for (const file of files) {
339
+ const fullPath = join3(hubDir, file.path);
340
+ mkdirSync(dirname(fullPath), { recursive: true });
341
+ writeFileSync(fullPath, file.content);
342
+ }
343
+ }
344
+ return files;
345
+ }
346
+
347
+ // src/generators/utils.ts
348
+ function isPlainObject(value) {
349
+ return typeof value === "object" && value !== null && !Array.isArray(value);
350
+ }
351
+ function deepMerge(base, overrides) {
352
+ const merged = { ...base };
353
+ for (const [key, value] of Object.entries(overrides)) {
354
+ const current = merged[key];
355
+ if (isPlainObject(current) && isPlainObject(value)) {
356
+ merged[key] = deepMerge(current, value);
357
+ continue;
358
+ }
359
+ merged[key] = value;
360
+ }
361
+ return merged;
362
+ }
363
+ function serializeToml(config) {
364
+ const lines = [];
365
+ appendTable([], config, lines);
366
+ return `${lines.join("\n")}
367
+ `;
368
+ }
369
+ function appendTable(path, table, lines) {
370
+ const entries = Object.entries(table).filter(([, value]) => value !== void 0 && value !== null);
371
+ const scalarEntries = entries.filter(([, value]) => !isPlainObject(value));
372
+ const tableEntries = entries.filter(([, value]) => isPlainObject(value));
373
+ if (path.length > 0 && scalarEntries.length === 0 && tableEntries.length === 0) {
374
+ return;
375
+ }
376
+ if (path.length > 0) {
377
+ if (lines.length > 0) lines.push("");
378
+ lines.push(`[${path.map(escapeTomlKey).join(".")}]`);
379
+ }
380
+ for (const [key, value] of scalarEntries) {
381
+ lines.push(`${escapeTomlKey(key)} = ${formatTomlValue(value)}`);
382
+ }
383
+ for (const [key, value] of tableEntries) {
384
+ appendTable([...path, key], value, lines);
385
+ }
386
+ }
387
+ function formatTomlValue(value) {
388
+ if (typeof value === "string") return `"${escapeTomlString(value)}"`;
389
+ if (typeof value === "number") {
390
+ if (!Number.isFinite(value)) {
391
+ throw new Error(`Cannot serialize non-finite number to TOML: ${value}`);
392
+ }
393
+ return String(value);
394
+ }
395
+ if (typeof value === "boolean") return value ? "true" : "false";
396
+ if (Array.isArray(value)) {
397
+ return `[${value.map((item) => formatTomlArrayValue(item)).join(", ")}]`;
398
+ }
399
+ throw new Error(`Unsupported TOML value type: ${typeof value}`);
400
+ }
401
+ function formatTomlArrayValue(value) {
402
+ if (typeof value === "string") return `"${escapeTomlString(value)}"`;
403
+ if (typeof value === "number") {
404
+ if (!Number.isFinite(value)) {
405
+ throw new Error(`Cannot serialize non-finite number to TOML: ${value}`);
406
+ }
407
+ return String(value);
408
+ }
409
+ if (typeof value === "boolean") return value ? "true" : "false";
410
+ throw new Error(`Unsupported TOML array value type: ${typeof value}`);
411
+ }
412
+ function escapeTomlKey(key) {
413
+ return /^[A-Za-z0-9_-]+$/.test(key) ? key : `"${escapeTomlString(key)}"`;
414
+ }
415
+ function escapeTomlString(value) {
416
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
417
+ }
418
+
419
+ // src/generators/claude-code.ts
420
+ var ClaudeCodeGenerator = class {
421
+ name = "claude-code";
422
+ generate(config, context) {
423
+ const files = [];
424
+ files.push({
425
+ path: "CLAUDE.md",
426
+ content: this.buildInstructions(context)
427
+ });
428
+ files.push({
429
+ path: ".mcp.json",
430
+ content: this.buildMcpConfig(config)
431
+ });
432
+ files.push({
433
+ path: ".claude/settings.json",
434
+ content: this.buildSettings(config, context.hubDir)
435
+ });
436
+ for (const command of context.commands) {
437
+ files.push({
438
+ path: `.claude/commands/meld/${command.name}.md`,
439
+ content: command.content
440
+ });
441
+ }
442
+ for (const skill of context.skills) {
443
+ files.push({
444
+ path: `.claude/skills/meld-${skill.name}/SKILL.md`,
445
+ content: this.buildSkill(skill)
446
+ });
447
+ }
448
+ for (const file of context.contextFiles) {
449
+ files.push({ path: file.path, content: file.content });
450
+ }
451
+ return files;
452
+ }
453
+ buildInstructions(context) {
454
+ const sections = [context.hubPreamble];
455
+ if (context.projectTable) {
456
+ sections.push(context.projectTable);
457
+ }
458
+ sections.push(context.artifactsSection);
459
+ if (context.context) {
460
+ sections.push(context.context);
461
+ }
462
+ return sections.join("\n\n");
463
+ }
464
+ buildMcpConfig(config) {
465
+ const servers = {};
466
+ for (const [name, server] of Object.entries(config.mcp)) {
467
+ if (server.agents && !server.agents.includes("claude-code")) continue;
468
+ if (isHttpMcp(server)) {
469
+ const entry = { type: "http", url: server.url };
470
+ if (server.headers) entry.headers = server.headers;
471
+ if (server.env) entry.env = server.env;
472
+ servers[name] = entry;
473
+ } else {
474
+ const entry = { command: server.command, args: server.args };
475
+ if (server.env) entry.env = server.env;
476
+ servers[name] = entry;
477
+ }
478
+ }
479
+ return JSON.stringify({ mcpServers: servers }, null, 2);
480
+ }
481
+ buildSettings(config, hubDir) {
482
+ const settings = {};
483
+ settings.env = { ENABLE_TOOL_SEARCH: "true" };
484
+ const allow = [];
485
+ const safeTools = ["Task", "WebSearch", "WebFetch", "ToolSearch"];
486
+ allow.push(...safeTools);
487
+ const safeBashCommands = [
488
+ "cd",
489
+ "ls",
490
+ "mkdir",
491
+ "cp",
492
+ "mv",
493
+ "cat",
494
+ "git",
495
+ "gh",
496
+ "node",
497
+ "npx",
498
+ "npm",
499
+ "yarn",
500
+ "pnpm",
501
+ "bun",
502
+ "which",
503
+ "pwd",
504
+ "ast-grep"
505
+ ];
506
+ for (const cmd of safeBashCommands) {
507
+ allow.push(`Bash(command:${cmd} *)`);
508
+ }
509
+ allow.push(`Read(//${hubDir}/**)`);
510
+ allow.push(`Glob(//${hubDir}/**)`);
511
+ allow.push(`Grep(//${hubDir}/**)`);
512
+ allow.push(`Write(//${hubDir}/**)`);
513
+ allow.push(`Edit(//${hubDir}/**)`);
514
+ const additionalDirectories = [];
515
+ for (const [, project] of Object.entries(config.projects)) {
516
+ allow.push(`Read(//${project.path}/**)`);
517
+ allow.push(`Glob(//${project.path}/**)`);
518
+ allow.push(`Grep(//${project.path}/**)`);
519
+ allow.push(`Write(//${project.path}/**)`);
520
+ allow.push(`Edit(//${project.path}/**)`);
521
+ additionalDirectories.push(project.path);
522
+ }
523
+ settings.permissions = { allow, additionalDirectories };
524
+ const overrides = config.agents["claude-code"].overrides;
525
+ const mergedSettings = isPlainObject(overrides) ? deepMerge(settings, overrides) : settings;
526
+ return JSON.stringify(mergedSettings, null, 2);
527
+ }
528
+ buildSkill(skill) {
529
+ const fm = { ...skill.frontmatter };
530
+ if (fm.model && typeof fm.model === "object" && !Array.isArray(fm.model)) {
531
+ const modelMap = fm.model;
532
+ if (modelMap["claude-code"]) {
533
+ fm.model = modelMap["claude-code"];
534
+ } else {
535
+ delete fm.model;
536
+ }
537
+ }
538
+ const frontmatterLines = this.serializeFrontmatter(fm);
539
+ return `---
540
+ ${frontmatterLines}
541
+ ---
542
+
543
+ ${skill.body}`;
544
+ }
545
+ serializeFrontmatter(fm) {
546
+ const lines = [];
547
+ for (const [key, value] of Object.entries(fm)) {
548
+ if (typeof value === "string") {
549
+ lines.push(`${key}: ${value}`);
550
+ } else if (typeof value === "boolean") {
551
+ lines.push(`${key}: ${value}`);
552
+ } else if (Array.isArray(value)) {
553
+ lines.push(`${key}: [${value.join(", ")}]`);
554
+ }
555
+ }
556
+ return lines.join("\n");
557
+ }
558
+ };
559
+
560
+ // src/generators/codex-cli.ts
561
+ var CodexCliGenerator = class {
562
+ name = "codex-cli";
563
+ generate(config, context) {
564
+ const files = [];
565
+ files.push({
566
+ path: "AGENTS.md",
567
+ content: this.buildInstructions(context)
568
+ });
569
+ files.push({
570
+ path: ".codex/config.toml",
571
+ content: this.buildConfigToml(config, context)
572
+ });
573
+ for (const command of context.commands) {
574
+ files.push({
575
+ path: `.agents/skills/meld-cmd-${command.name}/SKILL.md`,
576
+ content: command.content
577
+ });
578
+ }
579
+ for (const skill of context.skills) {
580
+ files.push({
581
+ path: `.agents/skills/meld-${skill.name}/SKILL.md`,
582
+ content: this.buildSkill(skill)
583
+ });
584
+ }
585
+ for (const file of context.contextFiles) {
586
+ files.push({ path: file.path, content: file.content });
587
+ }
588
+ return files;
589
+ }
590
+ buildInstructions(context) {
591
+ const sections = [context.hubPreamble];
592
+ if (context.projectTable) {
593
+ sections.push(context.projectTable);
594
+ }
595
+ sections.push(context.artifactsSection);
596
+ if (context.context) {
597
+ sections.push(context.context);
598
+ }
599
+ return sections.join("\n\n");
600
+ }
601
+ buildConfigToml(config, context) {
602
+ const writableRoots = Array.from(/* @__PURE__ */ new Set([
603
+ context.hubDir,
604
+ ...Object.values(config.projects).map((project) => project.path)
605
+ ]));
606
+ const generatedConfig = {
607
+ approval_policy: "on-request",
608
+ sandbox_mode: "workspace-write",
609
+ sandbox_workspace_write: {
610
+ writable_roots: writableRoots
611
+ }
612
+ };
613
+ const mcpServers = this.buildMcpServers(config);
614
+ if (Object.keys(mcpServers).length > 0) {
615
+ generatedConfig.mcp_servers = mcpServers;
616
+ }
617
+ const overrides = config.agents["codex-cli"].overrides;
618
+ const mergedConfig = isPlainObject(overrides) ? deepMerge(generatedConfig, overrides) : generatedConfig;
619
+ return serializeToml(mergedConfig);
620
+ }
621
+ buildMcpServers(config) {
622
+ const servers = {};
623
+ for (const [name, server] of Object.entries(config.mcp)) {
624
+ if (server.agents && !server.agents.includes("codex-cli")) continue;
625
+ if (isHttpMcp(server)) {
626
+ const entry = { url: server.url };
627
+ if (server.headers && Object.keys(server.headers).length > 0) {
628
+ entry.http_headers = server.headers;
629
+ }
630
+ if (server.env && Object.keys(server.env).length > 0) {
631
+ entry.env = server.env;
632
+ }
633
+ servers[name] = entry;
634
+ } else {
635
+ const entry = { command: server.command, args: server.args };
636
+ if (server.env && Object.keys(server.env).length > 0) {
637
+ entry.env = server.env;
638
+ }
639
+ servers[name] = entry;
640
+ }
641
+ }
642
+ return servers;
643
+ }
644
+ buildSkill(skill) {
645
+ const fm = { ...skill.frontmatter };
646
+ if (fm.model && typeof fm.model === "object" && !Array.isArray(fm.model)) {
647
+ const modelMap = fm.model;
648
+ if (modelMap["codex-cli"]) {
649
+ fm.model = modelMap["codex-cli"];
650
+ } else {
651
+ delete fm.model;
652
+ }
653
+ }
654
+ const frontmatterLines = this.serializeFrontmatter(fm);
655
+ return `---
656
+ ${frontmatterLines}
657
+ ---
658
+
659
+ ${skill.body}`;
660
+ }
661
+ serializeFrontmatter(fm) {
662
+ const lines = [];
663
+ for (const [key, value] of Object.entries(fm)) {
664
+ if (typeof value === "string") {
665
+ lines.push(`${key}: ${value}`);
666
+ } else if (typeof value === "boolean") {
667
+ lines.push(`${key}: ${value}`);
668
+ } else if (Array.isArray(value)) {
669
+ lines.push(`${key}: [${value.join(", ")}]`);
670
+ }
671
+ }
672
+ return lines.join("\n");
673
+ }
674
+ };
675
+
676
+ // src/generators/gemini-cli.ts
677
+ var GeminiCliGenerator = class {
678
+ name = "gemini-cli";
679
+ generate(config, context) {
680
+ const files = [];
681
+ files.push({
682
+ path: "GEMINI.md",
683
+ content: this.buildInstructions(context)
684
+ });
685
+ files.push({
686
+ path: ".gemini/settings.json",
687
+ content: this.buildSettings(config)
688
+ });
689
+ for (const command of context.commands) {
690
+ files.push({
691
+ path: `.gemini/commands/meld/${command.name}.toml`,
692
+ content: this.buildCommandToml(command.name, command.content)
693
+ });
694
+ }
695
+ for (const skill of context.skills) {
696
+ files.push({
697
+ path: `.gemini/commands/meld/${skill.name}.toml`,
698
+ content: this.buildSkillToml(skill)
699
+ });
700
+ }
701
+ for (const file of context.contextFiles) {
702
+ files.push({ path: file.path, content: file.content });
703
+ }
704
+ return files;
705
+ }
706
+ buildInstructions(context) {
707
+ const sections = [context.hubPreamble];
708
+ if (context.projectTable) {
709
+ sections.push(context.projectTable);
710
+ }
711
+ sections.push(context.artifactsSection);
712
+ if (context.context) {
713
+ sections.push(context.context);
714
+ }
715
+ return sections.join("\n\n");
716
+ }
717
+ buildSettings(config) {
718
+ const servers = {};
719
+ for (const [name, server] of Object.entries(config.mcp)) {
720
+ if (server.agents && !server.agents.includes("gemini-cli")) continue;
721
+ if (isHttpMcp(server)) {
722
+ const entry = { type: "http", url: server.url };
723
+ if (server.headers) entry.headers = server.headers;
724
+ if (server.env) entry.env = server.env;
725
+ servers[name] = entry;
726
+ } else {
727
+ const entry = { command: server.command, args: server.args };
728
+ if (server.env) entry.env = server.env;
729
+ servers[name] = entry;
730
+ }
731
+ }
732
+ const settings = { mcpServers: servers };
733
+ const overrides = config.agents["gemini-cli"].overrides;
734
+ const mergedSettings = isPlainObject(overrides) ? deepMerge(settings, overrides) : settings;
735
+ return JSON.stringify(mergedSettings, null, 2);
736
+ }
737
+ buildCommandToml(name, content) {
738
+ return [
739
+ `description = "${name}"`,
740
+ "",
741
+ `[template]`,
742
+ `prompt = """`,
743
+ content,
744
+ `"""`
745
+ ].join("\n");
746
+ }
747
+ buildSkillToml(skill) {
748
+ const description = skill.frontmatter.description ?? skill.name;
749
+ return [
750
+ `# skill: ${skill.name}`,
751
+ `description = "${description}"`,
752
+ "",
753
+ `[template]`,
754
+ `prompt = """`,
755
+ skill.body,
756
+ `"""`
757
+ ].join("\n");
758
+ }
759
+ };
760
+
761
+ // src/generators/workspace.ts
762
+ import { homedir } from "os";
763
+ var WorkspaceGenerator = class {
764
+ name = "workspace";
765
+ generate(config, _context) {
766
+ const folders = [
767
+ { name: config.ide.workspaceName, path: "." },
768
+ ...Object.entries(config.projects).map(([name, project]) => ({
769
+ name,
770
+ path: resolveTilde(project.path)
771
+ }))
772
+ ];
773
+ const workspace = {
774
+ folders,
775
+ settings: {}
776
+ };
777
+ return [
778
+ {
779
+ path: `${config.ide.workspaceName}.code-workspace`,
780
+ content: JSON.stringify(workspace, null, 2)
781
+ }
782
+ ];
783
+ }
784
+ };
785
+ function resolveTilde(path) {
786
+ if (path.startsWith("~/")) {
787
+ return homedir() + path.slice(1);
788
+ }
789
+ return path;
790
+ }
791
+
792
+ // src/generators/gitignore.ts
793
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
794
+ import { join as join4 } from "path";
795
+ var START_MARKER = "# \u2500\u2500 meld managed (do not edit) \u2500\u2500";
796
+ var END_MARKER = "# \u2500\u2500 end meld managed \u2500\u2500";
797
+ var GitignoreGenerator = class {
798
+ name = "gitignore";
799
+ generate(_config, context) {
800
+ const lines = [
801
+ "agents/",
802
+ "scratch/"
803
+ ];
804
+ const managedBlock = [
805
+ START_MARKER,
806
+ ...lines,
807
+ END_MARKER
808
+ ].join("\n");
809
+ const content = spliceIntoExisting(context.hubDir, managedBlock);
810
+ return [{ path: ".gitignore", content }];
811
+ }
812
+ };
813
+ function spliceIntoExisting(hubDir, managedBlock) {
814
+ const gitignorePath = join4(hubDir, ".gitignore");
815
+ if (!existsSync3(gitignorePath)) {
816
+ return managedBlock + "\n";
817
+ }
818
+ const existing = readFileSync3(gitignorePath, "utf-8");
819
+ const startIdx = existing.indexOf(START_MARKER);
820
+ const endIdx = existing.indexOf(END_MARKER);
821
+ if (startIdx === -1 || endIdx === -1) {
822
+ const trimmed = existing.trimEnd();
823
+ return trimmed + (trimmed.length > 0 ? "\n\n" : "") + managedBlock + "\n";
824
+ }
825
+ const before = existing.slice(0, startIdx);
826
+ const after = existing.slice(endIdx + END_MARKER.length);
827
+ return before + managedBlock + after;
828
+ }
829
+
830
+ // src/generate.ts
831
+ var AGENT_GENERATORS = {
832
+ "claude-code": () => new ClaudeCodeGenerator(),
833
+ "codex-cli": () => new CodexCliGenerator(),
834
+ "gemini-cli": () => new GeminiCliGenerator()
835
+ };
836
+ function generate(hubDir, options = {}) {
837
+ const loadResult = loadConfig(hubDir);
838
+ if (!loadResult.ok) {
839
+ return { ok: false, errors: loadResult.errors };
840
+ }
841
+ const { config, warnings } = interpolateEnv(loadResult.config);
842
+ const context = composeContext(hubDir, config);
843
+ const allFiles = [];
844
+ for (const [name, agentConfig] of Object.entries(config.agents)) {
845
+ if (!agentConfig.enabled) continue;
846
+ if (options.agent && options.agent !== name) continue;
847
+ const agentDir = resolveAgentDir(name, agentConfig);
848
+ const factory = AGENT_GENERATORS[name];
849
+ if (!factory) continue;
850
+ const files = factory().generate(config, context);
851
+ for (const file of files) {
852
+ file.path = `${AGENTS_DIR}/${agentDir}/${file.path}`;
853
+ }
854
+ allFiles.push(...files);
855
+ }
856
+ if (!options.agent) {
857
+ allFiles.push(...new WorkspaceGenerator().generate(config, context));
858
+ allFiles.push(...new GitignoreGenerator().generate(config, context));
859
+ }
860
+ if (!options.dryRun) {
861
+ for (const name of Object.keys(config.projects)) {
862
+ mkdirSync2(join5(hubDir, "artifacts", "projects", name), { recursive: true });
863
+ }
864
+ }
865
+ writeGeneratedFiles(hubDir, allFiles, { dryRun: options.dryRun });
866
+ return { ok: true, files: allFiles, warnings };
867
+ }
868
+
869
+ // src/cli/gen.ts
870
+ var genCommand = new Command("gen").description("Generate all agent configs from meld.jsonc").option("--dry-run", "Preview changes without writing files").option("--agent <name>", "Generate for a single agent (claude-code, codex-cli, gemini-cli)").action((options) => {
871
+ const hubDir = process.cwd();
872
+ const result = generate(hubDir, {
873
+ dryRun: options.dryRun,
874
+ agent: options.agent
875
+ });
876
+ if (!result.ok) {
877
+ console.error("Generation failed:");
878
+ for (const error of result.errors) {
879
+ console.error(` - ${error}`);
880
+ }
881
+ process.exit(1);
882
+ }
883
+ if (result.warnings.length > 0) {
884
+ for (const w of result.warnings) {
885
+ console.warn(` warning: ${w}`);
886
+ }
887
+ console.warn();
888
+ }
889
+ if (options.dryRun) {
890
+ console.log("Dry run \u2014 would generate:");
891
+ } else {
892
+ console.log("Generated:");
893
+ }
894
+ for (const file of result.files) {
895
+ console.log(` ${file.path}`);
896
+ }
897
+ console.log(`
898
+ ${result.files.length} file(s) ${options.dryRun ? "would be" : ""} generated.`);
899
+ });
900
+
901
+ // src/cli/init.ts
902
+ import { Command as Command2 } from "commander";
903
+ import * as p from "@clack/prompts";
904
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
905
+ import { execSync } from "child_process";
906
+ import { join as join7 } from "path";
907
+
908
+ // src/hub-schema.ts
909
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
910
+ import { dirname as dirname2, join as join6 } from "path";
911
+ import { fileURLToPath } from "url";
912
+ function readPackageSchema() {
913
+ let dir = dirname2(fileURLToPath(import.meta.url));
914
+ while (dir !== dirname2(dir)) {
915
+ const schemaPath = join6(dir, "meld.schema.json");
916
+ if (existsSync4(schemaPath)) {
917
+ return readFileSync4(schemaPath, "utf-8");
918
+ }
919
+ dir = dirname2(dir);
920
+ }
921
+ throw new Error("Could not locate meld.schema.json");
922
+ }
923
+
924
+ // src/readme.ts
925
+ function generateReadme(config) {
926
+ const agentLabels = {
927
+ "claude-code": "Claude Code",
928
+ "codex-cli": "Codex CLI",
929
+ "gemini-cli": "Gemini CLI"
930
+ };
931
+ const agentCommands = {
932
+ "claude-code": "`meld claude`",
933
+ "codex-cli": "`meld codex`",
934
+ "gemini-cli": "`meld gemini`"
935
+ };
936
+ const enabledAgents = Object.entries(config.agents).filter(([, a]) => a.enabled).map(([name]) => name);
937
+ const enabled = enabledAgents.map((a) => agentLabels[a] ?? a).join(", ");
938
+ const launchCmds = enabledAgents.map((a) => agentCommands[a] ?? `\`meld ${a}\``).join(" / ");
939
+ return [
940
+ `# ${config.ide.workspaceName}`,
941
+ "",
942
+ `A multi-project workspace powered by [meld](https://github.com/vitld/meld-cli).`,
943
+ "",
944
+ `**Enabled agents:** ${enabled}`,
945
+ "",
946
+ "## Getting Started",
947
+ "",
948
+ "1. Add projects: `meld project add`",
949
+ "2. Add agent instructions in `context/` (markdown files)",
950
+ "3. Generate configs: `meld gen`",
951
+ `4. Launch an agent: ${launchCmds}`,
952
+ "",
953
+ "## Structure",
954
+ "",
955
+ "| Path | Purpose |",
956
+ "|------|---------|",
957
+ "| `meld.jsonc` | Central configuration |",
958
+ "| `context/` | Markdown instructions for agents |",
959
+ "| `commands/` | Slash commands |",
960
+ "| `skills/` | Reusable agent skills |",
961
+ "| `artifacts/` | Research, plans, and notes |",
962
+ "| `scratch/` | Temporary work (gitignored) |",
963
+ "| `agents/` | Generated output (gitignored) |",
964
+ "",
965
+ "## Context",
966
+ "",
967
+ "Files in the root of `context/` are inlined into agent instructions.",
968
+ "Subfolders are copied into each agent's working directory, so you can",
969
+ "link to them with relative paths like `./subfolder/doc.md`.",
970
+ "",
971
+ "```",
972
+ "context/",
973
+ " 01-role.md # Inlined into CLAUDE.md / AGENTS.md / GEMINI.md",
974
+ " 02-guardrails.md # Inlined (alphabetical order)",
975
+ " reference/ # Copied as agents/<name>/reference/",
976
+ " api.md",
977
+ " patterns.md",
978
+ "```",
979
+ "",
980
+ "The filename determines order \u2014 use numeric prefixes to control sequencing.",
981
+ "After editing, run `meld gen` to regenerate agent configs.",
982
+ "",
983
+ "## MCP Servers",
984
+ "",
985
+ "MCP servers are defined once in `meld.jsonc` under the `mcp` key and automatically",
986
+ "translated into each agent's native config format (`.mcp.json`, `.codex/config.toml`,",
987
+ "`.gemini/settings.json`).",
988
+ "",
989
+ "### Stdio server (local process)",
990
+ "",
991
+ "Runs a command as a child process, communicating over stdin/stdout.",
992
+ "",
993
+ "```jsonc",
994
+ '"my-server": {',
995
+ ' "command": "npx",',
996
+ ' "args": ["-y", "my-mcp-server@latest"],',
997
+ ' "env": { // optional',
998
+ ' "API_KEY": "sk-..."',
999
+ " }",
1000
+ "}",
1001
+ "```",
1002
+ "",
1003
+ "### HTTP server (remote)",
1004
+ "",
1005
+ "Connects to a remote MCP endpoint over HTTP.",
1006
+ "",
1007
+ "```jsonc",
1008
+ '"my-server": {',
1009
+ ' "type": "http",',
1010
+ ' "url": "https://mcp.example.com/mcp",',
1011
+ ' "headers": { // optional',
1012
+ ' "Authorization": "Bearer tok-..."',
1013
+ " }",
1014
+ "}",
1015
+ "```",
1016
+ "",
1017
+ "### Scoping servers to specific agents",
1018
+ "",
1019
+ "By default every MCP server is available to all enabled agents.",
1020
+ "Use `agents` to restrict a server to specific agents:",
1021
+ "",
1022
+ "```jsonc",
1023
+ '"my-server": {',
1024
+ ' "command": "node",',
1025
+ ' "args": ["server.js"],',
1026
+ ' "agents": ["claude-code"] // only generated for Claude Code',
1027
+ "}",
1028
+ "```",
1029
+ "",
1030
+ "Valid agent names: `claude-code`, `codex-cli`, `gemini-cli`.",
1031
+ "",
1032
+ "## Commands",
1033
+ "",
1034
+ "| Command | Description |",
1035
+ "|---------|-------------|",
1036
+ "| `meld gen` | Generate agent configs from `meld.jsonc` |",
1037
+ "| `meld gen --dry-run` | Preview without writing files |",
1038
+ "| `meld project add` | Register a project |",
1039
+ "| `meld project list` | List registered projects |",
1040
+ "| `meld open` | Open workspace in IDE |",
1041
+ "| `meld update` | Re-scaffold hub structure |",
1042
+ "",
1043
+ "> Edit `meld.jsonc` for configuration. Run `meld gen` after changes.",
1044
+ ""
1045
+ ].join("\n");
1046
+ }
1047
+
1048
+ // src/cli/init.ts
1049
+ function ensureDir(path) {
1050
+ mkdirSync3(path, { recursive: true });
1051
+ }
1052
+ var initCommand = new Command2("init").description("Initialize a new meld hub").action(async () => {
1053
+ p.intro("meld \u2014 agent config generator");
1054
+ const hubDir = process.cwd();
1055
+ if (existsSync5(join7(hubDir, "meld.jsonc"))) {
1056
+ p.log.warn("meld.jsonc already exists in this directory.");
1057
+ const overwrite = await p.confirm({
1058
+ message: "Overwrite existing config?",
1059
+ initialValue: false
1060
+ });
1061
+ if (p.isCancel(overwrite) || !overwrite) {
1062
+ p.cancel("Cancelled.");
1063
+ process.exit(0);
1064
+ }
1065
+ }
1066
+ const ignored = /* @__PURE__ */ new Set([".git", "meld.jsonc"]);
1067
+ const existing = readdirSync2(hubDir).filter((f) => !ignored.has(f));
1068
+ if (existing.length > 0) {
1069
+ p.log.warn(
1070
+ `Directory contains ${existing.length} existing file(s): ${existing.slice(0, 5).join(", ")}${existing.length > 5 ? ", ..." : ""}`
1071
+ );
1072
+ const proceed = await p.confirm({
1073
+ message: "This directory contains existing files. Running init here will commit them all. Continue?",
1074
+ initialValue: false
1075
+ });
1076
+ if (p.isCancel(proceed) || !proceed) {
1077
+ p.cancel("Cancelled.");
1078
+ process.exit(0);
1079
+ }
1080
+ }
1081
+ const agents = await p.multiselect({
1082
+ message: "Which agents do you want to generate configs for?",
1083
+ options: [
1084
+ { value: "claude-code", label: "Claude Code" },
1085
+ { value: "codex-cli", label: "Codex CLI" },
1086
+ { value: "gemini-cli", label: "Gemini CLI" }
1087
+ ],
1088
+ required: true
1089
+ });
1090
+ if (p.isCancel(agents)) {
1091
+ p.cancel("Cancelled.");
1092
+ process.exit(0);
1093
+ }
1094
+ const ide = await p.select({
1095
+ message: "Default IDE?",
1096
+ options: [
1097
+ { value: "cursor", label: "Cursor" },
1098
+ { value: "code", label: "VS Code" },
1099
+ { value: "windsurf", label: "Windsurf" }
1100
+ ]
1101
+ });
1102
+ if (p.isCancel(ide)) {
1103
+ p.cancel("Cancelled.");
1104
+ process.exit(0);
1105
+ }
1106
+ const workspaceName = await p.text({
1107
+ message: "Workspace name",
1108
+ placeholder: "meld-hub",
1109
+ defaultValue: "meld-hub"
1110
+ });
1111
+ if (p.isCancel(workspaceName)) {
1112
+ p.cancel("Cancelled.");
1113
+ process.exit(0);
1114
+ }
1115
+ const config = {
1116
+ projects: {},
1117
+ agents: {
1118
+ "claude-code": { enabled: agents.includes("claude-code") },
1119
+ "codex-cli": { enabled: agents.includes("codex-cli") },
1120
+ "gemini-cli": { enabled: agents.includes("gemini-cli") }
1121
+ },
1122
+ mcp: {},
1123
+ ide: {
1124
+ default: ide,
1125
+ workspaceName
1126
+ }
1127
+ };
1128
+ const configWithSchema = { $schema: "./meld.schema.json", ...config };
1129
+ writeFileSync2(join7(hubDir, "meld.jsonc"), JSON.stringify(configWithSchema, null, 2));
1130
+ writeFileSync2(join7(hubDir, "meld.schema.json"), readPackageSchema());
1131
+ writeFileSync2(join7(hubDir, "README.md"), generateReadme(config));
1132
+ ensureDir(join7(hubDir, "context"));
1133
+ ensureDir(join7(hubDir, "commands"));
1134
+ ensureDir(join7(hubDir, "skills"));
1135
+ ensureDir(join7(hubDir, "artifacts", "hub"));
1136
+ ensureDir(join7(hubDir, "scratch"));
1137
+ writeFileSync2(join7(hubDir, ".gitignore"), [
1138
+ "# \u2500\u2500 meld managed (do not edit) \u2500\u2500",
1139
+ "agents/",
1140
+ "scratch/",
1141
+ "# \u2500\u2500 end meld managed \u2500\u2500"
1142
+ ].join("\n") + "\n");
1143
+ const hasGit = existsSync5(join7(hubDir, ".git"));
1144
+ if (!hasGit) {
1145
+ execSync("git init", { cwd: hubDir, stdio: "ignore" });
1146
+ }
1147
+ const meldFiles = [
1148
+ "meld.jsonc",
1149
+ "meld.schema.json",
1150
+ "README.md",
1151
+ ".gitignore"
1152
+ ];
1153
+ execSync(`git add ${meldFiles.join(" ")}`, { cwd: hubDir, stdio: "ignore" });
1154
+ execSync('git commit -m "init meld hub"', { cwd: hubDir, stdio: "ignore" });
1155
+ p.outro(
1156
+ [
1157
+ "Done! Created:",
1158
+ ` meld.jsonc \u2014 central config (with schema for IDE autocomplete)`,
1159
+ ` meld.schema.json \u2014 JSON schema for validation`,
1160
+ ` README.md \u2014 getting started guide`,
1161
+ ` context/ \u2014 agent instructions`,
1162
+ ` commands/ \u2014 slash commands`,
1163
+ ` skills/ \u2014 reusable skills`,
1164
+ ` artifacts/hub/ \u2014 research & notes`,
1165
+ ` scratch/ \u2014 temporary work`,
1166
+ "",
1167
+ "Next: add projects with `meld project add`, edit context/, then run `meld gen`"
1168
+ ].join("\n")
1169
+ );
1170
+ });
1171
+
1172
+ // src/cli/project.ts
1173
+ import { Command as Command3 } from "commander";
1174
+ import * as p2 from "@clack/prompts";
1175
+ import { mkdirSync as mkdirSync4 } from "fs";
1176
+ import { join as join9 } from "path";
1177
+
1178
+ // src/config/writer.ts
1179
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1180
+ import { join as join8 } from "path";
1181
+ import { parse as parseJsonc2 } from "jsonc-parser";
1182
+ function updateConfig(hubDir, updater) {
1183
+ const configPath = join8(hubDir, "meld.jsonc");
1184
+ const raw = readFileSync5(configPath, "utf-8");
1185
+ const config = parseJsonc2(raw);
1186
+ updater(config);
1187
+ writeFileSync3(configPath, JSON.stringify(config, null, 2));
1188
+ }
1189
+
1190
+ // src/cli/project.ts
1191
+ var projectCommand = new Command3("project").description("Manage projects");
1192
+ projectCommand.command("add").description("Add a project interactively").action(async () => {
1193
+ const hubDir = process.cwd();
1194
+ const name = await p2.text({
1195
+ message: "Project name (used as config key)",
1196
+ placeholder: "my-project"
1197
+ });
1198
+ if (p2.isCancel(name)) return;
1199
+ const path = await p2.text({
1200
+ message: "Path to project",
1201
+ placeholder: "~/projects/my-project"
1202
+ });
1203
+ if (p2.isCancel(path)) return;
1204
+ const aliasesRaw = await p2.text({
1205
+ message: "Aliases (comma-separated, how you refer to it)",
1206
+ placeholder: "my proj, the project",
1207
+ defaultValue: ""
1208
+ });
1209
+ if (p2.isCancel(aliasesRaw)) return;
1210
+ const aliases = aliasesRaw.split(",").map((a) => a.trim()).filter(Boolean);
1211
+ const repo = await p2.text({
1212
+ message: "GitHub repo (org/repo, optional)",
1213
+ placeholder: "org/repo",
1214
+ defaultValue: ""
1215
+ });
1216
+ if (p2.isCancel(repo)) return;
1217
+ updateConfig(hubDir, (config) => {
1218
+ const projects = config.projects ?? {};
1219
+ const entry = {
1220
+ path,
1221
+ aliases
1222
+ };
1223
+ if (repo) entry.repo = repo;
1224
+ projects[name] = entry;
1225
+ config.projects = projects;
1226
+ });
1227
+ mkdirSync4(join9(hubDir, "artifacts", "projects", name), { recursive: true });
1228
+ generate(hubDir);
1229
+ console.log(`Added project "${name}" and regenerated configs.`);
1230
+ });
1231
+ projectCommand.command("list").description("List registered projects").action(() => {
1232
+ const hubDir = process.cwd();
1233
+ const result = loadConfig(hubDir);
1234
+ if (!result.ok) {
1235
+ console.error("Failed to load config:", result.errors.join(", "));
1236
+ process.exit(1);
1237
+ }
1238
+ const projects = Object.entries(result.config.projects);
1239
+ if (projects.length === 0) {
1240
+ console.log("No projects registered. Run: meld project add");
1241
+ return;
1242
+ }
1243
+ console.log("Projects:\n");
1244
+ for (const [name, project] of projects) {
1245
+ console.log(` ${name}`);
1246
+ console.log(` Path: ${project.path}`);
1247
+ if (project.aliases.length > 0) {
1248
+ console.log(` Aliases: ${project.aliases.join(", ")}`);
1249
+ }
1250
+ if (project.repo) {
1251
+ console.log(` Repo: ${project.repo}`);
1252
+ }
1253
+ console.log("");
1254
+ }
1255
+ });
1256
+ projectCommand.command("remove <name>").description("Remove a project").action((name) => {
1257
+ const hubDir = process.cwd();
1258
+ updateConfig(hubDir, (config) => {
1259
+ const projects = config.projects;
1260
+ if (!(name in projects)) {
1261
+ console.error(`Project "${name}" not found.`);
1262
+ process.exit(1);
1263
+ }
1264
+ delete projects[name];
1265
+ });
1266
+ generate(hubDir);
1267
+ console.log(`Removed project "${name}" and regenerated configs.`);
1268
+ });
1269
+
1270
+ // src/cli/open.ts
1271
+ import { Command as Command4 } from "commander";
1272
+ import { execSync as execSync2 } from "child_process";
1273
+ import { existsSync as existsSync6 } from "fs";
1274
+ import { join as join10 } from "path";
1275
+ var IDE_COMMANDS = {
1276
+ cursor: "cursor",
1277
+ code: "code",
1278
+ windsurf: "windsurf"
1279
+ };
1280
+ var openCommand = new Command4("open").description("Open the hub workspace in your IDE").option("--ide <name>", "IDE to use (cursor, code, windsurf)").action((options) => {
1281
+ const hubDir = process.cwd();
1282
+ const result = loadConfig(hubDir);
1283
+ if (!result.ok) {
1284
+ console.error("Failed to load config:", result.errors.join(", "));
1285
+ process.exit(1);
1286
+ }
1287
+ const ide = options.ide ?? result.config.ide.default;
1288
+ const command = IDE_COMMANDS[ide];
1289
+ if (!command) {
1290
+ console.error(`Unknown IDE: ${ide}. Supported: ${Object.keys(IDE_COMMANDS).join(", ")}`);
1291
+ process.exit(1);
1292
+ }
1293
+ const workspaceFile = join10(
1294
+ hubDir,
1295
+ `${result.config.ide.workspaceName}.code-workspace`
1296
+ );
1297
+ if (!existsSync6(workspaceFile)) {
1298
+ console.error(`Workspace file not found: ${workspaceFile}
1299
+ Run: meld gen`);
1300
+ process.exit(1);
1301
+ }
1302
+ execSync2(`${command} "${workspaceFile}"`, { stdio: "inherit" });
1303
+ });
1304
+
1305
+ // src/cli/update.ts
1306
+ import { Command as Command5 } from "commander";
1307
+ import * as p3 from "@clack/prompts";
1308
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
1309
+ import { join as join11 } from "path";
1310
+ function ensureDir2(path) {
1311
+ mkdirSync5(path, { recursive: true });
1312
+ }
1313
+ function ensureSchema(hubDir) {
1314
+ const configPath = join11(hubDir, "meld.jsonc");
1315
+ const raw = readFileSync6(configPath, "utf-8");
1316
+ if (raw.includes('"$schema"')) return;
1317
+ const patched = raw.replace(/^\s*\{/, '{\n "$schema": "./meld.schema.json",');
1318
+ writeFileSync4(configPath, patched);
1319
+ }
1320
+ var updateCommand = new Command5("update").description("Re-scaffold hub structure and update defaults without touching config").action(() => {
1321
+ const hubDir = process.cwd();
1322
+ const loadResult = loadConfig(hubDir);
1323
+ if (!loadResult.ok) {
1324
+ p3.log.error(
1325
+ `Cannot update: ${loadResult.errors.join(", ")}
1326
+ Run \`meld init\` first.`
1327
+ );
1328
+ process.exit(1);
1329
+ }
1330
+ const config = loadResult.config;
1331
+ const standardDirs = [
1332
+ "context",
1333
+ "commands",
1334
+ "skills",
1335
+ join11("artifacts", "hub"),
1336
+ "scratch"
1337
+ ];
1338
+ for (const dir of standardDirs) {
1339
+ ensureDir2(join11(hubDir, dir));
1340
+ }
1341
+ for (const name of Object.keys(config.projects)) {
1342
+ ensureDir2(join11(hubDir, "artifacts", "projects", name));
1343
+ }
1344
+ ensureSchema(hubDir);
1345
+ writeFileSync4(join11(hubDir, "meld.schema.json"), readPackageSchema());
1346
+ writeFileSync4(join11(hubDir, "README.md"), generateReadme(config));
1347
+ const result = generate(hubDir);
1348
+ if (!result.ok) {
1349
+ p3.log.error(`Generation failed: ${result.errors.join(", ")}`);
1350
+ process.exit(1);
1351
+ }
1352
+ p3.log.success("Updated hub structure and defaults.");
1353
+ if (result.files.length > 0) {
1354
+ p3.log.info("Generated files:");
1355
+ for (const file of result.files) {
1356
+ p3.log.message(` ${file.path}`);
1357
+ }
1358
+ }
1359
+ p3.log.info("Review changes, then commit when ready.");
1360
+ });
1361
+
1362
+ // src/cli/run.ts
1363
+ import { Command as Command6 } from "commander";
1364
+ import { spawnSync } from "child_process";
1365
+ import { existsSync as existsSync7 } from "fs";
1366
+ import { join as join12 } from "path";
1367
+ var AGENT_COMMANDS = {
1368
+ "claude-code": "claude",
1369
+ "codex-cli": "codex",
1370
+ "gemini-cli": "gemini"
1371
+ };
1372
+ function runAgent(agentName, args) {
1373
+ const hubDir = process.cwd();
1374
+ const result = loadConfig(hubDir);
1375
+ if (!result.ok) {
1376
+ console.error("Failed to load config:", result.errors.join(", "));
1377
+ process.exit(1);
1378
+ }
1379
+ const agentConfig = result.config.agents[agentName];
1380
+ if (!agentConfig?.enabled) {
1381
+ console.error(`Agent "${agentName}" is not enabled in meld.jsonc`);
1382
+ process.exit(1);
1383
+ }
1384
+ const agentDir = join12(hubDir, AGENTS_DIR, resolveAgentDir(agentName, agentConfig));
1385
+ if (!existsSync7(agentDir)) {
1386
+ console.error(`Agent directory not found: ${agentDir}
1387
+ Run: meld gen`);
1388
+ process.exit(1);
1389
+ }
1390
+ const command = AGENT_COMMANDS[agentName];
1391
+ const { status, signal } = spawnSync(command, args, { cwd: agentDir, stdio: "inherit" });
1392
+ if (signal) {
1393
+ process.kill(process.pid, signal);
1394
+ }
1395
+ if (status !== null && status !== 0) {
1396
+ process.exit(status);
1397
+ }
1398
+ }
1399
+ function createAgentCommands() {
1400
+ return Object.keys(AGENT_COMMANDS).map((agent) => {
1401
+ return new Command6(agent).description(`Start ${agent} in its agent directory`).allowUnknownOption().allowExcessArguments().action((_options, cmd) => {
1402
+ runAgent(agent, cmd.args);
1403
+ });
1404
+ });
1405
+ }
1406
+
1407
+ // src/cli/index.ts
1408
+ function createCli() {
1409
+ const program = new Command7("meld").description("Agent-agnostic settings generator").version("0.1.0");
1410
+ program.addCommand(initCommand);
1411
+ program.addCommand(genCommand);
1412
+ program.addCommand(projectCommand);
1413
+ program.addCommand(openCommand);
1414
+ program.addCommand(updateCommand);
1415
+ for (const cmd of createAgentCommands()) {
1416
+ program.addCommand(cmd);
1417
+ }
1418
+ return program;
1419
+ }
1420
+
1421
+ // src/index.ts
1422
+ createCli().parse();
1423
+ //# sourceMappingURL=index.js.map