tina4-nodejs 3.13.59 → 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 +1 -1
- package/package.json +1 -1
- package/packages/core/src/ai.ts +166 -26
package/CLAUDE.md
CHANGED
|
@@ -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
package/packages/core/src/ai.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
*
|
|
7
7
|
* import { showMenu, installSelected } from "@tina4/core";
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync, mkdirSync, writeFileSync,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
340
|
-
|
|
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
|
|