@atolis-hq/corum 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,13 +30,13 @@ npm update -g @atolis-hq/corum
30
30
 
31
31
  ## Quick Start
32
32
 
33
- Scaffold a config file in your project:
33
+ Scaffold a new project in the current directory:
34
34
 
35
35
  ```bash
36
36
  corum init
37
37
  ```
38
38
 
39
- This creates `.corum/config.yaml` with commented defaults. Edit it to point at your graph directory or git repository, then start the MCP server:
39
+ This creates `.corum/config.yaml`, scaffolds a graph at `.corum/graph/`, and downloads the official template packs (`core`, `domain`, `rest`, `messaging`). Then start the MCP server:
40
40
 
41
41
  ```bash
42
42
  corum mcp
@@ -71,12 +71,43 @@ Options:
71
71
 
72
72
  ### `corum init`
73
73
 
74
- Scaffold `.corum/config.yaml` with commented defaults. Does not overwrite an existing file.
74
+ Scaffold a `.corum/` project structure and install the four default template packs (`core`, `domain`, `rest`, `messaging`). Skips any step where the target already exists.
75
75
 
76
76
  ```bash
77
77
  corum init
78
78
  ```
79
79
 
80
+ Creates:
81
+ - `.corum/config.yaml` - project configuration
82
+ - `.corum/graph/graph.yaml` - graph definition
83
+ - `.corum/graph/components/` and `.corum/graph/edges/` - empty directories ready for nodes
84
+ - `.corum/packs/` - downloaded template packs
85
+ - `.corum/packs.yaml` - local manifest of installed packs
86
+
87
+ ### `corum pack install`
88
+
89
+ Install a template pack from the registry into `.corum/packs/`. Appends the pack to `.corum/graph/graph.yaml` and records it in `.corum/packs.yaml`.
90
+
91
+ ```bash
92
+ corum pack install <name> # install latest tag
93
+ corum pack install <name>@<ref> # install a specific tag
94
+ ```
95
+
96
+ Examples:
97
+
98
+ ```bash
99
+ corum pack install domain
100
+ corum pack install domain@v0.1.5
101
+ ```
102
+
103
+ ### `corum pack list`
104
+
105
+ List installed packs with their resolved version and install date.
106
+
107
+ ```bash
108
+ corum pack list
109
+ ```
110
+
80
111
  ### `corum import`
81
112
 
82
113
  Import specifications into the graph.
@@ -108,6 +139,7 @@ Run `corum init` to generate a `.corum/config.yaml` with all available options.
108
139
 
109
140
  | Config key | Environment variable | Description |
110
141
  |---|---|---|
142
+ | `pack_registry` | - | URL of the pack registry YAML (set by `corum init`) |
111
143
  | `source` | `CORUM_SOURCE` | `file` (default) or `git` |
112
144
  | `graph` | `CORUM_GRAPH_PATH` | Path to the graph directory |
113
145
  | `git_local_path` | `CORUM_GIT_LOCAL_PATH` | Local git repo path |
@@ -1,11 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
4
+ import { readFile as fsReadFile } from 'node:fs/promises';
4
5
  import path from 'node:path';
5
- import { loadImportConfig, buildOpenAPIConfig } from '../import/config.js';
6
+ import { parse as parseYaml } from 'yaml';
7
+ import { buildOpenAPIConfig, loadImportConfig } from '../import/config.js';
6
8
  import { runImport } from '../import/runner.js';
7
9
  import { loadGraph } from '../loader/index.js';
8
10
  import { startMcpServer } from '../mcp/index.js';
11
+ import { parseGitHubRepo, toPackRawBaseUrl } from '../pack/github-urls.js';
12
+ import { registerPackInGraph } from '../pack/graph-yaml.js';
13
+ import { installPackFiles } from '../pack/installer.js';
14
+ import { readManifest, upsertPack } from '../pack/manifest.js';
15
+ import { fetchRegistry, findPack, resolveRef } from '../pack/registry.js';
9
16
  import { createGraphRuntimeConfig } from '../source/config.js';
10
17
  import { startWebServer } from '../web/server.js';
11
18
  const program = new Command();
@@ -13,7 +20,6 @@ program
13
20
  .name('corum')
14
21
  .description('Corum graph CLI')
15
22
  .version('0.1.0');
16
- // ── mcp ──────────────────────────────────────────────────────────────────────
17
23
  program
18
24
  .command('mcp')
19
25
  .description('Start the MCP stdio server (+ web UI by default)')
@@ -25,7 +31,6 @@ program
25
31
  process.env.CORUM_GRAPH_PATH = path.resolve(opts.graph);
26
32
  await startMcpServer({ noWeb: !opts.web, watch: opts.watch ?? false });
27
33
  });
28
- // ── web ──────────────────────────────────────────────────────────────────────
29
34
  program
30
35
  .command('web')
31
36
  .description('Start the web UI')
@@ -49,21 +54,23 @@ program
49
54
  port: opts.port,
50
55
  });
51
56
  });
52
- // ── init ─────────────────────────────────────────────────────────────────────
53
57
  const CONFIG_TEMPLATE = `# Corum project configuration
