fenrir-runtime 0.2.0 β†’ 0.3.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,155 @@
1
+ <div align="center">
2
+
3
+ # Fenrir
4
+
5
+ **A Lightweight Runtime**
6
+
7
+ [![npm](https://img.shields.io/npm/v/fenrir-runtime?style=flat-square&color=000&labelColor=000)](https://www.npmjs.com/package/fenrir-runtime)
8
+
9
+ <br/>
10
+
11
+ 🌐 &nbsp;[Türkçe](/READMEs/turkish.md)
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ <br/>
18
+
19
+ Fenrir is a minimal scripting runtime that runs `.fnr` files using a clean, expressive syntax on top of Node.js. It ships with a built-in standard library, package management, and a snapshot/rollback archive system.
20
+
21
+ <br/>
22
+
23
+ ## Features
24
+
25
+ ```
26
+ Clean syntax fn, use, log β€” no verbose JS boilerplate
27
+ Built-in stdlib File I/O, HTTP, math, arrays β€” globally available, no imports
28
+ Package manager Install and remove npm packages via fenrir install
29
+ Archive system Snapshot your project and roll back at any time
30
+ ```
31
+
32
+ <br/>
33
+
34
+ ## Installation
35
+
36
+ **Via npm**
37
+
38
+ ```bash
39
+ npm install -g fenrir-runtime
40
+ ```
41
+
42
+ **Via GitHub**
43
+
44
+ ```bash
45
+ git clone https://github.com/yourusername/fenrir.git
46
+ cd fenrir
47
+ npm install
48
+ npm link
49
+ ```
50
+
51
+ <br/>
52
+
53
+ ## Usage
54
+
55
+ ```bash
56
+ fenrir main.fnr # Run a file
57
+ fenrir install <pkg> # Install a package
58
+ fenrir remove <pkg> # Remove a package
59
+ fenrir archive "message" # Save a snapshot
60
+ fenrir archives # List snapshots
61
+ fenrir rollback # Restore latest snapshot
62
+ fenrir rollback <id> # Restore specific snapshot
63
+ fenrir clean <n> # Keep last n snapshots
64
+ fenrir help # Show help
65
+ ```
66
+
67
+ <br/>
68
+
69
+ ## Syntax
70
+
71
+ `.fnr` files are mostly JavaScript with a cleaner surface:
72
+
73
+ ```js
74
+ use express from 'express'
75
+
76
+ const app = express()
77
+ const PORT = env('PORT', '3000')
78
+
79
+ fn greet(name) {
80
+ log(`Hello, ${name}!`)
81
+ }
82
+
83
+ app.get('/', fn(req, res) {
84
+ res.json({ message: 'Hello from Fenrir' })
85
+ })
86
+
87
+ app.listen(PORT, fn() {
88
+ log(`🐺 Server running on http://localhost:${PORT}`)
89
+ })
90
+ ```
91
+
92
+ | Fenrir | JavaScript |
93
+ |--------|------------|
94
+ | `fn name() {}` | `function name() {}` |
95
+ | `fn(args) {}` | `function(args) {}` |
96
+ | `use X from 'mod'` | `import X from 'mod'` |
97
+ | `log()` | `console.log()` |
98
+ | `err()` | `console.error()` |
99
+ | `warn()` | `console.warn()` |
100
+ | `info()` | `console.info()` |
101
+
102
+ <br/>
103
+
104
+ ## Standard Library
105
+
106
+ All functions are globally available β€” no imports needed.
107
+
108
+ | Category | Functions |
109
+ |----------|-----------|
110
+ | **I/O** | `readFile` `writeFile` `appendFile` `fileExists` `readDir` `readJSON` `writeJSON` `mkdir` `rm` |
111
+ | **HTTP** | `get` `post` |
112
+ | **System** | `exec` `run` `spawn` `env` `args` `exit` `cwd` |
113
+ | **Time** | `now` `time` `sleep` `bench` |
114
+ | **Array** | `range` `pick` `shuffle` `chunk` `unique` `flatten` `sum` `avg` `min` `max` `clamp` |
115
+ | **String** | `capitalize` `truncate` `pad` `repeat` |
116
+ | **Object** | `keys` `values` `entries` `merge` `clone` |
117
+ | **Async** | `all` `race` `retry` |
118
+ | **Types** | `isString` `isNumber` `isBool` `isArray` `isObject` `isNull` `isDefined` |
119
+ | **Math** | `rand` `randInt` `abs` `ceil` `floor` `round` `sqrt` `pow` `PI` `E` |
120
+
121
+ <br/>
122
+
123
+ ## Archive System
124
+
125
+ ```bash
126
+ fenrir archive "before refactor" # Save snapshot
127
+ fenrir archives # List all snapshots
128
+ fenrir rollback # Restore latest
129
+ fenrir rollback 2024-01-15_14-30-00 # Restore specific
130
+ fenrir clean 10 # Keep last 10, delete rest
131
+ ```
132
+
133
+ <br/>
134
+
135
+ ## Debug Mode
136
+
137
+ ```bash
138
+ FENRIR_DEBUG=1 fenrir main.fnr
139
+ ```
140
+
141
+ Prints the transformed JavaScript before execution.
142
+
143
+ <br/>
144
+
145
+ ## Requirements
146
+
147
+ - Node.js **v18** or higher
148
+
149
+ <br/>
150
+
151
+ ---
152
+
153
+ <div align="center">
154
+ <sub>BSD-3-Clause License</sub>
155
+ </div>
@@ -0,0 +1,156 @@
1
+ <div align="center">
2
+
3
+ # Fenrir
4
+
5
+ **Hafif bir Runtime**
6
+
7
+ [![npm](https://img.shields.io/npm/v/fenrir-runtime?style=flat-square&color=000&labelColor=000)](https://www.npmjs.com/package/fenrir-runtime)
8
+
9
+
10
+ <br/>
11
+
12
+ 🌐 &nbsp;[English](README.md)
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ <br/>
19
+
20
+ Fenrir, `.fnr` uzantılı dosyaları Node.js üzerinde temiz ve ifade edici bir sâz dizimiyle çalıştıran minimal bir scripting runtime'ıdır. Dahili standart kütüphane, paket yânetimi ve anlık gârüntü/geri alma arşiv sistemiyle birlikte gelir.
21
+
22
+ <br/>
23
+
24
+ ## Γ–zellikler
25
+
26
+ ```
27
+ Temiz sΓΆz dizimi fn, use, log β€” uzun JS kalΔ±plarΔ± yok
28
+ Dahili stdlib Dosya I/O, HTTP, matematik β€” import olmadan global erişim
29
+ Paket yΓΆnetimi fenrir install ile npm paket kur/kaldΔ±r
30
+ Arşiv sistemi Anlık gârüntü al, istediğin zaman geri dân
31
+ ```
32
+
33
+ <br/>
34
+
35
+ ## Kurulum
36
+
37
+ **npm ile**
38
+
39
+ ```bash
40
+ npm install -g fenrir-runtime
41
+ ```
42
+
43
+ **GitHub'dan**
44
+
45
+ ```bash
46
+ git clone https://github.com/yourusername/fenrir.git
47
+ cd fenrir
48
+ npm install
49
+ npm link
50
+ ```
51
+
52
+ <br/>
53
+
54
+ ## KullanΔ±m
55
+
56
+ ```bash
57
+ fenrir main.fnr # Dosya çalıştır
58
+ fenrir install <paket> # Paket kur
59
+ fenrir remove <paket> # Paket kaldΔ±r
60
+ fenrir archive "mesaj" # AnlΔ±k gΓΆrΓΌntΓΌ kaydet
61
+ fenrir archives # GΓΆrΓΌntΓΌleri listele
62
+ fenrir rollback # Son gΓΆrΓΌntΓΌye geri dΓΆn
63
+ fenrir rollback <id> # Belirli gΓΆrΓΌntΓΌye geri dΓΆn
64
+ fenrir clean <n> # Son n gΓΆrΓΌntΓΌyΓΌ tut, kalanΔ± sil
65
+ fenrir help # YardΔ±m gΓΆster
66
+ ```
67
+
68
+ <br/>
69
+
70
+ ## SΓΆz Dizimi
71
+
72
+ `.fnr` dosyalarΔ± bΓΌyΓΌk ΓΆlΓ§ΓΌde JavaScript'tir, birkaΓ§ daha temiz alternatifle:
73
+
74
+ ```js
75
+ use express from 'express'
76
+
77
+ const app = express()
78
+ const PORT = env('PORT', '3000')
79
+
80
+ fn selamla(isim) {
81
+ log(`Merhaba, ${isim}!`)
82
+ }
83
+
84
+ app.get('/', fn(req, res) {
85
+ res.json({ mesaj: 'Fenrir ile merhaba' })
86
+ })
87
+
88
+ app.listen(PORT, fn() {
89
+ log(`🐺 Sunucu http://localhost:${PORT} adresinde çalışıyor`)
90
+ })
91
+ ```
92
+
93
+ | Fenrir | JavaScript |
94
+ |--------|------------|
95
+ | `fn isim() {}` | `function isim() {}` |
96
+ | `fn(args) {}` | `function(args) {}` |
97
+ | `use X from 'mod'` | `import X from 'mod'` |
98
+ | `log()` | `console.log()` |
99
+ | `err()` | `console.error()` |
100
+ | `warn()` | `console.warn()` |
101
+ | `info()` | `console.info()` |
102
+
103
+ <br/>
104
+
105
+ ## Standart KΓΌtΓΌphane
106
+
107
+ TΓΌm fonksiyonlar global olarak kullanΔ±labilir β€” import gerekmez.
108
+
109
+ | Kategori | Fonksiyonlar |
110
+ |----------|--------------|
111
+ | **I/O** | `readFile` `writeFile` `appendFile` `fileExists` `readDir` `readJSON` `writeJSON` `mkdir` `rm` |
112
+ | **HTTP** | `get` `post` |
113
+ | **Sistem** | `exec` `run` `spawn` `env` `args` `exit` `cwd` |
114
+ | **Zaman** | `now` `time` `sleep` `bench` |
115
+ | **Dizi** | `range` `pick` `shuffle` `chunk` `unique` `flatten` `sum` `avg` `min` `max` `clamp` |
116
+ | **String** | `capitalize` `truncate` `pad` `repeat` |
117
+ | **Nesne** | `keys` `values` `entries` `merge` `clone` |
118
+ | **Async** | `all` `race` `retry` |
119
+ | **Tip** | `isString` `isNumber` `isBool` `isArray` `isObject` `isNull` `isDefined` |
120
+ | **Math** | `rand` `randInt` `abs` `ceil` `floor` `round` `sqrt` `pow` `PI` `E` |
121
+
122
+ <br/>
123
+
124
+ ## Arşiv Sistemi
125
+
126
+ ```bash
127
+ fenrir archive "refactor ΓΆncesi" # AnlΔ±k gΓΆrΓΌntΓΌ kaydet
128
+ fenrir archives # TΓΌm gΓΆrΓΌntΓΌleri listele
129
+ fenrir rollback # En son gΓΆrΓΌntΓΌye geri dΓΆn
130
+ fenrir rollback 2024-01-15_14-30-00 # Belirli gΓΆrΓΌntΓΌye geri dΓΆn
131
+ fenrir clean 10 # Son 10'u tut, kalanΔ± sil
132
+ ```
133
+
134
+ <br/>
135
+
136
+ ## Debug Modu
137
+
138
+ ```bash
139
+ FENRIR_DEBUG=1 fenrir main.fnr
140
+ ```
141
+
142
+ Γ‡alıştΔ±rmadan ΓΆnce dΓΆnüştΓΌrΓΌlmüş JavaScript kodunu ekrana basar.
143
+
144
+ <br/>
145
+
146
+ ## Gereksinimler
147
+
148
+ - Node.js **v18** veya ΓΌzeri
149
+
150
+ <br/>
151
+
152
+ ---
153
+
154
+ <div align="center">
155
+ <sub>BSD-3-Clause License</sub>
156
+ </div>
package/example/main.fnr CHANGED
@@ -1,67 +1,103 @@
1
- use express from 'express'
1
+ const PORT = 3000
2
2
 
3
- const app = express()
4
- const PORT = env('PORT', '3000')
3
+ const routes = {}
5
4
 
6
- app.use(express.json())
5
+ fn route(method, path, handler) {
6
+ routes[`${method}:${path}`] = handler
7
+ }
7
8
 
8
- const users = [
9
- { id: 1, name: 'Alice', role: 'admin' },
10
- { id: 2, name: 'Bob', role: 'user' },
11
- ]
9
+ fn notFound() {
10
+ return response({ error: "Not found" }, 404)
11
+ }
12
12
 
13
- fn logger(req, res, next) {
14
- log(`[${time()}] ${req.method} ${req.url}`)
15
- next()
13
+ fn methodNotAllowed() {
14
+ return response({ error: "Method not allowed" }, 405)
16
15
  }
17
16
 
18
- fn notFound(req, res) {
19
- res.status(404).json({ error: 'not found' })
17
+ const db = {
18
+ users: [
19
+ { id: 1, name: "Alice", email: "alice@example.com" },
20
+ { id: 2, name: "Bob", email: "bob@example.com" },
21
+ ],
22
+ nextId: 3
20
23
  }
21
24
 
22
- app.use(logger)
25
+ route("GET", "/", req => ({
26
+ body: {
27
+ name: "Fenrir API",
28
+ version: "1.0.0",
29
+ endpoints: ["GET /", "GET /users", "GET /users/:id", "POST /users", "DELETE /users/:id"]
30
+ }
31
+ }))
23
32
 
24
- app.get('/', fn(req, res) {
25
- res.json({
26
- name: 'Fenrir API',
27
- version: '0.2.0',
28
- uptime: process.uptime(),
29
- })
30
- })
33
+ route("GET", "/users", req => ({
34
+ body: { users: db.users, total: db.users.length }
35
+ }))
31
36
 
32
- app.get('/users', fn(req, res) {
33
- res.json({ users, count: users.length })
37
+ route("POST", "/users", req => {
38
+ const { name, email } = req.body || {}
39
+ if (!name || !email) {
40
+ return response({ error: "name and email required" }, 400)
41
+ }
42
+ const user = { id: db.nextId++, name, email }
43
+ db.users.push(user)
44
+ return response({ created: user }, 201)
34
45
  })
35
46
 
36
- app.get('/users/:id', fn(req, res) {
37
- const id = parseInt(req.params.id)
38
- const user = users.find(u => u.id === id)
39
- if (!user) return res.status(404).json({ error: 'user not found' })
40
- res.json(user)
41
- })
47
+ const server = serve(PORT, async req => {
48
+ const method = req.method
49
+ const url = req.url.split("?")[0]
42
50
 
43
- app.post('/users', fn(req, res) {
44
- const { name, role } = req.body
45
- if (!name) return res.status(400).json({ error: 'name is required' })
46
- const user = { id: users.length + 1, name, role: role || 'user' }
47
- users.push(user)
48
- res.status(201).json(user)
49
- })
51
+ info(f"[${method}] ${url}")
52
+
53
+ const exact = routes[`${method}:${url}`]
54
+ if (exact) return exact(req)
50
55
 
51
- app.delete('/users/:id', fn(req, res) {
52
- const id = parseInt(req.params.id)
53
- const idx = users.findIndex(u => u.id === id)
54
- if (idx === -1) return res.status(404).json({ error: 'user not found' })
55
- const removed = users.splice(idx, 1)[0]
56
- res.json({ deleted: removed })
56
+ const dynamicKey = Object.keys(routes).find(k => {
57
+ const [km, kp] = k.split(":")
58
+ if (km !== method) return false
59
+ const kParts = kp.split("/")
60
+ const uParts = url.split("/")
61
+ if (kParts.length !== uParts.length) return false
62
+ return kParts.every((seg, i) => seg.startsWith(":") || seg === uParts[i])
63
+ })
64
+
65
+ if (dynamicKey) {
66
+ const [, pattern] = dynamicKey.split(":")
67
+ const patParts = pattern.split("/")
68
+ const urlParts = url.split("/")
69
+ req.params = {}
70
+ patParts.forEach((seg, i) => {
71
+ if (seg.startsWith(":")) req.params[seg.slice(1)] = urlParts[i]
72
+ })
73
+ return routes[dynamicKey](req)
74
+ }
75
+
76
+ const anyMethod = Object.keys(routes).find(k => k.split(":")[1] === url)
77
+ if (anyMethod) return methodNotAllowed()
78
+
79
+ return notFound()
57
80
  })
58
81
 
59
- app.use(notFound)
82
+ route("GET", "/users/:id", req => {
83
+ const id = parseInt(req.params.id)
84
+ const user = db.users.find(u => u.id === id)
85
+ if (!user) return response({ error: f"User ${id} not found" }, 404)
86
+ return { body: { user } }
87
+ })
60
88
 
61
- app.listen(PORT, fn() {
62
- log(`🐺 Server running on http://localhost:${PORT}`)
63
- log(` GET /users`)
64
- log(` GET /users/:id`)
65
- log(` POST /users`)
66
- log(` DELETE /users/:id`)
89
+ route("DELETE", "/users/:id", req => {
90
+ const id = parseInt(req.params.id)
91
+ const idx = db.users.findIndex(u => u.id === id)
92
+ if (idx === -1) return response({ error: f"User ${id} not found" }, 404)
93
+ const [removed] = db.users.splice(idx, 1)
94
+ return { body: { deleted: removed } }
67
95
  })
