tina4-nodejs 3.0.0-rc.2
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/BENCHMARK_REPORT.md +96 -0
- package/CARBONAH.md +140 -0
- package/CLAUDE.md +599 -0
- package/COMPARISON.md +194 -0
- package/README.md +595 -0
- package/package.json +59 -0
- package/packages/cli/src/bin.ts +110 -0
- package/packages/cli/src/commands/init.ts +194 -0
- package/packages/cli/src/commands/migrate.ts +96 -0
- package/packages/cli/src/commands/migrateCreate.ts +59 -0
- package/packages/cli/src/commands/routes.ts +61 -0
- package/packages/cli/src/commands/serve.ts +58 -0
- package/packages/cli/src/commands/test.ts +83 -0
- package/packages/core/gallery/auth/meta.json +1 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +22 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +16 -0
- package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +97 -0
- package/packages/core/gallery/database/meta.json +1 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +13 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +17 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +23 -0
- package/packages/core/gallery/error-overlay/meta.json +1 -0
- package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +17 -0
- package/packages/core/gallery/orm/meta.json +1 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +12 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +7 -0
- package/packages/core/gallery/queue/meta.json +1 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +16 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +10 -0
- package/packages/core/gallery/rest-api/meta.json +1 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +6 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +7 -0
- package/packages/core/gallery/templates/meta.json +1 -0
- package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +15 -0
- package/packages/core/gallery/templates/src/templates/gallery_page.twig +257 -0
- package/packages/core/public/css/tina4.css +2463 -0
- package/packages/core/public/css/tina4.min.css +1 -0
- package/packages/core/public/favicon.ico +0 -0
- package/packages/core/public/images/logo.svg +5 -0
- package/packages/core/public/images/tina4-logo-icon.webp +0 -0
- package/packages/core/public/js/frond.min.js +420 -0
- package/packages/core/public/js/tina4-dev-admin.min.js +327 -0
- package/packages/core/public/js/tina4.min.js +93 -0
- package/packages/core/public/swagger/index.html +90 -0
- package/packages/core/public/swagger/oauth2-redirect.html +63 -0
- package/packages/core/src/ai.ts +359 -0
- package/packages/core/src/api.ts +248 -0
- package/packages/core/src/auth.ts +287 -0
- package/packages/core/src/cache.ts +121 -0
- package/packages/core/src/constants.ts +48 -0
- package/packages/core/src/container.ts +90 -0
- package/packages/core/src/devAdmin.ts +2024 -0
- package/packages/core/src/devMailbox.ts +316 -0
- package/packages/core/src/dotenv.ts +172 -0
- package/packages/core/src/errorOverlay.test.ts +122 -0
- package/packages/core/src/errorOverlay.ts +278 -0
- package/packages/core/src/events.ts +112 -0
- package/packages/core/src/fakeData.ts +309 -0
- package/packages/core/src/graphql.ts +812 -0
- package/packages/core/src/health.ts +31 -0
- package/packages/core/src/htmlElement.ts +172 -0
- package/packages/core/src/i18n.ts +136 -0
- package/packages/core/src/index.ts +88 -0
- package/packages/core/src/logger.ts +226 -0
- package/packages/core/src/messenger.ts +822 -0
- package/packages/core/src/middleware.ts +138 -0
- package/packages/core/src/queue.ts +481 -0
- package/packages/core/src/queueBackends/kafkaBackend.ts +348 -0
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +479 -0
- package/packages/core/src/rateLimiter.ts +107 -0
- package/packages/core/src/request.ts +189 -0
- package/packages/core/src/response.ts +146 -0
- package/packages/core/src/routeDiscovery.ts +87 -0
- package/packages/core/src/router.ts +398 -0
- package/packages/core/src/scss.ts +366 -0
- package/packages/core/src/server.ts +610 -0
- package/packages/core/src/service.ts +380 -0
- package/packages/core/src/session.ts +480 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +286 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +184 -0
- package/packages/core/src/static.ts +58 -0
- package/packages/core/src/testing.ts +233 -0
- package/packages/core/src/types.ts +98 -0
- package/packages/core/src/watcher.ts +37 -0
- package/packages/core/src/websocket.ts +408 -0
- package/packages/core/src/wsdl.ts +546 -0
- package/packages/core/templates/errors/302.twig +14 -0
- package/packages/core/templates/errors/401.twig +9 -0
- package/packages/core/templates/errors/403.twig +29 -0
- package/packages/core/templates/errors/404.twig +29 -0
- package/packages/core/templates/errors/500.twig +38 -0
- package/packages/core/templates/errors/502.twig +9 -0
- package/packages/core/templates/errors/503.twig +12 -0
- package/packages/core/templates/errors/base.twig +37 -0
- package/packages/frond/src/engine.ts +1475 -0
- package/packages/frond/src/index.ts +2 -0
- package/packages/orm/src/adapters/firebird.ts +455 -0
- package/packages/orm/src/adapters/mssql.ts +440 -0
- package/packages/orm/src/adapters/mysql.ts +355 -0
- package/packages/orm/src/adapters/postgres.ts +362 -0
- package/packages/orm/src/adapters/sqlite.ts +270 -0
- package/packages/orm/src/autoCrud.ts +231 -0
- package/packages/orm/src/baseModel.ts +536 -0
- package/packages/orm/src/database.ts +321 -0
- package/packages/orm/src/fakeData.ts +118 -0
- package/packages/orm/src/index.ts +49 -0
- package/packages/orm/src/migration.ts +392 -0
- package/packages/orm/src/model.ts +56 -0
- package/packages/orm/src/query.ts +113 -0
- package/packages/orm/src/seeder.ts +120 -0
- package/packages/orm/src/sqlTranslation.ts +272 -0
- package/packages/orm/src/types.ts +110 -0
- package/packages/orm/src/validation.ts +93 -0
- package/packages/swagger/src/generator.ts +189 -0
- package/packages/swagger/src/index.ts +2 -0
- package/packages/swagger/src/ui.ts +48 -0
- package/skills/tina4-developer.skill +0 -0
- package/skills/tina4-js.skill +0 -0
- package/skills/tina4-maintainer.skill +0 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 AI — Detect AI coding assistants and scaffold context files.
|
|
3
|
+
*
|
|
4
|
+
* Detect which AI coding tools are available and install framework-aware
|
|
5
|
+
* context so that any AI assistant understands how to build with Tina4.
|
|
6
|
+
*
|
|
7
|
+
* import { detectAi, installAiContext } from "@tina4/core";
|
|
8
|
+
*
|
|
9
|
+
* const tools = detectAi(); // [{ name: "claude-code", ... }]
|
|
10
|
+
* installAiContext(); // Scaffold context for all detected tools
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, copyFileSync, cpSync, statSync } from "node:fs";
|
|
13
|
+
import { join, resolve, relative, dirname } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
// ── Types ────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface AiTool {
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
configDir: string | null;
|
|
22
|
+
contextFile: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AiDetection {
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
configFile: string;
|
|
29
|
+
status: "detected" | "not-detected";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Tool definitions ─────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const AI_TOOLS: Record<string, AiTool> = {
|
|
35
|
+
"claude-code": {
|
|
36
|
+
name: "claude-code",
|
|
37
|
+
description: "Claude Code (Anthropic CLI)",
|
|
38
|
+
configDir: ".claude",
|
|
39
|
+
contextFile: "CLAUDE.md",
|
|
40
|
+
},
|
|
41
|
+
cursor: {
|
|
42
|
+
name: "cursor",
|
|
43
|
+
description: "Cursor IDE",
|
|
44
|
+
configDir: ".cursor",
|
|
45
|
+
contextFile: ".cursorules",
|
|
46
|
+
},
|
|
47
|
+
copilot: {
|
|
48
|
+
name: "copilot",
|
|
49
|
+
description: "GitHub Copilot",
|
|
50
|
+
configDir: ".github",
|
|
51
|
+
contextFile: ".github/copilot-instructions.md",
|
|
52
|
+
},
|
|
53
|
+
windsurf: {
|
|
54
|
+
name: "windsurf",
|
|
55
|
+
description: "Windsurf (Codeium)",
|
|
56
|
+
configDir: null,
|
|
57
|
+
contextFile: ".windsurfrules",
|
|
58
|
+
},
|
|
59
|
+
aider: {
|
|
60
|
+
name: "aider",
|
|
61
|
+
description: "Aider",
|
|
62
|
+
configDir: null,
|
|
63
|
+
contextFile: "CONVENTIONS.md",
|
|
64
|
+
},
|
|
65
|
+
cline: {
|
|
66
|
+
name: "cline",
|
|
67
|
+
description: "Cline (VS Code)",
|
|
68
|
+
configDir: null,
|
|
69
|
+
contextFile: ".clinerules",
|
|
70
|
+
},
|
|
71
|
+
codex: {
|
|
72
|
+
name: "codex",
|
|
73
|
+
description: "OpenAI Codex CLI",
|
|
74
|
+
configDir: null,
|
|
75
|
+
contextFile: "AGENTS.md",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ── Detection helpers ────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function detectTool(root: string, toolName: string): boolean {
|
|
82
|
+
const r = resolve(root);
|
|
83
|
+
switch (toolName) {
|
|
84
|
+
case "claude-code":
|
|
85
|
+
return existsSync(join(r, ".claude")) || existsSync(join(r, "CLAUDE.md"));
|
|
86
|
+
case "cursor":
|
|
87
|
+
return existsSync(join(r, ".cursor")) || existsSync(join(r, ".cursorules"));
|
|
88
|
+
case "copilot":
|
|
89
|
+
return existsSync(join(r, ".github", "copilot-instructions.md")) || existsSync(join(r, ".github"));
|
|
90
|
+
case "windsurf":
|
|
91
|
+
return existsSync(join(r, ".windsurfrules"));
|
|
92
|
+
case "aider":
|
|
93
|
+
return existsSync(join(r, ".aider.conf.yml")) || existsSync(join(r, "CONVENTIONS.md"));
|
|
94
|
+
case "cline":
|
|
95
|
+
return existsSync(join(r, ".clinerules"));
|
|
96
|
+
case "codex":
|
|
97
|
+
return existsSync(join(r, "AGENTS.md")) || existsSync(join(r, "codex.md"));
|
|
98
|
+
default:
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Public API ───────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Detect which AI coding tools are present in the project.
|
|
107
|
+
*/
|
|
108
|
+
export function detectAi(root: string = "."): AiDetection[] {
|
|
109
|
+
const r = resolve(root);
|
|
110
|
+
const results: AiDetection[] = [];
|
|
111
|
+
|
|
112
|
+
for (const [name, tool] of Object.entries(AI_TOOLS)) {
|
|
113
|
+
results.push({
|
|
114
|
+
name,
|
|
115
|
+
description: tool.description,
|
|
116
|
+
configFile: tool.contextFile,
|
|
117
|
+
status: detectTool(r, name) ? "detected" : "not-detected",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Return just the names of detected AI tools.
|
|
126
|
+
*/
|
|
127
|
+
export function detectAiNames(root: string = "."): string[] {
|
|
128
|
+
return detectAi(root)
|
|
129
|
+
.filter((t) => t.status === "detected")
|
|
130
|
+
.map((t) => t.name);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Generate a universal Tina4 context document for any AI assistant.
|
|
135
|
+
*/
|
|
136
|
+
export function generateContext(): string {
|
|
137
|
+
return `# Tina4 Node.js — AI Context
|
|
138
|
+
|
|
139
|
+
This project uses **Tina4 for Node.js/TypeScript**, a lightweight, batteries-included
|
|
140
|
+
web framework with zero third-party dependencies for core features.
|
|
141
|
+
|
|
142
|
+
**Documentation:** https://tina4.com
|
|
143
|
+
|
|
144
|
+
## Quick Start
|
|
145
|
+
|
|
146
|
+
\`\`\`bash
|
|
147
|
+
tina4nodejs init . # Scaffold project
|
|
148
|
+
tina4nodejs serve # Start dev server on port 7148
|
|
149
|
+
tina4nodejs migrate # Run database migrations
|
|
150
|
+
tina4nodejs test # Run test suite
|
|
151
|
+
tina4nodejs routes # List all registered routes
|
|
152
|
+
\`\`\`
|
|
153
|
+
|
|
154
|
+
## Project Structure
|
|
155
|
+
|
|
156
|
+
\`\`\`
|
|
157
|
+
src/routes/ — Route handlers (auto-discovered, file-based routing)
|
|
158
|
+
src/models/ — ORM models (one per file, convention-based)
|
|
159
|
+
src/templates/ — Twig templates
|
|
160
|
+
src/public/ — Static assets served at /
|
|
161
|
+
src/scss/ — SCSS files (auto-compiled to public/css/)
|
|
162
|
+
migrations/ — SQL migration files (sequential numbered)
|
|
163
|
+
test/ — Test files
|
|
164
|
+
\`\`\`
|
|
165
|
+
|
|
166
|
+
## Built-in Features (No External Packages Needed)
|
|
167
|
+
|
|
168
|
+
| Feature | Module | Import |
|
|
169
|
+
|---------|--------|--------|
|
|
170
|
+
| Routing | router | \`import { get, post, put, del } from "@tina4/core"\` |
|
|
171
|
+
| ORM | orm | \`import { BaseModel } from "@tina4/orm"\` |
|
|
172
|
+
| Database | database | \`import { initDatabase } from "@tina4/orm"\` |
|
|
173
|
+
| Templates | twig | \`import { renderTemplate } from "@tina4/twig"\` |
|
|
174
|
+
| JWT Auth | auth | \`import { createToken, validateToken } from "@tina4/core"\` |
|
|
175
|
+
| REST API Client | api | \`import { Api } from "@tina4/core"\` |
|
|
176
|
+
| GraphQL | graphql | \`import { GraphQL } from "@tina4/core"\` |
|
|
177
|
+
| WebSocket | websocket | \`import { WebSocketServer } from "@tina4/core"\` |
|
|
178
|
+
| SOAP/WSDL | wsdl | \`import { WSDLService } from "@tina4/core"\` |
|
|
179
|
+
| Email (SMTP+IMAP) | messenger | \`import { Messenger } from "@tina4/core"\` |
|
|
180
|
+
| Background Queue | queue | \`import { Queue } from "@tina4/core"\` |
|
|
181
|
+
| SCSS Compilation | scss | Auto-compiled from src/scss/ |
|
|
182
|
+
| Migrations | migration | \`tina4nodejs migrate\` CLI command |
|
|
183
|
+
| i18n | i18n | \`import { I18n } from "@tina4/core"\` |
|
|
184
|
+
| Swagger/OpenAPI | swagger | Auto-generated at /swagger |
|
|
185
|
+
| Sessions | session | \`import { Session } from "@tina4/core"\` |
|
|
186
|
+
| Middleware | middleware | \`import { MiddlewareChain } from "@tina4/core"\` |
|
|
187
|
+
| Cache | cache | \`import { responseCache } from "@tina4/core"\` |
|
|
188
|
+
|
|
189
|
+
## Key Conventions
|
|
190
|
+
|
|
191
|
+
1. **Route files export a default async function** — \`export default async function(req, res) {}\`
|
|
192
|
+
2. **File-based routing** — directory structure mirrors URL paths
|
|
193
|
+
3. **Dynamic params use brackets** — \`[id]\` for params, \`[...slug]\` for catch-all
|
|
194
|
+
4. **GET routes are public**, POST/PUT/PATCH/DELETE require auth by default
|
|
195
|
+
5. **ESM everywhere** — use \`.js\` extensions in imports
|
|
196
|
+
6. **No inline styles** — use SCSS in \`src/scss/\`
|
|
197
|
+
7. **All schema changes via migrations** — never create tables in route code
|
|
198
|
+
8. **Use built-in features** — never install packages for things Tina4 already provides
|
|
199
|
+
|
|
200
|
+
## Common Patterns
|
|
201
|
+
|
|
202
|
+
### Route
|
|
203
|
+
\`\`\`typescript
|
|
204
|
+
// src/routes/api/users/post.ts
|
|
205
|
+
import type { Tina4Request, Tina4Response } from "@tina4/core";
|
|
206
|
+
|
|
207
|
+
export const meta = { summary: "Create a user", tags: ["users"] };
|
|
208
|
+
|
|
209
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
210
|
+
const data = req.body;
|
|
211
|
+
return res.json({ created: true }, 201);
|
|
212
|
+
}
|
|
213
|
+
\`\`\`
|
|
214
|
+
|
|
215
|
+
### Model
|
|
216
|
+
\`\`\`typescript
|
|
217
|
+
// src/models/User.ts
|
|
218
|
+
export default class User {
|
|
219
|
+
static tableName = "users";
|
|
220
|
+
static fields = {
|
|
221
|
+
id: { type: "integer" as const, primaryKey: true, autoIncrement: true },
|
|
222
|
+
name: { type: "string" as const, required: true },
|
|
223
|
+
email: { type: "string" as const, required: true },
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
\`\`\`
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Install Tina4 context files for detected (or specified) AI tools.
|
|
232
|
+
*
|
|
233
|
+
* Returns list of files created/updated.
|
|
234
|
+
*/
|
|
235
|
+
export function installAiContext(
|
|
236
|
+
root: string = ".",
|
|
237
|
+
options?: { tools?: string[]; force?: boolean },
|
|
238
|
+
): string[] {
|
|
239
|
+
const r = resolve(root);
|
|
240
|
+
const force = options?.force ?? false;
|
|
241
|
+
const created: string[] = [];
|
|
242
|
+
|
|
243
|
+
const toolNames = options?.tools ?? detectAiNames(r);
|
|
244
|
+
const context = generateContext();
|
|
245
|
+
|
|
246
|
+
for (const toolName of toolNames) {
|
|
247
|
+
const tool = AI_TOOLS[toolName];
|
|
248
|
+
if (!tool) continue;
|
|
249
|
+
|
|
250
|
+
const contextPath = join(r, tool.contextFile);
|
|
251
|
+
|
|
252
|
+
// Create config directory if needed
|
|
253
|
+
if (tool.configDir) {
|
|
254
|
+
mkdirSync(join(r, tool.configDir), { recursive: true });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Ensure parent directory of context file exists
|
|
258
|
+
const parentDir = join(contextPath, "..");
|
|
259
|
+
mkdirSync(parentDir, { recursive: true });
|
|
260
|
+
|
|
261
|
+
if (!existsSync(contextPath) || force) {
|
|
262
|
+
writeFileSync(contextPath, context, "utf-8");
|
|
263
|
+
created.push(relative(r, contextPath));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Install Claude Code skills if it's Claude
|
|
267
|
+
if (toolName === "claude-code") {
|
|
268
|
+
const skillFiles = installClaudeSkills(r, force);
|
|
269
|
+
created.push(...skillFiles);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return created;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Copy Claude Code skill files from the framework's directories.
|
|
278
|
+
*/
|
|
279
|
+
function installClaudeSkills(root: string, force: boolean): string[] {
|
|
280
|
+
const created: string[] = [];
|
|
281
|
+
|
|
282
|
+
// Determine the framework root (where packages/core/src/ lives)
|
|
283
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
284
|
+
const frameworkRoot = resolve(thisDir, "..", "..", "..");
|
|
285
|
+
|
|
286
|
+
// Copy .skill files from the framework's skills/ directory to project root
|
|
287
|
+
const skillsSource = join(frameworkRoot, "skills");
|
|
288
|
+
if (existsSync(skillsSource)) {
|
|
289
|
+
for (const entry of readdirSync(skillsSource)) {
|
|
290
|
+
if (entry.endsWith(".skill")) {
|
|
291
|
+
const srcFile = join(skillsSource, entry);
|
|
292
|
+
const target = join(root, entry);
|
|
293
|
+
if (!existsSync(target) || force) {
|
|
294
|
+
copyFileSync(srcFile, target);
|
|
295
|
+
created.push(entry);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Copy skill directories from .claude/skills/ in the framework to the project
|
|
302
|
+
const frameworkSkillsDir = join(frameworkRoot, ".claude", "skills");
|
|
303
|
+
if (existsSync(frameworkSkillsDir)) {
|
|
304
|
+
const targetSkillsDir = join(root, ".claude", "skills");
|
|
305
|
+
mkdirSync(targetSkillsDir, { recursive: true });
|
|
306
|
+
for (const entry of readdirSync(frameworkSkillsDir)) {
|
|
307
|
+
const skillDir = join(frameworkSkillsDir, entry);
|
|
308
|
+
if (existsSync(skillDir) && statSync(skillDir).isDirectory()) {
|
|
309
|
+
const targetDir = join(targetSkillsDir, entry);
|
|
310
|
+
if (!existsSync(targetDir) || force) {
|
|
311
|
+
cpSync(skillDir, targetDir, { recursive: true, force: true });
|
|
312
|
+
created.push(relative(root, targetDir));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return created;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Install Tina4 context for ALL known AI tools (not just detected ones).
|
|
323
|
+
*/
|
|
324
|
+
export function installAllAiContext(root: string = ".", force: boolean = false): string[] {
|
|
325
|
+
return installAiContext(root, {
|
|
326
|
+
tools: Object.keys(AI_TOOLS),
|
|
327
|
+
force,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Generate a human-readable report of AI tool detection.
|
|
333
|
+
*/
|
|
334
|
+
export function aiStatusReport(root: string = "."): string {
|
|
335
|
+
const tools = detectAi(root);
|
|
336
|
+
const installed = tools.filter((t) => t.status === "detected");
|
|
337
|
+
const missing = tools.filter((t) => t.status === "not-detected");
|
|
338
|
+
|
|
339
|
+
const lines: string[] = ["\nTina4 AI Context Status\n"];
|
|
340
|
+
|
|
341
|
+
if (installed.length > 0) {
|
|
342
|
+
lines.push("Detected AI tools:");
|
|
343
|
+
for (const t of installed) {
|
|
344
|
+
lines.push(` + ${t.description} (${t.name})`);
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
lines.push("No AI coding tools detected.");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (missing.length > 0) {
|
|
351
|
+
lines.push("\nNot detected (install context with `tina4nodejs ai --all`):");
|
|
352
|
+
for (const t of missing) {
|
|
353
|
+
lines.push(` - ${t.description} (${t.name})`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
lines.push("");
|
|
358
|
+
return lines.join("\n");
|
|
359
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 API Client — HTTP client using Node.js built-in modules only.
|
|
3
|
+
*
|
|
4
|
+
* import { Api } from "@tina4/core";
|
|
5
|
+
*
|
|
6
|
+
* const api = new Api("https://api.example.com");
|
|
7
|
+
* const result = await api.get("/users");
|
|
8
|
+
* const result = await api.post("/users", { name: "Alice" });
|
|
9
|
+
*/
|
|
10
|
+
import http from "node:http";
|
|
11
|
+
import https from "node:https";
|
|
12
|
+
import { URL } from "node:url";
|
|
13
|
+
|
|
14
|
+
export interface ApiResult {
|
|
15
|
+
http_code: number | null;
|
|
16
|
+
body: unknown;
|
|
17
|
+
headers: Record<string, string>;
|
|
18
|
+
error: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class Api {
|
|
22
|
+
private baseUrl: string;
|
|
23
|
+
private headers: Record<string, string>;
|
|
24
|
+
private timeout: number;
|
|
25
|
+
private authHeader: string;
|
|
26
|
+
private ignoreSsl: boolean;
|
|
27
|
+
|
|
28
|
+
constructor(baseUrl: string = "", authHeader: string = "", timeout: number = 30) {
|
|
29
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
30
|
+
this.authHeader = authHeader;
|
|
31
|
+
this.timeout = timeout;
|
|
32
|
+
this.headers = {};
|
|
33
|
+
this.ignoreSsl = false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Add custom headers to all subsequent requests.
|
|
38
|
+
*/
|
|
39
|
+
addCustomHeaders(headers: Record<string, string>): void {
|
|
40
|
+
Object.assign(this.headers, headers);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Set Bearer token authentication.
|
|
45
|
+
*/
|
|
46
|
+
setBearerToken(token: string): void {
|
|
47
|
+
this.authHeader = `Bearer ${token}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Set Basic authentication.
|
|
52
|
+
*/
|
|
53
|
+
setBasicAuth(username: string, password: string): void {
|
|
54
|
+
const encoded = Buffer.from(`${username}:${password}`).toString("base64");
|
|
55
|
+
this.authHeader = `Basic ${encoded}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Disable SSL certificate verification (dev/self-signed certs only).
|
|
60
|
+
*/
|
|
61
|
+
setIgnoreSsl(ignore: boolean): void {
|
|
62
|
+
this.ignoreSsl = ignore;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* HTTP GET request.
|
|
67
|
+
*/
|
|
68
|
+
async get(path: string, params?: Record<string, string>): Promise<ApiResult> {
|
|
69
|
+
let url = this.buildUrl(path);
|
|
70
|
+
if (params && Object.keys(params).length > 0) {
|
|
71
|
+
const qs = new URLSearchParams(params).toString();
|
|
72
|
+
url += (url.includes("?") ? "&" : "?") + qs;
|
|
73
|
+
}
|
|
74
|
+
return this.execute("GET", url);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* HTTP POST request.
|
|
79
|
+
*/
|
|
80
|
+
async post(path: string, body?: unknown, contentType: string = "application/json"): Promise<ApiResult> {
|
|
81
|
+
return this.sendRequest(path, "POST", body, contentType);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* HTTP PUT request.
|
|
86
|
+
*/
|
|
87
|
+
async put(path: string, body?: unknown, contentType: string = "application/json"): Promise<ApiResult> {
|
|
88
|
+
return this.sendRequest(path, "PUT", body, contentType);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* HTTP PATCH request.
|
|
93
|
+
*/
|
|
94
|
+
async patch(path: string, body?: unknown, contentType: string = "application/json"): Promise<ApiResult> {
|
|
95
|
+
return this.sendRequest(path, "PATCH", body, contentType);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* HTTP DELETE request.
|
|
100
|
+
*/
|
|
101
|
+
async delete(path: string, body?: unknown): Promise<ApiResult> {
|
|
102
|
+
return this.sendRequest(path, "DELETE", body);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generic request method — public entry point for any HTTP method.
|
|
107
|
+
*/
|
|
108
|
+
async sendRequest(
|
|
109
|
+
path: string,
|
|
110
|
+
method: string,
|
|
111
|
+
body?: unknown,
|
|
112
|
+
contentType: string = "application/json",
|
|
113
|
+
): Promise<ApiResult> {
|
|
114
|
+
const url = this.buildUrl(path);
|
|
115
|
+
return this.execute(method.toUpperCase(), url, body, contentType);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Internal helpers ──────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
private buildUrl(path: string): string {
|
|
121
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
122
|
+
return path;
|
|
123
|
+
}
|
|
124
|
+
if (!path) {
|
|
125
|
+
return this.baseUrl;
|
|
126
|
+
}
|
|
127
|
+
return `${this.baseUrl}/${path.replace(/^\/+/, "")}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private execute(
|
|
131
|
+
method: string,
|
|
132
|
+
url: string,
|
|
133
|
+
body?: unknown,
|
|
134
|
+
contentType: string = "application/json",
|
|
135
|
+
): Promise<ApiResult> {
|
|
136
|
+
return new Promise<ApiResult>((resolve) => {
|
|
137
|
+
try {
|
|
138
|
+
const parsed = new URL(url);
|
|
139
|
+
const isHttps = parsed.protocol === "https:";
|
|
140
|
+
const transport = isHttps ? https : http;
|
|
141
|
+
|
|
142
|
+
// Build headers
|
|
143
|
+
const reqHeaders: Record<string, string> = { ...this.headers };
|
|
144
|
+
if (this.authHeader) {
|
|
145
|
+
reqHeaders["Authorization"] = this.authHeader;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Serialize body
|
|
149
|
+
let data: Buffer | undefined;
|
|
150
|
+
if (body !== undefined && body !== null) {
|
|
151
|
+
if (contentType === "application/json" && typeof body === "object") {
|
|
152
|
+
data = Buffer.from(JSON.stringify(body), "utf-8");
|
|
153
|
+
reqHeaders["Content-Type"] = "application/json";
|
|
154
|
+
} else if (typeof body === "string") {
|
|
155
|
+
data = Buffer.from(body, "utf-8");
|
|
156
|
+
reqHeaders["Content-Type"] = contentType;
|
|
157
|
+
} else if (Buffer.isBuffer(body)) {
|
|
158
|
+
data = body;
|
|
159
|
+
reqHeaders["Content-Type"] = contentType;
|
|
160
|
+
} else {
|
|
161
|
+
// Fallback: stringify anything else as JSON
|
|
162
|
+
data = Buffer.from(JSON.stringify(body), "utf-8");
|
|
163
|
+
reqHeaders["Content-Type"] = "application/json";
|
|
164
|
+
}
|
|
165
|
+
if (data) {
|
|
166
|
+
reqHeaders["Content-Length"] = String(data.length);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const options: http.RequestOptions = {
|
|
171
|
+
hostname: parsed.hostname,
|
|
172
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
173
|
+
path: parsed.pathname + parsed.search,
|
|
174
|
+
method,
|
|
175
|
+
headers: reqHeaders,
|
|
176
|
+
timeout: this.timeout * 1000,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
if (isHttps && this.ignoreSsl) {
|
|
180
|
+
(options as https.RequestOptions).rejectUnauthorized = false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const req = transport.request(options, (res) => {
|
|
184
|
+
const chunks: Buffer[] = [];
|
|
185
|
+
|
|
186
|
+
res.on("data", (chunk: Buffer) => {
|
|
187
|
+
chunks.push(chunk);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
res.on("end", () => {
|
|
191
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
192
|
+
const respHeaders: Record<string, string> = {};
|
|
193
|
+
for (const [key, val] of Object.entries(res.headers)) {
|
|
194
|
+
if (val !== undefined) {
|
|
195
|
+
respHeaders[key] = Array.isArray(val) ? val.join(", ") : val;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let parsed: unknown;
|
|
200
|
+
try {
|
|
201
|
+
parsed = JSON.parse(raw);
|
|
202
|
+
} catch {
|
|
203
|
+
parsed = raw;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
resolve({
|
|
207
|
+
http_code: res.statusCode ?? null,
|
|
208
|
+
body: parsed,
|
|
209
|
+
headers: respHeaders,
|
|
210
|
+
error: null,
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
req.on("timeout", () => {
|
|
216
|
+
req.destroy();
|
|
217
|
+
resolve({
|
|
218
|
+
http_code: null,
|
|
219
|
+
body: null,
|
|
220
|
+
headers: {},
|
|
221
|
+
error: `Request timed out after ${this.timeout}s`,
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
req.on("error", (err) => {
|
|
226
|
+
resolve({
|
|
227
|
+
http_code: null,
|
|
228
|
+
body: null,
|
|
229
|
+
headers: {},
|
|
230
|
+
error: err.message,
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (data) {
|
|
235
|
+
req.write(data);
|
|
236
|
+
}
|
|
237
|
+
req.end();
|
|
238
|
+
} catch (err) {
|
|
239
|
+
resolve({
|
|
240
|
+
http_code: null,
|
|
241
|
+
body: null,
|
|
242
|
+
headers: {},
|
|
243
|
+
error: err instanceof Error ? err.message : String(err),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|