@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 +35 -3
- package/dist/src/bin/corum.js +110 -15
- package/dist/src/pack/github-urls.js +30 -0
- package/dist/src/pack/graph-yaml.js +12 -0
- package/dist/src/pack/installer.js +29 -0
- package/dist/src/pack/manifest.js +26 -0
- package/dist/src/pack/registry.js +29 -0
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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 |
|
package/dist/src/bin/corum.js
CHANGED
|
@@ -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 {
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
96
|
-
.action(() => {
|
|
97
|
-
const
|
|
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(
|
|
100
|
-
|
|
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
|
+
}
|