@exchanet/enet 1.0.11 → 1.0.12
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/package.json +1 -1
- package/src/index.js +47 -4
- package/src/utils/registry.js +42 -13
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { program } from 'commander'
|
|
4
4
|
import chalk from 'chalk'
|
|
5
|
+
import { createRequire } from 'module'
|
|
5
6
|
import { installCommand } from './commands/install.js'
|
|
6
7
|
import { listCommand } from './commands/list.js'
|
|
7
8
|
import { initCommand } from './commands/init.js'
|
|
@@ -11,10 +12,48 @@ import { updateCommand } from './commands/update.js'
|
|
|
11
12
|
import { statusCommand } from './commands/status.js'
|
|
12
13
|
import { doctorCommand } from './commands/doctor.js'
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
// Read version from package.json — single source of truth
|
|
16
|
+
const require = createRequire(import.meta.url)
|
|
17
|
+
const pkg = require('../package.json')
|
|
18
|
+
const VERSION = pkg.version
|
|
19
|
+
|
|
20
|
+
// ── Version check ─────────────────────────────────────────────────────────────
|
|
21
|
+
// Runs in background — never blocks the command, never crashes if offline
|
|
22
|
+
|
|
23
|
+
async function checkForUpdate() {
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(
|
|
26
|
+
'https://registry.npmjs.org/@exchanet/enet/latest',
|
|
27
|
+
{ signal: AbortSignal.timeout(3000) }
|
|
28
|
+
)
|
|
29
|
+
if (!res.ok) return
|
|
30
|
+
const data = await res.json()
|
|
31
|
+
const latest = data.version
|
|
32
|
+
if (latest && latest !== VERSION) {
|
|
33
|
+
console.log(
|
|
34
|
+
chalk.yellow(' ⚠ Update available: ') +
|
|
35
|
+
chalk.dim(`v${VERSION}`) +
|
|
36
|
+
chalk.white(' → ') +
|
|
37
|
+
chalk.green(`v${latest}`) + '\n' +
|
|
38
|
+
chalk.dim(' Run ') +
|
|
39
|
+
chalk.white('npm install -g @exchanet/enet') +
|
|
40
|
+
chalk.dim(' to update.\n')
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Offline or npm unreachable — silently skip
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fire in background without await — command runs immediately
|
|
49
|
+
checkForUpdate()
|
|
50
|
+
|
|
51
|
+
// ── Header ────────────────────────────────────────────────────────────────────
|
|
15
52
|
|
|
16
53
|
console.log(chalk.cyan(`\n◆ enet v${VERSION} — exchanet methods manager\n`))
|
|
17
54
|
|
|
55
|
+
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
18
57
|
program
|
|
19
58
|
.name('enet')
|
|
20
59
|
.description('Install, scaffold and manage exchanet AI coding methods')
|
|
@@ -23,8 +62,9 @@ program
|
|
|
23
62
|
program
|
|
24
63
|
.command('install <method>')
|
|
25
64
|
.description('Install a method into the current project')
|
|
26
|
-
.option('-a, --agent <agent>', 'Force agent: cursor | windsurf | copilot | generic')
|
|
65
|
+
.option('-a, --agent <agent>', 'Force agent: cursor | windsurf | antigravity | claudecode | copilot | generic')
|
|
27
66
|
.option('-g, --global', 'Install globally to home directory')
|
|
67
|
+
.option('--all', 'Install for all detected agents without prompting')
|
|
28
68
|
.action(installCommand)
|
|
29
69
|
|
|
30
70
|
program
|
|
@@ -58,8 +98,11 @@ program
|
|
|
58
98
|
|
|
59
99
|
program
|
|
60
100
|
.command('update [method]')
|
|
61
|
-
.description('Update installed methods
|
|
62
|
-
.option('--all', '
|
|
101
|
+
.description('Update installed methods and add adapters for new agents')
|
|
102
|
+
.option('--all', 'Add all new agents without prompting')
|
|
103
|
+
.option('--add-only', 'Only add new agents, skip re-downloading existing')
|
|
104
|
+
.option('--update-only', 'Only re-download existing, skip new agent prompt')
|
|
105
|
+
.option('-g, --global', 'Update global install')
|
|
63
106
|
.action(updateCommand)
|
|
64
107
|
|
|
65
108
|
program
|
package/src/utils/registry.js
CHANGED
|
@@ -1,43 +1,60 @@
|
|
|
1
|
-
import fetch from 'node-fetch'
|
|
2
1
|
import fs from 'fs-extra'
|
|
3
2
|
import path from 'path'
|
|
4
3
|
import { fileURLToPath } from 'url'
|
|
5
4
|
|
|
6
5
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
-
const RAW_BASE
|
|
6
|
+
const RAW_BASE = 'https://raw.githubusercontent.com'
|
|
8
7
|
const REGISTRY_URL = `${RAW_BASE}/exchanet/enet/main/registry.json`
|
|
9
8
|
const CACHE_FILE = path.join(__dirname, '../../.registry-cache.json')
|
|
10
9
|
const CACHE_TTL_MS = 1000 * 60 * 60 // 1 hour
|
|
11
10
|
|
|
12
11
|
// ── Registry ──────────────────────────────────────────────────────────────────
|
|
13
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Loads the registry from:
|
|
15
|
+
* 1. Remote GitHub (exchanet/enet/registry.json) — always fresh
|
|
16
|
+
* 2. Local cache if remote fails — fallback
|
|
17
|
+
* 3. Bundled registry.json in the package — last resort
|
|
18
|
+
*/
|
|
14
19
|
export async function loadRegistry() {
|
|
15
20
|
try {
|
|
16
|
-
const res = await fetch(REGISTRY_URL, {
|
|
21
|
+
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(5000) })
|
|
17
22
|
if (res.ok) {
|
|
18
23
|
const data = await res.json()
|
|
19
24
|
await fs.writeJson(CACHE_FILE, { ...data, _cachedAt: Date.now() }).catch(() => {})
|
|
20
25
|
return data
|
|
21
26
|
}
|
|
22
|
-
} catch {
|
|
27
|
+
} catch {
|
|
28
|
+
// Network unavailable — fall through to cache
|
|
29
|
+
}
|
|
23
30
|
|
|
24
31
|
try {
|
|
25
32
|
if (await fs.pathExists(CACHE_FILE)) {
|
|
26
33
|
const cached = await fs.readJson(CACHE_FILE)
|
|
27
34
|
const age = Date.now() - (cached._cachedAt || 0)
|
|
28
|
-
if (age < CACHE_TTL_MS * 24)
|
|
35
|
+
if (age < CACHE_TTL_MS * 24) {
|
|
36
|
+
return cached
|
|
37
|
+
}
|
|
29
38
|
}
|
|
30
|
-
} catch {
|
|
39
|
+
} catch {
|
|
40
|
+
// Cache corrupted — fall through to bundled
|
|
41
|
+
}
|
|
31
42
|
|
|
32
43
|
const bundled = path.join(__dirname, '../../registry.json')
|
|
33
44
|
return fs.readJson(bundled)
|
|
34
45
|
}
|
|
35
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Returns a single method from the registry, or null if not found.
|
|
49
|
+
*/
|
|
36
50
|
export async function getMethod(id) {
|
|
37
51
|
const registry = await loadRegistry()
|
|
38
52
|
return registry.methods?.[id] ?? null
|
|
39
53
|
}
|
|
40
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Returns all methods from the registry as an array.
|
|
57
|
+
*/
|
|
41
58
|
export async function getAllMethods() {
|
|
42
59
|
const registry = await loadRegistry()
|
|
43
60
|
return Object.values(registry.methods ?? {})
|
|
@@ -45,22 +62,27 @@ export async function getAllMethods() {
|
|
|
45
62
|
|
|
46
63
|
// ── GitHub file fetcher ───────────────────────────────────────────────────────
|
|
47
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Fetches a raw file from a GitHub repo (main branch).
|
|
67
|
+
*/
|
|
48
68
|
export async function fetchFromGitHub(repo, filePath) {
|
|
49
69
|
const url = `${RAW_BASE}/${repo}/main/${filePath}`
|
|
50
70
|
const res = await fetch(url)
|
|
71
|
+
|
|
51
72
|
if (!res.ok) {
|
|
52
73
|
throw new Error(
|
|
53
74
|
`Could not fetch ${filePath} from ${repo} (HTTP ${res.status})\n` +
|
|
54
75
|
` URL: ${url}`
|
|
55
76
|
)
|
|
56
77
|
}
|
|
78
|
+
|
|
57
79
|
return res.text()
|
|
58
80
|
}
|
|
59
81
|
|
|
60
82
|
// ── Install state ─────────────────────────────────────────────────────────────
|
|
61
83
|
//
|
|
62
84
|
// Tracks which agents have each method installed.
|
|
63
|
-
// Stored in
|
|
85
|
+
// Stored in .enet/installed.json in the project root.
|
|
64
86
|
//
|
|
65
87
|
// Format:
|
|
66
88
|
// {
|
|
@@ -68,15 +90,19 @@ export async function fetchFromGitHub(repo, filePath) {
|
|
|
68
90
|
// "agents": ["cursor", "claudecode", "antigravity"],
|
|
69
91
|
// "version": "3.0.0",
|
|
70
92
|
// "updatedAt": "2025-03-01T10:00:00.000Z"
|
|
71
|
-
// }
|
|
93
|
+
// },
|
|
94
|
+
// "modular-design": { ... }
|
|
72
95
|
// }
|
|
73
96
|
|
|
74
97
|
function getInstallRecordFile() {
|
|
98
|
+
// Resolve relative to cwd so it works in any project
|
|
75
99
|
return path.join(process.cwd(), '.enet', 'installed.json')
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
/**
|
|
79
|
-
*
|
|
103
|
+
* Reads the install record for a single method.
|
|
104
|
+
* Returns { agents: ['cursor', ...], version: '3.0.0', updatedAt: '...' }
|
|
105
|
+
* or null if the method has never been installed.
|
|
80
106
|
*/
|
|
81
107
|
export async function readInstallRecord(methodId) {
|
|
82
108
|
try {
|
|
@@ -90,22 +116,25 @@ export async function readInstallRecord(methodId) {
|
|
|
90
116
|
}
|
|
91
117
|
|
|
92
118
|
/**
|
|
93
|
-
* Writes the install record for a single method.
|
|
94
|
-
* Merges with existing records — other methods are
|
|
119
|
+
* Writes (or updates) the install record for a single method.
|
|
120
|
+
* Merges with existing records — other methods are not touched.
|
|
95
121
|
*/
|
|
96
122
|
export async function writeInstallRecord(methodId, record) {
|
|
97
123
|
try {
|
|
98
124
|
const file = getInstallRecordFile()
|
|
99
125
|
await fs.ensureDir(path.dirname(file))
|
|
126
|
+
|
|
100
127
|
let data = {}
|
|
101
128
|
if (await fs.pathExists(file)) {
|
|
102
129
|
data = await fs.readJson(file).catch(() => ({}))
|
|
103
130
|
}
|
|
131
|
+
|
|
104
132
|
data[methodId] = {
|
|
105
|
-
agents:
|
|
106
|
-
version:
|
|
133
|
+
agents: record.agents,
|
|
134
|
+
version: record.version ?? null,
|
|
107
135
|
updatedAt: new Date().toISOString()
|
|
108
136
|
}
|
|
137
|
+
|
|
109
138
|
await fs.writeJson(file, data, { spaces: 2 })
|
|
110
139
|
} catch {
|
|
111
140
|
// Non-fatal — install works correctly even if state cannot be saved
|