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.
Files changed (52) hide show
  1. package/.agent/rules/memory.md +10 -1
  2. package/.github/workflows/release.yml +1 -5
  3. package/AGENTS.md +47 -0
  4. package/CHANGELOG.md +58 -0
  5. package/README.md +11 -1
  6. package/bin/odac.js +359 -6
  7. package/client/odac.js +15 -11
  8. package/docs/ai/README.md +49 -0
  9. package/docs/ai/skills/SKILL.md +40 -0
  10. package/docs/ai/skills/backend/authentication.md +74 -0
  11. package/docs/ai/skills/backend/config.md +39 -0
  12. package/docs/ai/skills/backend/controllers.md +69 -0
  13. package/docs/ai/skills/backend/cron.md +57 -0
  14. package/docs/ai/skills/backend/database.md +37 -0
  15. package/docs/ai/skills/backend/forms.md +26 -0
  16. package/docs/ai/skills/backend/ipc.md +62 -0
  17. package/docs/ai/skills/backend/mail.md +41 -0
  18. package/docs/ai/skills/backend/migrations.md +80 -0
  19. package/docs/ai/skills/backend/request_response.md +42 -0
  20. package/docs/ai/skills/backend/routing.md +58 -0
  21. package/docs/ai/skills/backend/storage.md +50 -0
  22. package/docs/ai/skills/backend/streaming.md +41 -0
  23. package/docs/ai/skills/backend/structure.md +64 -0
  24. package/docs/ai/skills/backend/translations.md +49 -0
  25. package/docs/ai/skills/backend/utilities.md +31 -0
  26. package/docs/ai/skills/backend/validation.md +60 -0
  27. package/docs/ai/skills/backend/views.md +68 -0
  28. package/docs/ai/skills/frontend/core.md +73 -0
  29. package/docs/ai/skills/frontend/forms.md +28 -0
  30. package/docs/ai/skills/frontend/navigation.md +27 -0
  31. package/docs/ai/skills/frontend/realtime.md +54 -0
  32. package/docs/backend/08-database/04-migrations.md +258 -37
  33. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +2 -0
  34. package/docs/backend/10-authentication/05-session-management.md +25 -3
  35. package/package.json +1 -1
  36. package/src/Auth.js +128 -17
  37. package/src/Config.js +1 -1
  38. package/src/Database/ConnectionFactory.js +69 -0
  39. package/src/Database/Migration.js +1203 -0
  40. package/src/Database.js +35 -35
  41. package/src/Route/Internal.js +21 -18
  42. package/src/Route/MimeTypes.js +56 -0
  43. package/src/Route.js +40 -63
  44. package/src/View/Form.js +91 -51
  45. package/src/View.js +8 -3
  46. package/template/schema/users.js +23 -0
  47. package/test/Auth.test.js +310 -0
  48. package/test/Client.test.js +29 -0
  49. package/test/Config.test.js +7 -0
  50. package/test/Database/ConnectionFactory.test.js +80 -0
  51. package/test/Migration.test.js +943 -0
  52. package/test/View/Form.test.js +37 -0
@@ -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, password hashing, and authentication helpers.
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, // Valid for npm/npx compatibility if local not found
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
- if (dataType === 'json') {
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
- console.error('JSON parse error:', e)
150
- error(xhr, 'parseerror', e)
151
- return
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
- var xhr = new window.XMLHttpRequest()
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
- var req = new XMLHttpRequest()
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
- var req_data = JSON.parse(req.response)
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
- var return_token = this.#token.hash.shift()
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
- var result = JSON.parse(JSON.stringify(data))
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