tina4-nodejs 3.13.58 → 3.13.60

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.58)
1
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.59)
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.58 — The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.13.59 — 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
 
@@ -1246,7 +1246,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
1246
1246
  Always read and follow the instructions in .claude/skills/tina4-maintainer/SKILL.md when working on this codebase. Read its referenced files in .claude/skills/tina4-maintainer/references/ as needed for specific subsystems.
1247
1247
 
1248
1248
  ## Tina4 Developer Skill
1249
- Always read and follow the instructions in .claude/skills/tina4-developer/SKILL.md when building applications with this framework. Read its referenced files in .claude/skills/tina4-developer/references/ as needed.
1249
+ Always read and follow the instructions in .claude/skills/tina4-developer-nodejs/SKILL.md when building applications with this framework. Read its referenced files in .claude/skills/tina4-developer-nodejs/references/ as needed.
1250
1250
 
1251
1251
  ## Tina4-js Frontend Skill
1252
1252
  Always read and follow the instructions in .claude/skills/tina4-js/SKILL.md when working with tina4-js frontend code. Read its referenced files in .claude/skills/tina4-js/references/ as needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.58",
3
+ "version": "3.13.60",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - 54 built-in features, zero dependencies",
6
6
  "keywords": [
@@ -6,10 +6,11 @@
6
6
  *
7
7
  * import { showMenu, installSelected } from "@tina4/core";
8
8
  */
9
- import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync, cpSync, statSync } from "node:fs";
9
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
10
11
  import { join, resolve, relative, dirname } from "node:path";
11
12
  import { fileURLToPath } from "node:url";
12
- import { execSync } from "node:child_process";
13
+ import { execSync, execFileSync } from "node:child_process";
13
14
  import { createInterface } from "node:readline";
14
15
 
15
16
  // ── Types ────────────────────────────────────────────────────
@@ -53,6 +54,156 @@ function readVersion(): string {
53
54
  }
54
55
  }
55
56
 