96
+
97
+ print(f"Server running β†’ http://localhost:${PORT}")
98
+ print("Routes:")
99
+ print(" GET /")
100
+ print(" GET /users")
101
+ print(" POST /users body: { name, email }")
102
+ print(" GET /users/:id")
103
+ print(" DELETE /users/:id")
package/package.json CHANGED
@@ -1,16 +1,20 @@
1
1
  {
2
- "name": "fenrir-runtime",
3
- "version": "0.2.0",
4
- "description": "Fenrir β€” a modern JavaScript-based runtime",
5
- "main": "src/cli/index.js",
6
- "bin": {
7
- "fenrir": "./src/cli/index.js"
8
- },
9
- "type": "module",
10
- "scripts": {
11
- "link": "npm link"
12
- },
13
- "dependencies": {
14
- "glob": "^11.0.0"
15
- }
16
- }
2
+ "name": "fenrir-runtime",
3
+ "version": "0.3.0",
4
+ "description": "Fenrir β€” a modern JavaScript-based runtime",
5
+ "main": "src/cli/index.js",
6
+ "bin": {
7
+ "fenrir": "./src/cli/index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "link": "npm link"
12
+ },
13
+ "dependencies": {
14
+ "glob": "^11.0.0",
15
+ "sql.js": "^1.12.0",
16
+ "js-yaml": "^4.1.0",
17
+ "toml": "^3.0.0",
18
+ "chokidar": "^3.6.0"
19
+ }
20
+ }
@@ -21,9 +21,13 @@ export class ArchiveSystem {
21
21
  fs.mkdirSync(path.dirname(out), { recursive: true });
22
22
  fs.copyFileSync(path.join(this.root, f), out);
23
23
  }
