tkeron 5.2.0 → 6.0.0
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/README.md +17 -16
- package/bun.lock +13 -196
- package/changelog.md +52 -3
- package/examples/init_sample/websrc/api-service.ts +24 -24
- package/examples/init_sample/websrc/counter-card.com.html +5 -2
- package/examples/init_sample/websrc/hero-section.com.html +35 -5
- package/examples/init_sample/websrc/html-components-card.com.html +6 -2
- package/examples/init_sample/websrc/index.html +58 -25
- package/examples/init_sample/websrc/index.pre.ts +20 -15
- package/examples/init_sample/websrc/ts-components-card.com.html +7 -2
- package/examples/with_style_dedup/websrc/index.html +29 -0
- package/examples/with_style_dedup/websrc/tag-chip.com.ts +19 -0
- package/index.ts +15 -0
- package/package.json +10 -23
- package/skills/tkeron/SKILL.md +286 -0
- package/skills/tkeron-components/SKILL.md +785 -0
- package/skills/tkeron-organization/SKILL.md +230 -0
- package/skills/tkeron-patterns/SKILL.md +495 -0
- package/skills/tkeron-testing/SKILL.md +194 -0
- package/skills/tkeron-troubleshooting/SKILL.md +259 -0
- package/src/build.ts +7 -6
- package/src/deduplicateComStyles.ts +41 -0
- package/src/develop.ts +1 -2
- package/src/init.ts +1 -2
- package/src/processCom.ts +2 -2
- package/src/processComTs.ts +2 -2
- package/src/skills.ts +139 -0
- package/src/skillsWrapper.ts +79 -0
- package/docs/mcp-server.md +0 -240
- package/mcp-server.ts +0 -272
- package/src/cleanupOrphanedTempDirs.ts +0 -31
- package/src/promptUser.ts +0 -15
- package/src/setupSigintHandler.ts +0 -6
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tkeron-troubleshooting
|
|
3
|
+
description: "Troubleshooting tkeron builds and the dev server: common errors with cause-and-fix (Bundle failed, Cannot resolve, missing hyphen, component not replaced, build hangs, websrc not found), dev server singleton recovery after a crash, debugging workflow (read web/, isolate the offending file, check iteration limits), and a quick diagnostic flowchart. Use this skill when a build fails, a component is not inlined, the dev server is down, or output looks wrong."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tkeron — Troubleshooting
|
|
7
|
+
|
|
8
|
+
## Diagnostic Flowchart
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Something is wrong
|
|
12
|
+
│
|
|
13
|
+
├─ Build fails with an error message? → "Common Build Errors" section
|
|
14
|
+
├─ Build succeeds but output is wrong? → "Output Looks Wrong" section
|
|
15
|
+
├─ Dev server is down / hot reload not working? → "Dev Server Recovery" section
|
|
16
|
+
├─ Test fails? → see tkeron-testing → "Common Test Failures"
|
|
17
|
+
└─ Type error in IDE only? → "IDE vs Build" section
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Common Build Errors
|
|
23
|
+
|
|
24
|
+
### `Bundle failed` / `Cannot resolve "./index.js"`
|
|
25
|
+
|
|
26
|
+
**Cause**: HTML references `.js` but the source is `.ts`.
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<!-- ❌ -->
|
|
30
|
+
<script src="index.js"></script>
|
|
31
|
+
|
|
32
|
+
<!-- ✅ -->
|
|
33
|
+
<script type="module" src="./index.ts"></script>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### `Cannot resolve "/styles.css"`
|
|
37
|
+
|
|
38
|
+
**Cause**: absolute path. Bun resolves from the source file, not from the server root.
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<!-- ❌ -->
|
|
42
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
43
|
+
<!-- ✅ -->
|
|
44
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### `Component name must contain hyphen`
|
|
48
|
+
|
|
49
|
+
**Cause**: a `.com.html` / `.com.ts` / `.com.md` whose base name has no hyphen, or collides with a standard HTML tag.
|
|
50
|
+
|
|
51
|
+
Fix: rename to kebab-case with a hyphen: `card.com.html` → `info-card.com.html`. See `tkeron-organization` for naming rules.
|
|
52
|
+
|
|
53
|
+
### `Source directory websrc does not exist`
|
|
54
|
+
|
|
55
|
+
Fix: run `tk init .` to scaffold, or `mkdir websrc` and create at least an `index.html`.
|
|
56
|
+
|
|
57
|
+
### `Skill files already exist: ...` (from `tk skills`)
|
|
58
|
+
|
|
59
|
+
Fix: pass `--force` to overwrite, or delete the existing skill folder first.
|
|
60
|
+
|
|
61
|
+
### `Skills directory not found in tkeron installation`
|
|
62
|
+
|
|
63
|
+
**Cause**: corrupted or partial install of the `tkeron` package.
|
|
64
|
+
|
|
65
|
+
Fix: `bun add -g tkeron@latest` (or reinstall locally).
|
|
66
|
+
|
|
67
|
+
### Build hangs forever
|
|
68
|
+
|
|
69
|
+
**Cause**: `fetch()` without timeout in `.pre.ts` / `.post.ts` / `.com.ts`. The build event loop waits for the promise.
|
|
70
|
+
|
|
71
|
+
Fix: always wrap external fetches with `AbortController` + a 5 s timeout. See pattern in `tkeron-patterns`.
|
|
72
|
+
|
|
73
|
+
### `Maximum iterations reached` (or component loop never settles)
|
|
74
|
+
|
|
75
|
+
**Cause**: a `.com.ts` keeps emitting the same custom element it is processing, producing infinite expansion. Or two components reference each other circularly.
|
|
76
|
+
|
|
77
|
+
Fix:
|
|
78
|
+
|
|
79
|
+
1. Find the component whose `innerHTML` contains its own tag.
|
|
80
|
+
2. Break the cycle (emit raw HTML instead of the wrapper, or use a different tag).
|
|
81
|
+
3. Limit: 10 iterations, 50 nesting levels — exceeding either aborts the build.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Output Looks Wrong
|
|
86
|
+
|
|
87
|
+
### A component tag is still in `web/index.html`
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<!-- Final output still shows: -->
|
|
91
|
+
<user-card></user-card>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Causes (in order of frequency):
|
|
95
|
+
|
|
96
|
+
1. **File name mismatch**: tag is `<user-card>` but file is `userCard.com.html` or `usercard.com.html`. Tkeron is case-insensitive but expects matching kebab-case.
|
|
97
|
+
2. **Wrong extension**: file ends in `.html` instead of `.com.html`.
|
|
98
|
+
3. **File outside `websrc/`**: tkeron only scans `websrc/`.
|
|
99
|
+
4. **Typo**: `<user-cards>` vs `user-card.com.html`.
|
|
100
|
+
|
|
101
|
+
Verify:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
ls websrc/**/*.com.* | grep -i user-card
|
|
105
|
+
grep -rn "<user-card" websrc/
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### A `.com.ts` runs but `com.innerHTML` is empty in output
|
|
109
|
+
|
|
110
|
+
**Cause**: the `.com.ts` did not assign `com.innerHTML`. Only `com.innerHTML` survives — `textContent`, `appendChild`, `setAttribute` on `com` itself are all discarded.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// ❌ — discarded
|
|
114
|
+
com.textContent = "hi";
|
|
115
|
+
com.appendChild(document.createElement("span"));
|
|
116
|
+
|
|
117
|
+
// ✅ — preserved
|
|
118
|
+
com.innerHTML = `<span>hi</span>`;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### A `<script>` inside `com.innerHTML` is corrupted in output
|
|
122
|
+
|
|
123
|
+
**Cause**: the HTML parser re-serializes `innerHTML`, and `<`, `>`, `&` inside script JS get double-escaped.
|
|
124
|
+
|
|
125
|
+
Fix:
|
|
126
|
+
|
|
127
|
+
- Avoid comparison operators inside the inline script (`>=`, `<=`, `>`, `<`).
|
|
128
|
+
- Use `matchMedia('(min-width: Xpx)')` instead of `window.innerWidth >= X`.
|
|
129
|
+
- Move complex logic to a separate browser `.ts` file and only emit a `<script type="module" src="./x.ts"></script>` tag.
|
|
130
|
+
|
|
131
|
+
### Custom CSS selector targets the wrapper tag and matches nothing
|
|
132
|
+
|
|
133
|
+
**Cause**: the wrapper tag (`<my-section>`) disappears from the output — only the children of `innerHTML` remain.
|
|
134
|
+
|
|
135
|
+
Fix: write CSS targeting the class/element you emit **inside** `innerHTML`, never the custom tag. See `tkeron-components` → "wrapper tag disappears".
|
|
136
|
+
|
|
137
|
+
### `.pre.ts` ran but the page does not reflect it
|
|
138
|
+
|
|
139
|
+
Checklist:
|
|
140
|
+
|
|
141
|
+
1. Is the file named EXACTLY `<page>.pre.ts` next to `<page>.html`? Pairing is by file name.
|
|
142
|
+
2. Did the script throw silently? Run `tk build` and read the full stderr.
|
|
143
|
+
3. Are you querying nodes that don't exist yet because the component loop hasn't run? `.pre.ts` runs BEFORE components — query only what is in the source HTML.
|
|
144
|
+
|
|
145
|
+
### `.post.ts` ran but did not see component output
|
|
146
|
+
|
|
147
|
+
Same as above, but reversed: `.post.ts` runs AFTER the component loop, so you CAN query nodes emitted by components. If you can't, check the script name pairs the page, and that you didn't accidentally use `.pre.ts` instead.
|
|
148
|
+
|
|
149
|
+
### Assets disappeared from `web/`
|
|
150
|
+
|
|
151
|
+
Check:
|
|
152
|
+
|
|
153
|
+
1. File names contain spaces? Tkeron silently fails to copy them. Rename to kebab-case.
|
|
154
|
+
2. File is inside a `.com.*` or `.pre.ts` pair? Those are excluded from output by design.
|
|
155
|
+
3. File has the `.com.html` / `.com.ts` / `.com.md` / `.pre.ts` / `.post.ts` extension? Those are never copied.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Dev Server Recovery
|
|
160
|
+
|
|
161
|
+
`tk dev` is a **singleton process** per port. The first thing to check is whether it is even running.
|
|
162
|
+
|
|
163
|
+
### Check status
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
fuser 3000/tcp 2>/dev/null
|
|
167
|
+
# Empty output → not running
|
|
168
|
+
# Has output → running
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Recovery after a crash
|
|
172
|
+
|
|
173
|
+
`tk dev` can crash when an edit in `websrc/` introduces a compile or syntax error. Steps:
|
|
174
|
+
|
|
175
|
+
1. Confirm it's down: `fuser 3000/tcp 2>/dev/null` returns empty.
|
|
176
|
+
2. Read the terminal where `tk dev` was running — the last lines explain why it crashed.
|
|
177
|
+
3. Identify the offending file in `websrc/` (TS error, malformed component, broken import).
|
|
178
|
+
4. Fix the file.
|
|
179
|
+
5. Relaunch **in background**:
|
|
180
|
+
```bash
|
|
181
|
+
tk dev
|
|
182
|
+
```
|
|
183
|
+
(In agent sessions, this MUST be an async terminal — `tk dev` blocks indefinitely.)
|
|
184
|
+
6. Confirm it is up: `fuser 3000/tcp 2>/dev/null` returns output.
|
|
185
|
+
|
|
186
|
+
### Port already in use
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
Error: listen EADDRINUSE :::3000
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Cause: another `tk dev` (or another process) holds port 3000.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
fuser -k 3000/tcp # kill the holder
|
|
196
|
+
# OR launch on a different port:
|
|
197
|
+
tk dev 3001
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Hot reload not firing
|
|
201
|
+
|
|
202
|
+
1. Browser tab too old (before the reload script was injected) → hard refresh once (`Ctrl+Shift+R`).
|
|
203
|
+
2. SSE connection blocked by a proxy/extension → check DevTools → Network → `/dev-reload` should be `text/event-stream`, status `200`, hanging open.
|
|
204
|
+
3. The watcher missed the file (rare on network volumes / Docker bind mounts) → restart `tk dev`.
|
|
205
|
+
|
|
206
|
+
### `tk dev` blocks the agent terminal
|
|
207
|
+
|
|
208
|
+
`tk dev` runs forever by design. In agent sessions:
|
|
209
|
+
|
|
210
|
+
- ALWAYS launch it as an async terminal (background mode).
|
|
211
|
+
- NEVER run it synchronously — it will time out and leave a zombie.
|
|
212
|
+
- Use `fuser 3000/tcp 2>/dev/null` as the readiness check, not a sleep.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Debugging Workflow
|
|
217
|
+
|
|
218
|
+
When something is wrong, follow this order:
|
|
219
|
+
|
|
220
|
+
1. **Reproduce with `tk build`** (not `tk dev`) — it's deterministic and gives full stderr.
|
|
221
|
+
2. **Read `web/<page>.html`** — does the component appear unprocessed? Are scripts referenced as `.js`?
|
|
222
|
+
3. **Isolate**: comment out custom elements one by one to find the offender.
|
|
223
|
+
4. **Strip to minimum**: replace the suspect `.com.ts` body with `com.innerHTML = "OK";` and rebuild — confirms whether the file is even loaded.
|
|
224
|
+
5. **Check naming**: file name vs tag name (kebab-case, hyphen, no collisions with standard HTML).
|
|
225
|
+
6. **Check pairing**: `.pre.ts` / `.post.ts` must share the base name with the `.html`.
|
|
226
|
+
7. **Check imports**: relative paths, no `.js` extensions for source TS files imported from build-time scripts.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## IDE vs Build
|
|
231
|
+
|
|
232
|
+
The IDE can show errors that the build does not (and vice versa):
|
|
233
|
+
|
|
234
|
+
| Symptom | What it means |
|
|
235
|
+
| --------------------------------------- | ---------------------------------------------------------------- |
|
|
236
|
+
| IDE: red squiggle on `com` | `tkeron.d.ts` missing or not in `tsconfig.json` `include`. |
|
|
237
|
+
| IDE: red squiggle on `document` in .pre | Same as above for the `*.pre.ts` declaration. |
|
|
238
|
+
| Build: passes but IDE shows type errors | Tkeron does NOT type-check. Run `bun x tsc --noEmit` to confirm. |
|
|
239
|
+
| Build: fails with a TS-looking error | It's a Bun **compile** error, not a type error — fix the syntax. |
|
|
240
|
+
|
|
241
|
+
Recommended pre-commit / CI step:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
bun x tsc --noEmit && tk build
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Last-Resort Reset
|
|
250
|
+
|
|
251
|
+
If the project is in a weird state (stale temp dirs, mismatched output):
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# Remove the output and any leftover temp build dirs
|
|
255
|
+
rm -rf web/ .tktmp_*
|
|
256
|
+
tk build
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`getBuildResult` cleans its own temp dirs (`.tktmp_buildresult-*`), but a hard-killed build can leave them behind. They are always siblings of `websrc/`.
|
package/src/build.ts
CHANGED
|
@@ -5,13 +5,12 @@ import { processCom } from "./processCom";
|
|
|
5
5
|
import { processComTs } from "./processComTs";
|
|
6
6
|
import { processComMd } from "./processComMd";
|
|
7
7
|
import { processPost } from "./processPost";
|
|
8
|
+
import { deduplicateComStyles } from "./deduplicateComStyles";
|
|
8
9
|
import { rm, exists, mkdir, cp } from "fs/promises";
|
|
9
10
|
import type { Logger } from "@tkeron/tools";
|
|
10
|
-
import { silentLogger } from "@tkeron/tools";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
TEMP_DIR_PREFIX,
|
|
14
|
-
} from "./cleanupOrphanedTempDirs";
|
|
11
|
+
import { silentLogger, cleanupOrphanedTempDirs } from "@tkeron/tools";
|
|
12
|
+
|
|
13
|
+
export const TEMP_DIR_PREFIX = ".tktmp_build-";
|
|
15
14
|
|
|
16
15
|
export interface BuildOptions {
|
|
17
16
|
sourceDir?: string;
|
|
@@ -37,7 +36,7 @@ export const build = async (options: BuildOptions) => {
|
|
|
37
36
|
`${TEMP_DIR_PREFIX}${crypto.randomUUID()}`,
|
|
38
37
|
);
|
|
39
38
|
|
|
40
|
-
await cleanupOrphanedTempDirs(sourceParent, log);
|
|
39
|
+
await cleanupOrphanedTempDirs(sourceParent, TEMP_DIR_PREFIX, log);
|
|
41
40
|
|
|
42
41
|
try {
|
|
43
42
|
await mkdir(tempDir, { recursive: true });
|
|
@@ -57,6 +56,8 @@ export const build = async (options: BuildOptions) => {
|
|
|
57
56
|
}
|
|
58
57
|
}
|
|
59
58
|
|
|
59
|
+
await deduplicateComStyles(tempDir);
|
|
60
|
+
|
|
60
61
|
await processPost(tempDir, { logger: log, projectRoot: sourceParent });
|
|
61
62
|
|
|
62
63
|
if (await exists(target)) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parseHTML } from "@tkeron/html-parser";
|
|
2
|
+
import { getFilePaths } from "@tkeron/tools";
|
|
3
|
+
|
|
4
|
+
const DOCTYPE = "<!doctype html>\n";
|
|
5
|
+
|
|
6
|
+
export const deduplicateComStyles = async (tempDir: string): Promise<void> => {
|
|
7
|
+
if (!tempDir || typeof tempDir !== "string") return;
|
|
8
|
+
|
|
9
|
+
const htmlFiles = getFilePaths(tempDir, "**/*.html", true).filter(
|
|
10
|
+
(p) => !p.endsWith(".com.html"),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
await Promise.all(
|
|
14
|
+
htmlFiles.map(async (htmlFile) => {
|
|
15
|
+
const htmlContent = await Bun.file(htmlFile).text();
|
|
16
|
+
const document = parseHTML(htmlContent);
|
|
17
|
+
|
|
18
|
+
const allStyles = document.querySelectorAll("style[data-tk-com]");
|
|
19
|
+
if (!allStyles || allStyles.length === 0) return;
|
|
20
|
+
|
|
21
|
+
const seen = new Set<string>();
|
|
22
|
+
const head = document.querySelector("head");
|
|
23
|
+
|
|
24
|
+
for (const style of allStyles) {
|
|
25
|
+
const tkId = style.getAttribute("data-tk-com");
|
|
26
|
+
if (!tkId) continue;
|
|
27
|
+
|
|
28
|
+
if (seen.has(tkId)) {
|
|
29
|
+
style.remove();
|
|
30
|
+
} else {
|
|
31
|
+
seen.add(tkId);
|
|
32
|
+
if (head) head.appendChild(style);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const htmlElement = document.documentElement;
|
|
37
|
+
const output = DOCTYPE + (htmlElement?.outerHTML || "");
|
|
38
|
+
await Bun.write(htmlFile, output);
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
};
|
package/src/develop.ts
CHANGED
|
@@ -2,8 +2,7 @@ import { resolve, dirname } from "path";
|
|
|
2
2
|
import { watch } from "fs";
|
|
3
3
|
import { build } from "./build";
|
|
4
4
|
import type { Logger } from "@tkeron/tools";
|
|
5
|
-
import { logger as defaultLogger } from "@tkeron/tools";
|
|
6
|
-
import { setupSigintHandler } from "./setupSigintHandler";
|
|
5
|
+
import { logger as defaultLogger, setupSigintHandler } from "@tkeron/tools";
|
|
7
6
|
|
|
8
7
|
export interface TkeronDevOptions {
|
|
9
8
|
outputDir?: string;
|
package/src/init.ts
CHANGED
|
@@ -9,8 +9,7 @@ import {
|
|
|
9
9
|
} from "fs";
|
|
10
10
|
import { join, resolve, basename } from "path";
|
|
11
11
|
import type { Logger } from "@tkeron/tools";
|
|
12
|
-
import { silentLogger } from "@tkeron/tools";
|
|
13
|
-
import { promptUser } from "./promptUser";
|
|
12
|
+
import { silentLogger, promptUser } from "@tkeron/tools";
|
|
14
13
|
import { ensureTsconfig } from "./ensureTsconfig";
|
|
15
14
|
|
|
16
15
|
export interface InitOptions {
|
package/src/processCom.ts
CHANGED
|
@@ -192,11 +192,11 @@ const processComponents = async (
|
|
|
192
192
|
for (const node of nodesToInsert) {
|
|
193
193
|
if ((node as any).nodeType === 1) {
|
|
194
194
|
if ((node as any).tagName?.toLowerCase() === "style") {
|
|
195
|
-
(node as any).setAttribute("data-tk-
|
|
195
|
+
(node as any).setAttribute("data-tk-com", tagName);
|
|
196
196
|
}
|
|
197
197
|
const nested = (node as any).querySelectorAll?.("style") || [];
|
|
198
198
|
for (const styleEl of nested) {
|
|
199
|
-
(styleEl as any).setAttribute("data-tk-
|
|
199
|
+
(styleEl as any).setAttribute("data-tk-com", tagName);
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
}
|
package/src/processComTs.ts
CHANGED
|
@@ -338,11 +338,11 @@ ${codeWithoutImports}
|
|
|
338
338
|
for (const node of nodesToInsert) {
|
|
339
339
|
if ((node as any).nodeType === 1) {
|
|
340
340
|
if ((node as any).tagName?.toLowerCase() === "style") {
|
|
341
|
-
(node as any).setAttribute("data-tk-
|
|
341
|
+
(node as any).setAttribute("data-tk-com", tagName);
|
|
342
342
|
}
|
|
343
343
|
const nested = (node as any).querySelectorAll?.("style") || [];
|
|
344
344
|
for (const styleEl of nested) {
|
|
345
|
-
(styleEl as any).setAttribute("data-tk-
|
|
345
|
+
(styleEl as any).setAttribute("data-tk-com", tagName);
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
}
|
package/src/skills.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
readdirSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
cpSync,
|
|
6
|
+
statSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "fs";
|
|
10
|
+
import { join, resolve } from "path";
|
|
11
|
+
import type { Logger } from "@tkeron/tools";
|
|
12
|
+
import {
|
|
13
|
+
silentLogger,
|
|
14
|
+
detectEnvironments,
|
|
15
|
+
addFrontmatterField,
|
|
16
|
+
} from "@tkeron/tools";
|
|
17
|
+
import type { Environment } from "@tkeron/tools";
|
|
18
|
+
|
|
19
|
+
export interface SkillsOptions {
|
|
20
|
+
target?: string;
|
|
21
|
+
force?: boolean;
|
|
22
|
+
dryRun?: boolean;
|
|
23
|
+
logger?: Logger;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getTransform = (env: Environment) => {
|
|
27
|
+
if (env === "cursor") {
|
|
28
|
+
return (content: string) =>
|
|
29
|
+
addFrontmatterField(content, "alwaysApply", false);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const installToDir = (
|
|
35
|
+
skillNames: string[],
|
|
36
|
+
sourceDir: string,
|
|
37
|
+
targetDir: string,
|
|
38
|
+
transform: ((content: string) => string) | null,
|
|
39
|
+
force: boolean,
|
|
40
|
+
log: Logger,
|
|
41
|
+
) => {
|
|
42
|
+
const conflicts: string[] = [];
|
|
43
|
+
for (const name of skillNames) {
|
|
44
|
+
const dest = join(targetDir, name, "SKILL.md");
|
|
45
|
+
if (existsSync(dest)) {
|
|
46
|
+
conflicts.push(name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (conflicts.length > 0 && !force) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Skill files already exist: ${conflicts.join(", ")}. Use --force to overwrite.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
mkdirSync(targetDir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
for (const name of skillNames) {
|
|
59
|
+
const src = join(sourceDir, name);
|
|
60
|
+
const dest = join(targetDir, name);
|
|
61
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
62
|
+
|
|
63
|
+
if (transform) {
|
|
64
|
+
const skillFile = join(dest, "SKILL.md");
|
|
65
|
+
if (existsSync(skillFile)) {
|
|
66
|
+
const content = readFileSync(skillFile, "utf-8");
|
|
67
|
+
writeFileSync(skillFile, transform(content), "utf-8");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
log.log(`✓ Installed skill: ${name}`);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const previewToDir = (skillNames: string[], targetDir: string, log: Logger) => {
|
|
76
|
+
for (const name of skillNames) {
|
|
77
|
+
const dest = join(targetDir, name, "SKILL.md");
|
|
78
|
+
log.log(`○ Would install: ${name} → ${dest}`);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const skills = async (options: SkillsOptions = {}) => {
|
|
83
|
+
const {
|
|
84
|
+
target,
|
|
85
|
+
force = false,
|
|
86
|
+
dryRun = false,
|
|
87
|
+
logger: log = silentLogger,
|
|
88
|
+
} = options;
|
|
89
|
+
|
|
90
|
+
const sourceDir = join(import.meta.dir, "..", "skills");
|
|
91
|
+
|
|
92
|
+
if (!existsSync(sourceDir)) {
|
|
93
|
+
throw new Error("Skills directory not found in tkeron installation");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const skillNames = readdirSync(sourceDir).filter((name) =>
|
|
97
|
+
statSync(join(sourceDir, name)).isDirectory(),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (skillNames.length === 0) {
|
|
101
|
+
throw new Error("No skills available to install");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (target !== undefined) {
|
|
105
|
+
const targetDir = resolve(process.cwd(), target);
|
|
106
|
+
if (dryRun) {
|
|
107
|
+
previewToDir(skillNames, targetDir, log);
|
|
108
|
+
log.log(
|
|
109
|
+
`\n📋 ${skillNames.length} skill(s) would be installed in ${targetDir}`,
|
|
110
|
+
);
|
|
111
|
+
} else {
|
|
112
|
+
installToDir(skillNames, sourceDir, targetDir, null, force, log);
|
|
113
|
+
log.log(`\n✅ ${skillNames.length} skill(s) installed in ${targetDir}`);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const detected = detectEnvironments(process.cwd());
|
|
119
|
+
|
|
120
|
+
if (detected.length === 0) {
|
|
121
|
+
throw new Error("NO_ENVIRONMENTS_DETECTED");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const { env, target: envTarget } of detected) {
|
|
125
|
+
const targetDir = resolve(process.cwd(), envTarget);
|
|
126
|
+
if (dryRun) {
|
|
127
|
+
previewToDir(skillNames, targetDir, log);
|
|
128
|
+
log.log(
|
|
129
|
+
`\n📋 ${skillNames.length} skill(s) would be installed for ${env} in ${targetDir}`,
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
const transform = getTransform(env);
|
|
133
|
+
installToDir(skillNames, sourceDir, targetDir, transform, force, log);
|
|
134
|
+
log.log(
|
|
135
|
+
`\n✅ ${skillNames.length} skill(s) installed for ${env} in ${targetDir}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { skills } from "./skills.js";
|
|
2
|
+
import type { Logger } from "@tkeron/tools";
|
|
3
|
+
import { logger as defaultLogger } from "@tkeron/tools";
|
|
4
|
+
import { createInterface } from "readline";
|
|
5
|
+
|
|
6
|
+
const stdinConfirm = async (message: string): Promise<boolean> => {
|
|
7
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
return new Promise<boolean>((resolve) => {
|
|
9
|
+
rl.question(message, (answer) => {
|
|
10
|
+
rl.close();
|
|
11
|
+
const trimmed = answer.trim().toLowerCase();
|
|
12
|
+
resolve(trimmed === "y" || trimmed === "yes");
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface SkillsWrapperOptions {
|
|
18
|
+
target?: string;
|
|
19
|
+
force?: boolean;
|
|
20
|
+
dryRun?: boolean;
|
|
21
|
+
logger?: Logger;
|
|
22
|
+
confirm?: (message: string) => Promise<boolean>;
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const skillsWrapper = async (options: SkillsWrapperOptions = {}) => {
|
|
27
|
+
const log = options?.logger || defaultLogger;
|
|
28
|
+
const confirm = options?.confirm ?? stdinConfirm;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await skills({
|
|
32
|
+
target: options.target,
|
|
33
|
+
force: options.force,
|
|
34
|
+
dryRun: options.dryRun,
|
|
35
|
+
logger: log,
|
|
36
|
+
});
|
|
37
|
+
} catch (error: any) {
|
|
38
|
+
if (error.message === "NO_ENVIRONMENTS_DETECTED") {
|
|
39
|
+
if (options.dryRun) {
|
|
40
|
+
log.log(
|
|
41
|
+
"\n⚠ No known AI environment detected (.cursor/, .github/, .claude/).",
|
|
42
|
+
);
|
|
43
|
+
log.log(" Would create a skills/ directory in the current directory.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
log.log(
|
|
47
|
+
"\n⚠ No known AI environment detected (.cursor/, .github/, .claude/).",
|
|
48
|
+
);
|
|
49
|
+
log.log(
|
|
50
|
+
" A skills/ directory will be created in the current directory instead.",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const proceed = await confirm(" Continue? (y/N): ");
|
|
54
|
+
if (!proceed) {
|
|
55
|
+
log.log("Aborted.");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await skills({ target: "skills", force: options.force, logger: log });
|
|
61
|
+
} catch (err: any) {
|
|
62
|
+
log.error(`\n❌ ${err.message}\n`);
|
|
63
|
+
if (/already exist/i.test(err.message)) {
|
|
64
|
+
log.error(
|
|
65
|
+
"💡 Tip: pass --force to overwrite existing skill files.\n",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
log.error(`\n❌ ${error.message}\n`);
|
|
74
|
+
if (/already exist/i.test(error.message)) {
|
|
75
|
+
log.error("💡 Tip: pass --force to overwrite existing skill files.\n");
|
|
76
|
+
}
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
};
|