@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 +142 -142
- package/dist/src/bin/corum.js +111 -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 +20 -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
|
@@ -1,220 +1,220 @@
|
|
|
1
1
|
# Corum
|
|
2
2
|
|
|
3
|
-
Corum
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
```powershell
|
|
15
|
-
npm install
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @atolis-hq/corum
|
|
16
9
|
```
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
Or run without installing:
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
```bash
|
|
14
|
+
npx @atolis-hq/corum <command>
|
|
15
|
+
```
|
|
21
16
|
|
|
22
|
-
|
|
17
|
+
**Windows:** If you see an "execution policy" error in PowerShell, run this once:
|
|
23
18
|
|
|
24
19
|
```powershell
|
|
25
|
-
|
|
20
|
+
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
|
|
26
21
|
```
|
|
27
22
|
|
|
28
|
-
|
|
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
|
-
##
|
|
25
|
+
## Update
|
|
31
26
|
|
|
32
|
-
|
|
27
|
+
```bash
|
|
28
|
+
npm update -g @atolis-hq/corum
|
|
29
|
+
```
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- `test/mcp.test.ts`
|
|
44
|
-
- `test/writer.test.ts`
|
|
45
|
-
- `test/serializer.test.ts`
|
|
41
|
+
```bash
|
|
42
|
+
corum mcp
|
|
43
|
+
```
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
## Commands
|
|
48
46
|
|
|
49
|
-
|
|
47
|
+
### `corum mcp`
|
|
50
48
|
|
|
51
|
-
|
|
49
|
+
Start the MCP stdio server. Also starts a web UI by default.
|
|
52
50
|
|
|
53
|
-
```
|
|
54
|
-
|
|
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
|
-
|
|
60
|
+
### `corum web`
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
npm run mcp
|
|
61
|
-
```
|
|
62
|
+
Start the web UI only.
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
```bash
|
|
65
|
+
corum web [options]
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
Options:
|
|
68
|
+
--port <n> Port to listen on (default: 3000)
|
|
69
|
+
--graph <path> Override the graph directory
|
|
67
70
|
```
|
|
68
71
|
|
|
69
|
-
|
|
72
|
+
### `corum init`
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
node dist/src/mcp/index.js --watch
|
|
80
|
-
```
|
|
87
|
+
### `corum pack install`
|
|
81
88
|
|
|
82
|
-
|
|
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
|
-
|
|
91
|
+
```bash
|
|
92
|
+
corum pack install <name> # install latest tag
|
|
93
|
+
corum pack install <name>@<ref> # install a specific tag
|
|
85
94
|
```
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
corum pack install domain
|
|
100
|
+
corum pack install domain@v0.1.5
|
|
90
101
|
```
|
|
91
102
|
|
|
92
|
-
|
|
103
|
+
### `corum pack list`
|
|
93
104
|
|
|
94
|
-
|
|
105
|
+
List installed packs with their resolved version and install date.
|
|
95
106
|
|
|
96
|
-
```
|
|
97
|
-
|
|
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
|
-
|
|
111
|
+
### `corum import`
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
119
|
+
#### `corum import openapi <spec>`
|
|
120
|
+
|
|
121
|
+
Import an OpenAPI spec directly.
|
|
117
122
|
|
|
118
|
-
|
|
123
|
+
```bash
|
|
124
|
+
corum import openapi <spec> [options]
|
|
119
125
|
|
|
120
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
138
|
+
**Precedence (highest to lowest):** CLI flags → environment variables → `.corum/config.yaml`
|
|
125
139
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
152
|
+
### File source (default)
|
|
133
153
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
```yaml
|
|
155
|
+
# .corum/config.yaml
|
|
156
|
+
source: file
|
|
157
|
+
graph: .corum/graph
|
|
158
|
+
```
|
|
137
159
|
|
|
138
|
-
|
|
160
|
+
### Git source
|
|
139
161
|
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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": "
|
|
169
|
-
"args": ["
|
|
180
|
+
"command": "npx",
|
|
181
|
+
"args": ["@atolis-hq/corum", "mcp", "--no-web"],
|
|
170
182
|
"env": {
|
|
171
|
-
"CORUM_GRAPH_PATH": "
|
|
183
|
+
"CORUM_GRAPH_PATH": "/path/to/your/graph"
|
|
172
184
|
}
|
|
173
185
|
}
|
|
174
186
|
}
|
|
175
187
|
}
|
|
176
188
|
```
|
|
177
189
|
|
|
178
|
-
|
|
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
|
-
```
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
+
## Contributing
|
|
210
219
|
|
|
211
|
-
|
|
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.
|
package/dist/src/bin/corum.js
CHANGED
|
@@ -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 {
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
95
|
-
.action(() => {
|
|
96
|
-
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');
|
|
97
113
|
if (existsSync(configPath)) {
|
|
98
|
-
process.stdout.write(
|
|
99
|
-
|
|
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
|
+
}
|