54
58
  # Uncomment and set the options relevant to your setup.
55
59
  # All values can be overridden by environment variables (CORUM_*) or CLI flags.
56
60
 
61
+ # Registry URL for discovering and installing template packs.
62
+ pack_registry: https://github.com/atolis-hq/corum/packs/registry.yaml
63
+
57
64
  # Source type: 'file' (default) or 'git'
58
65
  # Maps to: CORUM_SOURCE
59
66
  # source: file
60
67
 
61
- # ── File source (default) ─────────────────────────────────────────────────────
68
+ # File source (default)
62
69
  # Local path to the graph directory.
63
70
  # Maps to: CORUM_GRAPH_PATH
64
71
  # graph: .corum/graph
65
72
 
66
- # ── Git source ────────────────────────────────────────────────────────────────
73
+ # Git source
67
74
  # Uncomment 'source: git' above and configure one of the following:
68
75
 
69
76
  # Local path to a git repository containing the graph.
@@ -90,20 +97,47 @@ const CONFIG_TEMPLATE = `# Corum project configuration
90
97
  # Maps to: CORUM_GIT_USERNAME
91
98
  # git_username: x-access-token
92
99
  `;
100
+ const GRAPH_TEMPLATE = `schema-version: '1.0'
101
+ name: My Graph
102
+ templatePacks: []
103
+ components: []
104
+ `;
93
105
  program
94
106
  .command('init')
95
- .description('Scaffold .corum/config.yaml with commented defaults')
96
- .action(() => {
97
- const configPath = path.join(process.cwd(), '.corum', 'config.yaml');
107
+ .description('Scaffold .corum project structure and install default packs')
108
+ .action(async () => {
109
+ const cwd = process.cwd();
110
+ const corumDir = path.join(cwd, '.corum');
111
+ const configPath = path.join(corumDir, 'config.yaml');
112
+ const graphPath = path.join(corumDir, 'graph', 'graph.yaml');
98
113
  if (existsSync(configPath)) {
99
- process.stdout.write(`.corum/config.yaml already exists not overwriting\n`);
100
- return;
114
+ process.stdout.write('.corum/config.yaml already exists - not overwriting\n');
115
+ }
116
+ else {
117
+ mkdirSync(path.dirname(configPath), { recursive: true });
118
+ writeFileSync(configPath, CONFIG_TEMPLATE);
119
+ process.stdout.write('Created .corum/config.yaml\n');
120
+ }
121
+ if (existsSync(graphPath)) {
122
+ process.stdout.write('.corum/graph/graph.yaml already exists - not overwriting\n');
123
+ }
124
+ else {
125
+ mkdirSync(path.dirname(graphPath), { recursive: true });
126
+ writeFileSync(graphPath, GRAPH_TEMPLATE);
127
+ mkdirSync(path.join(corumDir, 'graph', 'components'), { recursive: true });
128
+ mkdirSync(path.join(corumDir, 'graph', 'edges'), { recursive: true });
129
+ process.stdout.write('Created .corum/graph/graph.yaml\n');
130
+ }
131
+ for (const packName of ['core', 'domain', 'rest', 'messaging']) {
132
+ try {
133
+ await installPack(packName, cwd);
134
+ }
135
+ catch (err) {
136
+ process.stderr.write(`[ERROR] Failed to install pack ${packName}: ${err instanceof Error ? err.message : String(err)}\n`);
137
+ process.exit(1);
138
+ }
101
139
  }
102
- mkdirSync(path.dirname(configPath), { recursive: true });
103
- writeFileSync(configPath, CONFIG_TEMPLATE);
104
- process.stdout.write(`Created .corum/config.yaml\n`);
105
140
  });
106
- // ── import ───────────────────────────────────────────────────────────────────
107
141
  const importCmd = program.command('import')
108
142
  .description('Import specifications into the graph')
109
143
  .option('--config <path>', 'Path to import config YAML')
@@ -148,6 +182,38 @@ importCmd
148
182
  process.exit(2);
149
183
  }
150
184
  });
