@atolis-hq/corum 0.1.5 → 0.1.7

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
@@ -1,220 +1,220 @@
1
1
  # Corum
2
2
 
3
- Corum loads design graph files from disk into an in-memory graph and exposes the graph through MCP tools.
4
-
5
- ## Requirements
6
-
7
- - Node.js 20 or newer
8
- - npm
3
+ Corum is a Git-native design graph for service architecture. It models components (APIs, domain models, schemas, fields) as nodes with typed edges, and exposes the graph through MCP tools so AI assistants can reason about your architecture.
9
4
 
10
5
  ## Install
11
6
 
12
- From the repository root:
13
-
14
- ```powershell
15
- npm install
7
+ ```bash
8
+ npm install -g @atolis-hq/corum
16
9
  ```
17
10
 
18
- This installs TypeScript, the MCP SDK, and YAML parsing dependencies.
11
+ Or run without installing:
19
12
 
20
- ## Build
13
+ ```bash
14
+ npx @atolis-hq/corum <command>
15
+ ```
21
16
 
22
- Compile the TypeScript sources into `dist/`:
17
+ **Windows:** If you see an "execution policy" error in PowerShell, run this once:
23
18
 
24
19
  ```powershell
25
- npm run build
20
+ Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
26
21
  ```
27
22
 
28
- The build command runs `tsc`.
23
+ This is a standard one-time setup step for Node.js development on Windows. Alternatively, `npx @atolis-hq/corum <command>` works without any setup on all platforms.
29
24
 
30
- ## Test
25
+ ## Update
31
26
 
32
- Run the full test suite:
27
+ ```bash
28
+ npm update -g @atolis-hq/corum
29
+ ```
33
30
 
34
- ```powershell
35
- npm test
31
+ ## Quick Start
32
+
33
+ Scaffold a new project in the current directory:
34
+
35
+ ```bash
36
+ corum init
36
37
  ```
37
38
 
38
- This compiles the project and runs the Node test runner against:
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:
39
40
 
40
- - `test/schema.test.ts`
41
- - `test/loader.test.ts`
42
- - `test/graph.test.ts`
43
- - `test/mcp.test.ts`
44
- - `test/writer.test.ts`
45
- - `test/serializer.test.ts`
41
+ ```bash
42
+ corum mcp
43
+ ```
46
44
 
47
- The fixture graph used by the tests is in `fixtures/sample-graph`. The tests verify that it loads as `45` nodes and `38` edges.
45
+ ## Commands
48
46
 
49
- ## Run The MCP Server
47
+ ### `corum mcp`
50
48
 
51
- Build first:
49
+ Start the MCP stdio server. Also starts a web UI by default.
52
50
 
53
- ```powershell
54
- npm run build
51
+ ```bash
52
+ corum mcp [options]
53
+
54
+ Options:
55
+ --no-web Suppress the web UI
56
+ --watch Reload graph on file changes
57
+ --graph <path> Override the graph directory
55
58
  ```
56
59
 
57
- Run the MCP server against the default graph path:
60
+ ### `corum web`
58
61
 
59
- ```powershell
60
- npm run mcp
61
- ```
62
+ Start the web UI only.
62
63
 
63
- By default, the server loads:
64
+ ```bash
65
+ corum web [options]
64
66
 
65
- ```text
66
- .corum/graph
67
+ Options:
68
+ --port <n> Port to listen on (default: 3000)
69
+ --graph <path> Override the graph directory
67
70
  ```
68
71
 
69
- To run it against the sample graph fixture instead:
72
+ ### `corum init`
70
73
 
71
- ```powershell
72
- $env:CORUM_GRAPH_PATH = "fixtures/sample-graph"
73
- npm run mcp
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
+
76
+ ```bash
77
+ corum init
74
78
  ```
75
79
 
76
- To reload the in-memory graph when graph YAML or template YAML files change, pass `--watch` to the built server:
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
77
86
 
78
- ```powershell
79
- node dist/src/mcp/index.js --watch
80
- ```
87
+ ### `corum pack install`
81
88
 
82
- The same watcher can be enabled for the web server with `node dist/src/web/server.js --watch`, or for either server by setting `CORUM_FILE_WATCHER=true`.
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`.
83
90
 
84
- Starting with powershell
91
+ ```bash
92
+ corum pack install <name> # install latest tag
93
+ corum pack install <name>@<ref> # install a specific tag
85
94
  ```
86
- $env:CORUM_GRAPH_PATH = "fixtures/sample-graph";
87
- $env:CORUM_WEB_PORT = 3001;
88
- $env:CORUM_FILE_WATCHER="true";
89
- npm run web
95
+
96
+ Examples:
97
+
98
+ ```bash
99
+ corum pack install domain
100
+ corum pack install domain@v0.1.5
90
101
  ```
