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 +155 -0
- package/READMEs/turkish.md +156 -0
- package/example/main.fnr +85 -49
- package/package.json +19 -15
- package/src/cli/archiveSystem.js +42 -3
- package/src/cli/doctor.js +87 -0
- package/src/cli/index.js +68 -21
- package/src/cli/packageManager.js +1 -1
- package/src/cli/repl.js +108 -0
- package/src/cli/scaffolder.js +68 -0
- package/src/cli/testRunner.js +99 -0
- package/src/runtime/runner.js +264 -20
- package/src/stdlib/index.js +196 -0
- package/src/stdlib/sandbox.js +32 -0
- package/test_fenrir.sh +435 -0
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Fenrir
|
|
4
|
+
|
|
5
|
+
**A Lightweight Runtime**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/fenrir-runtime)
|
|
8
|
+
|
|
9
|
+
<br/>
|
|
10
|
+
|
|
11
|
+
π [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
|
+
[](https://www.npmjs.com/package/fenrir-runtime)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
<br/>
|
|
11
|
+
|
|
12
|
+
π [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
|
-
|
|
1
|
+
const PORT = 3000
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const PORT = env('PORT', '3000')
|
|
3
|
+
const routes = {}
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
fn route(method, path, handler) {
|
|
6
|
+
routes[`${method}:${path}`] = handler
|
|
7
|
+
}
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
]
|
|
9
|
+
fn notFound() {
|
|
10
|
+
return response({ error: "Not found" }, 404)
|
|
11
|
+
}
|
|
12
12
|
|
|
13
|
-
fn
|
|
14
|
-
|
|
15
|
-
next()
|
|
13
|
+
fn methodNotAllowed() {
|
|
14
|
+
return response({ error: "Method not allowed" }, 405)
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
+
}
|
package/src/cli/archiveSystem.js
CHANGED
|
@@ -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 = {
|
|
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
|
+
}
|