odac 1.3.0 → 1.4.1
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/.agent/rules/memory.md +10 -1
- package/.github/workflows/release.yml +1 -5
- package/AGENTS.md +47 -0
- package/CHANGELOG.md +58 -0
- package/README.md +11 -1
- package/bin/odac.js +359 -6
- package/client/odac.js +15 -11
- package/docs/ai/README.md +49 -0
- package/docs/ai/skills/SKILL.md +40 -0
- package/docs/ai/skills/backend/authentication.md +74 -0
- package/docs/ai/skills/backend/config.md +39 -0
- package/docs/ai/skills/backend/controllers.md +69 -0
- package/docs/ai/skills/backend/cron.md +57 -0
- package/docs/ai/skills/backend/database.md +37 -0
- package/docs/ai/skills/backend/forms.md +26 -0
- package/docs/ai/skills/backend/ipc.md +62 -0
- package/docs/ai/skills/backend/mail.md +41 -0
- package/docs/ai/skills/backend/migrations.md +80 -0
- package/docs/ai/skills/backend/request_response.md +42 -0
- package/docs/ai/skills/backend/routing.md +58 -0
- package/docs/ai/skills/backend/storage.md +50 -0
- package/docs/ai/skills/backend/streaming.md +41 -0
- package/docs/ai/skills/backend/structure.md +64 -0
- package/docs/ai/skills/backend/translations.md +49 -0
- package/docs/ai/skills/backend/utilities.md +31 -0
- package/docs/ai/skills/backend/validation.md +60 -0
- package/docs/ai/skills/backend/views.md +68 -0
- package/docs/ai/skills/frontend/core.md +73 -0
- package/docs/ai/skills/frontend/forms.md +28 -0
- package/docs/ai/skills/frontend/navigation.md +27 -0
- package/docs/ai/skills/frontend/realtime.md +54 -0
- package/docs/backend/08-database/04-migrations.md +258 -37
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +2 -0
- package/docs/backend/10-authentication/05-session-management.md +25 -3
- package/package.json +1 -1
- package/src/Auth.js +128 -17
- package/src/Config.js +1 -1
- package/src/Database/ConnectionFactory.js +69 -0
- package/src/Database/Migration.js +1203 -0
- package/src/Database.js +35 -35
- package/src/Route/Internal.js +21 -18
- package/src/Route/MimeTypes.js +56 -0
- package/src/Route.js +40 -63
- package/src/View/Form.js +91 -51
- package/src/View.js +8 -3
- package/template/schema/users.js +23 -0
- package/test/Auth.test.js +310 -0
- package/test/Client.test.js +29 -0
- package/test/Config.test.js +7 -0
- package/test/Database/ConnectionFactory.test.js +80 -0
- package/test/Migration.test.js +943 -0
- package/test/View/Form.test.js +37 -0
package/.agent/rules/memory.md
CHANGED
|
@@ -36,7 +36,16 @@ trigger: always_on
|
|
|
36
36
|
## Naming & Text Conventions
|
|
37
37
|
- **ODAC Casing:** Always write "ODAC" in uppercase letters when referring to the framework name in strings, comments, log messages, or user-facing text. **EXCEPTION:** The class name itself (`class Odac`) and variable references to it should remain `Odac` (PascalCase) as per code conventions.
|
|
38
38
|
|
|
39
|
+
## Documentation Standards
|
|
40
|
+
- **AI Skill Front Matter:** Every file under `docs/ai/skills/**/*.md` must start with YAML front matter containing `name`, `description`, and `metadata.tags`; values must be specific to that document's topic (never copied from generic examples).
|
|
41
|
+
|
|
39
42
|
## Testing & Validation
|
|
40
43
|
- **Mandatory Test Coverage:** Every new feature, method, or significant logic change MUST be accompanied by a corresponding unit or integration test.
|
|
41
44
|
- **Verify Correctness:** do not assume code works; prove it with a test that covers both success and failure scenarios (e.g., edge cases, error conditions).
|
|
42
|
-
- **Update Existing Tests:** If a feature modifies existing behavior, update the relevant tests to reflect the new logic and ensure they pass.
|
|
45
|
+
- **Update Existing Tests:** If a feature modifies existing behavior, update the relevant tests to reflect the new logic and ensure they pass.
|
|
46
|
+
|
|
47
|
+
## Client Library (odac.js)
|
|
48
|
+
- **Automatic JSON Parsing:** The `#ajax` method (and by extension `odac.get`) must automatically parse the response if the `Content-Type` header contains `application/json`, even if `dataType` is not explicitly set to `json`.
|
|
49
|
+
|
|
50
|
+
## Security Logic & Authentication
|
|
51
|
+
- **Enterprise Token Rotation:** The `Auth.js` system utilizes a non-blocking refresh token rotation mechanism for cookies (`odac_x`/`odac_y`). To prevent race conditions during concurrent requests in high-throughput SPAs, rotated tokens are **not** immediately deleted. Instead, their `active` timestamp is set to naturally expire in 60 seconds (Grace Period), and their `date` timestamp is set to the Unix Epoch (`new Date(0)`) as an identifier mark. Never delete rotated tokens immediately.
|
|
@@ -41,10 +41,6 @@ jobs:
|
|
|
41
41
|
env:
|
|
42
42
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
43
43
|
run: npx semantic-release
|
|
44
|
-
|
|
45
|
-
- name: Publish to npm
|
|
46
|
-
run: npm publish --provenance --access public
|
|
47
|
-
|
|
48
44
|
- name: Get version
|
|
49
45
|
id: version
|
|
50
46
|
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
|
@@ -66,6 +62,7 @@ jobs:
|
|
|
66
62
|
with:
|
|
67
63
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
68
64
|
michelangelo: ${{ secrets.MICHELANGELO }}
|
|
65
|
+
version: ${{ steps.version.outputs.version }}
|
|
69
66
|
timeout-minutes: 15
|
|
70
67
|
continue-on-error: true
|
|
71
68
|
|
|
@@ -100,7 +97,6 @@ jobs:
|
|
|
100
97
|
if: steps.ai_notes.outcome == 'success' && steps.version.outputs.version != ''
|
|
101
98
|
with:
|
|
102
99
|
tag_name: v${{ steps.version.outputs.version }}
|
|
103
|
-
name: ${{ steps.ai_notes.outputs.release-title }}
|
|
104
100
|
body_path: RELEASE_NOTE.md
|
|
105
101
|
draft: false
|
|
106
102
|
prerelease: false
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# ODAC Agent Instructions
|
|
2
|
+
|
|
3
|
+
You are an AI Agent operating within the **ODAC Framework** repository. This document outlines the core principles, architectural standards, and operational guidelines you MUST follow to maintain the integrity and performance of this enterprise-grade system.
|
|
4
|
+
|
|
5
|
+
## 1. Project Identity & Philosophy
|
|
6
|
+
- **Name:** ODAC (Always uppercase in strings/docs/logs).
|
|
7
|
+
- **Core Goal:** To provide a robust, zero-config, high-performance Node.js framework for distributed cloud applications.
|
|
8
|
+
- **The "Big 3" Priorities:**
|
|
9
|
+
1. **Enterprise-Level Security:** Security is foundational. Default to secure, validate all inputs, sanitize all outputs.
|
|
10
|
+
2. **Zero-Config:** Works out-of-the-box. Convention over configuration.
|
|
11
|
+
3. **High Performance:** Optimize for throughput and low latency (Sub-millisecond targets).
|
|
12
|
+
|
|
13
|
+
## 2. Architectural Principles
|
|
14
|
+
- **Asynchronous & Non-Blocking:** Exclusively use non-blocking I/O. Use `fs.promises` instead of sync methods.
|
|
15
|
+
- **Dependency Injection (DI):** Build components with DI for maximum testability.
|
|
16
|
+
- **Single Responsibility Principle (SRP):** Keep classes and functions focused and small.
|
|
17
|
+
- **Memory Management:** Be paranoid about leaks. Clean up listeners, streams, and connections.
|
|
18
|
+
- **O(n log n) Bound:** Prioritize O(1) or O(n log n) algorithms. Justify any O(n²) operations.
|
|
19
|
+
|
|
20
|
+
## 3. Coding Standards & Integrity
|
|
21
|
+
- **Modern JavaScript:** Use ES6+ features, ES Modules (import/export).
|
|
22
|
+
- **Strictly Prohibited:** **No usage of `var`**. Use `const` (preferred) or `let`.
|
|
23
|
+
- **Fail-Fast Pattern:** Implement early returns for negative cases. Avoid deeply nested `if/else`.
|
|
24
|
+
- **Anti-Spaghetti Rules:**
|
|
25
|
+
- Resolve Promises upfront (e.g., `Promise.all`) before loops.
|
|
26
|
+
- Avoid mixing `await` inside deep logic.
|
|
27
|
+
- Capture mutable state synchronously before async operations.
|
|
28
|
+
- **No Quick/Lazy Fixes:** Implement correctly from the start. Refactor if necessary; no "band-aid" patches.
|
|
29
|
+
|
|
30
|
+
## 4. Technical Constraints (Strict Compliance)
|
|
31
|
+
- **Session Safety:** `Odac.Request.setSession()` MUST be called before accessing `Odac.Request.session()`.
|
|
32
|
+
- **Structured Logging:** No `console.log`. Use the internal JSON logger with appropriate levels.
|
|
33
|
+
- **Native APIs:** Prefer native Node.js/Browser APIs (like `fetch`) over external libraries to minimize overhead.
|
|
34
|
+
- **Token Rotation:** In `Auth.js`, use the 60-second grace period for rotated tokens. Never delete them immediately.
|
|
35
|
+
- **Ajax Parsing:** `odac.js` must automatically parse JSON responses if headers allow.
|
|
36
|
+
|
|
37
|
+
## 5. Testing & Documentation
|
|
38
|
+
- **TDD Requirement:** No feature is complete without unit/integration tests covering both success and edge cases.
|
|
39
|
+
- **Documentation:** Every exported member must have JSDoc explaining *Why* it exists, not just *What* it does.
|
|
40
|
+
- **No User Dialogues in Code:** Do not include assistant-user interaction in comments or code files.
|
|
41
|
+
|
|
42
|
+
## 6. Communication Style
|
|
43
|
+
- **Authoritative & Precise:** Be the expert. Do not explain basic concepts.
|
|
44
|
+
- **Proactive Correction:** If the user suggests a sub-optimal or insecure pattern (e.g., synchronous reads), refuse and implement the correct async version, explaining the trade-off.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
*Note: This file is a living document. Updates should be reflected in `memory.md` and subsequent AI interactions.*
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,61 @@
|
|
|
1
|
+
### doc
|
|
2
|
+
|
|
3
|
+
- enhance AI skills documentation with structured YAML front matter and detailed descriptions
|
|
4
|
+
|
|
5
|
+
### ⚙️ Engine Tuning
|
|
6
|
+
|
|
7
|
+
- **database:** centralize knex connection bootstrap for runtime and CLI
|
|
8
|
+
|
|
9
|
+
### 📚 Documentation
|
|
10
|
+
|
|
11
|
+
- add section for loading and updating AI skills in projects
|
|
12
|
+
|
|
13
|
+
### 🛠️ Fixes & Improvements
|
|
14
|
+
|
|
15
|
+
- **auth:** improve token rotation logic and ensure proper cookie attributes
|
|
16
|
+
- **cli:** parse .env values consistently in migration loader
|
|
17
|
+
- **config:** update interpolation regex to support variable names with hyphens
|
|
18
|
+
- **migration:** normalize column-level unique constraints and enhance idempotency in migrations
|
|
19
|
+
- **release:** add version output to release notes and update release title condition
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Powered by [⚡ ODAC](https://odac.run)
|
|
26
|
+
|
|
27
|
+
### ⚙️ Engine Tuning
|
|
28
|
+
|
|
29
|
+
- Extract MIME type definitions into a dedicated module.
|
|
30
|
+
|
|
31
|
+
### ⚡️ Performance Upgrades
|
|
32
|
+
|
|
33
|
+
- prevent redundant database table migration calls by introducing a static cache to track completed migrations.
|
|
34
|
+
|
|
35
|
+
### ✨ What's New
|
|
36
|
+
|
|
37
|
+
- add comprehensive ODAC Agent instructions and guidelines
|
|
38
|
+
- **auth:** implement enterprise refresh token rotation with grace period and session persistence
|
|
39
|
+
- Automatically parse JSON responses in client-side AJAX requests based on the `Content-Type` header.
|
|
40
|
+
- implement comprehensive AI agent skills system and automated CLI setup
|
|
41
|
+
|
|
42
|
+
### 🛠️ Fixes & Improvements
|
|
43
|
+
|
|
44
|
+
- add HTML escaping functionality to Form class and corresponding tests
|
|
45
|
+
- **ai:** correct target path for syncing AI skills
|
|
46
|
+
- **auth:** replace magic number with constant for rotated token threshold
|
|
47
|
+
- **auth:** replace magic number with constant for token rotation grace period
|
|
48
|
+
- enhance odac:form parser with nested quotes and dynamic binding support
|
|
49
|
+
- **route:** support nested property paths in actions and resolve App class conflict
|
|
50
|
+
- **view:** ensure odac:for with 'in' attribute parses correctly as javascript
|
|
51
|
+
- **view:** implement clear attribute in odac:form to control auto-clearing
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
Powered by [⚡ ODAC](https://odac.run)
|
|
58
|
+
|
|
1
59
|
### ⚙️ Engine Tuning
|
|
2
60
|
|
|
3
61
|
- Improve disposable domain cache management by relocating the cache path, ensuring directory existence, and standardizing error logging.
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 🔗 **Powerful Routing:** Create clean, custom URLs and manage infinite pages with a flexible routing system.
|
|
11
11
|
* ✨ **Seamless SPA Experience:** Automatic AJAX handling for forms and page transitions eliminates the need for complex client-side code.
|
|
12
12
|
* 🛡️ **Built-in Security:** Automatic CSRF protection and secure default headers keep your application safe.
|
|
13
|
-
* 🔐 **Authentication:** Ready-to-use session management
|
|
13
|
+
* 🔐 **Authentication:** Ready-to-use session management with enterprise-grade **Refresh Token Rotation**, secure password hashing, and authentication helpers.
|
|
14
14
|
* 🗄️ **Database Agnostic:** Integrated support for major databases (PostgreSQL, MySQL, SQLite) and Redis via Knex.js.
|
|
15
15
|
* 🌍 **i18n Support:** Native multi-language support to help you reach a global audience.
|
|
16
16
|
* ⏰ **Task Scheduling:** Built-in Cron job system for handling background tasks and recurring operations.
|
|
@@ -53,6 +53,16 @@ cd my-app
|
|
|
53
53
|
npm run dev
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
## 🤖 AI Skills in Projects
|
|
57
|
+
|
|
58
|
+
Load or update ODAC skills from your project root with:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx odac skills
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This command syncs built-in skills to your selected AI tool folder and can be re-run anytime to update them.
|
|
65
|
+
|
|
56
66
|
## 📂 Project Structure
|
|
57
67
|
|
|
58
68
|
```
|
package/bin/odac.js
CHANGED
|
@@ -5,6 +5,8 @@ const path = require('node:path')
|
|
|
5
5
|
const readline = require('node:readline')
|
|
6
6
|
const {execSync, spawn} = require('node:child_process')
|
|
7
7
|
const cluster = require('node:cluster')
|
|
8
|
+
const Env = require('../src/Env')
|
|
9
|
+
const {buildConnections} = require('../src/Database/ConnectionFactory')
|
|
8
10
|
|
|
9
11
|
const command = process.argv[2]
|
|
10
12
|
const args = process.argv.slice(3)
|
|
@@ -16,6 +18,72 @@ const rl = readline.createInterface({
|
|
|
16
18
|
|
|
17
19
|
const ask = question => new Promise(resolve => rl.question(question, answer => resolve(answer.trim())))
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Interactive selection menu for CLI.
|
|
23
|
+
* @param {string} title Menu title
|
|
24
|
+
* @param {string[]} options List of choice strings
|
|
25
|
+
* @returns {Promise<number>} Selected index
|
|
26
|
+
*/
|
|
27
|
+
const select = async (title, options) => {
|
|
28
|
+
if (!process.stdout.isTTY) return 0
|
|
29
|
+
|
|
30
|
+
return new Promise(resolve => {
|
|
31
|
+
let current = 0
|
|
32
|
+
const hideCursor = '\u001B[?25l'
|
|
33
|
+
const showCursor = '\u001B[?25h'
|
|
34
|
+
|
|
35
|
+
// Calculate total lines the title occupies
|
|
36
|
+
const titleLines = title.split('\n')
|
|
37
|
+
const totalLines = titleLines.length + options.length
|
|
38
|
+
|
|
39
|
+
const render = () => {
|
|
40
|
+
// Clear all lines we previously wrote
|
|
41
|
+
titleLines.forEach(line => {
|
|
42
|
+
process.stdout.write('\r\x1b[K' + line + '\n')
|
|
43
|
+
})
|
|
44
|
+
options.forEach((opt, i) => {
|
|
45
|
+
const line = i === current ? `\x1b[36m ❯ ${opt}\x1b[0m` : ` ${opt}`
|
|
46
|
+
process.stdout.write('\r\x1b[K' + line + '\n')
|
|
47
|
+
})
|
|
48
|
+
process.stdout.write(`\x1b[${totalLines}A`) // Move back to the very start
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
process.stdout.write(hideCursor)
|
|
52
|
+
if (!process.stdin.isRaw) {
|
|
53
|
+
process.stdin.setRawMode(true)
|
|
54
|
+
process.stdin.resume()
|
|
55
|
+
}
|
|
56
|
+
readline.emitKeypressEvents(process.stdin)
|
|
57
|
+
|
|
58
|
+
render()
|
|
59
|
+
|
|
60
|
+
const onKey = (str, key) => {
|
|
61
|
+
if (key.name === 'up') {
|
|
62
|
+
current = current > 0 ? current - 1 : options.length - 1
|
|
63
|
+
render()
|
|
64
|
+
} else if (key.name === 'down') {
|
|
65
|
+
current = current < options.length - 1 ? current + 1 : 0
|
|
66
|
+
render()
|
|
67
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
68
|
+
cleanup()
|
|
69
|
+
process.stdout.write(`\x1b[${totalLines}B\n`) // Move down past everything
|
|
70
|
+
resolve(current)
|
|
71
|
+
} else if (key.ctrl && key.name === 'c') {
|
|
72
|
+
cleanup()
|
|
73
|
+
process.exit()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const cleanup = () => {
|
|
78
|
+
process.stdin.removeListener('keypress', onKey)
|
|
79
|
+
if (process.stdin.isRaw) process.stdin.setRawMode(false)
|
|
80
|
+
process.stdout.write(showCursor)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
process.stdin.on('keypress', onKey)
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
19
87
|
/**
|
|
20
88
|
* Resolves Tailwind CSS paths and ensures required directories/files exist.
|
|
21
89
|
* Supports multiple CSS entry points from 'view/css'.
|
|
@@ -73,6 +141,276 @@ function getTailwindConfigs() {
|
|
|
73
141
|
return configs
|
|
74
142
|
}
|
|
75
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Manages the AI Agent skills synchronization.
|
|
146
|
+
* @param {string} targetDir The directory to sync skills into.
|
|
147
|
+
*/
|
|
148
|
+
async function manageSkills(targetDir = process.cwd()) {
|
|
149
|
+
const aiSourceDir = path.resolve(__dirname, '../docs/ai')
|
|
150
|
+
|
|
151
|
+
if (!fs.existsSync(aiSourceDir)) {
|
|
152
|
+
console.error('❌ AI components not found in framework.')
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const options = [
|
|
157
|
+
'Antigravity / Cascade (.agent/skills)',
|
|
158
|
+
'Claude / Projects (.claude/skills)',
|
|
159
|
+
'Continue (.continue/skills)',
|
|
160
|
+
'Cursor (.cursor/skills)',
|
|
161
|
+
'Kilo Code (.kilocode/skills)',
|
|
162
|
+
'Kiro CLI (.kiro/skills)',
|
|
163
|
+
'Qwen Code (.qwen/skills)',
|
|
164
|
+
'Windsurf (.windsurf/skills)',
|
|
165
|
+
'Custom Path',
|
|
166
|
+
'Skip / Cancel'
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
const choiceIndex = await select('\n🤖 \x1b[36mODAC AI Agent Skills Manager\x1b[0m\nSelect your AI Agent / IDE for setup:', options)
|
|
170
|
+
|
|
171
|
+
let targetSubDir = ''
|
|
172
|
+
let copySkillsOnly = true
|
|
173
|
+
|
|
174
|
+
const SKIP_INDEX = 9
|
|
175
|
+
const CUSTOM_INDEX = 8
|
|
176
|
+
|
|
177
|
+
if (choiceIndex === SKIP_INDEX) return // Skip / Cancel
|
|
178
|
+
|
|
179
|
+
switch (choiceIndex) {
|
|
180
|
+
case 0:
|
|
181
|
+
targetSubDir = '.agent/skills'
|
|
182
|
+
break
|
|
183
|
+
case 1:
|
|
184
|
+
targetSubDir = '.claude/skills'
|
|
185
|
+
break
|
|
186
|
+
case 2:
|
|
187
|
+
targetSubDir = '.continue/skills'
|
|
188
|
+
break
|
|
189
|
+
case 3:
|
|
190
|
+
targetSubDir = '.cursor/skills'
|
|
191
|
+
break
|
|
192
|
+
case 4:
|
|
193
|
+
targetSubDir = '.kilocode/skills'
|
|
194
|
+
break
|
|
195
|
+
case 5:
|
|
196
|
+
targetSubDir = '.kiro/skills'
|
|
197
|
+
break
|
|
198
|
+
case 6:
|
|
199
|
+
targetSubDir = '.qwen/skills'
|
|
200
|
+
break
|
|
201
|
+
case 7:
|
|
202
|
+
targetSubDir = '.windsurf/skills'
|
|
203
|
+
break
|
|
204
|
+
case CUSTOM_INDEX:
|
|
205
|
+
targetSubDir = await ask('Enter custom path: ')
|
|
206
|
+
copySkillsOnly = false
|
|
207
|
+
break
|
|
208
|
+
default:
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const targetBase = path.resolve(targetDir, targetSubDir)
|
|
213
|
+
const targetPath = targetBase
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
fs.mkdirSync(targetPath, {recursive: true})
|
|
217
|
+
|
|
218
|
+
if (copySkillsOnly) {
|
|
219
|
+
const skillsSource = path.join(aiSourceDir, 'skills')
|
|
220
|
+
fs.cpSync(skillsSource, targetPath, {recursive: true})
|
|
221
|
+
} else {
|
|
222
|
+
fs.cpSync(aiSourceDir, targetPath, {recursive: true})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log(`\n✨ AI skills successfully synced to: \x1b[32m${targetSubDir}\x1b[0m`)
|
|
226
|
+
console.log('Your AI Agent now has full knowledge of the ODAC Framework. 🚀')
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error('❌ Failed to sync AI skills:', err.message)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Bootstraps the database and migration engine, then executes the requested migration command.
|
|
234
|
+
* Why: Migration commands need DB connections but not the full server stack.
|
|
235
|
+
* @param {string} cmd - The migration subcommand
|
|
236
|
+
* @param {string[]} cliArgs - CLI arguments (e.g. --db=analytics)
|
|
237
|
+
*/
|
|
238
|
+
async function runMigration(cmd, cliArgs) {
|
|
239
|
+
const projectDir = process.cwd()
|
|
240
|
+
const envPath = path.join(projectDir, '.env')
|
|
241
|
+
const configPath = path.join(projectDir, 'odac.json')
|
|
242
|
+
|
|
243
|
+
// Load .env
|
|
244
|
+
if (fs.existsSync(envPath)) {
|
|
245
|
+
const envContent = fs.readFileSync(envPath, 'utf8')
|
|
246
|
+
envContent.split('\n').forEach(line => {
|
|
247
|
+
line = line.trim()
|
|
248
|
+
if (!line || line.startsWith('#')) return
|
|
249
|
+
const idx = line.indexOf('=')
|
|
250
|
+
if (idx === -1) return
|
|
251
|
+
const key = line.slice(0, idx).trim()
|
|
252
|
+
const value = Env._parseValue(line.slice(idx + 1).trim())
|
|
253
|
+
if (process.env[key] === undefined) process.env[key] = value
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Load config
|
|
258
|
+
if (!fs.existsSync(configPath)) {
|
|
259
|
+
console.error('❌ No odac.json found in current directory.')
|
|
260
|
+
process.exit(1)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let config
|
|
264
|
+
try {
|
|
265
|
+
let raw = fs.readFileSync(configPath, 'utf8')
|
|
266
|
+
config = JSON.parse(raw)
|
|
267
|
+
// Interpolate env vars safely by traversing parsed object values.
|
|
268
|
+
const interpolateConfig = input => {
|
|
269
|
+
if (typeof input === 'string') {
|
|
270
|
+
return input.replace(/\$\{([^{}]+)\}/g, (_, key) => process.env[key] || '')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (Array.isArray(input)) {
|
|
274
|
+
return input.map(item => interpolateConfig(item))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (input && typeof input === 'object') {
|
|
278
|
+
const output = {}
|
|
279
|
+
for (const key of Object.keys(input)) {
|
|
280
|
+
output[key] = interpolateConfig(input[key])
|
|
281
|
+
}
|
|
282
|
+
return output
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return input
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
config = interpolateConfig(config)
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.error('❌ Failed to parse odac.json:', err.message)
|
|
291
|
+
process.exit(1)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const dbConfig = config.database
|
|
295
|
+
if (!dbConfig) {
|
|
296
|
+
console.error('❌ No database configuration found in odac.json.')
|
|
297
|
+
process.exit(1)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const connections = buildConnections(dbConfig)
|
|
301
|
+
|
|
302
|
+
for (const key of Object.keys(connections)) {
|
|
303
|
+
try {
|
|
304
|
+
await connections[key].raw('SELECT 1')
|
|
305
|
+
} catch (e) {
|
|
306
|
+
console.error(`❌ Failed to connect to '${key}' database:`, e.message)
|
|
307
|
+
process.exit(1)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Parse --db flag
|
|
312
|
+
const dbFlag = cliArgs.find(a => a.startsWith('--db='))
|
|
313
|
+
const options = dbFlag ? {db: dbFlag.split('=')[1]} : {}
|
|
314
|
+
|
|
315
|
+
// Initialize migration engine
|
|
316
|
+
const Migration = require('../src/Database/Migration')
|
|
317
|
+
Migration.init(projectDir, connections)
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
if (cmd === 'migrate') {
|
|
321
|
+
console.log('🔄 Running migrations...\n')
|
|
322
|
+
const summary = await Migration.migrate(options)
|
|
323
|
+
printMigrationSummary(summary)
|
|
324
|
+
} else if (cmd === 'migrate:status') {
|
|
325
|
+
console.log('📋 Migration Status (dry-run):\n')
|
|
326
|
+
const summary = await Migration.status(options)
|
|
327
|
+
printMigrationSummary(summary)
|
|
328
|
+
} else if (cmd === 'migrate:rollback') {
|
|
329
|
+
console.log('⏪ Rolling back last batch...\n')
|
|
330
|
+
const summary = await Migration.rollback(options)
|
|
331
|
+
printMigrationSummary(summary)
|
|
332
|
+
} else if (cmd === 'migrate:snapshot') {
|
|
333
|
+
console.log('📸 Generating schema files from database...\n')
|
|
334
|
+
const result = await Migration.snapshot(options)
|
|
335
|
+
for (const [key, files] of Object.entries(result)) {
|
|
336
|
+
console.log(` \x1b[36m${key}\x1b[0m: ${files.length} schema file(s) generated`)
|
|
337
|
+
for (const f of files) {
|
|
338
|
+
console.log(` → ${path.relative(projectDir, f)}`)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
console.log('\n✅ Snapshot complete.')
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
console.error('❌ Migration error:', err.message)
|
|
345
|
+
process.exit(1)
|
|
346
|
+
} finally {
|
|
347
|
+
for (const conn of Object.values(connections)) {
|
|
348
|
+
await conn.destroy()
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Formats and prints migration operation summary to stdout.
|
|
355
|
+
* @param {object} summary - Migration result per connection
|
|
356
|
+
*/
|
|
357
|
+
function printMigrationSummary(summary) {
|
|
358
|
+
let totalOps = 0
|
|
359
|
+
|
|
360
|
+
for (const [key, result] of Object.entries(summary)) {
|
|
361
|
+
console.log(` \x1b[36m[${key}]\x1b[0m`)
|
|
362
|
+
|
|
363
|
+
const allOps = [...(result.schema || []), ...(result.files || []), ...(result.seeds || [])]
|
|
364
|
+
|
|
365
|
+
if (allOps.length === 0) {
|
|
366
|
+
console.log(' Nothing to do.')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const op of allOps) {
|
|
370
|
+
totalOps++
|
|
371
|
+
const label = formatOp(op)
|
|
372
|
+
console.log(` ${label}`)
|
|
373
|
+
}
|
|
374
|
+
console.log('')
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
console.log(totalOps > 0 ? `✅ ${totalOps} operation(s) completed.` : '✅ Everything is up to date.')
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Formats a single migration operation for CLI display.
|
|
382
|
+
* @param {object} op - Operation descriptor
|
|
383
|
+
* @returns {string} Formatted label
|
|
384
|
+
*/
|
|
385
|
+
function formatOp(op) {
|
|
386
|
+
switch (op.type) {
|
|
387
|
+
case 'create_table':
|
|
388
|
+
return `\x1b[32m+ CREATE TABLE\x1b[0m ${op.table}`
|
|
389
|
+
case 'add_column':
|
|
390
|
+
return `\x1b[32m+ ADD COLUMN\x1b[0m ${op.table}.${op.column}`
|
|
391
|
+
case 'drop_column':
|
|
392
|
+
return `\x1b[31m- DROP COLUMN\x1b[0m ${op.table}.${op.column}`
|
|
393
|
+
case 'alter_column':
|
|
394
|
+
return `\x1b[33m~ ALTER COLUMN\x1b[0m ${op.table}.${op.column}`
|
|
395
|
+
case 'add_index':
|
|
396
|
+
return `\x1b[32m+ ADD INDEX\x1b[0m ${op.table} (${op.index.columns.join(', ')})`
|
|
397
|
+
case 'drop_index':
|
|
398
|
+
return `\x1b[31m- DROP INDEX\x1b[0m ${op.table} (${op.index.columns.join(', ')})`
|
|
399
|
+
case 'pending_file':
|
|
400
|
+
return `\x1b[33m⏳ PENDING\x1b[0m ${op.name}`
|
|
401
|
+
case 'applied_file':
|
|
402
|
+
return `\x1b[32m✓ APPLIED\x1b[0m ${op.name}`
|
|
403
|
+
case 'rolled_back':
|
|
404
|
+
return `\x1b[33m↩ ROLLED BACK\x1b[0m ${op.name}`
|
|
405
|
+
case 'seed_insert':
|
|
406
|
+
return `\x1b[32m+ SEED INSERT\x1b[0m ${op.table} (${op.key})`
|
|
407
|
+
case 'seed_update':
|
|
408
|
+
return `\x1b[33m~ SEED UPDATE\x1b[0m ${op.table} (${op.key})`
|
|
409
|
+
default:
|
|
410
|
+
return ` ${op.type}`
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
76
414
|
async function run() {
|
|
77
415
|
if (command === 'init') {
|
|
78
416
|
const projectName = args[0] || '.'
|
|
@@ -110,10 +448,21 @@ async function run() {
|
|
|
110
448
|
}
|
|
111
449
|
|
|
112
450
|
console.log('\n✨ Project initialized successfully!')
|
|
451
|
+
|
|
452
|
+
// Interactive AI Skills setup
|
|
453
|
+
if (process.stdout.isTTY) {
|
|
454
|
+
const setupAIIndex = await select('\n🤖 Should we setup AI Agent skills for your IDE?', ['Yes', 'No'])
|
|
455
|
+
if (setupAIIndex === 0) {
|
|
456
|
+
await manageSkills(targetDir)
|
|
457
|
+
} else {
|
|
458
|
+
console.log('\n💡 \x1b[33mTip:\x1b[0m You can always run \x1b[36mnpx odac skills\x1b[0m later.')
|
|
459
|
+
}
|
|
460
|
+
}
|
|
113
461
|
} catch (error) {
|
|
114
462
|
console.error('❌ Error initializing project:', error.message)
|
|
115
463
|
}
|
|
116
464
|
} else if (command === 'dev') {
|
|
465
|
+
// ... existing dev logic ...
|
|
117
466
|
if (cluster.isPrimary) {
|
|
118
467
|
const configs = getTailwindConfigs()
|
|
119
468
|
const tails = []
|
|
@@ -134,20 +483,16 @@ async function run() {
|
|
|
134
483
|
|
|
135
484
|
tailwindProcess = spawn(cmd, args, {
|
|
136
485
|
stdio: ['pipe', 'ignore', 'pipe'],
|
|
137
|
-
shell: !useLocal,
|
|
486
|
+
shell: !useLocal,
|
|
138
487
|
cwd: process.cwd()
|
|
139
488
|
})
|
|
140
489
|
|
|
141
|
-
// Filter stderr: suppress status noise, forward only real errors
|
|
142
490
|
tailwindProcess.stderr.on('data', chunk => {
|
|
143
491
|
const raw = chunk.toString()
|
|
144
492
|
const lines = raw.split('\n')
|
|
145
493
|
for (const line of lines) {
|
|
146
|
-
// Strip ANSI escape codes to ensure reliable filtering
|
|
147
494
|
const clean = line.replace(/\x1B\[[0-9;]*[JKmsu]/g, '').trim()
|
|
148
|
-
|
|
149
495
|
if (!clean || clean.startsWith('Done in') || clean.startsWith('≈')) continue
|
|
150
|
-
|
|
151
496
|
process.stderr.write(`\x1b[31m[ODAC Style Error]\x1b[0m ${line}\n`)
|
|
152
497
|
}
|
|
153
498
|
})
|
|
@@ -166,7 +511,6 @@ async function run() {
|
|
|
166
511
|
|
|
167
512
|
startWatcher()
|
|
168
513
|
|
|
169
|
-
// Push a wrapper compatible with the cleanup function
|
|
170
514
|
tails.push({
|
|
171
515
|
kill: () => {
|
|
172
516
|
if (tailwindProcess) tailwindProcess.kill()
|
|
@@ -214,6 +558,10 @@ async function run() {
|
|
|
214
558
|
} else if (command === 'start') {
|
|
215
559
|
process.env.NODE_ENV = 'production'
|
|
216
560
|
require('../index.js')
|
|
561
|
+
} else if (command === 'skills') {
|
|
562
|
+
await manageSkills()
|
|
563
|
+
} else if (command === 'migrate' || command === 'migrate:status' || command === 'migrate:rollback' || command === 'migrate:snapshot') {
|
|
564
|
+
await runMigration(command, args)
|
|
217
565
|
} else {
|
|
218
566
|
console.log('Usage:')
|
|
219
567
|
console.log(' npx odac init (Interactive mode)')
|
|
@@ -221,6 +569,11 @@ async function run() {
|
|
|
221
569
|
console.log(' npx odac dev (Development mode)')
|
|
222
570
|
console.log(' npx odac build (Production build)')
|
|
223
571
|
console.log(' npx odac start (Start server)')
|
|
572
|
+
console.log(' npx odac skills (Sync AI Agent skills)')
|
|
573
|
+
console.log(' npx odac migrate (Run pending migrations)')
|
|
574
|
+
console.log(' npx odac migrate:status (Show pending changes)')
|
|
575
|
+
console.log(' npx odac migrate:rollback (Rollback last batch)')
|
|
576
|
+
console.log(' npx odac migrate:snapshot (Reverse-engineer DB to schema/)')
|
|
224
577
|
}
|
|
225
578
|
|
|
226
579
|
rl.close()
|
package/client/odac.js
CHANGED
|
@@ -142,13 +142,18 @@ if (typeof window !== 'undefined') {
|
|
|
142
142
|
xhr.onload = () => {
|
|
143
143
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
144
144
|
let responseData = xhr.responseText
|
|
145
|
-
|
|
145
|
+
const contentTypeHeader = xhr.getResponseHeader('Content-Type')
|
|
146
|
+
const isJson = dataType === 'json' || (contentTypeHeader && contentTypeHeader.includes('application/json'))
|
|
147
|
+
|
|
148
|
+
if (isJson) {
|
|
146
149
|
try {
|
|
147
150
|
responseData = JSON.parse(responseData)
|
|
148
151
|
} catch (e) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
if (dataType === 'json') {
|
|
153
|
+
console.error('JSON parse error:', e)
|
|
154
|
+
error(xhr, 'parseerror', e)
|
|
155
|
+
return
|
|
156
|
+
}
|
|
152
157
|
}
|
|
153
158
|
}
|
|
154
159
|
|
|
@@ -596,7 +601,7 @@ if (typeof window !== 'undefined') {
|
|
|
596
601
|
}
|
|
597
602
|
},
|
|
598
603
|
xhr: () => {
|
|
599
|
-
|
|
604
|
+
const xhr = new window.XMLHttpRequest()
|
|
600
605
|
xhr.upload.addEventListener(
|
|
601
606
|
'progress',
|
|
602
607
|
evt => {
|
|
@@ -661,24 +666,23 @@ if (typeof window !== 'undefined') {
|
|
|
661
666
|
this.#token.listener = true
|
|
662
667
|
}
|
|
663
668
|
if (!this.#token.hash.length) {
|
|
664
|
-
|
|
669
|
+
const req = new XMLHttpRequest()
|
|
665
670
|
req.open('GET', '/', false)
|
|
666
671
|
req.setRequestHeader('X-Odac', 'token')
|
|
667
672
|
req.setRequestHeader('X-Odac-Client', this.client())
|
|
668
673
|
req.send(null)
|
|
669
|
-
|
|
674
|
+
const req_data = JSON.parse(req.response)
|
|
670
675
|
if (req_data.token) this.#token.hash.push(req_data.token)
|
|
671
676
|
}
|
|
672
|
-
this.#token.hash.filter(n => n)
|
|
673
|
-
|
|
677
|
+
this.#token.hash = this.#token.hash.filter(n => n)
|
|
678
|
+
const return_token = this.#token.hash.shift()
|
|
674
679
|
if (!this.#token.hash.length)
|
|
675
680
|
this.#ajax({
|
|
676
681
|
url: '/',
|
|
677
682
|
type: 'GET',
|
|
678
683
|
headers: {'X-Odac': 'token', 'X-Odac-Client': this.client()},
|
|
679
684
|
success: data => {
|
|
680
|
-
|
|
681
|
-
if (result.token) this.#token.hash.push(result.token)
|
|
685
|
+
if (data.token) this.#token.hash.push(data.token)
|
|
682
686
|
}
|
|
683
687
|
})
|
|
684
688
|
return return_token
|