91
102
 
92
- To run the web app against a git repository instead of a filesystem graph path, set `CORUM_SOURCE=git` before starting the app.
103
+ ### `corum pack list`
93
104
 
94
- For a local git repository:
105
+ List installed packs with their resolved version and install date.
95
106
 
96
- ```powershell
97
- $env:CORUM_SOURCE = "git"
98
- $env:CORUM_GIT_LOCAL_PATH = "C:\git\atolis-hq\corum-design-graph"
99
- $env:CORUM_GIT_BRANCH = "main"
100
- $env:CORUM_GIT_POLL_SECONDS = 10
101
- $env:CORUM_WEB_PORT = 3001
102
- npm run web
107
+ ```bash
108
+ corum pack list
103
109
  ```
104
110
 
105
- For a remote repository:
111
+ ### `corum import`
106
112
 
107
- ```powershell
108
- $env:CORUM_SOURCE = "git"
109
- $env:CORUM_GIT_REMOTE_URL = "https://github.com/org/design-repo.git"
110
- $env:CORUM_GIT_BRANCH = "main"
111
- $env:CORUM_GIT_POLL_SECONDS = 10
112
- $env:CORUM_WEB_PORT = 3001
113
- npm run web
113
+ Import specifications into the graph.
114
+
115
+ ```bash
116
+ corum import --config <path> Import using a config YAML file
114
117
  ```
115
118
 
116
- For private remote repositories, also set `CORUM_GIT_TOKEN`. `CORUM_GIT_USERNAME` defaults to `x-access-token` when a token is present.
119
+ #### `corum import openapi <spec>`
120
+
121
+ Import an OpenAPI spec directly.
117
122
 
118
- The same git source config is used by `npm run mcp`. Git-backed startup expects graph files in `.corum/graph` and template packs in `.corum/packs`, and loads the selected branch at process start.
123
+ ```bash
124
+ corum import openapi <spec> [options]
119
125
 
120
- `CORUM_GIT_POLL_SECONDS` is optional. When set to a positive number of seconds, the web server polls the git source for branch/ref changes, invalidates its cached multi-branch view, and reloads the app automatically. If it is not set, git-backed content is not polled.
126
+ Options:
127
+ --component-strategy <strategy> Component mapping: uri-segment (default), tag, hardcoded
128
+ --segment <n> URI segment index (uri-segment strategy)
129
+ --pattern <regex> Regex pattern (uri-segment strategy)
130
+ --component <name> Component name (hardcoded strategy)
131
+ --graph <path> Override the graph directory
132
+ ```
133
+
134
+ ## Configuration
121
135
 
122
- `CORUM_FILE_WATCHER` only watches filesystem graph paths. It does not watch git refs. For git-backed web sessions, you can either enable `CORUM_GIT_POLL_SECONDS` or use the always-visible `Reload` button in the branch bar to force a refresh.
136
+ Run `corum init` to generate a `.corum/config.yaml` with all available options. Corum walks up from the current directory to find it, so you can place it at your project root.
123
137
 
124
- The MCP server exposes these tools:
138
+ **Precedence (highest to lowest):** CLI flags → environment variables → `.corum/config.yaml`
125
139
 
126
- - `list_nodes`: lists graph nodes, optionally filtered by `template`, `component`, `state`, or `stability`
127
- - `list_templates`: lists loaded graph templates with summary metadata
128
- - `get_template`: returns full details for a loaded graph template
129
- - `get_cluster`: returns a root node, owned child nodes, and edges inside that cluster
130
- - `get_linked_fields`: returns `maps-to` edges touching fields owned by a root node
140
+ | Config key | Environment variable | Description |
141
+ |---|---|---|
142
+ | `pack_registry` | - | URL of the pack registry YAML (set by `corum init`) |
143
+ | `source` | `CORUM_SOURCE` | `file` (default) or `git` |
144
+ | `graph` | `CORUM_GRAPH_PATH` | Path to the graph directory |
145
+ | `git_local_path` | `CORUM_GIT_LOCAL_PATH` | Local git repo path |
146
+ | `git_remote_url` | `CORUM_GIT_REMOTE_URL` | Remote git repo URL |
147
+ | `git_branch` | `CORUM_GIT_BRANCH` | Branch to load |
148
+ | `git_poll_seconds` | `CORUM_GIT_POLL_SECONDS` | Polling interval for remote git |
149
+ | `git_token` | `CORUM_GIT_TOKEN` | Auth token for private repos |
150
+ | `git_username` | `CORUM_GIT_USERNAME` | Auth username (default: `x-access-token`) |
131
151
 
