@kaydash9999/banker-plugins 0.15.2 → 0.16.1
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-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +65 -0
- package/README.md +31 -9
- package/codex/manifest.json +6 -3
- package/codex/transform-matrix.md +2 -0
- package/commands/progress.md +10 -0
- package/commands/setup.md +1 -1
- package/hooks/graceful-pause.mjs +31 -71
- package/hooks/hooks.json +11 -1
- package/hooks/progress-fallback.mjs +10 -0
- package/hooks/progress.mjs +427 -0
- package/hooks/register.mjs +82 -5
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +238 -122
- package/skills/3d-intro-build/references/NOTICE.md +2 -1
- package/skills/3d-intro-build/references/assemble.mjs +164 -108
- package/skills/3d-intro-build/references/azure-adapter.mjs +204 -44
- package/skills/3d-intro-build/references/curate.mjs +573 -267
- package/skills/3d-intro-build/references/intro-fixes.css +61 -0
- package/skills/3d-intro-build/references/panel-glass.css +55 -0
- package/skills/3d-intro-build/references/preview-lib.mjs +256 -0
- package/skills/3d-intro-build/references/prompts.md +32 -9
- package/skills/3d-intro-build/references/serve.mjs +111 -103
- package/skills/3d-intro-build/references/step-nav.css +59 -0
- package/skills/3d-intro-build/references/step-nav.js +503 -0
- package/skills/3d-intro-build/references/verify-intro.mjs +474 -0
- package/skills/3d-intro-build/references/verify.md +45 -0
- package/skills/3d-intro-build/references/video-pool.mjs +817 -0
- package/skills/3d-intro-setup/SKILL.md +120 -47
- package/skills/3d-intro-setup/references/azure-adapter.mjs +204 -44
- package/skills/3d-intro-setup/references/video-pool.mjs +817 -0
- package/skills/motion-graphic-make/SKILL.md +73 -28
- package/skills/motion-graphic-make/references/curate.mjs +607 -0
- package/skills/motion-graphic-make/references/preview-lib.mjs +256 -0
- package/skills/motion-graphic-make/references/serve.mjs +131 -0
- package/skills/omc-patch/SKILL.md +143 -0
- package/skills/omc-patch/scripts/omc-patch.mjs +577 -0
- package/skills/remains/SKILL.md +125 -0
- package/skills/remains/scripts/boxes.mjs +448 -0
- package/skills/trouble-shooting/SKILL.md +50 -0
|
@@ -1,123 +1,131 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
3
|
+
* Local preview server for an assembled intro (index.html + scrub-engine.js + assets/), so the
|
|
4
|
+
* scroll-scrubbed page is checked in a real browser. The project convention is a self-contained
|
|
5
|
+
* local page on an unused port, never a claude.ai artifact. Shared by 3d-intro-build and
|
|
6
|
+
* motion-graphic-make (scripts/sync-adapter.js keeps the copies identical).
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* self-contained local HTML on an unused port, never a claude.ai artifact.
|
|
8
|
-
*
|
|
9
|
-
* Why a server at all: the page loads each .mp4 with fetch()->Blob (always seekable), and
|
|
10
|
-
* mp4 delivery is smoother with HTTP Range (206) support, which file:// cannot provide.
|
|
11
|
-
*
|
|
12
|
-
* Runtime: Node >=18 builtins ONLY (node:http / node:fs / node:path). No external deps,
|
|
13
|
-
* no shell — cross-platform (path.join, path.resolve, path.sep everywhere).
|
|
8
|
+
* Why a server at all: the engine loads each clip with fetch() into a Blob, which file:// cannot
|
|
9
|
+
* serve, and video plays smoother with byte ranges (206).
|
|
14
10
|
*
|
|
15
11
|
* Usage:
|
|
16
|
-
* node serve.mjs [dir] [--port N]
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* node serve.mjs [dir] [--port N] [--csp strict|"<policy>"] [--state file]
|
|
13
|
+
* node serve.mjs --stop [dir] [--state file]
|
|
14
|
+
* dir folder to serve (default: the current one)
|
|
15
|
+
* --port preferred port; when busy or absent the system picks a free one
|
|
16
|
+
* --csp sends Content-Security-Policy on every response: `strict` (no inline script or
|
|
17
|
+
* style; blob: media) or the production policy as written. Check a page for a site
|
|
18
|
+
* with a CSP under that same policy: a plain server hides CSP errors.
|
|
19
|
+
* --state state file (default: <dir>.server.json beside the folder, never inside it)
|
|
20
|
+
* --stop ends the server the state file records, only when it answers with that PID
|
|
21
|
+
* A start while the recorded server still answers with the same settings only reports its
|
|
22
|
+
* address; with other settings (another folder sharing the --state file, --csp, PREVIEW_HOST, a
|
|
23
|
+
* --port other than the one it listens on or its own start asked for) it exits 1: --stop first.
|
|
24
|
+
* A request whose Host is a DNS name other than localhost is refused (DNS rebinding).
|
|
25
|
+
* On listen it writes the state file and prints PREVIEW http://localhost:<port>/
|
|
26
|
+
* PREVIEW_HOST=0.0.0.0 binds every interface (on-device phone checks) and prints a warning.
|
|
27
|
+
*
|
|
28
|
+
* Runtime: Node >=18 builtins only. No shell, cross-platform.
|
|
20
29
|
*/
|
|
21
30
|
import http from 'node:http';
|
|
22
31
|
import fs from 'node:fs';
|
|
23
32
|
import path from 'node:path';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
import {
|
|
35
|
+
LOOPBACK, answersPid, cspFrom, decodePath, forgetSelf, hostAllowed, hostWarning, isRunning, listen, portMatches, readState,
|
|
36
|
+
resolveInside, send, sendFile, siblingState, stopServer, urlFor, writeState,
|
|
37
|
+
} from './preview-lib.mjs';
|
|
24
38
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
function handler(dir, headers, host) {
|
|
40
|
+
return (req, res) => {
|
|
41
|
+
if (!hostAllowed(req.headers.host, [host])) return send(res, 403, 'host not allowed', headers);
|
|
42
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') return send(res, 405, '405', headers);
|
|
43
|
+
const urlPath = decodePath(req.url);
|
|
44
|
+
if (urlPath === null) return send(res, 400, '400', headers);
|
|
45
|
+
if (answersPid(urlPath, res)) return;
|
|
46
|
+
const fp = resolveInside(dir, urlPath === '/' ? '/index.html' : urlPath);
|
|
47
|
+
if (!fp) return send(res, 403, '403', headers);
|
|
48
|
+
sendFile(req, res, fp, headers);
|
|
49
|
+
};
|
|
34
50
|
}
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Serves `dir` until close(). Resolves to { server, port, host, url, stateFile, close }.
|
|
54
|
+
* @param {{ port?: number, host?: string, csp?: string, stateFile?: string }} opts
|
|
55
|
+
*/
|
|
56
|
+
export async function startServe(dir, { port, host = process.env.PREVIEW_HOST || LOOPBACK, csp, stateFile } = {}) {
|
|
57
|
+
const root = path.resolve(dir);
|
|
58
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) throw new Error(`serve: not a directory: ${root}`);
|
|
59
|
+
const policy = cspFrom(csp);
|
|
60
|
+
const server = http.createServer(handler(root, policy ? { 'Content-Security-Policy': policy } : {}, host));
|
|
61
|
+
const bound = await listen(server, { host, port });
|
|
62
|
+
const url = urlFor(host, bound);
|
|
63
|
+
const file = stateFile ? path.resolve(stateFile) : siblingState(root);
|
|
64
|
+
writeState(file, {
|
|
65
|
+
pid: process.pid, port: bound, requestedPort: port || null, host, url, dir: root, csp: policy, startedAt: new Date().toISOString(),
|
|
66
|
+
});
|
|
67
|
+
const close = () => new Promise((resolve) => { forgetSelf(file); server.close(() => resolve()); });
|
|
68
|
+
return { server, port: bound, host, url, stateFile: file, close };
|
|
40
69
|
}
|
|
41
70
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
'.mov': 'video/quicktime',
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const server = http.createServer((req, res) => {
|
|
60
|
-
let p = decodeURIComponent((req.url || '/').split('?')[0]);
|
|
61
|
-
if (p === '/') p = '/index.html';
|
|
71
|
+
function parseArgs(argv) {
|
|
72
|
+
const o = { dir: null, port: null, csp: null, state: null, stop: false };
|
|
73
|
+
for (let i = 0; i < argv.length; i++) {
|
|
74
|
+
const a = argv[i];
|
|
75
|
+
const [key, inline] = a.includes('=') ? [a.slice(0, a.indexOf('=')), a.slice(a.indexOf('=') + 1)] : [a, null];
|
|
76
|
+
const value = () => (inline !== null ? inline : argv[++i]);
|
|
77
|
+
if (key === '--port') o.port = Number(value());
|
|
78
|
+
else if (key === '--csp') o.csp = value();
|
|
79
|
+
else if (key === '--state') o.state = value();
|
|
80
|
+
else if (key === '--stop') o.stop = true;
|
|
81
|
+
else if (!a.startsWith('--')) o.dir = a;
|
|
82
|
+
}
|
|
83
|
+
return o;
|
|
84
|
+
}
|
|
62
85
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
// Nothing left running is a success; a port held by another program, or one that stays open, is not.
|
|
87
|
+
async function stopMain(o, dir) {
|
|
88
|
+
const r = await stopServer(o.state ? path.resolve(o.state) : siblingState(dir));
|
|
89
|
+
console.log(`STOP ${r.reason}`);
|
|
90
|
+
process.exit(['stopped', 'not-running', 'no-state'].includes(r.reason) ? 0 : 1);
|
|
91
|
+
}
|
|
67
92
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
// Whether the recorded server answers this start: this folder, the same CSP and host, and the port
|
|
94
|
+
// asked for. A state file shared through --state can record another folder's server.
|
|
95
|
+
const sameSettings = (st, o, dir) => path.resolve(String(st.dir || '')) === dir && (st.csp || null) === cspFrom(o.csp)
|
|
96
|
+
&& (st.host || LOOPBACK) === (process.env.PREVIEW_HOST || LOOPBACK) && portMatches(st, o.port);
|
|
72
97
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
res.writeHead(206, {
|
|
83
|
-
'Content-Type': type,
|
|
84
|
-
'Content-Range': `bytes ${start}-${end}/${size}`,
|
|
85
|
-
'Accept-Ranges': 'bytes',
|
|
86
|
-
'Content-Length': end - start + 1,
|
|
87
|
-
'Cache-Control': 'no-store',
|
|
88
|
-
});
|
|
89
|
-
fs.createReadStream(fp, { start, end }).pipe(res);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
98
|
+
// A server already running for this state file: the same settings only report its address; other
|
|
99
|
+
// settings (folder, CSP, host, port) never pass for it, so a check meant to run under a CSP cannot
|
|
100
|
+
// run without one, and a page meant for one folder or port is not checked on another.
|
|
101
|
+
async function reportRunning(recorded, o, dir) {
|
|
102
|
+
if (!(await isRunning(recorded))) return false;
|
|
103
|
+
const st = readState(recorded);
|
|
104
|
+
if (sameSettings(st, o, dir)) {
|
|
105
|
+
console.log(`PREVIEW ${st.url} (already running, pid ${st.pid})`);
|
|
106
|
+
return true;
|
|
92
107
|
}
|
|
108
|
+
console.error(`serve: the state file records a server running with different settings (folder: ${st.dir}, `
|
|
109
|
+
+ `csp: ${st.csp ? 'on' : 'off'}, host: ${st.host}, port: ${st.port}); stop it with --stop and start again`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
93
112
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
(function tryPort(i) {
|
|
110
|
-
if (i >= ports.length) { console.error('serve: no free port available'); process.exit(1); }
|
|
111
|
-
server.once('error', (e) => {
|
|
112
|
-
if (e.code === 'EADDRINUSE') tryPort(i + 1);
|
|
113
|
-
else { console.error(e); process.exit(1); }
|
|
114
|
-
});
|
|
115
|
-
server.listen(ports[i], HOST, () => {
|
|
116
|
-
try { fs.writeFileSync(path.join(DIR, 'port.txt'), String(ports[i])); } catch { /* best effort */ }
|
|
117
|
-
console.log(`PREVIEW http://${HOST === '127.0.0.1' ? 'localhost' : HOST}:${ports[i]}/`);
|
|
118
|
-
console.log(`serving ${DIR}`);
|
|
119
|
-
});
|
|
120
|
-
})(0);
|
|
113
|
+
async function main() {
|
|
114
|
+
const o = parseArgs(process.argv.slice(2));
|
|
115
|
+
const dir = path.resolve(o.dir || process.cwd());
|
|
116
|
+
if (o.stop) return stopMain(o, dir);
|
|
117
|
+
if (await reportRunning(o.state ? path.resolve(o.state) : siblingState(dir), o, dir)) return;
|
|
118
|
+
const s = await startServe(dir, { port: o.port || undefined, csp: o.csp || undefined, stateFile: o.state || undefined });
|
|
119
|
+
const warn = hostWarning(s.host);
|
|
120
|
+
if (warn) console.warn(warn);
|
|
121
|
+
console.log(`PREVIEW ${s.url}`);
|
|
122
|
+
console.log(`serving ${dir}${o.csp ? ' (with Content-Security-Policy)' : ''} state: ${s.stateFile}`);
|
|
123
|
+
const quit = () => { forgetSelf(s.stateFile); s.server.close(); process.exit(0); };
|
|
124
|
+
process.on('SIGINT', quit);
|
|
125
|
+
process.on('SIGTERM', quit);
|
|
126
|
+
}
|
|
121
127
|
|
|
122
|
-
process.
|
|
123
|
-
|
|
128
|
+
const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
129
|
+
if (invokedDirectly) {
|
|
130
|
+
main().catch((e) => { console.error(String(e?.message || e)); process.exit(1); });
|
|
131
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/* Hints, skip link and main button of step-nav.js (scene-by-scene navigation, opt-in).
|
|
2
|
+
step-nav.js sets on <html>: isn-on, isn-mouse or isn-touch, isn-last (at the last scene),
|
|
3
|
+
isn-ready (the last clip has played to its end and the page has a body to go to), isn-body.
|
|
4
|
+
Colours are variables: set them from the brand (a light pill on a dark site, and so on). */
|
|
5
|
+
|
|
6
|
+
:root {
|
|
7
|
+
--isn-pill-bg: rgba(11, 18, 32, .72);
|
|
8
|
+
--isn-pill-ink: #F1F5F9;
|
|
9
|
+
--isn-skip-bg: rgba(11, 18, 32, .6);
|
|
10
|
+
--isn-main-bg: #F1F5F9;
|
|
11
|
+
--isn-main-ink: #0B1220;
|
|
12
|
+
--isn-focus: #38BDF8;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Mouse users: a dark pill with a wheel and a down key, bottom centre. It replaces the engine hint. */
|
|
16
|
+
.isn-hint {
|
|
17
|
+
position: fixed; left: 50%; bottom: 26px; z-index: 31; translate: -50% 0;
|
|
18
|
+
display: none; align-items: center; gap: 10px; padding: 9px 16px; border-radius: 999px;
|
|
19
|
+
background: var(--isn-pill-bg); color: var(--isn-pill-ink); pointer-events: none;
|
|
20
|
+
font: 600 .78rem/1.2 system-ui, -apple-system, "Segoe UI", sans-serif; letter-spacing: .02em;
|
|
21
|
+
}
|
|
22
|
+
html.isn-mouse .isn-hint { display: flex; }
|
|
23
|
+
html.isn-mouse .sw-hint { display: none; }
|
|
24
|
+
.isn-hint__wheel { width: 14px; height: 22px; border: 2px solid currentColor; border-radius: 8px; position: relative; opacity: .9; }
|
|
25
|
+
.isn-hint__wheel::after { content: ""; position: absolute; left: 50%; top: 4px; width: 2px; height: 5px; border-radius: 1px; background: currentColor; translate: -50% 0; }
|
|
26
|
+
.isn-hint__key { width: 20px; height: 20px; border: 2px solid currentColor; border-radius: 5px; position: relative; opacity: .9; }
|
|
27
|
+
.isn-hint__key::after { content: ""; position: absolute; left: 50%; top: 4px; width: 6px; height: 6px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; translate: -50% 0; rotate: 45deg; }
|
|
28
|
+
|
|
29
|
+
/* Touch users: the engine's own hint stays up until the last scene (the engine fades it with an
|
|
30
|
+
inline opacity, which !important overrides). Bottom left at 600px and below. */
|
|
31
|
+
html.isn-touch .sw-hint { opacity: 1 !important; }
|
|
32
|
+
@media (max-width: 600px) {
|
|
33
|
+
html.isn-touch .sw-hint { left: 18px; transform: none; align-items: flex-start; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* A narrow and short window has no room for the mouse hint. */
|
|
37
|
+
@media (max-width: 600px) and (max-height: 700px) {
|
|
38
|
+
.isn-hint { display: none !important; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Skip link: in every scene but the last. */
|
|
42
|
+
.isn-skip {
|
|
43
|
+
position: fixed; right: clamp(14px, 2.4vw, 30px); bottom: 22px; z-index: 32;
|
|
44
|
+
padding: 8px 14px; border-radius: 999px; background: var(--isn-skip-bg); color: var(--isn-pill-ink);
|
|
45
|
+
font: 600 .8rem/1.2 system-ui, -apple-system, "Segoe UI", sans-serif; text-decoration: none;
|
|
46
|
+
}
|
|
47
|
+
.isn-skip:focus-visible, .isn-main:focus-visible { outline: 3px solid var(--isn-focus); outline-offset: 3px; }
|
|
48
|
+
|
|
49
|
+
/* Main button: only once the last clip has played to its end. */
|
|
50
|
+
.isn-main {
|
|
51
|
+
position: fixed; left: 50%; bottom: 26px; z-index: 32; translate: -50% 0; display: none;
|
|
52
|
+
padding: 12px 22px; border: 0; border-radius: 999px; background: var(--isn-main-bg); color: var(--isn-main-ink); cursor: pointer;
|
|
53
|
+
font: 700 .95rem/1.2 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
54
|
+
}
|
|
55
|
+
html.isn-ready .isn-main { display: inline-flex; }
|
|
56
|
+
|
|
57
|
+
/* The last scene keeps the main button alone; the body shows none of these. */
|
|
58
|
+
html.isn-last .isn-hint, html.isn-last .sw-hint, html.isn-last .isn-skip,
|
|
59
|
+
html.isn-body .isn-hint, html.isn-body .sw-hint, html.isn-body .isn-skip, html.isn-body .isn-main { display: none !important; }
|