@exchanet/enet 1.0.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/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # ◆ enet — exchanet methods manager
2
+
3
+ Install, scaffold and manage exchanet AI coding methods in any project, with any agent.
4
+
5
+ ```bash
6
+ npm install -g @exchanet/enet
7
+ ```
8
+
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+ [![npm](https://img.shields.io/npm/v/@exchanet/enet.svg)](https://www.npmjs.com/package/@exchanet/enet)
11
+ [![Node: >=18](https://img.shields.io/badge/node-%3E%3D18-green.svg)]()
12
+
13
+ ---
14
+
15
+ ## What is enet?
16
+
17
+ `enet` is the package manager for exchanet methods. It installs AI coding method adapters directly into your project — in the right place for your agent — and provides tools to scaffold modules, validate manifests, and keep everything up to date.
18
+
19
+ Think of it as `brew` or `npm`, but for AI coding architecture methods.
20
+
21
+ ---
22
+
23
+ ## Available Methods
24
+
25
+ | Method | What it does |
26
+ |---|---|
27
+ | `reflex` | Universal modular architecture. Auto-generated Admin Panel, zero hardcoded config. |
28
+ | `pdca-t` | ≥99% test coverage, zero vulnerabilities, systematic quality validation. |
29
+ | `iris` | Continuous improvement of existing systems without breaking architecture. |
30
+ | `enterprise-builder` | Large-scale planning for complex projects before writing code. |
31
+
32
+ The registry is live — new methods appear automatically without updating enet.
33
+
34
+ ---
35
+
36
+ ## Usage
37
+
38
+ ### Install a method
39
+
40
+ ```bash
41
+ enet install reflex
42
+ enet install pdca-t
43
+ enet install iris
44
+ enet install enterprise-builder
45
+ ```
46
+
47
+ enet detects your AI agent automatically and places the adapter in the right location:
48
+
49
+ | Agent detected | Installs to |
50
+ |---|---|
51
+ | Cursor | `.cursor/rules/enet-reflex.md` |
52
+ | Windsurf | Appended to `.windsurfrules` |
53
+ | GitHub Copilot | `.github/copilot-instructions.md` |
54
+ | None detected | `.enet/reflex.md` |
55
+
56
+ Override with `--agent cursor\|windsurf\|copilot\|generic`.
57
+
58
+ ### See all available methods
59
+
60
+ ```bash
61
+ enet list
62
+ enet list --installed
63
+ ```
64
+
65
+ ### Project status and health
66
+
67
+ ```bash
68
+ enet status # installed methods + detected agent
69
+ enet doctor # full diagnostic — manifests, schema, agent, Node version
70
+ ```
71
+
72
+ ### Scaffold a new module
73
+
74
+ ```bash
75
+ enet new module products
76
+ enet new module activity-logger --section monitoring
77
+ enet new ui-pack dark-theme
78
+ enet new integration stripe --section billing
79
+ ```
80
+
81
+ Generates `manifest.json` + handler + README. Manifest first, always.
82
+
83
+ ### Create a manifest interactively
84
+
85
+ ```bash
86
+ enet init
87
+ enet init --json # print to stdout without writing
88
+ ```
89
+
90
+ ### Validate manifests
91
+
92
+ ```bash
93
+ enet validate # validate all modules in project
94
+ enet validate --all # recursive
95
+ enet validate --strict # warnings become errors
96
+ ```
97
+
98
+ ### Keep methods up to date
99
+
100
+ ```bash
101
+ enet update # update all installed methods
102
+ enet update reflex # update a specific method
103
+ ```
104
+
105
+ ---
106
+
107
+ ## How it works
108
+
109
+ 1. `enet install reflex` fetches `registry.json` from this repo
110
+ 2. Finds the repo for `reflex` (`exchanet/method_reflex`)
111
+ 3. Detects your agent (Cursor, Windsurf, Copilot...)
112
+ 4. Downloads the right adapter from GitHub in real time
113
+ 5. Writes it to the correct location in your project
114
+
115
+ The adapter content always comes live from the source repo. `enet update` re-fetches to get the latest version.
116
+
117
+ ---
118
+
119
+ ## The Registry
120
+
121
+ Methods are defined in [`registry.json`](./registry.json) in this repo. The CLI fetches it on every run — no local state, always current.
122
+
123
+ To add a new exchanet method to the registry, add an entry to `registry.json` and open a PR. No CLI code changes needed.
124
+
125
+ ```json
126
+ {
127
+ "methods": {
128
+ "your-method": {
129
+ "name": "Method Name",
130
+ "description": "What it does.",
131
+ "repo": "exchanet/method_your_repo",
132
+ "adapters": {
133
+ "cursor": "adapters/cursor.md",
134
+ "windsurf": "adapters/windsurf.md",
135
+ "copilot": "adapters/copilot.md",
136
+ "generic": "adapters/generic.md"
137
+ }
138
+ }
139
+ }
140
+ }
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Requirements
146
+
147
+ - Node.js 18 or higher
148
+ - An AI coding agent (Cursor, Windsurf, GitHub Copilot, or any agent)
149
+
150
+ ---
151
+
152
+ ## Repository Structure
153
+
154
+ ```
155
+ exchanet/enet/
156
+ ├── registry.json ← source of truth for all methods
157
+ ├── README.md
158
+ ├── package.json ← published as @exchanet/enet
159
+ └── src/
160
+ ├── index.js ← entry point, command definitions
161
+ ├── commands/
162
+ │ ├── install.js ← enet install
163
+ │ ├── list.js ← enet list
164
+ │ ├── init.js ← enet init
165
+ │ ├── validate.js ← enet validate
166
+ │ ├── new.js ← enet new
167
+ │ ├── update.js ← enet update
168
+ │ ├── status.js ← enet status
169
+ │ └── doctor.js ← enet doctor
170
+ └── utils/
171
+ ├── registry.js ← loads registry.json from GitHub, caches locally
172
+ └── agent-detector.js ← detects Cursor / Windsurf / Copilot
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Publishing
178
+
179
+ ```bash
180
+ cd enet
181
+ npm publish --access public
182
+ ```
183
+
184
+ ---
185
+
186
+ ## License
187
+
188
+ MIT — Francisco J Bernades ([@exchanet](https://github.com/exchanet))
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@exchanet/enet",
3
+ "version": "1.0.0",
4
+ "description": "enet — exchanet methods manager. Install, scaffold and manage AI coding methods.",
5
+ "bin": {
6
+ "enet": "./src/index.js"
7
+ },
8
+ "type": "module",
9
+ "scripts": {
10
+ "dev": "node src/index.js",
11
+ "test": "node --test"
12
+ },
13
+ "dependencies": {
14
+ "chalk": "^5.3.0",
15
+ "commander": "^12.0.0",
16
+ "enquirer": "^2.4.1",
17
+ "ora": "^8.0.1",
18
+ "node-fetch": "^3.3.2",
19
+ "ajv": "^8.12.0",
20
+ "ajv-formats": "^3.0.1",
21
+ "fs-extra": "^11.2.0"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ },
26
+ "keywords": [
27
+ "exchanet",
28
+ "method-reflex",
29
+ "method-pdca-t",
30
+ "modular-architecture",
31
+ "vibe-coding",
32
+ "cursor",
33
+ "windsurf",
34
+ "copilot",
35
+ "ai-coding"
36
+ ],
37
+ "author": "Francisco J Bernades (@exchanet)",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/exchanet/enet"
42
+ },
43
+ "homepage": "https://github.com/exchanet/enet"
44
+ }
package/registry.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "updated": "2025-01-01",
4
+ "methods": {
5
+ "reflex": {
6
+ "id": "reflex",
7
+ "name": "Method REFLEX",
8
+ "description": "Universal modular architecture. Auto-generated Admin Panel, zero hardcoded config, self-describing modules via manifest.json.",
9
+ "repo": "exchanet/method_reflex",
10
+ "version": "1.0.0",
11
+ "tags": ["architecture", "modular", "admin-panel", "manifest", "core"],
12
+ "adapters": {
13
+ "cursor": "adapters/cursor.md",
14
+ "windsurf": "adapters/windsurf.md",
15
+ "copilot": "adapters/copilot.md",
16
+ "generic": "adapters/generic.md"
17
+ },
18
+ "extras": {
19
+ "schema": "schemas/manifest.schema.json"
20
+ }
21
+ },
22
+ "pdca-t": {
23
+ "id": "pdca-t",
24
+ "name": "Method PDCA-T",
25
+ "description": "≥99% test coverage, zero vulnerabilities, systematic quality validation cycles. Use alongside REFLEX for pro-grade results.",
26
+ "repo": "exchanet/method_pdca-t_coding_Cursor",
27
+ "version": "1.0.0",
28
+ "tags": ["quality", "testing", "security", "coverage", "validation"],
29
+ "adapters": {
30
+ "cursor": ".cursor/rules/METHOD-PDCA-T.md",
31
+ "windsurf": ".cursor/rules/METHOD-PDCA-T.md",
32
+ "copilot": ".cursor/rules/METHOD-PDCA-T.md",
33
+ "generic": ".cursor/rules/METHOD-PDCA-T.md"
34
+ }
35
+ },
36
+ "iris": {
37
+ "id": "iris",
38
+ "name": "Method IRIS",
39
+ "description": "Continuous improvement of existing systems. Diagnose, refactor and evolve without breaking architecture.",
40
+ "repo": "exchanet/method_IRIS",
41
+ "version": "1.0.0",
42
+ "tags": ["refactoring", "improvement", "evolution", "maintenance"],
43
+ "adapters": {
44
+ "cursor": "adapters/cursor.md",
45
+ "windsurf": "adapters/windsurf.md",
46
+ "copilot": "adapters/copilot.md",
47
+ "generic": "adapters/generic.md"
48
+ }
49
+ },
50
+ "enterprise-builder": {
51
+ "id": "enterprise-builder",
52
+ "name": "Method Enterprise Builder",
53
+ "description": "Large-scale planning and architecture for complex projects. Define modules, dependencies and build order before writing code.",
54
+ "repo": "exchanet/method_enterprise_builder_planning",
55
+ "version": "1.0.0",
56
+ "tags": ["planning", "enterprise", "architecture", "scale"],
57
+ "adapters": {
58
+ "cursor": "adapters/cursor.md",
59
+ "windsurf": "adapters/windsurf.md",
60
+ "copilot": "adapters/copilot.md",
61
+ "generic": "adapters/generic.md"
62
+ }
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,127 @@
1
+ import chalk from 'chalk'
2
+ import fs from 'fs-extra'
3
+ import path from 'path'
4
+ import ora from 'ora'
5
+ import { detectAgent, getInstallPath } from '../utils/agent-detector.js'
6
+ import { getAllMethods } from '../utils/registry.js'
7
+
8
+ export async function doctorCommand() {
9
+ const cwd = process.cwd()
10
+
11
+ const spinner = ora('Running diagnostics...').start()
12
+ const [methods, agent] = await Promise.all([getAllMethods(), detectAgent()])
13
+ spinner.stop()
14
+
15
+ console.log(chalk.white(' ◆ enet doctor\n'))
16
+
17
+ const checks = []
18
+
19
+ // Agent detected
20
+ checks.push({
21
+ label: `AI agent detected (${agent.name})`,
22
+ pass: agent.key !== 'generic',
23
+ hint: 'Create .cursor/, .windsurfrules, or .github/ to auto-detect your agent.'
24
+ })
25
+
26
+ // At least one method installed
27
+ let installedCount = 0
28
+ for (const method of methods) {
29
+ if (await fs.pathExists(getInstallPath(agent, method.id))) installedCount++
30
+ }
31
+ checks.push({
32
+ label: `Methods installed (${installedCount}/${methods.length})`,
33
+ pass: installedCount > 0,
34
+ hint: `Run: enet install reflex`
35
+ })
36
+
37
+ // manifest.schema.json
38
+ const hasSchema = await fs.pathExists(path.join(cwd, 'manifest.schema.json'))
39
+ checks.push({
40
+ label: 'manifest.schema.json present',
41
+ pass: hasSchema,
42
+ hint: 'Run: enet install reflex (downloads schema automatically)'
43
+ })
44
+
45
+ // Modules directory
46
+ const modulesDir = await findModulesDir(cwd)
47
+ checks.push({
48
+ label: modulesDir
49
+ ? `Modules directory (${path.relative(cwd, modulesDir)})`
50
+ : 'Modules directory',
51
+ pass: !!modulesDir,
52
+ hint: 'Create a modules/ directory for your first module.'
53
+ })
54
+
55
+ // Manifests valid
56
+ if (hasSchema && modulesDir) {
57
+ const manifests = await findManifests(modulesDir)
58
+ if (manifests.length > 0) {
59
+ const { valid, invalid } = await quickValidate(path.join(cwd, 'manifest.schema.json'), manifests)
60
+ checks.push({
61
+ label: `Manifests valid (${valid}/${manifests.length})`,
62
+ pass: invalid === 0,
63
+ hint: 'Run: enet validate --all'
64
+ })
65
+ }
66
+ }
67
+
68
+ // Node version
69
+ const nodeVersion = parseInt(process.version.replace('v', '').split('.')[0])
70
+ checks.push({
71
+ label: `Node.js ${process.version}`,
72
+ pass: nodeVersion >= 18,
73
+ hint: 'Upgrade to Node.js 18 or higher.'
74
+ })
75
+
76
+ // Print
77
+ let allPass = true
78
+ for (const check of checks) {
79
+ const icon = check.pass ? chalk.green('✓') : chalk.red('✗')
80
+ console.log(` ${icon} ${check.pass ? chalk.white(check.label) : chalk.dim(check.label)}`)
81
+ if (!check.pass && check.hint) {
82
+ console.log(chalk.dim(` → ${check.hint}`))
83
+ }
84
+ if (!check.pass) allPass = false
85
+ }
86
+
87
+ console.log()
88
+ if (allPass) {
89
+ console.log(chalk.green(' ✓ Everything looks good!\n'))
90
+ } else {
91
+ console.log(chalk.yellow(' ⚠ Some issues found. Follow the hints above.\n'))
92
+ }
93
+ }
94
+
95
+ async function findModulesDir(cwd) {
96
+ for (const dir of ['modules', 'packs', 'src/modules']) {
97
+ const full = path.join(cwd, dir)
98
+ if (await fs.pathExists(full)) return full
99
+ }
100
+ return null
101
+ }
102
+
103
+ async function findManifests(dir) {
104
+ const results = []
105
+ const entries = await fs.readdir(dir)
106
+ for (const entry of entries) {
107
+ const p = path.join(dir, entry, 'manifest.json')
108
+ if (await fs.pathExists(p)) results.push(p)
109
+ }
110
+ return results
111
+ }
112
+
113
+ async function quickValidate(schemaPath, manifests) {
114
+ const { default: Ajv } = await import('ajv')
115
+ const { default: addFormats } = await import('ajv-formats')
116
+ const schema = await fs.readJson(schemaPath)
117
+ const ajv = new Ajv({ allErrors: false })
118
+ addFormats(ajv)
119
+ const validate = ajv.compile(schema)
120
+ let valid = 0, invalid = 0
121
+ for (const p of manifests) {
122
+ try {
123
+ validate(await fs.readJson(p)) ? valid++ : invalid++
124
+ } catch { invalid++ }
125
+ }
126
+ return { valid, invalid }
127
+ }
@@ -0,0 +1,115 @@
1
+ import chalk from 'chalk'
2
+ import fs from 'fs-extra'
3
+ import path from 'path'
4
+ import { prompt } from 'enquirer'
5
+
6
+ export async function initCommand(options) {
7
+ console.log(chalk.white(' Creating manifest.json\n'))
8
+ console.log(chalk.dim(' The manifest is the module. Complete it before writing any code.\n'))
9
+
10
+ try {
11
+ const basic = await prompt([
12
+ { type: 'input', name: 'name', message: 'Module name:', validate: v => v.length > 1 || 'Required' },
13
+ { type: 'input', name: 'id', message: 'Module ID (kebab-case):', validate: v => /^[a-z][a-z0-9-]*$/.test(v) || 'kebab-case only' },
14
+ { type: 'select', name: 'type', message: 'Type:', choices: ['functional', 'integration', 'ui', 'core'] },
15
+ { type: 'input', name: 'section', message: 'Admin section (e.g. monitoring):', validate: v => /^[a-z][a-z0-9-]*$/.test(v) || 'kebab-case only' }
16
+ ])
17
+
18
+ // Settings
19
+ const settings = {}
20
+ const { addSettings } = await prompt({ type: 'confirm', name: 'addSettings', message: 'Add configurable settings?', initial: true })
21
+
22
+ if (addSettings) {
23
+ let more = true
24
+ while (more) {
25
+ const { key } = await prompt({ type: 'input', name: 'key', message: ' Setting key (empty to finish):' })
26
+ if (!key) break
27
+
28
+ const details = await prompt([
29
+ { type: 'input', name: 'label', message: ' Label:' },
30
+ { type: 'select', name: 'type', message: ' Type:', choices: ['integer', 'string', 'boolean', 'select'] },
31
+ { type: 'select', name: 'ui', message: ' Widget:', choices: ['text', 'number', 'slider', 'toggle', 'select', 'textarea'] },
32
+ { type: 'input', name: 'default', message: ' Default:' }
33
+ ])
34
+
35
+ settings[key] = {
36
+ type: details.type,
37
+ label: details.label,
38
+ default: parseDefault(details.default, details.type),
39
+ ui: details.ui
40
+ }
41
+
42
+ const { cont } = await prompt({ type: 'confirm', name: 'cont', message: ' Add another?', initial: false })
43
+ more = cont
44
+ }
45
+ }
46
+
47
+ // Capabilities
48
+ const capabilities = []
49
+ const { addCaps } = await prompt({ type: 'confirm', name: 'addCaps', message: 'Add capabilities?', initial: true })
50
+
51
+ if (addCaps) {
52
+ let more = true
53
+ while (more) {
54
+ const cap = await prompt([
55
+ { type: 'select', name: 'type', message: ' Type:', choices: ['view', 'action', 'metric', 'widget', 'page'] },
56
+ { type: 'input', name: 'label', message: ' Label:', validate: v => v.length > 0 || 'Required' },
57
+ { type: 'input', name: 'handler', message: ' Handler (ClassName.method):', validate: v => /^[A-Z][a-zA-Z0-9]*\.[a-z][a-zA-Z0-9]*$/.test(v) || 'Format: ClassName.method' }
58
+ ])
59
+
60
+ const capability = { type: cap.type, label: cap.label }
61
+ if (cap.type === 'action') {
62
+ capability.handler = cap.handler
63
+ const { dangerous } = await prompt({ type: 'confirm', name: 'dangerous', message: ' Mark as dangerous?', initial: false })
64
+ if (dangerous) capability.dangerous = true
65
+ } else {
66
+ capability.data = cap.handler
67
+ }
68
+ capabilities.push(capability)
69
+
70
+ const { cont } = await prompt({ type: 'confirm', name: 'cont', message: ' Add another?', initial: false })
71
+ more = cont
72
+ }
73
+ }
74
+
75
+ const manifest = {
76
+ id: basic.id,
77
+ name: basic.name,
78
+ version: '1.0.0',
79
+ type: basic.type,
80
+ section: basic.section,
81
+ dependencies: [],
82
+ hooks: {},
83
+ ...(Object.keys(settings).length > 0 && { settings }),
84
+ ...(capabilities.length > 0 && { capabilities })
85
+ }
86
+
87
+ const json = JSON.stringify(manifest, null, 2)
88
+
89
+ if (options.json) { console.log('\n' + json + '\n'); return }
90
+
91
+ const { confirm } = await prompt({ type: 'confirm', name: 'confirm', message: 'Write manifest.json here?', initial: true })
92
+ if (!confirm) { console.log('\n' + json + '\n'); return }
93
+
94
+ const outPath = path.join(process.cwd(), 'manifest.json')
95
+ if (await fs.pathExists(outPath)) {
96
+ const { overwrite } = await prompt({ type: 'confirm', name: 'overwrite', message: chalk.yellow('manifest.json exists. Overwrite?'), initial: false })
97
+ if (!overwrite) { console.log(chalk.dim('\n Cancelled.\n')); return }
98
+ }
99
+
100
+ await fs.writeJson(outPath, manifest, { spaces: 2 })
101
+ console.log(chalk.green('\n ✓ manifest.json created\n'))
102
+ console.log(chalk.dim(` Implement the handlers, then run ${chalk.white('enet validate')}.\n`))
103
+
104
+ } catch (err) {
105
+ if (err === '') { console.log(chalk.dim('\n Cancelled.\n')); return }
106
+ console.log(chalk.red(`\n Error: ${err.message}\n`))
107
+ process.exit(1)
108
+ }
109
+ }
110
+
111
+ function parseDefault(value, type) {
112
+ if (type === 'integer') return parseInt(value) || 0
113
+ if (type === 'boolean') return value === 'true'
114
+ return value
115
+ }
@@ -0,0 +1,104 @@
1
+ import chalk from 'chalk'
2
+ import ora from 'ora'
3
+ import fs from 'fs-extra'
4
+ import path from 'path'
5
+ import { detectAgent, getInstallPath, AGENTS } from '../utils/agent-detector.js'
6
+ import { getMethod } from '../utils/registry.js'
7
+ import { fetchFromGitHub } from '../utils/registry.js'
8
+
9
+ export async function installCommand(methodId, options) {
10
+ const spinner = ora('Fetching registry...').start()
11
+ const method = await getMethod(methodId).catch(() => null)
12
+ spinner.stop()
13
+
14
+ if (!method) {
15
+ console.log(chalk.red(` ✗ Unknown method: "${methodId}"`))
16
+ console.log(chalk.dim(` Run ${chalk.white('enet list')} to see available methods.\n`))
17
+ process.exit(1)
18
+ }
19
+
20
+ // Detect or force agent
21
+ let agent
22
+ if (options.agent) {
23
+ if (!AGENTS[options.agent]) {
24
+ console.log(chalk.red(` ✗ Unknown agent: "${options.agent}"`))
25
+ console.log(chalk.dim(` Valid: cursor, windsurf, copilot, generic\n`))
26
+ process.exit(1)
27
+ }
28
+ agent = { key: options.agent, ...AGENTS[options.agent] }
29
+ } else {
30
+ agent = await detectAgent()
31
+ }
32
+
33
+ console.log(chalk.dim(` Method : ${chalk.white(method.name)}`))
34
+ console.log(chalk.dim(` Agent : ${chalk.white(agent.name)}${options.agent ? '' : chalk.dim(' (auto-detected)')}`))
35
+ console.log(chalk.dim(` Source : ${chalk.white(`github.com/${method.repo}`)}\n`))
36
+
37
+ const fetchSpinner = ora(`Fetching adapter...`).start()
38
+
39
+ try {
40
+ const adapterPath = method.adapters[agent.key] ?? method.adapters.generic
41
+ const content = await fetchFromGitHub(method.repo, adapterPath)
42
+
43
+ const installPath = options.global
44
+ ? path.join(process.env.HOME || process.env.USERPROFILE, '.enet', `${methodId}.md`)
45
+ : getInstallPath(agent, methodId)
46
+
47
+ await fs.ensureDir(path.dirname(installPath))
48
+
49
+ // Windsurf appends, others overwrite
50
+ if (agent.key === 'windsurf' && await fs.pathExists(installPath)) {
51
+ const existing = await fs.readFile(installPath, 'utf8')
52
+ if (existing.includes(method.name)) {
53
+ fetchSpinner.warn(chalk.yellow(`${method.name} already installed`))
54
+ return
55
+ }
56
+ await fs.appendFile(installPath, `\n\n---\n\n${content}`)
57
+ } else {
58
+ await fs.writeFile(installPath, content)
59
+ }
60
+
61
+ fetchSpinner.succeed(chalk.green(`${method.name} installed`))
62
+ console.log(chalk.dim(` → ${path.relative(process.cwd(), installPath)}`))
63
+ console.log(chalk.dim(` ${agent.configNote}\n`))
64
+
65
+ // Download extras (e.g. manifest.schema.json for reflex)
66
+ if (method.extras) {
67
+ for (const [key, filePath] of Object.entries(method.extras)) {
68
+ const extraSpinner = ora(`Fetching ${key}...`).start()
69
+ try {
70
+ const extraContent = await fetchFromGitHub(method.repo, filePath)
71
+ const extraOut = path.join(process.cwd(), path.basename(filePath))
72
+ await fs.writeFile(extraOut, extraContent)
73
+ extraSpinner.succeed(chalk.dim(`${key} → ${path.basename(filePath)}`))
74
+ } catch {
75
+ extraSpinner.warn(chalk.dim(`${key} not available (non-critical)`))
76
+ }
77
+ }
78
+ console.log()
79
+ }
80
+
81
+ printHints(methodId)
82
+
83
+ } catch (err) {
84
+ fetchSpinner.fail(chalk.red(`Failed: ${err.message}`))
85
+ console.log(chalk.dim(' Check your internet connection and try again.\n'))
86
+ process.exit(1)
87
+ }
88
+ }
89
+
90
+ function printHints(methodId) {
91
+ if (methodId === 'reflex') {
92
+ console.log(chalk.dim(' Next:'))
93
+ console.log(chalk.dim(` 1. Give your agent a spectech (stack + modules needed)`))
94
+ console.log(chalk.dim(` 2. Agent declares architecture — confirm it`))
95
+ console.log(chalk.dim(` 3. Agent builds Core → modules → Admin Panel`))
96
+ console.log()
97
+ console.log(chalk.dim(` ${chalk.white('enet new module <name>')} scaffold your first module`))
98
+ console.log(chalk.dim(` ${chalk.white('enet validate')} check manifests at any time\n`))
99
+ }
100
+ if (methodId === 'pdca-t') {
101
+ console.log(chalk.dim(` PDCA-T adds quality validation to your workflow.`))
102
+ console.log(chalk.dim(` Works best alongside ${chalk.white('enet install reflex')}.\n`))
103
+ }
104
+ }
@@ -0,0 +1,56 @@
1
+ import chalk from 'chalk'
2
+ import ora from 'ora'
3
+ import fs from 'fs-extra'
4
+ import { getAllMethods } from '../utils/registry.js'
5
+ import { detectAgent, getInstallPath } from '../utils/agent-detector.js'
6
+
7
+ export async function listCommand(options) {
8
+ const spinner = ora('Fetching registry...').start()
9
+
10
+ let methods
11
+ try {
12
+ methods = await getAllMethods()
13
+ spinner.stop()
14
+ } catch (err) {
15
+ spinner.fail(chalk.red(`Could not load registry: ${err.message}`))
16
+ process.exit(1)
17
+ }
18
+
19
+ const agent = await detectAgent()
20
+
21
+ console.log(chalk.dim(` Agent: ${chalk.white(agent.name)}\n`))
22
+
23
+ if (options.installed) {
24
+ console.log(chalk.white(' Installed methods:\n'))
25
+ } else {
26
+ console.log(chalk.white(` Available methods (${methods.length}):\n`))
27
+ }
28
+
29
+ let shown = 0
30
+
31
+ for (const method of methods) {
32
+ const installPath = getInstallPath(agent, method.id)
33
+ const isInstalled = await fs.pathExists(installPath)
34
+
35
+ if (options.installed && !isInstalled) continue
36
+
37
+ const status = isInstalled ? chalk.green('✓ installed') : chalk.dim('○ available')
38
+ const tags = method.tags.map(t => chalk.dim(`#${t}`)).join(' ')
39
+
40
+ console.log(` ${chalk.white(method.id.padEnd(24))} ${status}`)
41
+ console.log(` ${chalk.dim(method.description)}`)
42
+ console.log(` ${tags}\n`)
43
+ shown++
44
+ }
45
+
46
+ if (shown === 0 && options.installed) {
47
+ console.log(chalk.dim(' No methods installed yet.\n'))
48
+ console.log(chalk.dim(` Run ${chalk.white('enet install reflex')} to get started.\n`))
49
+ return
50
+ }
51
+
52
+ if (!options.installed) {
53
+ console.log(chalk.dim(` Install: ${chalk.white('enet install <method>')}`))
54
+ console.log(chalk.dim(` Example: ${chalk.white('enet install reflex')}\n`))
55
+ }
56
+ }
@@ -0,0 +1,134 @@
1
+ import chalk from 'chalk'
2
+ import fs from 'fs-extra'
3
+ import path from 'path'
4
+
5
+ export async function newCommand(type, name, options) {
6
+ const VALID = ['module', 'ui-pack', 'integration']
7
+ if (!VALID.includes(type)) {
8
+ console.log(chalk.red(` ✗ Unknown type: "${type}". Valid: ${VALID.join(', ')}\n`))
9
+ process.exit(1)
10
+ }
11
+
12
+ const id = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')
13
+ const section = options.section || (type === 'ui-pack' ? 'appearance' : 'modules')
14
+ const cwd = process.cwd()
15
+ const modulesDir = await findModulesDir(cwd)
16
+ const targetDir = path.join(modulesDir, id)
17
+
18
+ if (await fs.pathExists(targetDir)) {
19
+ console.log(chalk.red(` ✗ Already exists: ${path.relative(cwd, targetDir)}\n`))
20
+ process.exit(1)
21
+ }
22
+
23
+ const files = buildScaffold(type, id, name, section)
24
+
25
+ if (options.dryRun) {
26
+ console.log(chalk.dim('\n Files that would be created:\n'))
27
+ Object.keys(files).forEach(f => console.log(chalk.dim(` ${path.relative(cwd, path.join(targetDir, f))}`)))
28
+ console.log()
29
+ return
30
+ }
31
+
32
+ for (const [filePath, content] of Object.entries(files)) {
33
+ const full = path.join(targetDir, filePath)
34
+ await fs.ensureDir(path.dirname(full))
35
+ await fs.writeFile(full, content)
36
+ }
37
+
38
+ const rel = path.relative(cwd, targetDir)
39
+ console.log(chalk.green(`\n ✓ ${type} "${name}" scaffolded\n`))
40
+ Object.keys(files).forEach(f => console.log(chalk.dim(` ${rel}/${f}`)))
41
+ console.log()
42
+ console.log(chalk.dim(' Next:'))
43
+ console.log(chalk.dim(` 1. Complete ${chalk.white(`${rel}/manifest.json`)}`))
44
+ console.log(chalk.dim(` 2. Implement the handlers`))
45
+ console.log(chalk.dim(` 3. Run ${chalk.white('enet validate')}\n`))
46
+ }
47
+
48
+ function buildScaffold(type, id, name, section) {
49
+ const cls = toPascal(id)
50
+ if (type === 'module') return {
51
+ 'manifest.json': JSON.stringify({
52
+ id, name, version: '1.0.0', type: 'functional', section,
53
+ dependencies: [], hooks: {},
54
+ settings: {
55
+ enabled: { type: 'boolean', label: 'Enable module', default: true, ui: 'toggle' }
56
+ },
57
+ capabilities: [
58
+ { type: 'view', label: `${name} List`, data: `${cls}.getAll` },
59
+ { type: 'metric', label: `Total ${name}`, data: `${cls}.count` }
60
+ ]
61
+ }, null, 2),
62
+ [`handlers/${id}.js`]: `export class ${cls} {
63
+ constructor(context) { this.ctx = context; this.db = context.db }
64
+
65
+ async getAll(ctx, { page = 1, limit = 20 } = {}) {
66
+ // TODO: implement
67
+ return []
68
+ }
69
+
70
+ async count(ctx) {
71
+ // TODO: implement
72
+ return 0
73
+ }
74
+ }
75
+ `,
76
+ 'README.md': `# ${name}\n\nDescribe this module.\n`
77
+ }
78
+
79
+ if (type === 'ui-pack') return {
80
+ 'manifest.json': JSON.stringify({
81
+ id, name, version: '1.0.0', type: 'ui', section: 'appearance',
82
+ dependencies: [],
83
+ capabilities: [{
84
+ type: 'theme', label: `${name} Theme`,
85
+ variables: { 'primary-color': '#6366f1', 'font-family': 'Inter, sans-serif', 'border-radius': '8px' }
86
+ }]
87
+ }, null, 2),
88
+ 'styles/theme.css': `:root {\n --primary-color: #6366f1;\n --font-family: Inter, sans-serif;\n --border-radius: 8px;\n}\n`,
89
+ 'README.md': `# ${name} UI Pack\n\nActivate from Admin Panel → Appearance.\n`
90
+ }
91
+
92
+ if (type === 'integration') return {
93
+ 'manifest.json': JSON.stringify({
94
+ id, name, version: '1.0.0', type: 'integration', section,
95
+ dependencies: [], hooks: {},
96
+ settings: {
97
+ api_key: { type: 'string', label: 'API Key', default: '', ui: 'text' },
98
+ enabled: { type: 'boolean', label: 'Enable', default: false, ui: 'toggle' }
99
+ },
100
+ capabilities: [
101
+ { type: 'action', label: 'Test Connection', handler: `${cls}.testConnection` },
102
+ { type: 'metric', label: 'Status', data: `${cls}.getStatus` }
103
+ ]
104
+ }, null, 2),
105
+ [`handlers/${id}.js`]: `export class ${cls} {
106
+ constructor(context) { this.ctx = context }
107
+
108
+ get apiKey() { return this.ctx.settings.get('api_key') }
109
+
110
+ async testConnection(ctx) {
111
+ if (!this.apiKey) return { success: false, message: 'API key not configured' }
112
+ // TODO: implement
113
+ return { success: true, message: 'Connected' }
114
+ }
115
+
116
+ async getStatus(ctx) {
117
+ return this.ctx.settings.get('enabled') ? 'active' : 'disabled'
118
+ }
119
+ }
120
+ `,
121
+ 'README.md': `# ${name} Integration\n\nConfigure API Key in Admin Panel → ${section}.\n`
122
+ }
123
+ }
124
+
125
+ function toPascal(str) {
126
+ return str.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('')
127
+ }
128
+
129
+ async function findModulesDir(cwd) {
130
+ for (const dir of ['modules', 'packs', 'src/modules']) {
131
+ if (await fs.pathExists(path.join(cwd, dir))) return path.join(cwd, dir)
132
+ }
133
+ return path.join(cwd, 'modules')
134
+ }
@@ -0,0 +1,60 @@
1
+ import chalk from 'chalk'
2
+ import fs from 'fs-extra'
3
+ import path from 'path'
4
+ import ora from 'ora'
5
+ import { getAllMethods } from '../utils/registry.js'
6
+ import { detectAgent, getInstallPath } from '../utils/agent-detector.js'
7
+
8
+ export async function statusCommand() {
9
+ const cwd = process.cwd()
10
+
11
+ const spinner = ora('Loading...').start()
12
+ const [methods, agent] = await Promise.all([getAllMethods(), detectAgent()])
13
+ spinner.stop()
14
+
15
+ console.log(chalk.white(' Project Status\n'))
16
+ console.log(chalk.dim(` Directory : ${cwd}`))
17
+ console.log(chalk.dim(` Agent : ${chalk.white(agent.name)}\n`))
18
+
19
+ console.log(chalk.dim(' Installed methods:\n'))
20
+
21
+ let any = false
22
+ for (const method of methods) {
23
+ const installPath = getInstallPath(agent, method.id)
24
+ if (await fs.pathExists(installPath)) {
25
+ console.log(` ${chalk.green('✓')} ${chalk.white(method.name)}`)
26
+ console.log(chalk.dim(` ${path.relative(cwd, installPath)}\n`))
27
+ any = true
28
+ }
29
+ }
30
+
31
+ if (!any) {
32
+ console.log(chalk.dim(` None. Run ${chalk.white('enet install reflex')} to get started.\n`))
33
+ return
34
+ }
35
+
36
+ const hasSchema = await fs.pathExists(path.join(cwd, 'manifest.schema.json'))
37
+ console.log(` ${hasSchema ? chalk.green('✓') : chalk.dim('○')} manifest.schema.json`)
38
+
39
+ const moduleCount = await countModules(cwd)
40
+ if (moduleCount > 0) {
41
+ console.log(` ${chalk.green('✓')} ${moduleCount} module${moduleCount > 1 ? 's' : ''} found`)
42
+ }
43
+
44
+ console.log()
45
+ console.log(chalk.dim(` ${chalk.white('enet validate')} check all manifests`))
46
+ console.log(chalk.dim(` ${chalk.white('enet doctor')} full health check\n`))
47
+ }
48
+
49
+ async function countModules(cwd) {
50
+ let count = 0
51
+ for (const dir of ['modules', 'packs', 'src/modules']) {
52
+ const full = path.join(cwd, dir)
53
+ if (!await fs.pathExists(full)) continue
54
+ const entries = await fs.readdir(full)
55
+ for (const e of entries) {
56
+ if (await fs.pathExists(path.join(full, e, 'manifest.json'))) count++
57
+ }
58
+ }
59
+ return count
60
+ }
@@ -0,0 +1,47 @@
1
+ import chalk from 'chalk'
2
+ import ora from 'ora'
3
+ import fs from 'fs-extra'
4
+ import { getAllMethods, getMethod, fetchFromGitHub } from '../utils/registry.js'
5
+ import { detectAgent, getInstallPath } from '../utils/agent-detector.js'
6
+
7
+ export async function updateCommand(methodId, options) {
8
+ const spinner = ora('Fetching registry...').start()
9
+ const [allMethods, agent] = await Promise.all([getAllMethods(), detectAgent()])
10
+ spinner.stop()
11
+
12
+ const targets = methodId
13
+ ? [(await getMethod(methodId))].filter(Boolean)
14
+ : allMethods
15
+
16
+ if (methodId && targets.length === 0) {
17
+ console.log(chalk.red(` ✗ Unknown method: "${methodId}"\n`))
18
+ process.exit(1)
19
+ }
20
+
21
+ console.log(chalk.white(`\n Updating methods...\n`))
22
+
23
+ let updated = 0, skipped = 0
24
+
25
+ for (const method of targets) {
26
+ const installPath = getInstallPath(agent, method.id)
27
+ if (!await fs.pathExists(installPath)) { skipped++; continue }
28
+
29
+ const s = ora(`Updating ${method.name}...`).start()
30
+ try {
31
+ const adapterPath = method.adapters[agent.key] ?? method.adapters.generic
32
+ const content = await fetchFromGitHub(method.repo, adapterPath)
33
+ await fs.writeFile(installPath, content)
34
+ s.succeed(chalk.green(`${method.name} updated`))
35
+ updated++
36
+ } catch (err) {
37
+ s.fail(chalk.yellow(`${method.name} — ${err.message}`))
38
+ }
39
+ }
40
+
41
+ console.log()
42
+ if (updated === 0 && skipped > 0) {
43
+ console.log(chalk.dim(` No methods installed to update.\n`))
44
+ } else {
45
+ console.log(chalk.dim(` ${updated} updated, ${skipped} not installed\n`))
46
+ }
47
+ }
@@ -0,0 +1,105 @@
1
+ import chalk from 'chalk'
2
+ import fs from 'fs-extra'
3
+ import path from 'path'
4
+ import Ajv from 'ajv'
5
+ import addFormats from 'ajv-formats'
6
+
7
+ export async function validateCommand(targetPath, options) {
8
+ const cwd = process.cwd()
9
+ const schemaPath = path.join(cwd, 'manifest.schema.json')
10
+
11
+ if (!await fs.pathExists(schemaPath)) {
12
+ console.log(chalk.red('\n ✗ manifest.schema.json not found'))
13
+ console.log(chalk.dim(` Run ${chalk.white('enet install reflex')} to install it.\n`))
14
+ process.exit(1)
15
+ }
16
+
17
+ const schema = await fs.readJson(schemaPath)
18
+ const ajv = new Ajv({ allErrors: true })
19
+ addFormats(ajv)
20
+ const validate = ajv.compile(schema)
21
+
22
+ let manifests = []
23
+
24
+ if (targetPath) {
25
+ const p = path.resolve(cwd, targetPath, 'manifest.json')
26
+ if (await fs.pathExists(p)) manifests = [p]
27
+ else { console.log(chalk.red(`\n ✗ No manifest.json at ${targetPath}\n`)); process.exit(1) }
28
+ } else {
29
+ manifests = await findAllManifests(cwd)
30
+ if (manifests.length === 0) {
31
+ const local = path.join(cwd, 'manifest.json')
32
+ if (await fs.pathExists(local)) manifests = [local]
33
+ }
34
+ }
35
+
36
+ if (manifests.length === 0) {
37
+ console.log(chalk.dim('\n No manifest.json files found.'))
38
+ console.log(chalk.dim(` Run ${chalk.white('enet init')} to create one.\n`))
39
+ return
40
+ }
41
+
42
+ console.log(chalk.white(`\n Validating ${manifests.length} manifest${manifests.length > 1 ? 's' : ''}...\n`))
43
+
44
+ let passed = 0, failed = 0
45
+
46
+ for (const manifestPath of manifests) {
47
+ const rel = path.relative(cwd, manifestPath)
48
+ let data
49
+ try {
50
+ data = await fs.readJson(manifestPath)
51
+ } catch (e) {
52
+ console.log(` ${chalk.red('✗')} ${chalk.white(rel)}`)
53
+ console.log(chalk.red(` Invalid JSON: ${e.message}\n`))
54
+ failed++; continue
55
+ }
56
+
57
+ const valid = validate(data)
58
+ const warnings = semanticChecks(data)
59
+
60
+ if (valid) {
61
+ console.log(` ${chalk.green('✓')} ${chalk.white(rel)} ${chalk.dim(`— ${data.name} v${data.version}`)}`)
62
+ warnings.forEach(w => console.log(chalk.dim(` ⚠ ${w}`)))
63
+ if (options.strict && warnings.length > 0) failed++
64
+ else passed++
65
+ } else {
66
+ console.log(` ${chalk.red('✗')} ${chalk.white(rel)} ${chalk.dim(`— ${data.name || 'unknown'}`)}`)
67
+ validate.errors.forEach(err => {
68
+ console.log(chalk.red(` ✗ ${err.instancePath || err.schemaPath}: ${err.message}`))
69
+ })
70
+ failed++
71
+ }
72
+ console.log()
73
+ }
74
+
75
+ console.log(chalk.dim(' ' + '─'.repeat(40)))
76
+ if (failed === 0) {
77
+ console.log(chalk.green(` ✓ All ${passed} manifest${passed > 1 ? 's' : ''} valid\n`))
78
+ } else {
79
+ console.log(chalk.red(` ✗ ${failed} failed`) + chalk.dim(`, ${passed} passed\n`))
80
+ process.exit(1)
81
+ }
82
+ }
83
+
84
+ function semanticChecks(m) {
85
+ const w = []
86
+ if (!m.capabilities?.length) w.push('No capabilities — module invisible in Admin Panel')
87
+ if (m.settings && Object.keys(m.settings).length === 0) w.push('Empty settings object')
88
+ const unprotected = (m.capabilities || []).filter(c => c.type === 'action' && !c.permissions?.length)
89
+ if (unprotected.length) w.push(`${unprotected.length} action(s) without permissions`)
90
+ return w
91
+ }
92
+
93
+ async function findAllManifests(cwd) {
94
+ const results = []
95
+ for (const dir of ['modules', 'packs', 'src/modules']) {
96
+ const full = path.join(cwd, dir)
97
+ if (!await fs.pathExists(full)) continue
98
+ const entries = await fs.readdir(full)
99
+ for (const entry of entries) {
100
+ const p = path.join(full, entry, 'manifest.json')
101
+ if (await fs.pathExists(p)) results.push(p)
102
+ }
103
+ }
104
+ return results
105
+ }
package/src/index.js ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander'
4
+ import chalk from 'chalk'
5
+ import { installCommand } from './commands/install.js'
6
+ import { listCommand } from './commands/list.js'
7
+ import { initCommand } from './commands/init.js'
8
+ import { validateCommand } from './commands/validate.js'
9
+ import { newCommand } from './commands/new.js'
10
+ import { updateCommand } from './commands/update.js'
11
+ import { statusCommand } from './commands/status.js'
12
+ import { doctorCommand } from './commands/doctor.js'
13
+
14
+ const VERSION = '1.0.0'
15
+
16
+ console.log(chalk.cyan(`\n◆ enet v${VERSION} — exchanet methods manager\n`))
17
+
18
+ program
19
+ .name('enet')
20
+ .description('Install, scaffold and manage exchanet AI coding methods')
21
+ .version(VERSION)
22
+
23
+ program
24
+ .command('install <method>')
25
+ .description('Install a method into the current project')
26
+ .option('-a, --agent <agent>', 'Force agent: cursor | windsurf | copilot | generic')
27
+ .option('-g, --global', 'Install globally to home directory')
28
+ .action(installCommand)
29
+
30
+ program
31
+ .command('list')
32
+ .alias('ls')
33
+ .description('List all available methods')
34
+ .option('--installed', 'Show only installed methods')
35
+ .action(listCommand)
36
+
37
+ program
38
+ .command('init')
39
+ .description('Interactively create a manifest.json for a new module')
40
+ .option('-n, --name <name>', 'Module name')
41
+ .option('-s, --section <section>', 'Admin Panel section')
42
+ .option('--json', 'Print manifest as JSON without writing to disk')
43
+ .action(initCommand)
44
+
45
+ program
46
+ .command('validate [path]')
47
+ .description('Validate manifest.json files against the schema')
48
+ .option('-a, --all', 'Validate all modules recursively')
49
+ .option('--strict', 'Treat warnings as errors')
50
+ .action(validateCommand)
51
+
52
+ program
53
+ .command('new <type> <name>')
54
+ .description('Scaffold a new module, ui-pack or integration')
55
+ .option('-s, --section <section>', 'Admin Panel section')
56
+ .option('--dry-run', 'Preview files without writing')
57
+ .action(newCommand)
58
+
59
+ program
60
+ .command('update [method]')
61
+ .description('Update installed methods to latest version')
62
+ .option('--all', 'Update all installed methods')
63
+ .action(updateCommand)
64
+
65
+ program
66
+ .command('status')
67
+ .description('Show installed methods and detected agent')
68
+ .action(statusCommand)
69
+
70
+ program
71
+ .command('doctor')
72
+ .description('Diagnose project setup — manifests, methods, agent config')
73
+ .action(doctorCommand)
74
+
75
+ program.parse()
@@ -0,0 +1,56 @@
1
+ import fs from 'fs-extra'
2
+ import path from 'path'
3
+
4
+ export const AGENTS = {
5
+ cursor: {
6
+ name: 'Cursor',
7
+ signals: ['.cursor'],
8
+ installDir: '.cursor/rules',
9
+ filename: 'enet-{id}.md',
10
+ configNote: 'Rule auto-applies to all files (alwaysApply: true)'
11
+ },
12
+ windsurf: {
13
+ name: 'Windsurf',
14
+ signals: ['.windsurfrules', '.windsurf'],
15
+ installDir: '.',
16
+ filename: '.windsurfrules',
17
+ configNote: 'Appended to .windsurfrules'
18
+ },
19
+ copilot: {
20
+ name: 'GitHub Copilot',
21
+ signals: ['.github/copilot-instructions.md', '.github'],
22
+ installDir: '.github',
23
+ filename: 'copilot-instructions.md',
24
+ configNote: 'Written to .github/copilot-instructions.md'
25
+ },
26
+ generic: {
27
+ name: 'Generic Agent',
28
+ signals: [],
29
+ installDir: '.enet',
30
+ filename: '{id}.md',
31
+ configNote: 'Saved to .enet/ — paste contents into your agent\'s context'
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Detects which AI agent is active in the current project.
37
+ */
38
+ export async function detectAgent(cwd = process.cwd()) {
39
+ for (const [key, agent] of Object.entries(AGENTS)) {
40
+ if (key === 'generic') continue
41
+ for (const signal of agent.signals) {
42
+ if (await fs.pathExists(path.join(cwd, signal))) {
43
+ return { key, ...agent }
44
+ }
45
+ }
46
+ }
47
+ return { key: 'generic', ...AGENTS.generic }
48
+ }
49
+
50
+ /**
51
+ * Returns the full install path for a method adapter.
52
+ */
53
+ export function getInstallPath(agent, methodId, cwd = process.cwd()) {
54
+ const filename = agent.filename.replace('{id}', methodId)
55
+ return path.join(cwd, agent.installDir, filename)
56
+ }
@@ -0,0 +1,85 @@
1
+ import fetch from 'node-fetch'
2
+ import fs from 'fs-extra'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const RAW_BASE = 'https://raw.githubusercontent.com'
8
+ const REGISTRY_URL = `${RAW_BASE}/exchanet/enet/main/registry.json`
9
+ const CACHE_FILE = path.join(__dirname, '../../.registry-cache.json')
10
+ const CACHE_TTL_MS = 1000 * 60 * 60 // 1 hour
11
+
12
+ // ── Registry ──────────────────────────────────────────────────────────────────
13
+
14
+ /**
15
+ * Loads the registry from:
16
+ * 1. Remote GitHub (exchanet/enet/registry.json) — always fresh
17
+ * 2. Local cache if remote fails — fallback
18
+ * 3. Bundled registry.json in the package — last resort
19
+ */
20
+ export async function loadRegistry() {
21
+ // Try remote first
22
+ try {
23
+ const res = await fetch(REGISTRY_URL, { timeout: 5000 })
24
+ if (res.ok) {
25
+ const data = await res.json()
26
+ // Save to cache for offline fallback
27
+ await fs.writeJson(CACHE_FILE, { ...data, _cachedAt: Date.now() }).catch(() => {})
28
+ return data
29
+ }
30
+ } catch {
31
+ // Network unavailable — fall through to cache
32
+ }
33
+
34
+ // Try local cache
35
+ try {
36
+ if (await fs.pathExists(CACHE_FILE)) {
37
+ const cached = await fs.readJson(CACHE_FILE)
38
+ const age = Date.now() - (cached._cachedAt || 0)
39
+ if (age < CACHE_TTL_MS * 24) { // Accept cache up to 24h when offline
40
+ return cached
41
+ }
42
+ }
43
+ } catch {
44
+ // Cache corrupted — fall through to bundled
45
+ }
46
+
47
+ // Fallback to bundled registry.json
48
+ const bundled = path.join(__dirname, '../../registry.json')
49
+ return fs.readJson(bundled)
50
+ }
51
+
52
+ /**
53
+ * Returns a single method from the registry, or null if not found.
54
+ */
55
+ export async function getMethod(id) {
56
+ const registry = await loadRegistry()
57
+ return registry.methods?.[id] ?? null
58
+ }
59
+
60
+ /**
61
+ * Returns all methods from the registry as an array.
62
+ */
63
+ export async function getAllMethods() {
64
+ const registry = await loadRegistry()
65
+ return Object.values(registry.methods ?? {})
66
+ }
67
+
68
+ // ── GitHub file fetcher ───────────────────────────────────────────────────────
69
+
70
+ /**
71
+ * Fetches a raw file from a GitHub repo (main branch).
72
+ */
73
+ export async function fetchFromGitHub(repo, filePath) {
74
+ const url = `${RAW_BASE}/${repo}/main/${filePath}`
75
+ const res = await fetch(url)
76
+
77
+ if (!res.ok) {
78
+ throw new Error(
79
+ `Could not fetch ${filePath} from ${repo} (HTTP ${res.status})\n` +
80
+ ` URL: ${url}`
81
+ )
82
+ }
83
+
84
+ return res.text()
85
+ }