132
- Each tool accepts an optional `format` argument:
152
+ ### File source (default)
133
153
 
134
- - `yaml`: default, human-readable YAML
135
- - `json`: pretty JSON
136
- - `toon`: TOON output via the official `@toon-format/toon` encoder for lower token use
154
+ ```yaml
155
+ # .corum/config.yaml
156
+ source: file
157
+ graph: .corum/graph
158
+ ```
137
159
 
138
- Each tool also accepts `compact_keys: true` to shorten common graph keys before serialization. This works with all formats:
160
+ ### Git source
139
161
 
140
- ```text
141
- id -> i
142
- template -> t
143
- component -> cp
144
- state -> s
145
- stability -> st
146
- schemaVersion -> sv
147
- lastModifiedAt -> lm
148
- extractedFrom -> xf
149
- properties -> p
150
- root -> r
151
- children -> ch
152
- edges -> e
153
- nodes -> n
154
- from -> fr
155
- to -> to
156
- type -> ty
157
- notes -> nt
162
+ ```yaml
163
+ # .corum/config.yaml
164
+ source: git
165
+ git_remote_url: https://github.com/org/design-repo
166
+ git_branch: main
167
+ git_poll_seconds: 30
158
168
  ```
159
169
 
170
+ For private repositories, set `CORUM_GIT_TOKEN` as an environment variable rather than storing it in the config file.
171
+
160
172
  ## MCP Client Configuration
161
173
 
162
- This repo includes a project-level `.mcp.json`:
174
+ Configure your MCP client to run Corum. For Claude Code or Claude Desktop:
163
175
 
164
176
  ```json
165
177
  {
166
178
  "mcpServers": {
167
179
  "corum": {
168
- "command": "node",
169
- "args": ["dist/src/mcp/index.js"],
180
+ "command": "npx",
181
+ "args": ["@atolis-hq/corum", "mcp", "--no-web"],
170
182
  "env": {
171
- "CORUM_GRAPH_PATH": "fixtures/sample-graph"
183
+ "CORUM_GRAPH_PATH": "/path/to/your/graph"
172
184
  }
173
185
  }
174
186
  }
175
187
  }
176
188
  ```
177
189
 
178
- Build the project before using this config from an MCP client:
179
-
180
- ```powershell
181
- npm run build
182
- ```
183
-
184
- The checked-in config points at `fixtures/sample-graph` so the tools return sample nodes immediately. Change `CORUM_GRAPH_PATH` to `.corum/graph` when you have graph component files there.
185
-
186
- ## MCP Smoke Test
187
-
188
- Run a local MCP client against the configured server and print graph data:
190
+ Or if installed globally:
189
191
 
190
- ```powershell
191
- npm run mcp:smoke
192
+ ```json
193
+ {
194
+ "mcpServers": {
195
+ "corum": {
196
+ "command": "corum",
197
+ "args": ["mcp", "--no-web"],
198
+ "env": {
199
+ "CORUM_GRAPH_PATH": "/path/to/your/graph"
200
+ }
201
+ }
202
+ }
203
+ }
192
204
  ```
193
205
 
194
- The smoke test starts the MCP server over stdio and calls:
195
-
196
- - `list_nodes`
197
- - `list_nodes` filtered to `APIEndpoint`
198
- - `get_cluster` for `orders.DomainModel.order`
199
- - `get_linked_fields` for `orders.DomainModel.order`
206
+ ## MCP Tools
200
207
 
201
- ## Useful Development Commands
208
+ | Tool | Description |
209
+ |---|---|
210
+ | `list_nodes` | List graph nodes, optionally filtered by `template`, `component`, `state`, or `stability` |
211
+ | `list_templates` | List loaded templates with summary metadata |
212
+ | `get_template` | Return full details for a template |
213
+ | `get_cluster` | Return a root node, its owned children, and internal edges |
214
+ | `get_linked_fields` | Return `maps-to` edges touching fields owned by a root node |
202
215
 
203
- Type-check without emitting files:
204
-
205
- ```powershell
206
- npx tsc --noEmit
207
- ```
216
+ All tools accept an optional `format` argument: `yaml` (default), `json`, or `toon`. All tools also accept `compact_keys: true` to shorten common keys before serialization, reducing token usage.
208
217
 
209
- Run one compiled test file after building:
218
+ ## Contributing
210
219
 
211
- ```powershell
212
- npm run build
213
- node --test dist/test/loader.test.js
214
- ```
215
-
216
- Clean generated build output manually if needed:
217
-
218
- ```powershell
219
- Remove-Item -Recurse -Force dist
220
- ```
220
+ See [DEVELOPMENT.md](DEVELOPMENT.md) for build, test, and local development instructions.
@@ -1,10 +1,18 @@
1
+ #!/usr/bin/env node
1
2
  import { Command } from 'commander';