24
- const meta = { timestamp: now.toISOString(), message: message||'Snapshot', files: files.length,
24
+ const meta = {
25
+ timestamp: now.toISOString(),
26
+ message: message || 'Snapshot',
27
+ files: files.length,
25
28
  hash: crypto.createHash('md5').update(files.join('|')).digest('hex').slice(0,8),
26
- gitHash: (() => { try { return execSync('git rev-parse HEAD',{cwd:this.root,encoding:'utf8'}).trim(); } catch { return null; } })() };
29
+ gitHash: (() => { try { return execSync('git rev-parse HEAD',{cwd:this.root,encoding:'utf8'}).trim(); } catch { return null; } })()
30
+ };
27
31
  fs.writeFileSync(path.join(dest,'.metadata.json'), JSON.stringify(meta,null,2));
28
32
  console.log(`\x1b[32mβœ“ Archive: ${id} (${files.length} files)\x1b[0m`);
29
33
  return id;
@@ -53,7 +57,6 @@ export class ArchiveSystem {
53
57
  if (!fs.existsSync(ap)) { console.error(`Not found: ${target}`); return; }
54
58
  console.log('Saving current state...');
55
59
  await this.create(`Pre-rollback β€” ${new Date().toISOString()}`);
56
- const meta = JSON.parse(fs.readFileSync(path.join(ap,'.metadata.json'),'utf8'));
57
60
  console.log(`Rolling back to \x1b[36m${target}\x1b[0m`);
58
61
  for (const f of await this.collect()) { const fp=path.join(this.root,f); if(fs.existsSync(fp)) fs.rmSync(fp,{force:true}); }
59
62
  const af = await glob('**/*',{cwd:ap,nodir:true,ignore:['.metadata.json']});
@@ -70,6 +73,42 @@ export class ArchiveSystem {
70
73
  console.log(`\x1b[32mβœ“ Removed ${del.length}, kept ${keep}.\x1b[0m`);
71
74
  }
72
75
 
76
+ async diff(idA, idB) {
77
+ if (!fs.existsSync(this.dir)) { console.error('No archives.'); return; }
78
+ const all = fs.readdirSync(this.dir).sort().reverse();
79
+
80
+ const resolveId = (id, fallbackIdx) => id ?? all[fallbackIdx];
81
+ const a = resolveId(idA, 1);
82
+ const b = resolveId(idB, 0);
83
+
84
+ if (!a || !b) { console.error('Not enough archives to diff.'); return; }
85
+ const pathA = path.join(this.dir, a);
86
+ const pathB = path.join(this.dir, b);
87
+ if (!fs.existsSync(pathA)) { console.error(`Not found: ${a}`); return; }
88
+ if (!fs.existsSync(pathB)) { console.error(`Not found: ${b}`); return; }
89
+
90
+ const filesA = new Set(await glob('**/*', { cwd: pathA, nodir: true, ignore: ['.metadata.json'] }));
91
+ const filesB = new Set(await glob('**/*', { cwd: pathB, nodir: true, ignore: ['.metadata.json'] }));
92
+
93
+ const added = [...filesB].filter(f => !filesA.has(f));
94
+ const removed = [...filesA].filter(f => !filesB.has(f));
95
+ const changed = [...filesA].filter(f => {
96
+ if (!filesB.has(f)) return false;
97
+ const ha = crypto.createHash('md5').update(fs.readFileSync(path.join(pathA, f))).digest('hex');
98
+ const hb = crypto.createHash('md5').update(fs.readFileSync(path.join(pathB, f))).digest('hex');
99
+ return ha !== hb;
100
+ });
101
+
102
+ console.log(`\x1b[1mDiff: \x1b[36m${a}\x1b[0m\x1b[1m β†’ \x1b[36m${b}\x1b[0m\n`);
103
+ if (!added.length && !removed.length && !changed.length) {
104
+ console.log(' \x1b[90mNo differences.\x1b[0m'); return;
105
+ }
106
+ for (const f of added) console.log(` \x1b[32m+ ${f}\x1b[0m`);
107
+ for (const f of removed) console.log(` \x1b[31m- ${f}\x1b[0m`);
108
+ for (const f of changed) console.log(` \x1b[33m~ ${f}\x1b[0m`);
109
+ console.log(`\n \x1b[90m${added.length} added, ${removed.length} removed, ${changed.length} changed\x1b[0m`);
110
+ }
111
+
73
112
  async collect() {
74
113
  const patterns = ['**/*.{py,java,cpp,c,h,cs,rb,php,swift,go,rs}','**/*.{js,ts,jsx,tsx,mjs,cjs}','**/*.{css,scss,sass,less}','**/*.{html,htm,json,xml,yml,yaml}','**/*.{md,txt,sh,bash,lock,fnr}'];
75
114
  const ignore = ['.fenrir/**','node_modules/**','.git/**','*.log','.env*','dist/**','build/**'];
@@ -0,0 +1,87 @@
1
+ import { execSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ export class Doctor {
6
+ async run() {
7
+ console.log('\x1b[1m\nFenrir Doctor\x1b[0m\n');
8
+ const checks = [
9
+ this.checkNode(),
10
+ this.checkNpm(),
11
+ this.checkFenrirJson(),
12
+ this.checkPackageJson(),
13
+ this.checkNodeModules(),
14
+ this.checkGit(),
15
+ ];
16
+
17
+ let allOk = true;
18
+ for (const { ok, label, detail } of checks) {
19
+ const icon = ok ? '\x1b[32mβœ“\x1b[0m' : '\x1b[31m✘\x1b[0m';
20
+ console.log(` ${icon} ${label}${detail ? ' \x1b[90m' + detail + '\x1b[0m' : ''}`);
21
+ if (!ok) allOk = false;
22
+ }
23
+
24
+ console.log(allOk
25
+ ? '\n\x1b[32m All checks passed.\x1b[0m\n'
26
+ : '\n\x1b[33m Some checks failed. Review above.\x1b[0m\n'
27
+ );
28
+ }
29
+
30
+ checkNode() {
31
+ try {
32
+ const v = execSync('node --version', { encoding: 'utf8' }).trim();
33
+ const major = parseInt(v.slice(1));
34
+ return { ok: major >= 18, label: `Node.js ${v}`, detail: major < 18 ? 'Requires Node 18+' : '' };
35
+ } catch {
36
+ return { ok: false, label: 'Node.js not found' };
37
+ }
38
+ }
39
+
40
+ checkNpm() {
41
+ try {
42
+ const v = execSync('npm --version', { encoding: 'utf8' }).trim();
43
+ return { ok: true, label: `npm v${v}` };
44
+ } catch {
45
+ return { ok: false, label: 'npm not found' };
46
+ }
47
+ }
48
+
49
+ checkFenrirJson() {
50
+ const p = path.join(process.cwd(), 'fenrir.json');
51
+ if (!fs.existsSync(p)) return { ok: false, label: 'fenrir.json', detail: 'Not found (run: fenrir install)' };
52
+ try {
53
+ const j = JSON.parse(fs.readFileSync(p, 'utf8'));
54
+ return { ok: true, label: 'fenrir.json', detail: `v${j.version}` };
55
+ } catch {
56
+ return { ok: false, label: 'fenrir.json', detail: 'Invalid JSON' };
57
+ }
58
+ }
59
+
60
+ checkPackageJson() {
61
+ const p = path.join(process.cwd(), 'package.json');
62
+ if (!fs.existsSync(p)) return { ok: false, label: 'package.json', detail: 'Not found' };
63
+ try {
64
+ const j = JSON.parse(fs.readFileSync(p, 'utf8'));
65
+ const hasModule = j.type === 'module';
66
+ return { ok: hasModule, label: 'package.json', detail: hasModule ? '' : 'Missing "type": "module"' };
67
+ } catch {
68
+ return { ok: false, label: 'package.json', detail: 'Invalid JSON' };
69
+ }
70
+ }
71
+
72
+ checkNodeModules() {
73
+ const p = path.join(process.cwd(), 'node_modules');
74
+ const exists = fs.existsSync(p);
75
+ return { ok: exists, label: 'node_modules', detail: exists ? '' : 'Not found (run: fenrir install)' };
76
+ }
77
+
78
+ checkGit() {
79
+ try {
80
+ execSync('git rev-parse --git-dir', { cwd: process.cwd(), encoding: 'utf8', stdio: 'pipe' });
81
+ const branch = execSync('git branch --show-current', { encoding: 'utf8', stdio: 'pipe' }).trim();
82
+ return { ok: true, label: 'Git repository', detail: `branch: ${branch}` };
83
+ } catch {
84
+ return { ok: false, label: 'Git repository', detail: 'Not a git repo' };
85
+ }
86
+ }
87
+ }