@litfamily/lithermes 1.0.32 → 1.0.34
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 +7 -7
- package/README_Ko-KR.md +7 -7
- package/assets/lithermes-plugin/README.md +0 -12
- package/assets/lithermes-plugin/adhd_stack.py +43 -1
- package/assets/lithermes-plugin/auto_handoff.py +13 -2
- package/assets/lithermes-plugin/bounded_work.py +33 -3
- package/assets/lithermes-plugin/core.py +59 -18
- package/assets/lithermes-plugin/core_review.py +2 -2
- package/assets/lithermes-plugin/lit_mark.py +2 -7
- package/assets/lithermes-plugin/payload-version.json +49 -53
- package/assets/lithermes-plugin/plugin.yaml +1 -1
- package/assets/lithermes-plugin/rules/bundled-rules/baseline-discipline.md +7 -13
- package/assets/lithermes-plugin/rules/globmatch.py +1 -1
- package/assets/lithermes-plugin/skills/debugging/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/debugging/references/methodology/00-setup.md +1 -1
- package/assets/lithermes-plugin/skills/debugging/references/methodology/02-investigate.md +2 -2
- package/assets/lithermes-plugin/skills/debugging/references/methodology/09-cleanup.md +1 -1
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/native-binary.md +1 -1
- package/assets/lithermes-plugin/skills/debugging/references/runtimes/rust.md +4 -4
- package/assets/lithermes-plugin/skills/debugging/references/tools/ghidra.md +1 -1
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/corpus/ui-ux-db/data/stacks/nuxtjs.csv +1 -1
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/_canonical-corpus/manifest.json +2 -2
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +2 -2
- package/assets/lithermes-plugin/skills/lit-adhd/SKILL.md +10 -10
- package/assets/lithermes-plugin/skills/lit-adhd/references/brief-contract.md +12 -7
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/collect.mjs +81 -27
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/next-step.mjs +34 -6
- package/assets/lithermes-plugin/skills/lit-burnoff/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/lit-code/SKILL.md +4 -4
- package/assets/lithermes-plugin/skills/lit-code/references/rust/README.md +2 -2
- package/assets/lithermes-plugin/skills/lit-code/references/rust/async-tokio.md +4 -4
- package/assets/lithermes-plugin/skills/lit-code/references/rust/axum-stack.md +1 -1
- package/assets/lithermes-plugin/skills/lit-code/references/rust/clap-stack.md +1 -1
- package/assets/lithermes-plugin/skills/lit-code/references/rust/libraries.md +3 -3
- package/assets/lithermes-plugin/skills/lit-code/references/rust/proptest-insta.md +10 -10
- package/assets/lithermes-plugin/skills/lit-code/references/rust/zero-cost-safety.md +1 -1
- package/assets/lithermes-plugin/skills/lit-code/references/typescript/README.md +2 -2
- package/assets/lithermes-plugin/skills/lit-code/references/typescript/backend-hono.md +1 -1
- package/assets/lithermes-plugin/skills/lit-code/references/typescript/tsconfig-strict.md +1 -1
- package/assets/lithermes-plugin/skills/lit-code/scripts/typescript/check-no-excuse-rules.ts +2 -2
- package/assets/lithermes-plugin/skills/lit-typographic-motion/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover.mjs +1 -1
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-gate.mjs +1 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-kit.js +42 -3
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage.mjs +133 -4
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/stage.md +55 -1
- package/assets/lithermes-plugin/skills/readme-studio/templates/remotion/package-lock.json +3 -3
- package/assets/lithermes-plugin/skills/readme-studio/templates/remotion/package.json +2 -1
- package/assets/lithermes-plugin/skills/rules/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +2 -2
- package/assets/{lithermes-plugin → npm-channel}/auto_update.py +43 -9
- package/package.json +1 -1
- package/readme-assets/badge-version.svg +1 -1
- package/src/lib/canonicalCorpus.js +1 -1
- package/src/lib/install.js +22 -0
- package/src/lib/skillPayload.js +10 -5
|
@@ -31,7 +31,7 @@ These are deliberate project choices. Violations are always wrong, not "style pr
|
|
|
31
31
|
3. **Exhaustive switch** — every `switch` on a discriminated union ends with `default: assertNever(x)`. No fall-through.
|
|
32
32
|
4. **No any** — `any` is banned in annotations, returns, and parameters. Use `unknown` and narrow.
|
|
33
33
|
5. **No type assertions** — `as any`, `as unknown` banned. `as const` and `satisfies` are fine.
|
|
34
|
-
6. **No non-null assertion** — `x
|
|
34
|
+
6. **No non-null assertion** — `x!.y` is banned. Use narrowing or optional chaining (`x?.y`).
|
|
35
35
|
7. **No @ts-ignore / @ts-expect-error** — fix the type.
|
|
36
36
|
8. **No enum** — use `as const` objects + literal union types.
|
|
37
37
|
9. **Zod at boundaries** — external input (API, user, file) → Zod schema. Internal → plain types.
|
|
@@ -163,7 +163,7 @@ Violations caught by `../../scripts/typescript/check-no-excuse-rules.ts`. Run af
|
|
|
163
163
|
| `no-ts-ignore` | `@ts-ignore` | None — fix the type |
|
|
164
164
|
| `no-ts-expect-error` | `@ts-expect-error` | None — fix the type |
|
|
165
165
|
| `no-enum` | `enum` declarations | None — use `as const` |
|
|
166
|
-
| `no-non-null-assertion` | `x
|
|
166
|
+
| `no-non-null-assertion` | `x!.y` postfix | None — narrow or `?.` |
|
|
167
167
|
| `no-throw-literal` | `throw "string"` / `throw 123` | None — throw Error subclass |
|
|
168
168
|
| `no-mutable-export` | `export let` / `export var` | None — use `export const` |
|
|
169
169
|
| `no-any-annotation` | `: any` in parameter/return/variable types | `// no-excuse-ok: any` |
|
|
@@ -128,7 +128,7 @@ For Bun projects, add to `compilerOptions`:
|
|
|
128
128
|
| Rule | What |
|
|
129
129
|
|---|---|
|
|
130
130
|
| `noExplicitAny` | `any` in annotations is an error |
|
|
131
|
-
| `noNonNullAssertion` | `x
|
|
131
|
+
| `noNonNullAssertion` | `x!.y` is an error |
|
|
132
132
|
| `noDefaultExport` | Forces named exports |
|
|
133
133
|
| `useImportType` | Forces `import type` for type-only imports |
|
|
134
134
|
| `noParameterAssign` | No mutation of function parameters |
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* no-ts-ignore - `@ts-ignore` comments
|
|
9
9
|
* no-ts-expect-error - `@ts-expect-error` comments
|
|
10
10
|
* no-enum - `enum` declarations
|
|
11
|
-
* no-non-null-assertion - `x
|
|
11
|
+
* no-non-null-assertion - `x!.y` postfix operator
|
|
12
12
|
* no-throw-literal - `throw "string"` / `throw 123`
|
|
13
13
|
* no-mutable-export - `export let` / `export var`
|
|
14
14
|
* no-any-annotation - `: any` in annotations (opt out: `// no-excuse-ok: any`)
|
|
@@ -140,7 +140,7 @@ function analyzeFile(filePath: string): Violation[] {
|
|
|
140
140
|
// ── x! non-null assertion ──
|
|
141
141
|
if (ts.isNonNullExpression(node)) {
|
|
142
142
|
const p = pos(node)
|
|
143
|
-
violations.push({ ruleId: "no-non-null-assertion", filePath, ...p, message: "`x
|
|
143
|
+
violations.push({ ruleId: "no-non-null-assertion", filePath, ...p, message: "`x!.y` — use narrowing or optional chaining" })
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// ── throw "literal" ──
|
|
@@ -64,7 +64,7 @@ Done only when `$M complete --out DIR` exits 0: valid treatment, passing gate (a
|
|
|
64
64
|
|
|
65
65
|
## #contract.hard_stops
|
|
66
66
|
|
|
67
|
-
Exits 10, 11, 12, 14, 15: name the state and its fix (`references/runtime.md`). 16 the treatment field, 17 a stage break, 18 a nondeterministic frame, 19 a network request, 20 a sound fault, 21 no cover encoder, 22 a missing cover asset. Never install during a render, deliver a flash-failing film, or downgrade to pass.
|
|
67
|
+
Exits 10, 11, 12, 14, 15: name the state and its fix (`references/runtime.md`). 16 the treatment field, 17 a stage or clip break, 18 a nondeterministic frame, 19 a network request, 20 a sound fault, 21 no cover encoder, 22 a missing cover asset. Never install during a render, deliver a flash-failing film, or downgrade to pass.
|
|
68
68
|
|
|
69
69
|
## #contract.anti_patterns
|
|
70
70
|
|
|
@@ -102,7 +102,7 @@ function prepare(out, env) {
|
|
|
102
102
|
recordFirstTreatment(out, plan.treatment);
|
|
103
103
|
const coverDir = join(out, COVER_DIR);
|
|
104
104
|
if (!existsSync(join(coverDir, 'index.html'))) throw fail(EXIT.STAGE_CONTRACT_ERROR, `STAGE_CONTRACT_ERROR: no ${COVER_DIR}/index.html in ${out}; write the cover page first (references/cover.md)`);
|
|
105
|
-
const scan = scanStage(coverDir);
|
|
105
|
+
const scan = scanStage(coverDir, { clips: false });
|
|
106
106
|
if (scan.contract.length) throw fail(EXIT.STAGE_CONTRACT_ERROR, `STAGE_CONTRACT_ERROR: ${scan.contract.join('; ')}`);
|
|
107
107
|
if (scan.network.length) throw fail(EXIT.STAGE_NETWORK_REQUEST, `STAGE_NETWORK_REQUEST: ${scan.network.join('; ')}; a cover is served offline`);
|
|
108
108
|
const assets = checkAssets({ out, recipe: plan.treatment.recipe, rasters: scan.rasters.paths, coverDir: COVER_DIR });
|
|
@@ -85,6 +85,7 @@ export function renderStageReport(manifest, gate, { out, previewName }) {
|
|
|
85
85
|
lines.push(`path: stage; format ${manifest.format} (${manifest.resolution.join('x')}); ${manifest.durationSec.toFixed(2)} s at ${manifest.fps} fps (${manifest.totalFrames} frames); treatment ${manifest.targetDurationSec} s`);
|
|
86
86
|
lines.push(`renderer: ${manifest.renderer}; flags: ${manifest.chromeFlags.join(' ')}`);
|
|
87
87
|
lines.push(`frame time: p50 ${manifest.frameTimeMs.p50} ms, p95 ${manifest.frameTimeMs.p95} ms over ${manifest.frameTimeMs.captured} captures; master ${manifest.masterSec?.toFixed(1)} s`);
|
|
88
|
+
if (manifest.scan?.clips?.length) lines.push(`clips: ${manifest.scan.clips.map((c) => `${c.id} ${c.count} frames at ${c.fps} fps`).join(', ')} (keyed overlays, chosen from film time)`);
|
|
88
89
|
if (manifest.sound) lines.push(`sound: ${manifest.sound.label}`);
|
|
89
90
|
lines.push(`QA gate: ${gate.failed.length ? 'FAIL' : 'PASS'}`);
|
|
90
91
|
for (const rule of gate.rules) lines.push(`${rule.id}: ${rule.status} — ${rule.detail}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// LitHermes stage kit, served to stage pages at /lit/stage-kit.js.
|
|
2
2
|
// Motion primitives only: easing, springs, keyframes, timing, seeded
|
|
3
|
-
// randomness, text splitting, path drawing and morphing, masks, colour mixing
|
|
4
|
-
// and
|
|
3
|
+
// randomness, text splitting, path drawing and morphing, masks, colour mixing,
|
|
4
|
+
// text registration and declared keyed clips. It ships no scene, object, layout or copy; the page
|
|
5
5
|
// draws whatever its treatment calls for. Every function is a pure function of
|
|
6
6
|
// its arguments (and of t), so a frame depends only on the virtual clock.
|
|
7
7
|
(() => {
|
|
@@ -374,6 +374,45 @@
|
|
|
374
374
|
return target;
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
// A declared keyed clip (stage/clips/clips.json, served as window.LitClips by /lit/clips.js). One absolutely
|
|
378
|
+
// placed wrapper at `box` holds the clip's frame for t; the frame index is floor((t - start) * fps) + 1 counted
|
|
379
|
+
// from `from`, a pure function of film time, so no <video> and no real clock is involved. Outside from..to the
|
|
380
|
+
// wrapper is hidden (null returned) unless `hold` keeps the last frame or `loop` wraps by frame index. The page
|
|
381
|
+
// scales or fades the returned wrapper; the <img> inside is replaced on a frame change and the renderer waits
|
|
382
|
+
// for its decode before it captures.
|
|
383
|
+
function clip({ parent, id, box } = {}) {
|
|
384
|
+
const all = window.LitClips;
|
|
385
|
+
if (!all) throw new Error('LitStage.clip: load <script src="/lit/clips.js"></script> after the kit; the stage declares no clips (clips/clips.json)');
|
|
386
|
+
const c = all[id];
|
|
387
|
+
if (!c) throw new Error(`LitStage.clip: no clip "${id}" in clips/clips.json (declared: ${Object.keys(all).join(', ')})`);
|
|
388
|
+
if (!parent || !box || !(box.w > 0) || !(box.h > 0)) throw new Error('LitStage.clip: needs { parent, id, box: { x, y, w, h } }');
|
|
389
|
+
const wrap = document.createElement('div');
|
|
390
|
+
wrap.setAttribute('data-lit-clip', id);
|
|
391
|
+
Object.assign(wrap.style, { position: 'absolute', left: `${box.x}px`, top: `${box.y}px`, width: `${box.w}px`, height: `${box.h}px`, display: 'none', pointerEvents: 'none' });
|
|
392
|
+
parent.append(wrap);
|
|
393
|
+
let shown = 0;
|
|
394
|
+
let img = null;
|
|
395
|
+
return (t, { start = 0, from = 1, to = c.count, loop = false, hold = false } = {}) => {
|
|
396
|
+
if (!(from >= 1 && to <= c.count && from <= to)) throw new Error(`LitStage.clip: from..to must lie within 1..${c.count} (got ${from}..${to})`);
|
|
397
|
+
const n = to - from + 1;
|
|
398
|
+
const k = Math.floor((t - start) * c.fps + 1e-6);
|
|
399
|
+
const i = k < 0 ? 0 : k < n ? from + k : loop ? from + (k % n) : hold ? to : 0;
|
|
400
|
+
wrap.style.display = i ? 'block' : 'none';
|
|
401
|
+
if (!i) return null;
|
|
402
|
+
if (i !== shown) {
|
|
403
|
+
const next = document.createElement('img');
|
|
404
|
+
next.src = `${c.src}/frame_${String(i).padStart(4, '0')}.png`;
|
|
405
|
+
next.alt = '';
|
|
406
|
+
Object.assign(next.style, { display: 'block', width: '100%', height: '100%', objectFit: 'contain' });
|
|
407
|
+
if (img) img.replaceWith(next);
|
|
408
|
+
else wrap.append(next);
|
|
409
|
+
img = next;
|
|
410
|
+
shown = i;
|
|
411
|
+
}
|
|
412
|
+
return wrap;
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
377
416
|
function define(film) {
|
|
378
417
|
if (!film || typeof film !== 'object') throw new Error('LitStage.define needs { width, height, fps, duration, render }');
|
|
379
418
|
for (const key of ['width', 'height', 'duration']) if (!Number.isFinite(film[key])) throw new Error(`LitStage.define: ${key} must be a number`);
|
|
@@ -384,6 +423,6 @@
|
|
|
384
423
|
|
|
385
424
|
window.LitStage = Object.freeze({
|
|
386
425
|
ease, bezier, spring, kf, at, stagger, seq, rand, splitText, drawPath, morph,
|
|
387
|
-
clipInset, clipCircle, maskWipe, mix, text, define,
|
|
426
|
+
clipInset, clipCircle, maskWipe, mix, text, define, clip,
|
|
388
427
|
});
|
|
389
428
|
})();
|
|
@@ -22,6 +22,7 @@ import { runTextQa } from './stage-qa.mjs';
|
|
|
22
22
|
import { readStyle, styleCss, styleScript } from './styles.mjs';
|
|
23
23
|
import { addToStills, writeBoard } from './board.mjs';
|
|
24
24
|
import { FONT_FILES } from './frame.mjs';
|
|
25
|
+
import { decodePng } from './cover-png.mjs';
|
|
25
26
|
|
|
26
27
|
import { findCache, fontFile, fontState, hangulPath, runtimeRequire, WARM } from '../bin/runtime.mjs';
|
|
27
28
|
|
|
@@ -54,6 +55,12 @@ export const CONTENT_TYPES = Object.freeze({
|
|
|
54
55
|
const RASTER = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
|
|
55
56
|
const TEXTUAL = new Set(['.html', '.js', '.mjs', '.css', '.svg', '.json']);
|
|
56
57
|
export const RASTER_LIMITS = Object.freeze({ count: 24, bytes: 8 * 1024 * 1024, sameSize: 10 });
|
|
58
|
+
// Declared keyed clips (references/stage.md, Clips) answer to these caps instead of RASTER_LIMITS. A clip
|
|
59
|
+
// is a sticker on the film, never the film: 8-bit RGBA PNG frames, at most `side` px on the long side.
|
|
60
|
+
export const CLIP_LIMITS = Object.freeze({
|
|
61
|
+
clips: 12, framesPerClip: 360, framesTotal: 1500, bytesPerClip: 40 * 1024 * 1024, bytesTotal: 128 * 1024 * 1024, side: 720, fps: 30,
|
|
62
|
+
});
|
|
63
|
+
export const CLIPS_DIR = 'clips';
|
|
57
64
|
|
|
58
65
|
// The software rung only (SwiftShader, CPU raster): a mismatch between two
|
|
59
66
|
// captures can then only come from the page. Plus the stage set and the two
|
|
@@ -171,14 +178,130 @@ function walk(dir, root, out = []) {
|
|
|
171
178
|
return out;
|
|
172
179
|
}
|
|
173
180
|
|
|
174
|
-
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Declared clips: stage/clips/clips.json and the frame folders it names.
|
|
183
|
+
|
|
184
|
+
const CLIP_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$/u;
|
|
185
|
+
const CLIP_DIR = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/u;
|
|
186
|
+
const clipFrame = (i) => `frame_${String(i).padStart(4, '0')}.png`;
|
|
187
|
+
const mb = (bytes) => (bytes / 1048576).toFixed(1);
|
|
188
|
+
|
|
189
|
+
// The clips clips.json declares ([] without the file), checked for shape. Throws a plain Error naming the break.
|
|
190
|
+
function declaredClips(root, limits = CLIP_LIMITS) {
|
|
191
|
+
const file = join(root, CLIPS_DIR, 'clips.json');
|
|
192
|
+
if (!existsSync(file)) return [];
|
|
193
|
+
let doc;
|
|
194
|
+
try { doc = JSON.parse(readFileSync(file, 'utf8')); } catch (error) { throw new Error(`${CLIPS_DIR}/clips.json is not valid JSON (${error.message})`); }
|
|
195
|
+
if (!doc || !Array.isArray(doc.clips)) throw new Error(`${CLIPS_DIR}/clips.json needs { "clips": [ { "id", "dir", "count", "fps" } ] }`);
|
|
196
|
+
if (doc.clips.length > limits.clips) throw new Error(`${doc.clips.length} clips declared (at most ${limits.clips} clips)`);
|
|
197
|
+
const ids = new Set(), dirs = new Set();
|
|
198
|
+
return doc.clips.map((c, n) => {
|
|
199
|
+
const at = `${CLIPS_DIR}/clips.json clip #${n + 1}`;
|
|
200
|
+
if (!c || typeof c !== 'object') throw new Error(`${at} is not an object`);
|
|
201
|
+
if (typeof c.id !== 'string' || !CLIP_ID.test(c.id)) throw new Error(`${at}: id must be a plain token of letters, digits, - and _ (got ${JSON.stringify(c.id)})`);
|
|
202
|
+
if (ids.has(c.id)) throw new Error(`${at}: duplicate id ${c.id}`);
|
|
203
|
+
ids.add(c.id);
|
|
204
|
+
if (typeof c.dir !== 'string' || !CLIP_DIR.test(c.dir) || c.dir.includes('..')) throw new Error(`clip "${c.id}": dir must be a folder name directly under ${CLIPS_DIR}/ (got ${JSON.stringify(c.dir)})`);
|
|
205
|
+
if (dirs.has(c.dir)) throw new Error(`clip "${c.id}": dir ${c.dir} is already declared by another clip`);
|
|
206
|
+
dirs.add(c.dir);
|
|
207
|
+
if (!Number.isInteger(c.count) || c.count < 1) throw new Error(`clip "${c.id}": count must be a whole number of frames (got ${JSON.stringify(c.count)})`);
|
|
208
|
+
if (!Number.isInteger(c.fps) || c.fps < 1 || c.fps > limits.fps) throw new Error(`clip "${c.id}": fps must be a whole number from 1 to ${limits.fps} (got ${JSON.stringify(c.fps)})`);
|
|
209
|
+
return { id: c.id, dir: c.dir, count: c.count, fps: c.fps };
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// One declared clip against its bytes: exactly frame_0001.png..frame_<count>.png in clips/<dir>/, each an 8-bit
|
|
214
|
+
// non-animated RGBA PNG of one size within the overlay cap, real transparency in the first, middle and last frame.
|
|
215
|
+
function checkClip(d, byRel, limits) {
|
|
216
|
+
const fail = (message) => new Error(`clip "${d.id}": ${message}`);
|
|
217
|
+
if (d.count > limits.framesPerClip) throw fail(`${d.count} frames (at most ${limits.framesPerClip} per clip)`);
|
|
218
|
+
const names = [];
|
|
219
|
+
for (let i = 1; i <= d.count; i++) {
|
|
220
|
+
const rel = join(CLIPS_DIR, d.dir, clipFrame(i));
|
|
221
|
+
if (!byRel.has(rel)) throw fail(`${CLIPS_DIR}/${d.dir}/${clipFrame(i)} is missing (declared ${d.count} frames, named frame_0001.png onward)`);
|
|
222
|
+
names.push(rel);
|
|
223
|
+
}
|
|
224
|
+
const prefix = join(CLIPS_DIR, d.dir) + sep;
|
|
225
|
+
const stray = [...byRel.keys()].find((rel) => rel.startsWith(prefix) && !names.includes(rel));
|
|
226
|
+
if (stray) throw fail(`${stray.split(sep).join('/')} is not one of the ${d.count} declared frames; raise count or remove it`);
|
|
227
|
+
const bytes = names.reduce((sum, rel) => sum + statSync(byRel.get(rel)).size, 0);
|
|
228
|
+
if (bytes > limits.bytesPerClip) throw fail(`${mb(bytes)} MB (at most ${(limits.bytesPerClip / 1048576).toFixed(0)} MB per clip)`);
|
|
229
|
+
let size = null;
|
|
230
|
+
for (const rel of names) {
|
|
231
|
+
const data = readFileSync(byRel.get(rel));
|
|
232
|
+
const shown = rel.split(sep).join('/');
|
|
233
|
+
if (data.length < 33 || data.readUInt32BE(12) !== 0x49484452 || data.toString('latin1', 1, 4) !== 'PNG') throw fail(`${shown} is not a readable PNG`);
|
|
234
|
+
if (isAnimatedRaster(data, '.png')) throw fail(`${shown} is an animated image; a clip is a folder of still frames`);
|
|
235
|
+
if (data[25] !== 6 || data[24] !== 8) throw fail(`${shown} must be an 8-bit RGBA PNG with an alpha channel (a keyed frame), not an opaque image`);
|
|
236
|
+
const [width, height] = imageSize(data, '.png');
|
|
237
|
+
if (Math.max(width, height) > limits.side) throw fail(`${shown} is ${width}x${height}; a clip is an overlay of at most ${limits.side} px on the long side`);
|
|
238
|
+
if (size && (size[0] !== width || size[1] !== height)) throw fail(`${shown} is ${width}x${height} but frame_0001.png is ${size[0]}x${size[1]}; one size per clip`);
|
|
239
|
+
size = [width, height];
|
|
240
|
+
}
|
|
241
|
+
for (const rel of [names[0], names[Math.floor((names.length - 1) / 2)], names[names.length - 1]]) {
|
|
242
|
+
let rgba;
|
|
243
|
+
try { ({ rgba } = decodePng(readFileSync(byRel.get(rel)))); } catch (error) { throw fail(`${rel.split(sep).join('/')} cannot be decoded (${error.message}); use a non-interlaced 8-bit RGBA PNG`); }
|
|
244
|
+
let transparent = false;
|
|
245
|
+
for (let i = 3; i < rgba.length && !transparent; i += 4) transparent = rgba[i] < 255;
|
|
246
|
+
if (!transparent) throw fail(`${rel.split(sep).join('/')} has no transparent pixel; a clip is keyed, not a full-frame video`);
|
|
247
|
+
}
|
|
248
|
+
return { ...d, width: size[0], height: size[1], bytes, frames: names };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check every declared clip. Never throws: returns { clips, problems, claimed }, where `clips` are the clips that
|
|
253
|
+
* passed ({ id, dir, count, fps, width, height, bytes, frames }), `problems` are the contract findings, and
|
|
254
|
+
* `claimed` is the set of stage-relative raster paths inside declared clip folders, which are not page rasters.
|
|
255
|
+
*/
|
|
256
|
+
export function scanClips(stageDir, { limits = CLIP_LIMITS, files = null } = {}) {
|
|
257
|
+
const root = realpathSync(stageDir);
|
|
258
|
+
const problems = [];
|
|
259
|
+
let decls;
|
|
260
|
+
try { decls = declaredClips(root, limits); } catch (error) { return { clips: [], problems: [error.message], claimed: new Set() }; }
|
|
261
|
+
const byRel = new Map();
|
|
262
|
+
for (const f of files || walk(root, root)) if (!f.escape && RASTER.has(extname(f.path).toLowerCase())) byRel.set(relative(root, f.path), f.real);
|
|
263
|
+
const claimed = new Set();
|
|
264
|
+
const clips = [];
|
|
265
|
+
let frames = 0, bytes = 0;
|
|
266
|
+
for (const d of decls) {
|
|
267
|
+
const prefix = join(CLIPS_DIR, d.dir) + sep;
|
|
268
|
+
const mine = new Map([...byRel].filter(([rel]) => rel.startsWith(prefix)));
|
|
269
|
+
for (const rel of mine.keys()) claimed.add(rel);
|
|
270
|
+
try { clips.push(checkClip(d, mine, limits)); } catch (error) { problems.push(error.message); }
|
|
271
|
+
}
|
|
272
|
+
for (const c of clips) { frames += c.count; bytes += c.bytes; }
|
|
273
|
+
if (frames > limits.framesTotal) problems.push(`${frames} clip frames in all (at most ${limits.framesTotal})`);
|
|
274
|
+
if (bytes > limits.bytesTotal) problems.push(`${mb(bytes)} MB of clip frames in all (at most ${(limits.bytesTotal / 1048576).toFixed(0)} MB)`);
|
|
275
|
+
return { clips, problems, claimed };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* window.LitClips for LitStage.clip: id -> { src, count, fps, width, height }, from clips.json and the first frame's
|
|
280
|
+
* header. null when the stage declares no clips, or declares ones the scan would refuse.
|
|
281
|
+
*/
|
|
282
|
+
export function clipsScript(stageDir) {
|
|
283
|
+
try {
|
|
284
|
+
const root = realpathSync(stageDir);
|
|
285
|
+
const table = Object.fromEntries(declaredClips(root).map((c) => {
|
|
286
|
+
const [width, height] = imageSize(readFileSync(join(root, CLIPS_DIR, c.dir, clipFrame(1))), '.png');
|
|
287
|
+
return [c.id, { src: `${CLIPS_DIR}/${c.dir}`, count: c.count, fps: c.fps, width, height }];
|
|
288
|
+
}));
|
|
289
|
+
return Object.keys(table).length ? `window.LitClips = ${JSON.stringify(table)};\n` : null;
|
|
290
|
+
} catch { return null; }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// `clips: false` (cover mode) ignores clips.json, so a clip folder is held to the raster limits.
|
|
294
|
+
export function scanStage(stageDir, { clips: allowClips = true } = {}) {
|
|
175
295
|
const root = realpathSync(stageDir);
|
|
176
296
|
const contract = [], network = [];
|
|
177
297
|
const files = walk(root, root);
|
|
298
|
+
const clipScan = allowClips ? scanClips(root, { files }) : { clips: [], problems: [], claimed: new Set() };
|
|
299
|
+
contract.push(...clipScan.problems);
|
|
178
300
|
const rasters = [];
|
|
179
301
|
for (const file of files) {
|
|
180
302
|
const rel = relative(root, file.path);
|
|
181
303
|
if (file.escape) { contract.push(`${rel} is a symlink that leaves the stage dir`); continue; }
|
|
304
|
+
if (clipScan.claimed.has(rel)) continue;
|
|
182
305
|
const ext = extname(file.path).toLowerCase();
|
|
183
306
|
const bytes = readFileSync(file.real);
|
|
184
307
|
if (RASTER.has(ext)) {
|
|
@@ -199,7 +322,8 @@ export function scanStage(stageDir) {
|
|
|
199
322
|
const bySize = new Map();
|
|
200
323
|
for (const r of rasters) if (r.size) { const key = r.size.join('x'); bySize.set(key, (bySize.get(key) || 0) + 1); }
|
|
201
324
|
for (const [key, count] of bySize) if (count >= RASTER_LIMITS.sameSize) contract.push(`${count} rasters of the same size ${key} read as a flipbook; draw motion, do not play frames`);
|
|
202
|
-
|
|
325
|
+
const clips = clipScan.clips.map(({ id, count, fps, width, height, bytes }) => ({ id, count, fps, width, height, bytes }));
|
|
326
|
+
return { contract: [...new Set(contract)], network: [...new Set(network)], files: files.length, rasters: { count: rasters.length, bytes: total, paths: rasters.map((r) => r.rel) }, clips };
|
|
203
327
|
}
|
|
204
328
|
|
|
205
329
|
// ---------------------------------------------------------------------------
|
|
@@ -312,6 +436,7 @@ export async function openStage({ runtime, stageDir, profileRoot, width, height,
|
|
|
312
436
|
const cdp = await context.newCDPSession(page);
|
|
313
437
|
const root = realpathSync(stageDir);
|
|
314
438
|
const kit = readFileSync(join(here, 'stage-kit.js'));
|
|
439
|
+
const clipsJs = clipsScript(root);
|
|
315
440
|
const blocked = [], contract = [];
|
|
316
441
|
const serve = async (event) => {
|
|
317
442
|
const { requestId, request } = event;
|
|
@@ -329,6 +454,7 @@ export async function openStage({ runtime, stageDir, profileRoot, width, height,
|
|
|
329
454
|
const path = decodeURIComponent(url.pathname);
|
|
330
455
|
if (path === '/lit/stage-kit.js') return reply(200, CONTENT_TYPES['.js'], kit);
|
|
331
456
|
if (path === '/lit/fonts.css') return reply(200, CONTENT_TYPES['.css'], fontCss);
|
|
457
|
+
if (path === '/lit/clips.js' && clipsJs) return reply(200, CONTENT_TYPES['.js'], clipsJs);
|
|
332
458
|
if (path === '/lit/style.css' && style) return reply(200, CONTENT_TYPES['.css'], style.css);
|
|
333
459
|
if (path === '/lit/style.js' && style) return reply(200, CONTENT_TYPES['.js'], style.js);
|
|
334
460
|
if (path.startsWith('/lit/three/')) {
|
|
@@ -448,9 +574,12 @@ function beatFramesOf(treatment, fps, totalFrames) {
|
|
|
448
574
|
});
|
|
449
575
|
}
|
|
450
576
|
|
|
451
|
-
|
|
577
|
+
// The rasters decoded before frame 0. Declared clip frames are left out: a page decodes each one when it shows it.
|
|
578
|
+
export function imageUrlsIn(stageDir) {
|
|
452
579
|
const root = realpathSync(stageDir);
|
|
453
|
-
|
|
580
|
+
let prefixes = [];
|
|
581
|
+
try { prefixes = declaredClips(root).map((c) => join(CLIPS_DIR, c.dir) + sep); } catch { /* the scan already refused a bad declaration */ }
|
|
582
|
+
return walk(root, root).filter((f) => !f.escape && RASTER.has(extname(f.path).toLowerCase()) && !prefixes.some((p) => relative(root, f.path).startsWith(p)))
|
|
454
583
|
.map((f) => `${ORIGIN}/${relative(root, f.path).split(sep).map(encodeURIComponent).join('/')}`);
|
|
455
584
|
}
|
|
456
585
|
|
|
@@ -17,7 +17,8 @@ deliverable.
|
|
|
17
17
|
Never install, download or copy a third-party library into the stage dir. Use
|
|
18
18
|
the kit below and your own code. Rasters are textures and stills, never a frame
|
|
19
19
|
sequence: at most 24 raster images and 8 MB in total; 10 or more rasters of the
|
|
20
|
-
same size (a flipbook), or any animated GIF, APNG or WebP, is exit 17.
|
|
20
|
+
same size (a flipbook), or any animated GIF, APNG or WebP, is exit 17. The one
|
|
21
|
+
frame sequence a page may carry is a declared keyed clip (Clips).
|
|
21
22
|
|
|
22
23
|
Put all on-screen copy in one `COPY` object at the top of `stage/index.html`,
|
|
23
24
|
so the copy is easy to find and edit:
|
|
@@ -43,6 +44,8 @@ declares the film:
|
|
|
43
44
|
1080×1920 for 9:16. `duration` is the treatment's `durationSec`; the render
|
|
44
45
|
must land within ±10 % of it.
|
|
45
46
|
- `fps` is 60. Use 30 only when the treatment says so.
|
|
47
|
+
- A page that shows clips also loads `<script src="/lit/clips.js"></script>`
|
|
48
|
+
after the kit (Clips).
|
|
46
49
|
- `render(t)` draws the frame at time `t` in seconds. It may be omitted when
|
|
47
50
|
the page animates only with CSS or the Web Animations API.
|
|
48
51
|
- Setting `window.litStage` to the same object is equivalent to calling
|
|
@@ -168,6 +171,57 @@ scenes, no objects, no layouts, no copy. `window.LitStage` holds:
|
|
|
168
171
|
- **Text registration.** `LitStage.text(el, { decor: true })` and
|
|
169
172
|
`LitStage.text({ content, x, y, w, h })`, above.
|
|
170
173
|
|
|
174
|
+
- **Clips.** `LitStage.clip({ parent, id, box })` registers a declared keyed
|
|
175
|
+
clip once and returns `show(t, opts)`, which `render(t)` calls every frame
|
|
176
|
+
(Clips).
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
const boom = LitStage.clip({ parent: layer, id: "boom", box: { x: 1200, y: 300, w: 520, h: 511 } });
|
|
180
|
+
boom(t, { start: 2.1 });
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Clips
|
|
184
|
+
|
|
185
|
+
Real keyed footage (a reaction clip, a spinning loop shot on a green screen) joins the
|
|
186
|
+
film as a layer of the page. The footage is keyed outside the renderer into one 8-bit RGBA
|
|
187
|
+
PNG per frame, the frames are declared, and the page picks the frame from film time. Nothing
|
|
188
|
+
is encoded by hand: the renderer captures the page, so the flash audit, the determinism
|
|
189
|
+
replay and the text checks see the final pixels like any other film.
|
|
190
|
+
|
|
191
|
+
1. Put each clip's frames in the run's stage folder as `stage/clips/<dir>/frame_0001.png`,
|
|
192
|
+
`frame_0002.png` and on, with no gap and no other image in that folder.
|
|
193
|
+
2. Declare the clips in `stage/clips/clips.json`:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{ "clips": [ { "id": "boom", "dir": "01-boom", "count": 122, "fps": 30 } ] }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`id` is the name the page uses, `dir` a folder directly under `clips/`, `count` the number
|
|
200
|
+
of frames and `fps` the clip's own rate, 1 to 30. Declare a clip once even if it plays twice.
|
|
201
|
+
3. The page loads `<script src="/lit/clips.js"></script>` after the kit. For each place a clip
|
|
202
|
+
appears it calls `LitStage.clip({ parent, id, box })` once, outside `render`, and calls the
|
|
203
|
+
returned `show(t, opts)` from `render(t)`. `parent` holds the clip, `box` is its place in
|
|
204
|
+
pixels and the frame is fitted inside it. `show` returns the clip's wrapper element, or
|
|
205
|
+
`null` while the clip is hidden; scale, fade or move that wrapper from `t` like any layer.
|
|
206
|
+
|
|
207
|
+
The frame is `floor((t - start) * fps) + 1`, counted from `from`, so a cue is plain
|
|
208
|
+
arithmetic: `start = cue - (N - 1) / fps` puts frame N on the cue. A clip whose own sound
|
|
209
|
+
sits on frame 85 and has to land on a cue at 0.12 s takes `start = 0.12 - 84 / 30`. Options
|
|
210
|
+
are `start` (film seconds, default 0), `from` and `to` (the frame range, default the whole
|
|
211
|
+
clip), `loop` (wrap by frame index) and `hold` (keep the last frame). Before `start`, and after
|
|
212
|
+
`to` without `loop` or `hold`, the clip is hidden.
|
|
213
|
+
|
|
214
|
+
The pre-flight scan refuses with exit 17, naming the clip: a stage holds at most 12 clips; a
|
|
215
|
+
clip holds at most 360 frames and the stage 1500 in all; more than 40 MB in a clip or 128 MB in
|
|
216
|
+
all; a frame whose long side is above 720 px (a clip is an overlay, not the film); a frame rate
|
|
217
|
+
above 30; frames of mixed sizes; a missing or extra frame; a frame that is not an 8-bit RGBA
|
|
218
|
+
PNG, or has no transparent pixel in the first, middle and last frame (an opaque video is not
|
|
219
|
+
keyed); an animated image; a duplicate `id`; and a `dir` that leaves `clips/`. A folder that is
|
|
220
|
+
not declared, and any frame sequence beside a declared clip, is a flipbook as before. Cover mode takes no clips,
|
|
221
|
+
so a declaration there changes nothing and the folder meets the raster limits.
|
|
222
|
+
The text checks read DOM text, so keep clips clear of copy; a clip laid over a line is yours to
|
|
223
|
+
catch on the stills. The manifest and the gate report list each clip.
|
|
224
|
+
|
|
171
225
|
## How the capture works
|
|
172
226
|
|
|
173
227
|
The renderer serves the page to a software-rendered Chrome (SwiftShader, CPU
|
|
@@ -3771,9 +3771,9 @@
|
|
|
3771
3771
|
"license": "ISC"
|
|
3772
3772
|
},
|
|
3773
3773
|
"node_modules/zod": {
|
|
3774
|
-
"version": "4.
|
|
3775
|
-
"resolved": "https://registry.npmjs.org/zod/-/zod-4.
|
|
3776
|
-
"integrity": "sha512-
|
|
3774
|
+
"version": "4.6.6",
|
|
3775
|
+
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz",
|
|
3776
|
+
"integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==",
|
|
3777
3777
|
"license": "MIT",
|
|
3778
3778
|
"funding": {
|
|
3779
3779
|
"url": "https://github.com/sponsors/colinhacks"
|
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {"render": "remotion render", "still": "remotion still"},
|
|
6
|
-
"dependencies": {"@remotion/cli":"4.0.526", "remotion":"4.0.526", "react":"19.3.0", "react-dom":"19.3.0"}
|
|
6
|
+
"dependencies": {"@remotion/cli":"4.0.526", "remotion":"4.0.526", "react":"19.3.0", "react-dom":"19.3.0"},
|
|
7
|
+
"overrides": {"zod": "4.6.6"}
|
|
7
8
|
}
|
|
@@ -169,7 +169,7 @@ priority rules.
|
|
|
169
169
|
`paths` and Copilot `applyTo` aliases. A Cursor-style comma-separated scalar
|
|
170
170
|
(`globs: *.ts,*.tsx`) is split into separate patterns.
|
|
171
171
|
- **Glob support is a documented SUBSET**, not picomatch. `**`, `*`, `?`,
|
|
172
|
-
`[seq]`, `{a,b}` and a leading
|
|
172
|
+
`[seq]`, `{a,b}` and a leading (!) work. Extglob (`@(a|b)`) and POSIX classes
|
|
173
173
|
(`[[:alpha:]]`) do NOT — a rule using them silently matches nothing. The full
|
|
174
174
|
divergence list lives in `rules/globmatch.py` `DIVERGENCES`; read it before
|
|
175
175
|
claiming a glob "should have" matched.
|
|
@@ -485,7 +485,7 @@ isolated Hermes home. Use `--hermes-home`; never `--home`.
|
|
|
485
485
|
|
|
486
486
|
```text
|
|
487
487
|
$ node packages/lithermes-installer/bin/lithermes.js install --yes --offline --no-hud --hermes-home "$ISOLATED"
|
|
488
|
-
Installed LitHermes 1.0.
|
|
488
|
+
Installed LitHermes 1.0.34
|
|
489
489
|
plugin: $ISOLATED/plugins/lithermes
|
|
490
490
|
model config: updated
|
|
491
491
|
|
|
@@ -502,7 +502,7 @@ installed payload: PASS
|
|
|
502
502
|
enabled config: PASS
|
|
503
503
|
|
|
504
504
|
$ hermes lithermes doctor
|
|
505
|
-
[OK] plugin.yaml readable (version 1.0.
|
|
505
|
+
[OK] plugin.yaml readable (version 1.0.34)
|
|
506
506
|
[OK] skills bundled: 40
|
|
507
507
|
[OK] litgoal durable runtime importable
|
|
508
508
|
```
|
|
@@ -42,6 +42,8 @@ INSTALL_TIMEOUT_SECONDS = 90.0
|
|
|
42
42
|
MAX_RESPONSE_BYTES = 64 * 1024
|
|
43
43
|
AUTO_UPDATE_GUARD_ENV = "LITHERMES_AUTO_UPDATE_RUNNING"
|
|
44
44
|
INSTALLER_DISTRIBUTION = "npm"
|
|
45
|
+
NPM_CHANNEL_MARKER_NAME = ".lithermes-npm-channel"
|
|
46
|
+
NPM_CHANNEL_MARKER_TEXT = "lithermes npm channel\n"
|
|
45
47
|
ATTEMPT_RECORD_NAME = "auto-update-attempt.json"
|
|
46
48
|
# A broken npm fetch blocked each new session's first reply for about 70 s, so
|
|
47
49
|
# after a failed install the bridge waits a day before it tries the same
|
|
@@ -244,19 +246,51 @@ def _read_install_receipt(home: Path) -> dict[str, Any] | None:
|
|
|
244
246
|
return _read_state_json(home, "auto-update-receipt.json")
|
|
245
247
|
|
|
246
248
|
|
|
249
|
+
def _plugin_dir(home: Path, manifest: dict[str, Any] | None) -> Path:
|
|
250
|
+
"""Prefer the receipt's plugin path when it is a real directory, not a link."""
|
|
251
|
+
|
|
252
|
+
raw = manifest.get("pluginPath") if isinstance(manifest, dict) else None
|
|
253
|
+
if isinstance(raw, str) and raw.strip():
|
|
254
|
+
candidate = Path(raw).expanduser()
|
|
255
|
+
try:
|
|
256
|
+
info = candidate.lstat()
|
|
257
|
+
except OSError:
|
|
258
|
+
info = None
|
|
259
|
+
if info is not None and stat.S_ISDIR(info.st_mode) and not stat.S_ISLNK(info.st_mode):
|
|
260
|
+
return candidate
|
|
261
|
+
return home / "plugins" / "lithermes"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _channel_marker_matches(root: Path) -> bool:
|
|
265
|
+
marker = root / NPM_CHANNEL_MARKER_NAME
|
|
266
|
+
expected = NPM_CHANNEL_MARKER_TEXT.encode("utf-8")
|
|
267
|
+
try:
|
|
268
|
+
info = marker.lstat()
|
|
269
|
+
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode):
|
|
270
|
+
return False
|
|
271
|
+
fd = os.open(marker, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
|
|
272
|
+
try:
|
|
273
|
+
data = os.read(fd, len(expected) + 1)
|
|
274
|
+
finally:
|
|
275
|
+
os.close(fd)
|
|
276
|
+
except OSError:
|
|
277
|
+
return False
|
|
278
|
+
return data == expected
|
|
279
|
+
|
|
280
|
+
|
|
247
281
|
def _is_npm_channel(home: Path) -> bool:
|
|
248
|
-
"""Return true only for
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
treated as non-npm until the user runs the installer once, which preserves the
|
|
255
|
-
catalog safety boundary over an ambiguous channel.
|
|
282
|
+
"""Return true only for an npm receipt that also carries the channel marker.
|
|
283
|
+
|
|
284
|
+
The receipt lives outside the plugin directory and survives a catalog replace,
|
|
285
|
+
so ``distribution: npm`` alone is not permission to fetch and replace the
|
|
286
|
+
tree. The marker is written only by the npm installer, into the plugin
|
|
287
|
+
directory, and a catalog copy never contains it. A symlink is not the marker.
|
|
256
288
|
"""
|
|
257
289
|
|
|
258
290
|
manifest = _read_state_json(home, "install-manifest.json")
|
|
259
|
-
|
|
291
|
+
if not (isinstance(manifest, dict) and manifest.get("distribution") == INSTALLER_DISTRIBUTION):
|
|
292
|
+
return False
|
|
293
|
+
return _channel_marker_matches(_plugin_dir(home, manifest))
|
|
260
294
|
|
|
261
295
|
|
|
262
296
|
def _utc_stamp(seconds: float | None = None) -> str:
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.34"><title>release: 1.0.34</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.34</text></g></svg>
|
|
@@ -14,7 +14,7 @@ const EXPECTED = Object.freeze({
|
|
|
14
14
|
tree: "9188410be0af35f2421ba300d91a0d7a7341caf0",
|
|
15
15
|
fileCount: 167,
|
|
16
16
|
totalBytes: 2596360,
|
|
17
|
-
digest: "
|
|
17
|
+
digest: "c921ed87f41b5aaef14ef480c791f1c36bbd3ec616eaccc32f5e1246fcc547e6",
|
|
18
18
|
directories: ["design", "designpowers", "perfection", "ui-ux-db"],
|
|
19
19
|
attributes: "corpus/** -text\nlegal/** -text\n",
|
|
20
20
|
legal: [
|
package/src/lib/install.js
CHANGED
|
@@ -32,6 +32,9 @@ const { installSkins } = require("./skins");
|
|
|
32
32
|
|
|
33
33
|
const packageRoot = path.resolve(__dirname, "..", "..");
|
|
34
34
|
const assetRoot = path.join(packageRoot, "assets", "lithermes-plugin");
|
|
35
|
+
const channelRoot = path.join(packageRoot, "assets", "npm-channel");
|
|
36
|
+
const NPM_CHANNEL_MARKER_NAME = ".lithermes-npm-channel";
|
|
37
|
+
const NPM_CHANNEL_MARKER_TEXT = "lithermes npm channel\n";
|
|
35
38
|
const LEGACY_VENDOR_DIRS = new Map([
|
|
36
39
|
["vendor/022_handoff", "vendor/handoff"],
|
|
37
40
|
["vendor/045_scientific-visualization", "vendor/scientific-visualization"],
|
|
@@ -547,6 +550,24 @@ function previewModelRoute(flags = {}) {
|
|
|
547
550
|
};
|
|
548
551
|
}
|
|
549
552
|
|
|
553
|
+
function stageNpmChannel(dest) {
|
|
554
|
+
const source = path.join(channelRoot, "auto_update.py");
|
|
555
|
+
const sourceStat = fs.lstatSync(source);
|
|
556
|
+
if (!sourceStat.isFile()) {
|
|
557
|
+
throw new LitHermesError("npm channel updater is missing.", 5);
|
|
558
|
+
}
|
|
559
|
+
const target = path.join(dest, "auto_update.py");
|
|
560
|
+
fs.copyFileSync(source, target);
|
|
561
|
+
fs.chmodSync(target, 0o644);
|
|
562
|
+
const marker = path.join(dest, NPM_CHANNEL_MARKER_NAME);
|
|
563
|
+
fs.writeFileSync(marker, NPM_CHANNEL_MARKER_TEXT, { encoding: "utf8", mode: 0o644 });
|
|
564
|
+
fs.chmodSync(marker, 0o644);
|
|
565
|
+
return [
|
|
566
|
+
{ path: "auto_update.py", sha256: sha256(target) },
|
|
567
|
+
{ path: NPM_CHANNEL_MARKER_NAME, sha256: sha256(marker) },
|
|
568
|
+
];
|
|
569
|
+
}
|
|
570
|
+
|
|
550
571
|
function installLitHermes(flags = {}) {
|
|
551
572
|
const dryRun = Boolean(flags["dry-run"]);
|
|
552
573
|
const homeInfo = dryRun
|
|
@@ -655,6 +676,7 @@ function installLitHermes(flags = {}) {
|
|
|
655
676
|
assertPluginParent(hermesHome);
|
|
656
677
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
657
678
|
const files = copyTree(assetRoot, dest);
|
|
679
|
+
files.push(...stageNpmChannel(dest));
|
|
658
680
|
onProgress("Writing Hermes config");
|
|
659
681
|
checkState();
|
|
660
682
|
let afterConfig = beforeConfig;
|