2
3
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
4
+ import { readFile as fsReadFile } from 'node:fs/promises';
3
5
  import path from 'node:path';
4
- import { loadImportConfig, buildOpenAPIConfig } from '../import/config.js';
6
+ import { parse as parseYaml } from 'yaml';
7
+ import { buildOpenAPIConfig, loadImportConfig } from '../import/config.js';
5
8
  import { runImport } from '../import/runner.js';
6
9
  import { loadGraph } from '../loader/index.js';
7
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';
8
16
  import { createGraphRuntimeConfig } from '../source/config.js';
9
17
  import { startWebServer } from '../web/server.js';
10
18
  const program = new Command();
@@ -12,7 +20,6 @@ program
12
20
  .name('corum')
13
21
  .description('Corum graph CLI')
14
22
  .version('0.1.0');
15
- // ── mcp ──────────────────────────────────────────────────────────────────────
16
23
  program
17
24
  .command('mcp')
18
25
  .description('Start the MCP stdio server (+ web UI by default)')
@@ -24,7 +31,6 @@ program
24
31
  process.env.CORUM_GRAPH_PATH = path.resolve(opts.graph);
25
32
  await startMcpServer({ noWeb: !opts.web, watch: opts.watch ?? false });
26
33
  });
27
- // ── web ──────────────────────────────────────────────────────────────────────
28
34
  program
29
35
  .command('web')
30
36
  .description('Start the web UI')
@@ -48,21 +54,23 @@ program
48
54
  port: opts.port,
49
55
  });
50
56
  });
51
- // ── init ─────────────────────────────────────────────────────────────────────
52
57
  const CONFIG_TEMPLATE = `# Corum project configuration
53
58
  # Uncomment and set the options relevant to your setup.
54
59
  # All values can be overridden by environment variables (CORUM_*) or CLI flags.
55
60
 
61
+ # Registry URL for discovering and installing template packs.
62
+ pack_registry: https://github.com/atolis-hq/corum/packs/registry.yaml
63
+
56
64
  # Source type: 'file' (default) or 'git'
57
65
  # Maps to: CORUM_SOURCE
58
66
  # source: file
59
67
 
60
- # ── File source (default) ─────────────────────────────────────────────────────
68
+ # File source (default)
61
69
  # Local path to the graph directory.
62
70
  # Maps to: CORUM_GRAPH_PATH
63
71
  # graph: .corum/graph
64
72
 
65
- # ── Git source ────────────────────────────────────────────────────────────────
73
+ # Git source
66
74
  # Uncomment 'source: git' above and configure one of the following:
67
75
 
68
76
  # Local path to a git repository containing the graph.
@@ -89,20 +97,47 @@ const CONFIG_TEMPLATE = `# Corum project configuration
89
97
  # Maps to: CORUM_GIT_USERNAME
90
98
  # git_username: x-access-token
91
99
  `;
100
+ const GRAPH_TEMPLATE = `schema-version: '1.0'
101
+ name: My Graph
102
+ templatePacks: []
103
+ components: []
104
+ `;
92
105
  program
93
106
  .command('init')
94
- .description('Scaffold .corum/config.yaml with commented defaults')
95
- .action(() => {
96
- 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');
97
113
  if (existsSync(configPath)) {
98
- process.stdout.write(`.corum/config.yaml already exists not overwriting\n`);
99
- 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
+ }
100
139
  }
101
- mkdirSync(path.dirname(configPath), { recursive: true });
102
- writeFileSync(configPath, CONFIG_TEMPLATE);
103
- process.stdout.write(`Created .corum/config.yaml\n`);
104
140
  });
105
- // ── import ───────────────────────────────────────────────────────────────────
106
141
  const importCmd = program.command('import')
107
142
  .description('Import specifications into the graph')
108
143
  .option('--config <path>', 'Path to import config YAML')
@@ -147,6 +182,38 @@ importCmd
147
182
  process.exit(2);
148
183
  }
149
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
+ });
150
217
  function buildRuntimeConfig(graphOverride) {
151
218
  if (graphOverride)
152
219
  process.env.CORUM_GRAPH_PATH = path.resolve(graphOverride);
@@ -161,4 +228,33 @@ function reportDiagnostics(diagnostics) {
161
228
  const warnings = diagnostics.filter(d => d.severity === 'warning').length;
162
229
  process.stdout.write(`Import complete. ${errors} error(s), ${warnings} warning(s).\n`);
163
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
+ }
164
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,20 @@
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
+ }
@@ -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.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",