tina4-nodejs 3.13.6 → 3.13.9
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/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.13.
|
|
1
|
+
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.13.9)
|
|
2
2
|
|
|
3
3
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
4
4
|
|
|
5
5
|
## What This Project Is
|
|
6
6
|
|
|
7
|
-
Tina4 for Node.js/TypeScript v3.13.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.13.9 — The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
|
|
8
8
|
|
|
9
9
|
The philosophy: zero ceremony, batteries included, file system as source of truth.
|
|
10
10
|
|
package/package.json
CHANGED
package/packages/core/src/ai.ts
CHANGED
|
@@ -149,6 +149,125 @@ export function installAll(root: string = "."): string[] {
|
|
|
149
149
|
return installSelected(root, "all");
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
// \u2500\u2500 v3.13.9: non-destructive context-file writer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
153
|
+
//
|
|
154
|
+
// Pre-v3.13.9 the installer wrote a full developer guide to CLAUDE.md
|
|
155
|
+
// (and the other context files) on every run, clobbering whatever the
|
|
156
|
+
// user had put there. Now it writes only a marker-bracketed Tina4 skill
|
|
157
|
+
// block \u2014 pointing the assistant at .claude/skills/tina4-*/SKILL.md \u2014
|
|
158
|
+
// and leaves the rest of the file alone.
|
|
159
|
+
|
|
160
|
+
/** Return [start, end] markers for a context file. */
|
|
161
|
+
export function markersFor(contextFile: string): [string, string] {
|
|
162
|
+
if (contextFile.toLowerCase().endsWith(".md")) {
|
|
163
|
+
return ["<!-- tina4-skills:start -->", "<!-- tina4-skills:end -->"];
|
|
164
|
+
}
|
|
165
|
+
return ["# tina4-skills:start", "# tina4-skills:end"];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Return the marker-bracketed Tina4 skill registration block. */
|
|
169
|
+
export function skillBlock(contextFile: string): string {
|
|
170
|
+
const [start, end] = markersFor(contextFile);
|
|
171
|
+
const isMd = contextFile.toLowerCase().endsWith(".md");
|
|
172
|
+
const body = isMd
|
|
173
|
+
? [
|
|
174
|
+
"## Tina4 Skills",
|
|
175
|
+
"",
|
|
176
|
+
"When working on this Tina4 project, these skills give the assistant project-aware behaviour:",
|
|
177
|
+
"",
|
|
178
|
+
"- **tina4-developer** \u2014 Read `.claude/skills/tina4-developer/SKILL.md` before building features.",
|
|
179
|
+
"- **tina4-js** \u2014 Read `.claude/skills/tina4-js/SKILL.md` for frontend work.",
|
|
180
|
+
"- **tina4-maintainer** \u2014 Read `.claude/skills/tina4-maintainer/SKILL.md` for framework-level changes.",
|
|
181
|
+
"",
|
|
182
|
+
"See https://tina4.com for full docs.",
|
|
183
|
+
].join("\n")
|
|
184
|
+
: [
|
|
185
|
+
"Tina4 Skills \u2014 read these files before working on this project:",
|
|
186
|
+
" .claude/skills/tina4-developer/SKILL.md (feature development)",
|
|
187
|
+
" .claude/skills/tina4-js/SKILL.md (frontend / tina4-js)",
|
|
188
|
+
" .claude/skills/tina4-maintainer/SKILL.md (framework-level changes)",
|
|
189
|
+
"Docs: https://tina4.com",
|
|
190
|
+
].join("\n");
|
|
191
|
+
return `${start}\n${body}\n${end}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** True iff both start and end markers appear in order. */
|
|
195
|
+
export function hasMarkers(existing: string, start: string, end: string): boolean {
|
|
196
|
+
const sIdx = existing.indexOf(start);
|
|
197
|
+
if (sIdx === -1) return false;
|
|
198
|
+
return existing.indexOf(end, sIdx + start.length) !== -1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Replace the bracketed block in `existing` with `block`. */
|
|
202
|
+
export function replaceMarkerBlock(existing: string, block: string, start: string, end: string): string {
|
|
203
|
+
const sIdx = existing.indexOf(start);
|
|
204
|
+
if (sIdx === -1) return existing.replace(/\s+$/, "") + "\n\n" + block + "\n";
|
|
205
|
+
const eIdx = existing.indexOf(end, sIdx + start.length);
|
|
206
|
+
if (eIdx === -1) return existing.replace(/\s+$/, "") + "\n\n" + block + "\n";
|
|
207
|
+
const before = existing.slice(0, sIdx).replace(/\s+$/, "");
|
|
208
|
+
const after = existing.slice(eIdx + end.length).replace(/^\n+/, "");
|
|
209
|
+
const glueBefore = before ? "\n\n" : "";
|
|
210
|
+
const glueAfter = after ? "\n" + after : "\n";
|
|
211
|
+
return `${before}${glueBefore}${block}${glueAfter}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const OLD_FRAMEWORK_HEADERS = [
|
|
215
|
+
"# Tina4 Python",
|
|
216
|
+
"# Tina4 PHP",
|
|
217
|
+
"# Tina4 Ruby",
|
|
218
|
+
"# CLAUDE.md \u2014 AI Developer Guide for tina4-nodejs",
|
|
219
|
+
"# CLAUDE.md - AI Developer Guide for tina4-nodejs",
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* True if the file starts with a header the pre-v3.13.9 installer
|
|
224
|
+
* wrote. Used to migrate one-time off the old clobber-style install.
|
|
225
|
+
*/
|
|
226
|
+
export function looksLikeOldFrameworkInstall(existing: string): boolean {
|
|
227
|
+
const head = existing.replace(/^\s+/, "").slice(0, 400);
|
|
228
|
+
return OLD_FRAMEWORK_HEADERS.some((h) => head.startsWith(h));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Write the context file non-destructively. Returns a human-readable
|
|
233
|
+
* action verb for the caller's log line.
|
|
234
|
+
*
|
|
235
|
+
* Four branches:
|
|
236
|
+
* 1. Doesn't exist \u2192 write framework guide + skill block
|
|
237
|
+
* 2. Has markers \u2192 refresh just the skill block (idempotent)
|
|
238
|
+
* 3. Old header \u2192 migrate: replace old dump with new guide + block
|
|
239
|
+
* 4. User content \u2192 append the skill block, preserve everything else
|
|
240
|
+
*/
|
|
241
|
+
export function writeOrMerge(contextPath: string, contextFile: string, frameworkGuide: string): string {
|
|
242
|
+
const block = skillBlock(contextFile);
|
|
243
|
+
const [start, end] = markersFor(contextFile);
|
|
244
|
+
|
|
245
|
+
if (!existsSync(contextPath)) {
|
|
246
|
+
writeFileSync(contextPath, frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
247
|
+
return "Installed";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const existing = readFileSync(contextPath, "utf-8");
|
|
251
|
+
|
|
252
|
+
if (hasMarkers(existing, start, end)) {
|
|
253
|
+
writeFileSync(contextPath, replaceMarkerBlock(existing, block, start, end), "utf-8");
|
|
254
|
+
return "Refreshed skill block in";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (looksLikeOldFrameworkInstall(existing)) {
|
|
258
|
+
const head = existing.replace(/^\s+/, "");
|
|
259
|
+
const preamble = existing.slice(0, existing.length - head.length);
|
|
260
|
+
const newContent =
|
|
261
|
+
(preamble.trim() ? preamble.replace(/\s+$/, "") + "\n\n" : "") +
|
|
262
|
+
frameworkGuide.replace(/\s+$/, "") + "\n\n" + block + "\n";
|
|
263
|
+
writeFileSync(contextPath, newContent, "utf-8");
|
|
264
|
+
return "Migrated (replaced old framework dump in)";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
writeFileSync(contextPath, existing.replace(/\s+$/, "") + "\n\n" + block + "\n", "utf-8");
|
|
268
|
+
return "Appended skill block to";
|
|
269
|
+
}
|
|
270
|
+
|
|
152
271
|
/**
|
|
153
272
|
* Install context file for a single tool.
|
|
154
273
|
*/
|
|
@@ -163,11 +282,11 @@ function installForTool(root: string, tool: AiTool, context: string): string[] {
|
|
|
163
282
|
const parentDir = dirname(contextPath);
|
|
164
283
|
mkdirSync(parentDir, { recursive: true });
|
|
165
284
|
|
|
166
|
-
//
|
|
167
|
-
|
|
285
|
+
// v3.13.9: non-destructive write \u2014 see writeOrMerge above.
|
|
286
|
+
const action = writeOrMerge(contextPath, tool.contextFile, context);
|
|
168
287
|
const rel = relative(root, contextPath);
|
|
169
288
|
created.push(rel);
|
|
170
|
-
console.log(` ${GREEN}\u2713${RESET}
|
|
289
|
+
console.log(` ${GREEN}\u2713${RESET} ${action} ${rel}`);
|
|
171
290
|
|
|
172
291
|
// Claude-specific extras
|
|
173
292
|
if (tool.name === "claude-code") {
|
|
@@ -1278,7 +1278,31 @@ ${reset}
|
|
|
1278
1278
|
res({ error: "Not Found", statusCode: 404, message: `No route found for ${req.method} ${pathname}` }, 404);
|
|
1279
1279
|
}
|
|
1280
1280
|
} catch (err) {
|
|
1281
|
-
|
|
1281
|
+
// v3.13.7: log structured + surface to observability BEFORE rendering.
|
|
1282
|
+
// Listeners get the canonical {exception, request} payload mirrored
|
|
1283
|
+
// by Python / PHP / Ruby. Listener errors are swallowed + warning-
|
|
1284
|
+
// logged so a broken listener can't break the 500 page.
|
|
1285
|
+
Log.error(`Route error: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`, {
|
|
1286
|
+
method: req?.method,
|
|
1287
|
+
path: req?.path,
|
|
1288
|
+
});
|
|
1289
|
+
try {
|
|
1290
|
+
const { Events } = await import("./events.js");
|
|
1291
|
+
Events.emit("tina4.request.error", { exception: err, request: req });
|
|
1292
|
+
} catch (listenerErr) {
|
|
1293
|
+
try {
|
|
1294
|
+
Log.warn(
|
|
1295
|
+
`Listener for tina4.request.error raised: ${
|
|
1296
|
+
listenerErr instanceof Error
|
|
1297
|
+
? `${listenerErr.name}: ${listenerErr.message}`
|
|
1298
|
+
: String(listenerErr)
|
|
1299
|
+
}`
|
|
1300
|
+
);
|
|
1301
|
+
} catch {
|
|
1302
|
+
// Log failures must never block the 500 render.
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1282
1306
|
if (!res.raw.writableEnded) {
|
|
1283
1307
|
if (isDevMode() && err instanceof Error) {
|
|
1284
1308
|
// Rich error overlay with stack trace, source context, and line numbers
|
|
@@ -1287,9 +1311,12 @@ ${reset}
|
|
|
1287
1311
|
res.raw.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
1288
1312
|
res.raw.end(overlayHtml);
|
|
1289
1313
|
} else {
|
|
1290
|
-
|
|
1314
|
+
// v3.13.7 SECURITY (CWE-209): production response body must NOT
|
|
1315
|
+
// contain the stack trace or exception message. Pass an empty
|
|
1316
|
+
// error_message — the 500.twig template only renders the trace
|
|
1317
|
+
// block when error_message is truthy.
|
|
1291
1318
|
const html500 = await renderErrorPage(500, {
|
|
1292
|
-
error_message:
|
|
1319
|
+
error_message: "",
|
|
1293
1320
|
request_id: `${Date.now().toString(36)}`,
|
|
1294
1321
|
path: req.path,
|
|
1295
1322
|
}, templatesDir);
|
|
@@ -1297,7 +1324,7 @@ ${reset}
|
|
|
1297
1324
|
res.raw.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
1298
1325
|
res.raw.end(html500);
|
|
1299
1326
|
} else {
|
|
1300
|
-
res({ error: "Internal Server Error", statusCode: 500
|
|
1327
|
+
res({ error: "Internal Server Error", statusCode: 500 }, 500);
|
|
1301
1328
|
}
|
|
1302
1329
|
}
|
|
1303
1330
|
}
|
|
@@ -27,7 +27,7 @@ body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; c
|
|
|
27
27
|
<div class="error-title">Server Error</div>
|
|
28
28
|
</div>
|
|
29
29
|
<div class="error-msg">Something went wrong while processing your request.</div>
|
|
30
|
-
<pre class="error-trace">{{ error_message }}</pre>
|
|
30
|
+
{% if error_message %}<pre class="error-trace">{{ error_message }}</pre>{% endif %}
|
|
31
31
|
<div class="error-footer">
|
|
32
32
|
<span class="error-hint">Fix the error and save to auto-reload</span>
|
|
33
33
|
<span class="error-id">{{ request_id }}</span>
|