57
+ // ── Skill files (the SKILL.md system) ───────────────────────────────────────
58
+ // `tina4 ai` installs the ACTUAL skills — not just a CLAUDE.md pointer to them —
59
+ // into BOTH the project (.claude/skills, so they travel with the repo) AND the
60
+ // user's global ~/.claude/skills (so they're available in every project). The
61
+ // Node developer skill lives in the tina4-nodejs repo; tina4-js + tina4-maintainer
62
+ // are the shared skills whose canonical copy is served from tina4-python. This
63
+ // mirrors the canonical install-skills.sh mapping exactly.
64
+
65
+ export const DEV_SKILL = "tina4-developer-nodejs";
66
+
67
+ interface SkillSpec {
68
+ repo: string;
69
+ references: string[];
70
+ }
71
+
72
+ const SKILLS: Record<string, SkillSpec> = {
73
+ [DEV_SKILL]: {
74
+ repo: "tina4-nodejs",
75
+ references: [
76
+ "auth-and-services.md", "data-and-orm.md", "deployment.md",
77
+ "routes-and-api.md", "templates-and-frontend.md", "realtime.md",
78
+ ],
79
+ },
80
+ "tina4-js": {
81
+ repo: "tina4-python",
82
+ references: [
83
+ "html-and-components.md", "signals-and-reactivity.md",
84
+ "persistence.md", "rtc.md",
85
+ ],
86
+ },
87
+ "tina4-maintainer": {
88
+ repo: "tina4-python",
89
+ references: [
90
+ "cli-and-deployment.md", "frond-and-frontend.md",
91
+ "routing-and-orm.md", "subsystems.md",
92
+ ],
93
+ },
94
+ };
95
+
96
+ /**
97
+ * Release ref to pull skills from — the installed framework version,
98
+ * overridable with TINA4_SKILLS_REF (e.g. to test a branch/tag). Falls
99
+ * back to "main" when the version can't be read.
100
+ */
101
+ function skillsRef(): string {
102
+ const ref = process.env.TINA4_SKILLS_REF;
103
+ if (ref) return ref;
104
+ const v = readVersion();
105
+ return v && v !== "0.0.0" ? v : "main";
106
+ }
107
+
108
+ /**
109
+ * Fetch a set of skill files over the network SYNCHRONOUSLY.
110
+ *
111
+ * Node has no built-in synchronous HTTP, and the whole installer chain
112
+ * (installSelected → installForTool → installClaudeSkills) is synchronous and
113
+ * can't be made async without breaking its existing callers/tests. So we run
114
+ * ONE blocking child `node` process that fetches every URL in parallel with the
115
+ * global `fetch` (Node 18+) and writes each body to all of its destinations.
116
+ * The child prints a JSON array of the URLs it fetched successfully; any fetch
117
+ * failure is skipped, never fatal.
118
+ *
119
+ * @param jobs one entry per unique URL, with every file path it should land in
120
+ * @returns the set of URLs that were fetched and written to disk
121
+ */
122
+ function downloadSkillsSync(jobs: { url: string; dests: string[] }[]): Set<string> {
123
+ if (jobs.length === 0) return new Set();
124
+ const child = `
125
+ const jobs = JSON.parse(process.argv[1]);
126
+ const fs = require("node:fs");
127
+ const path = require("node:path");
128
+ (async () => {
129
+ const ok = [];
130
+ await Promise.all(jobs.map(async (job) => {
131
+ try {
132
+ const resp = await fetch(job.url, { signal: AbortSignal.timeout(15000) });
133
+ if (!resp.ok) return;
134
+ const buf = Buffer.from(await resp.arrayBuffer());
135
+ for (const dest of job.dests) {
136
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
137
+ fs.writeFileSync(dest, buf);
138
+ }
139
+ ok.push(job.url);
140
+ } catch { /* skip this file */ }
141
+ }));
142
+ process.stdout.write(JSON.stringify(ok));
143
+ })();
144
+ `;
145
+ try {
146
+ const out = execFileSync(process.execPath, ["-e", child, JSON.stringify(jobs)], {
147
+ encoding: "utf-8",
148
+ timeout: 45000,
149
+ maxBuffer: 8 * 1024 * 1024,
150
+ });
151
+ return new Set<string>(JSON.parse(out || "[]"));
152
+ } catch {
153
+ return new Set();
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Install the Tina4 SKILL.md skills into the project AND the global
159
+ * ~/.claude/skills, fetched from the release ref matching this framework
160
+ * version. Returns the skills that were fully installed. Network-dependent —
161
+ * on a fetch failure the skill is skipped, never fatal.
162
+ */
163
+ export function installSkills(root: string = ".", targets?: string[]): string[] {
164
+ const ref = skillsRef();
165
+ const dests = targets ?? [
166
+ join(resolve(root), ".claude", "skills"),
167
+ join(homedir(), ".claude", "skills"),
168
+ ];
169
+
170
+ // Deduplicate: fetch each unique URL once, write it to every target path.
171
+ const jobs: { url: string; dests: string[] }[] = [];
172
+ const index = new Map<string, number>();
173
+ const add = (url: string, filePath: string) => {
174
+ let i = index.get(url);
175
+ if (i === undefined) {
176
+ i = jobs.length;
177
+ index.set(url, i);
178
+ jobs.push({ url, dests: [] });
179
+ }
180
+ jobs[i].dests.push(filePath);
181
+ };
182
+
183
+ const skillMdUrl: Record<string, string> = {};
184
+ for (const [skill, spec] of Object.entries(SKILLS)) {
185
+ const base = `https://raw.githubusercontent.com/tina4stack/${spec.repo}/${ref}/.claude/skills/${skill}`;
186
+ skillMdUrl[skill] = `${base}/SKILL.md`;
187
+ for (const dest of dests) {
188
+ add(`${base}/SKILL.md`, join(dest, skill, "SKILL.md"));
189
+ for (const r of spec.references) {
190
+ add(`${base}/references/${r}`, join(dest, skill, "references", r));
191
+ }
192
+ }
193
+ }
194
+
195
+ const ok = downloadSkillsSync(jobs);
196
+
197
+ // A skill counts as installed once its SKILL.md landed. downloadSkillsSync
198
+ // writes a fetched URL to ALL of its destinations atomically, so a hit on the
199
+ // SKILL.md URL means it reached every target.
200
+ const installed: string[] = [];
201
+ for (const skill of Object.keys(SKILLS)) {
202
+ if (ok.has(skillMdUrl[skill])) installed.push(skill);
203
+ }
204
+ return installed;
205
+ }
206
+
56
207
  /**
57
208
  * Check if a tool's context file already exists.
58
209
  */
@@ -175,7 +326,7 @@ export function skillBlock(contextFile: string): string {
175
326
  "",
176
327
  "When working on this Tina4 project, these skills give the assistant project-aware behaviour:",
177
328
  "",
178
- "- **tina4-developer** \u2014 Read `.claude/skills/tina4-developer/SKILL.md` before building features.",
329
+ `- **${DEV_SKILL}** \u2014 Read \`.claude/skills/${DEV_SKILL}/SKILL.md\` before building features.`,
179
330
  "- **tina4-js** \u2014 Read `.claude/skills/tina4-js/SKILL.md` for frontend work.",
180
331
  "- **tina4-maintainer** \u2014 Read `.claude/skills/tina4-maintainer/SKILL.md` for framework-level changes.",
181
332
  "",
@@ -187,7 +338,7 @@ export function skillBlock(contextFile: string): string {
187
338
  ].join("\n")
188
339
  : [
189
340
  "Tina4 Skills \u2014 read these files before working on this project:",
190
- " .claude/skills/tina4-developer/SKILL.md (feature development)",
341
+ ` .claude/skills/${DEV_SKILL}/SKILL.md (feature development)`,
191
342
  " .claude/skills/tina4-js/SKILL.md (frontend / tina4-js)",
192
343
  " .claude/skills/tina4-maintainer/SKILL.md (framework-level changes)",
193
344
  "Found a skill that disagrees with how Tina4 actually behaves? Tell the developer,",
@@ -331,32 +482,21 @@ function installTina4Ai(): void {
331
482
  }
332
483
 
333
484
  /**
334
- * Copy Claude Code skill files from the framework's directories.
485
+ * Install the Claude Code skills for this project.
486
+ *
487
+ * Fetches the SKILL.md skills from the release ref matching this framework
488
+ * version into BOTH the project (.claude/skills) and the user's global
489
+ * ~/.claude/skills. (The previous code copied from frameworkRoot/.claude/skills,
490
+ * which exists only in a dev checkout \u2014 the npm package never ships
491
+ * .claude/skills, so installed users got no skill files at all, only a
492
+ * CLAUDE.md pointer to files that weren't there.)
335
493
  */
336
494
  function installClaudeSkills(root: string): string[] {
337
495
  const created: string[] = [];
338
-
339
- // Determine the framework root (where packages/core/src/ lives)
340
- const thisDir = dirname(fileURLToPath(import.meta.url));
341
- const frameworkRoot = resolve(thisDir, "..", "..", "..");
342
-
343
- // Copy skill directories from .claude/skills/ in the framework to the project
344
- const frameworkSkillsDir = join(frameworkRoot, ".claude", "skills");
345
- if (existsSync(frameworkSkillsDir)) {
346
- const targetSkillsDir = join(root, ".claude", "skills");
347
- mkdirSync(targetSkillsDir, { recursive: true });
348
- for (const entry of readdirSync(frameworkSkillsDir)) {
349
- const skillDir = join(frameworkSkillsDir, entry);
350
- if (existsSync(skillDir) && statSync(skillDir).isDirectory()) {
351
- const targetDir = join(targetSkillsDir, entry);
352
- cpSync(skillDir, targetDir, { recursive: true, force: true });
353
- const rel = relative(root, targetDir);
354
- created.push(rel);
355
- console.log(` ${GREEN}\u2713${RESET} Updated ${rel}`);
356
- }
357
- }
496
+ for (const skill of installSkills(root)) {
497
+ created.push(join(".claude", "skills", skill));
498
+ console.log(` ${GREEN}\u2713${RESET} Installed .claude/skills/${skill} (project + global)`);
358
499
  }
359
-
360
500
  return created;
361
501
  }
362
502
 
@@ -347,6 +347,12 @@ export function parseDatabaseUrl(url: string, username?: string, password?: stri
347
347
  // sqlite://./relative or sqlite://relative — legacy two-slash form
348
348
  const path = url.slice("sqlite://".length);
349
349
  result = { type: "sqlite", path };
350
+ } else if (url.startsWith("sqlite:")) {
351
+ // sqlite:/abs/app.db (one slash = a real absolute path) or sqlite:app.db (relative).
352
+ // Keep the leading slash so resolveSqlitePath's isAbsolute() sees the absolute path —
353
+ // this form used to fall through and throw "unsupported scheme" (the naive-abs footgun).
354
+ const path = url.slice("sqlite:".length);
355
+ result = { type: "sqlite", path };
350
356
  } else if (url.startsWith("mssql://") || url.startsWith("sqlserver://")) {
351
357
  // Handle mssql:// and sqlserver:// with custom parsing (URL class doesn't know these schemes)
352
358
  const match = url.match(/(?:mssql|sqlserver):\/\/(?:([^:]+):([^@]+)@)?([^:/]+)(?::(\d+))?\/(.*)/);