tux-review 0.1.6 → 0.1.8
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/package.json +1 -1
- package/src/design.js +2 -0
- package/src/live.js +3 -0
- package/src/server.js +27 -19
- package/src/skills.js +46 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tux-review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "TUX - TUX UIX review system: attach structured, machine-readable feedback directly to running web UIs (pages, routes, components, elements, states) with a deterministic CLI, canonical JSON and LLM-friendly skills.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
package/src/design.js
CHANGED
|
@@ -11,6 +11,7 @@ import { canonicalJson } from './canonical.js';
|
|
|
11
11
|
import { CONFIG_FILE, defaultConfig } from './config.js';
|
|
12
12
|
import { SUPPORTED_FRAMEWORKS, designTemplate } from './templates.js';
|
|
13
13
|
import { startServer, opLiveStatus, opLiveStop } from './live.js';
|
|
14
|
+
import { deploySkills } from './skills.js';
|
|
14
15
|
|
|
15
16
|
function textReport(lines) {
|
|
16
17
|
return { stdout: lines.join('\n') + '\n', exit: EXIT.ok };
|
|
@@ -40,6 +41,7 @@ export function opDesignInstall(opts, spec) {
|
|
|
40
41
|
if (spec.root) config.design.root = spec.root;
|
|
41
42
|
const root = config.design.root ?? 'requirements';
|
|
42
43
|
writeFileSync(configPath, canonicalJson(config) + '\n', 'utf8');
|
|
44
|
+
deploySkills(cwd);
|
|
43
45
|
const report = {
|
|
44
46
|
action: 'install',
|
|
45
47
|
kind: 'design',
|
package/src/live.js
CHANGED
|
@@ -13,6 +13,7 @@ import { CliError, EXIT } from './errors.js';
|
|
|
13
13
|
import { canonicalJson } from './canonical.js';
|
|
14
14
|
import { canonicalTimestamp } from './ids.js';
|
|
15
15
|
import { loadStore } from './store.js';
|
|
16
|
+
import { deploySkills } from './skills.js';
|
|
16
17
|
|
|
17
18
|
const SERVER_STATE_FILE = '.tux/server.json';
|
|
18
19
|
|
|
@@ -59,6 +60,7 @@ export function opLiveInstall(opts) {
|
|
|
59
60
|
const m = devCommand.match(/--port[= ](\d+)/);
|
|
60
61
|
if (m) devPort = Number(m[1]);
|
|
61
62
|
}
|
|
63
|
+
deploySkills(cwd);
|
|
62
64
|
const report = {
|
|
63
65
|
action: 'install',
|
|
64
66
|
kind: 'live',
|
|
@@ -87,6 +89,7 @@ export async function startServer(opts, spec) {
|
|
|
87
89
|
if (spec.dryRun) {
|
|
88
90
|
return { stdout: canonicalJson(spec.plan) + '\n', exit: EXIT.ok };
|
|
89
91
|
}
|
|
92
|
+
deploySkills(opts.cwd);
|
|
90
93
|
mkdirSync(join(opts.cwd, '.tux'), { recursive: true });
|
|
91
94
|
let appPid = null;
|
|
92
95
|
if (spec.cmd) {
|
package/src/server.js
CHANGED
|
@@ -287,6 +287,22 @@ function static_(state, res, path) {
|
|
|
287
287
|
res.end(body);
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
// Transparent proxy response headers (SPC 34): forward every upstream header
|
|
291
|
+
// except the hop-by-hop / length headers this server manages itself. Must
|
|
292
|
+
// produce the same header set as py/tux/server.py::_proxy.
|
|
293
|
+
function proxyHeaders(upHeaders, { html, bodyLength }) {
|
|
294
|
+
const headers = {};
|
|
295
|
+
for (const [k, v] of Object.entries(upHeaders)) {
|
|
296
|
+
const lk = k.toLowerCase();
|
|
297
|
+
if (lk === 'connection' || lk === 'transfer-encoding' || lk === 'content-length') continue;
|
|
298
|
+
if (html && lk === 'cache-control') continue;
|
|
299
|
+
headers[k] = v;
|
|
300
|
+
}
|
|
301
|
+
if (html) headers['cache-control'] = 'no-store';
|
|
302
|
+
headers['content-length'] = String(bodyLength);
|
|
303
|
+
return headers;
|
|
304
|
+
}
|
|
305
|
+
|
|
290
306
|
function proxy(state, req, res) {
|
|
291
307
|
const target = new URL(state.target);
|
|
292
308
|
const options = {
|
|
@@ -298,25 +314,17 @@ function proxy(state, req, res) {
|
|
|
298
314
|
headers: { ...req.headers, host: target.host },
|
|
299
315
|
};
|
|
300
316
|
const upstream = httpRequest(options, (up) => {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
headers['cache-control'] = 'no-store';
|
|
313
|
-
res.writeHead(up.statusCode, headers);
|
|
314
|
-
res.end(body);
|
|
315
|
-
});
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
res.writeHead(up.statusCode, up.headers);
|
|
319
|
-
up.pipe(res);
|
|
317
|
+
const isHtml = (up.headers['content-type'] ?? '').startsWith('text/html');
|
|
318
|
+
const chunks = [];
|
|
319
|
+
up.on('data', (c) => chunks.push(c));
|
|
320
|
+
up.on('end', () => {
|
|
321
|
+
const raw = Buffer.concat(chunks);
|
|
322
|
+
// Injection lengthens the body, so the upstream Content-Length must be
|
|
323
|
+
// recomputed; non-HTML passes through as raw bytes (no text round-trip).
|
|
324
|
+
const out = isHtml ? Buffer.from(inject(raw.toString('utf8')), 'utf8') : raw;
|
|
325
|
+
res.writeHead(up.statusCode, proxyHeaders(up.headers, { html: isHtml, bodyLength: out.length }));
|
|
326
|
+
res.end(out);
|
|
327
|
+
});
|
|
320
328
|
});
|
|
321
329
|
upstream.on('error', (err) => {
|
|
322
330
|
sendJson(res, 502, { error: { code: 'bad_gateway', message: `target unreachable: ${err.message}` } });
|
package/src/skills.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical skill deployment (SPC sections 28, 32): `tux design install`,
|
|
3
|
+
* `tux live install` and the start-review commands deploy the packaged
|
|
4
|
+
* skills verbatim into the project's agent skill directories, so a package
|
|
5
|
+
* installation plus one CLI invocation leaves the project ready, and a
|
|
6
|
+
* package update refreshes the deployed copies on the next invocation.
|
|
7
|
+
*
|
|
8
|
+
* @module skills
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { CliError, EXIT } from './errors.js';
|
|
14
|
+
|
|
15
|
+
/** Agent skill directories relative to the project root (SPC sections 28, 32). */
|
|
16
|
+
export const AGENT_DIRS = ['.kilo', '.claude', '.hermes'];
|
|
17
|
+
|
|
18
|
+
const packagedSkillsDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'skills');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Deploy every packaged `tux-<name>.md` skill verbatim into
|
|
22
|
+
* `<root>/<agent>/skills/<name>/SKILL.md` for each agent directory.
|
|
23
|
+
* Idempotent: deployed copies are rewritten on every run; files not
|
|
24
|
+
* shipped by this package are never touched. Returns the sorted names.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} root project root directory
|
|
27
|
+
* @returns {string[]} deployed skill names, sorted
|
|
28
|
+
*/
|
|
29
|
+
export function deploySkills(root) {
|
|
30
|
+
const names = readdirSync(packagedSkillsDir)
|
|
31
|
+
.filter((f) => /^tux-.+\.md$/.test(f))
|
|
32
|
+
.sort()
|
|
33
|
+
.map((f) => f.replace(/\.md$/, ''));
|
|
34
|
+
try {
|
|
35
|
+
for (const agent of AGENT_DIRS) {
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
const dst = join(root, agent, 'skills', name, 'SKILL.md');
|
|
38
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
39
|
+
writeFileSync(dst, readFileSync(join(packagedSkillsDir, `${name}.md`)));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
throw new CliError(EXIT.general, `skill deployment failed: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
return names;
|
|
46
|
+
}
|