185
+ const packCmd = program.command('pack').description('Manage template packs');
186
+ packCmd
187
+ .command('install <name>')
188
+ .description('Install a pack from the registry (e.g. core, domain@v0.1.5)')
189
+ .action(async (name) => {
190
+ try {
191
+ await installPack(name);
192
+ }
193
+ catch (err) {
194
+ process.stderr.write(`[ERROR] ${err instanceof Error ? err.message : String(err)}\n`);
195
+ process.exit(1);
196
+ }
197
+ });
198
+ packCmd
199
+ .command('list')
200
+ .description('List installed packs')
201
+ .action(async () => {
202
+ try {
203
+ const manifest = await readManifest(path.join(process.cwd(), '.corum', 'packs.yaml'));
204
+ if (manifest.packs.length === 0) {
205
+ process.stdout.write('No packs installed. Run: corum pack install <name>\n');
206
+ return;
207
+ }
208
+ for (const p of manifest.packs) {
209
+ process.stdout.write(`${p.name}@${p.ref} (${p.installedAt})\n`);
210
+ }
211
+ }
212
+ catch (err) {
213
+ process.stderr.write(`[ERROR] ${err instanceof Error ? err.message : String(err)}\n`);
214
+ process.exit(1);
215
+ }
216
+ });
151
217
  function buildRuntimeConfig(graphOverride) {
152
218
  if (graphOverride)
153
219
  process.env.CORUM_GRAPH_PATH = path.resolve(graphOverride);
@@ -162,4 +228,33 @@ function reportDiagnostics(diagnostics) {
162
228
  const warnings = diagnostics.filter(d => d.severity === 'warning').length;
163
229
  process.stdout.write(`Import complete. ${errors} error(s), ${warnings} warning(s).\n`);
164
230
  }
231
+ async function readPackRegistryUrl(cwd) {
232
+ const configPath = path.join(cwd, '.corum', 'config.yaml');
233
+ const text = await fsReadFile(configPath, 'utf8');
234
+ const config = parseYaml(text);
235
+ if (!config.pack_registry)
236
+ throw new Error('pack_registry not set in .corum/config.yaml - run corum init first');
237
+ return config.pack_registry;
238
+ }
239
+ export async function installPack(nameWithRef, cwd = process.cwd()) {
240
+ const atIdx = nameWithRef.indexOf('@');
241
+ const name = atIdx >= 0 ? nameWithRef.slice(0, atIdx) : nameWithRef;
242
+ const specifiedRef = atIdx >= 0 ? nameWithRef.slice(atIdx + 1) : undefined;
243
+ const registryUrl = await readPackRegistryUrl(cwd);
244
+ const registry = await fetchRegistry(registryUrl);
245
+ const pack = findPack(registry, name);
246
+ const { owner, repo } = parseGitHubRepo(pack.repo);
247
+ const ref = await resolveRef(owner, repo, specifiedRef);
248
+ const baseUrl = toPackRawBaseUrl(owner, repo, ref, pack.path);
249
+ await installPackFiles(baseUrl, path.join(cwd, '.corum', 'packs', name));
250
+ await upsertPack(path.join(cwd, '.corum', 'packs.yaml'), {
251
+ name,
252
+ repo: pack.repo,
253
+ path: pack.path,
254
+ ref,
255
+ installedAt: new Date().toISOString(),
256
+ });
257
+ await registerPackInGraph(path.join(cwd, '.corum', 'graph', 'graph.yaml'), name, `../packs/${name}`);
258
+ process.stdout.write(`Installed pack: ${name}@${ref}\n`);
259
+ }
165
260
  program.parse();
@@ -0,0 +1,30 @@
1
+ export function parseGitHubUrl(url) {
2
+ const u = new URL(url);
3
+ if (u.hostname !== 'github.com')
4
+ throw new Error(`Not a github.com URL: ${url}`);
5
+ const segments = u.pathname.replace(/^\//, '').split('/').filter(Boolean);
6
+ if (segments.length < 3)
7
+ throw new Error(`Missing path in GitHub URL: ${url}`);
8
+ const [owner, repo, ...rest] = segments;
9
+ return { owner, repo, path: rest.join('/') };
10
+ }
11
+ export function parseGitHubRepo(repoUrl) {
12
+ const u = new URL(repoUrl);
13
+ if (u.hostname !== 'github.com')
14
+ throw new Error(`Not a github.com URL: ${repoUrl}`);
15
+ const segments = u.pathname.replace(/^\//, '').split('/').filter(Boolean);
16
+ if (segments.length < 2)
17
+ throw new Error(`Missing repository in GitHub URL: ${repoUrl}`);
18
+ const [owner, repo] = segments;
19
+ return { owner, repo };
20
+ }
21
+ export function toRegistryFetchUrl(configUrl) {
22
+ const { owner, repo, path } = parseGitHubUrl(configUrl);
23
+ return `https://raw.githubusercontent.com/${owner}/${repo}/HEAD/${path}`;
24
+ }
25
+ export function toPackRawBaseUrl(owner, repo, ref, packPath) {
26
+ return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${packPath}`;
27
+ }
28
+ export function toTagsApiUrl(owner, repo) {
29
+ return `https://api.github.com/repos/${owner}/${repo}/tags?per_page=1`;
30
+ }
@@ -0,0 +1,12 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { parse, stringify } from 'yaml';
3
+ export async function registerPackInGraph(graphYamlPath, packName, relativePath) {
4
+ const text = await readFile(graphYamlPath, 'utf8');
5
+ const graph = parse(text);
6
+ if (!Array.isArray(graph.templatePacks))
7
+ graph.templatePacks = [];
8
+ if (graph.templatePacks.some(p => p.name === packName))
9
+ return;
10
+ graph.templatePacks.push({ name: packName, path: relativePath });
11
+ await writeFile(graphYamlPath, stringify(graph));
12
+ }
@@ -0,0 +1,29 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { parse } from 'yaml';
4
+ export async function installPackFiles(baseUrl, destDir, fetchFn = fetch) {
5
+ const packYamlUrl = `${baseUrl}/pack.yaml`;
6
+ const packRes = await fetchFn(packYamlUrl);
7
+ if (!packRes.ok)
8
+ throw new Error(`Failed to fetch pack.yaml: ${packRes.status} ${packRes.statusText}`);
9
+ const packText = await packRes.text();
10
+ const meta = parse(packText);
11
+ await mkdir(path.join(destDir, 'templates'), { recursive: true });
12
+ await writeFile(path.join(destDir, 'pack.yaml'), packText);
13
+ for (const templateName of meta.templates) {
14
+ const templateUrl = `${baseUrl}/templates/${templateName}.yaml`;
15
+ const res = await fetchFn(templateUrl);
16
+ if (!res.ok)
17
+ throw new Error(`Failed to fetch template ${templateName}: ${res.status} ${res.statusText}`);
18
+ await writeFile(path.join(destDir, 'templates', `${templateName}.yaml`), await res.text());
19
+ }
20
+ for (const filePath of meta.files ?? []) {
21
+ const fileUrl = `${baseUrl}/${filePath}`;
22
+ const res = await fetchFn(fileUrl);
23
+ if (!res.ok)
24
+ throw new Error(`Failed to fetch file ${filePath}: ${res.status} ${res.statusText}`);
25
+ const dest = path.join(destDir, filePath);
26
+ await mkdir(path.dirname(dest), { recursive: true });
27
+ await writeFile(dest, await res.text());
28
+ }
29
+ }
@@ -0,0 +1,26 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { parse, stringify } from 'yaml';
4
+ export async function readManifest(manifestPath) {
5
+ try {
6
+ const text = await readFile(manifestPath, 'utf8');
7
+ return parse(text) ?? { packs: [] };
8
+ }
9
+ catch (err) {
10
+ if (err.code === 'ENOENT')
11
+ return { packs: [] };
12
+ throw err;
13
+ }
14
+ }
15
+ export async function upsertPack(manifestPath, entry) {
16
+ const manifest = await readManifest(manifestPath);
17
+ const idx = manifest.packs.findIndex(p => p.name === entry.name);
18
+ if (idx >= 0) {
19
+ manifest.packs[idx] = entry;
20
+ }
21
+ else {
22
+ manifest.packs.push(entry);
23
+ }
24
+ await mkdir(path.dirname(manifestPath), { recursive: true });
25
+ await writeFile(manifestPath, stringify(manifest));
26
+ }
@@ -0,0 +1,29 @@
1
+ import { parse } from 'yaml';
2
+ import { toRegistryFetchUrl, toTagsApiUrl } from './github-urls.js';
3
+ export async function fetchRegistry(configUrl, fetchFn = fetch) {
4
+ const rawUrl = toRegistryFetchUrl(configUrl);
5
+ const res = await fetchFn(rawUrl);
6
+ if (!res.ok)
7
+ throw new Error(`Failed to fetch registry: ${res.status} ${res.statusText}`);
8
+ return parse(await res.text());
9
+ }
10
+ export function findPack(registry, name) {
11
+ const pack = registry.packs.find(p => p.name === name);
12
+ if (!pack) {
13
+ const available = registry.packs.map(p => p.name).join(', ');
14
+ throw new Error(`Pack "${name}" not found in registry. Available: ${available}`);
15
+ }
16
+ return pack;
17
+ }
18
+ export async function resolveRef(owner, repo, specifiedRef, fetchFn = fetch) {
19
+ if (specifiedRef)
20
+ return specifiedRef;
21
+ const url = toTagsApiUrl(owner, repo);
22
+ const res = await fetchFn(url);
23
+ if (!res.ok)
24
+ throw new Error(`Failed to resolve latest tag: ${res.status} ${res.statusText}`);
25
+ const data = await res.json();
26
+ if (data.length === 0 || !data[0].name)
27
+ throw new Error(`Failed to resolve latest tag: no tags found for ${owner}/${repo}`);
28
+ return data[0].name;
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/corum",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",