@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 +188 -0
- package/package.json +44 -0
- package/registry.json +65 -0
- package/src/commands/doctor.js +127 -0
- package/src/commands/init.js +115 -0
- package/src/commands/install.js +104 -0
- package/src/commands/list.js +56 -0
- package/src/commands/new.js +134 -0
- package/src/commands/status.js +60 -0
- package/src/commands/update.js +47 -0
- package/src/commands/validate.js +105 -0
- package/src/index.js +75 -0
- package/src/utils/agent-detector.js +56 -0
- package/src/utils/registry.js +85 -0
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
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
[](https://www.npmjs.com/package/@exchanet/enet)
|
|
11
|
+
[]()
|
|
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
|
+
}
|