@kaydash9999/banker-plugins 0.20.2 → 0.21.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +203 -1
- package/PRIVACY.md +34 -4
- package/README.md +46 -15
- package/bin/banker.js +228 -51
- package/bin/lib/codex-hooks.mjs +249 -0
- package/bin/lib/telemetry-config.mjs +33 -16
- package/codex/manifest.json +14 -11
- package/codex/skills/graceful-pause/SKILL.md +30 -0
- package/codex/skills/progress/SKILL.md +42 -0
- package/codex/skills/progress/scripts/progress-plan.mjs +722 -0
- package/codex/skills/progress/scripts/progress.mjs +295 -0
- package/codex/transform-matrix.md +19 -14
- package/commands/setup.md +23 -0
- package/hooks/obsidize-hook.mjs +116 -4
- package/hooks/progress-plan.mjs +1 -1
- package/hooks/progress.mjs +143 -38
- package/hooks/register.mjs +13 -3
- package/hooks/telemetry-count-codex.mjs +222 -0
- package/hooks/update-checkin.mjs +15 -7
- package/hooks/update-fetch.mjs +15 -9
- package/hooks/update-notify.mjs +30 -17
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +11 -5
- package/skills/3d-intro-build/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-build/references/curate.mjs +48 -14
- package/skills/3d-intro-build/references/preview-lib.mjs +51 -15
- package/skills/3d-intro-build/references/prompts.md +4 -4
- package/skills/3d-intro-build/references/serve.mjs +6 -1
- package/skills/3d-intro-build/references/verify-intro.mjs +16 -9
- package/skills/3d-intro-build/references/video-pool.mjs +250 -135
- package/skills/3d-intro-setup/SKILL.md +41 -10
- package/skills/3d-intro-setup/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-setup/references/video-pool.mjs +250 -135
- package/skills/anti-drm/SKILL.md +6 -3
- package/skills/anti-drm/scripts/anti-drm.mjs +49 -12
- package/skills/anti-drm/scripts/instruction-toggle.mjs +105 -20
- package/skills/append_wiki/SKILL.md +34 -8
- package/skills/audit-mock/SKILL.md +1 -1
- package/skills/audit-security/SKILL.md +64 -2
- package/skills/compact-wiki/SKILL.md +37 -6
- package/skills/deep-research/SKILL.md +11 -0
- package/skills/docs-setup/SKILL.md +37 -7
- package/skills/lfm/SKILL.md +148 -0
- package/skills/lineage/SKILL.md +25 -15
- package/skills/lineage/lineage.py +564 -301
- package/skills/motion-graphic-make/SKILL.md +1 -1
- package/skills/motion-graphic-make/references/curate.mjs +48 -14
- package/skills/motion-graphic-make/references/preview-lib.mjs +51 -15
- package/skills/motion-graphic-make/references/serve.mjs +6 -1
- package/skills/motion-graphic-setup/SKILL.md +58 -13
- package/skills/obsidizer/SKILL.md +30 -11
- package/skills/obsidizer/obsidize.mjs +50 -12
- package/skills/omc-patch/SKILL.md +86 -23
- package/skills/omc-patch/scripts/omc-patch.mjs +460 -116
- package/skills/payload-mon/SKILL.md +6 -2
- package/skills/payload-mon/scripts/payload-mon.mjs +88 -12
- package/skills/payload-mon/scripts/payload-size.mjs +4 -1
- package/skills/pdf-vision-extract/SKILL.md +11 -6
- package/skills/ralph-qa/SKILL.md +125 -151
- package/skills/ralph-qa/references/gemini-seat.mjs +67 -19
- package/skills/ralph-qa/references/payload-scan.mjs +22 -7
- package/skills/ralph-qa/references/seat-payload.mjs +646 -0
- package/skills/ralph-qa/references/seat-run.mjs +956 -0
- package/skills/ralph-qa/references/seat-spawn.mjs +205 -0
- package/skills/ralph-qa/references/verifier-probe.mjs +57 -16
- package/skills/rfp-author/SKILL.md +5 -3
- package/skills/rfp-author/references/vendor-rfp-structure.md +1 -1
- package/skills/setup-bypass-permissions/SKILL.md +27 -9
- package/skills/setup-bypass-permissions/scripts/codex-bypass.mjs +135 -0
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +210 -0
- package/skills/setup-harness-factory/SKILL.md +17 -1
- package/skills/setup-insane-search/SKILL.md +16 -0
- package/skills/setup-java/SKILL.md +20 -1
- package/skills/setup-lsp/SKILL.md +56 -13
- package/skills/setup-mcp/SKILL.md +73 -4
- package/skills/setup-node/SKILL.md +31 -6
- package/skills/setup-omc/SKILL.md +24 -0
- package/skills/setup-omc-hud/SKILL.md +42 -9
- package/skills/setup-omc-hud/assets/apply-hud.mjs +242 -27
- package/skills/setup-omc-hud/assets/codex-config.mjs +210 -0
- package/skills/setup-omc-hud/assets/codex-status-line.mjs +90 -0
- package/skills/setup-omc-hud/assets/omc-hud-custom.mjs +81 -14
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +5 -3
- package/skills/setup-playwright/SKILL.md +45 -2
- package/skills/setup-pwsh/SKILL.md +19 -0
- package/skills/setup-python/SKILL.md +45 -7
- package/skills/setup-sandbox/SKILL.md +30 -5
- package/skills/setup-stitch/SKILL.md +22 -0
- package/skills/setup-tmux/SKILL.md +24 -4
- package/skills/smart-compact/SKILL.md +58 -12
- package/skills/smart-compact/scripts/codex-gate.mjs +384 -0
- package/skills/tone-compact/SKILL.md +2 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +105 -20
- package/skills/ultra-chain/SKILL.md +12 -7
- package/skills/ultra-chain/references/preview-lib.mjs +51 -15
- package/skills/ultra-chain/references/serve.mjs +6 -1
- package/skills/ultra-interview/SKILL.md +14 -2
- package/skills/update-banker/SKILL.md +19 -3
- package/skills/vertical-pptx/SKILL.md +6 -3
- package/skills/vertical-pptx-setup/SKILL.md +76 -20
|
@@ -64,10 +64,28 @@ const MEDIA = /\.(png|jpe?g|webp|gif|mp4|webm|mov)$/i;
|
|
|
64
64
|
function linkRefusal(dir, fp) {
|
|
65
65
|
let real;
|
|
66
66
|
try { real = fs.realpathSync(fp); } catch { return null; }
|
|
67
|
+
return realRefusal(dir, real);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function realRefusal(dir, real) {
|
|
67
71
|
if (!real.startsWith(fs.realpathSync(dir) + path.sep)) return '링크가 프로젝트 폴더 밖을 가리켜 보이지 않습니다.';
|
|
68
72
|
return MEDIA.test(real) ? null : '링크가 이미지나 영상이 아닌 파일을 가리켜 보이지 않습니다.';
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
// What a listed path may be served as: { dev, ino } (BigInt) of the file its links lead to, taken without
|
|
76
|
+
// following a link, so a link put in its place after this check is not that file. null when there is
|
|
77
|
+
// no such file, or its links leave the project or end at a file that is not an image or a video.
|
|
78
|
+
function servedFile(dir, fp) {
|
|
79
|
+
try {
|
|
80
|
+
const real = fs.realpathSync(fp);
|
|
81
|
+
if (realRefusal(dir, real)) return null;
|
|
82
|
+
const st = fs.lstatSync(real, { bigint: true });
|
|
83
|
+
return { dev: st.dev, ino: st.ino };
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
// Why a listed file is neither shown nor served nor hashed, or null when it may be: it must be an
|
|
72
90
|
// image or a video inside the project, by its path and by where its links lead, so a key file or
|
|
73
91
|
// a record listed by mistake never leaves the folder.
|
|
@@ -103,9 +121,11 @@ export function readDecisions(dir) {
|
|
|
103
121
|
}
|
|
104
122
|
|
|
105
123
|
// Written whole or not at all: a crash mid-write leaves the previous file.
|
|
124
|
+
// The temp name is random and opened exclusively: a link a repository put at a guessable name is never written through.
|
|
106
125
|
function writeJson(file, value) {
|
|
107
|
-
|
|
108
|
-
fs.
|
|
126
|
+
const tmp = `${file}.${crypto.randomBytes(8).toString('hex')}.tmp`;
|
|
127
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', { flag: 'wx' });
|
|
128
|
+
try { fs.renameSync(tmp, file); } catch (e) { fs.rmSync(tmp, { force: true }); throw e; }
|
|
109
129
|
}
|
|
110
130
|
|
|
111
131
|
const writeDecisions = (dir, all) => writeJson(path.join(dir, DECISIONS), all);
|
|
@@ -247,20 +267,26 @@ export function archiveRejected(dir, { now = new Date() } = {}) {
|
|
|
247
267
|
// path turns into one after the first check).
|
|
248
268
|
const hashes = new Map();
|
|
249
269
|
const notRegular = () => new Error('일반 파일 아님');
|
|
270
|
+
// A file changed within this long of a hash may change again in the same timestamp step (2 s on FAT):
|
|
271
|
+
// its hash is not kept, so the next view reads it again.
|
|
272
|
+
const RACY_MS = 3000n;
|
|
273
|
+
const keyOf = (fp, st) => `${fp}:${st.dev}:${st.ino}:${st.size}:${st.mtimeNs}:${st.ctimeNs}`;
|
|
274
|
+
const racy = (st, startedNs) => startedNs - (st.ctimeNs > st.mtimeNs ? st.ctimeNs : st.mtimeNs) < RACY_MS * 1_000_000n;
|
|
250
275
|
export async function hashFile(fp) {
|
|
251
276
|
if (!(await fs.promises.stat(fp)).isFile()) throw notRegular();
|
|
252
277
|
const fh = await fs.promises.open(fp, READ_FLAGS);
|
|
253
278
|
let done;
|
|
254
279
|
try {
|
|
255
|
-
const
|
|
280
|
+
const startedNs = BigInt(Date.now()) * 1_000_000n;
|
|
281
|
+
const st = await fh.stat({ bigint: true });
|
|
256
282
|
if (!st.isFile()) throw notRegular();
|
|
257
|
-
const key =
|
|
283
|
+
const key = keyOf(fp, st);
|
|
258
284
|
done = hashes.get(key);
|
|
259
285
|
if (done) return done;
|
|
260
|
-
done = sha256Of(fh, st.size);
|
|
261
|
-
done.catch(() => hashes.delete(key));
|
|
286
|
+
done = sha256Of(fh, Number(st.size));
|
|
262
287
|
hashes.set(key, done);
|
|
263
288
|
await done.catch(() => {});
|
|
289
|
+
if (racy(st, startedNs) || !(await done.then(() => true, () => false))) hashes.delete(key);
|
|
264
290
|
} finally {
|
|
265
291
|
await fh.close();
|
|
266
292
|
}
|
|
@@ -287,7 +313,10 @@ async function hashText(dir, file) {
|
|
|
287
313
|
|
|
288
314
|
// The hash text of every listed take by file, four files at a time per call (a long list must not
|
|
289
315
|
// run out of file handles), before the page is drawn.
|
|
290
|
-
|
|
316
|
+
// A take not hashed within waitMs shows that its read goes on; the next view shows its hash.
|
|
317
|
+
const HASH_WAIT_MS = 5000;
|
|
318
|
+
const STILL_READING = '읽는 중(새로 고치면 보임)';
|
|
319
|
+
async function takeHashes(input, dir, waitMs = HASH_WAIT_MS) {
|
|
291
320
|
const files = [...new Set(input.scenes.flatMap((s) => s.takes.map((t) => t.file)))];
|
|
292
321
|
const out = new Map();
|
|
293
322
|
let next = 0;
|
|
@@ -297,8 +326,12 @@ async function takeHashes(input, dir) {
|
|
|
297
326
|
out.set(f, await hashText(dir, f));
|
|
298
327
|
}
|
|
299
328
|
};
|
|
300
|
-
|
|
301
|
-
|
|
329
|
+
const all = Promise.all(Array.from({ length: Math.min(4, files.length) }, worker));
|
|
330
|
+
let timer;
|
|
331
|
+
const late = new Promise((resolve) => { timer = setTimeout(resolve, waitMs); });
|
|
332
|
+
await Promise.race([all, late]);
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
return new Map(files.map((f) => [f, out.has(f) ? out.get(f) : STILL_READING]));
|
|
302
335
|
}
|
|
303
336
|
|
|
304
337
|
function media(t, dir) {
|
|
@@ -366,11 +399,11 @@ function summary(input, entries) {
|
|
|
366
399
|
}
|
|
367
400
|
|
|
368
401
|
/** The review page for `input`. Resolves once every listed take is hashed. */
|
|
369
|
-
export async function renderPage(input, decisions, { dir }) {
|
|
402
|
+
export async function renderPage(input, decisions, { dir, hashWaitMs }) {
|
|
370
403
|
const stage = decisions[input.stage] || { scenes: [] };
|
|
371
404
|
const entries = new Map(stage.scenes.map((e) => [e.id, { ...e, rejected: e.rejected || [] }]));
|
|
372
405
|
const promptLabel = input.promptLabel || (INSTRUCTION_STAGES.has(input.stage) ? '구성 지시문' : '프롬프트');
|
|
373
|
-
const ctx = { dir, promptLabel, entries, hashes: await takeHashes(input, dir) };
|
|
406
|
+
const ctx = { dir, promptLabel, entries, hashes: await takeHashes(input, dir, hashWaitMs) };
|
|
374
407
|
const body = input.scenes.map((s, i) => sceneBlock(s, i, ctx)).join('\n') || '<p class="empty">curate-input.json 에 장면이 없습니다.</p>';
|
|
375
408
|
return `<!doctype html>
|
|
376
409
|
<html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -505,8 +538,9 @@ async function page(dir, res) {
|
|
|
505
538
|
function serveListed(dir, req, res, urlPath) {
|
|
506
539
|
const fp = resolveInside(dir, urlPath);
|
|
507
540
|
if (!fp) return send(res, 403, '403');
|
|
508
|
-
|
|
509
|
-
|
|
541
|
+
const expect = listedFiles(dir).has(fp) ? servedFile(dir, fp) : null;
|
|
542
|
+
if (!expect) return send(res, 404, '404');
|
|
543
|
+
sendFile(req, res, fp, {}, { expect });
|
|
510
544
|
}
|
|
511
545
|
|
|
512
546
|
function route(dir, req, res, urlPath) {
|
|
@@ -546,7 +580,7 @@ export async function startCurate(dir, { port, host = process.env.PREVIEW_HOST |
|
|
|
546
580
|
// Starts hashing the listed takes, so the first view of the page does not wait for a large render.
|
|
547
581
|
// An input that cannot be read hashes nothing here; the page reports it.
|
|
548
582
|
function warmHashes(dir) {
|
|
549
|
-
try { return takeHashes(readInput(dir), dir).catch(() => new Map()); } catch { return Promise.resolve(new Map()); }
|
|
583
|
+
try { return takeHashes(readInput(dir), dir, 2 ** 31 - 1).catch(() => new Map()); } catch { return Promise.resolve(new Map()); }
|
|
550
584
|
}
|
|
551
585
|
|
|
552
586
|
// ---- CLI ------------------------------------------------------------------------
|
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Runtime: Node >=18 builtins only (node:fs / node:http / node:net / node:path). No shell.
|
|
18
18
|
*/
|
|
19
|
+
import crypto from 'node:crypto';
|
|
19
20
|
import fs from 'node:fs';
|
|
20
21
|
import http from 'node:http';
|
|
21
22
|
import net from 'node:net';
|
|
23
|
+
import os from 'node:os';
|
|
22
24
|
import path from 'node:path';
|
|
23
|
-
import { pipeline } from 'node:stream';
|
|
25
|
+
import { Transform, pipeline } from 'node:stream';
|
|
24
26
|
|
|
25
27
|
export const LOOPBACK = '127.0.0.1';
|
|
26
28
|
// Answers { pid } so a stop can tell this server from another process on the same port.
|
|
@@ -80,39 +82,57 @@ export function send(res, status, body, headers = {}) {
|
|
|
80
82
|
export const READ_FLAGS = fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0);
|
|
81
83
|
|
|
82
84
|
// An open regular file: { fd, size }, or { status } when there is none (404) or it cannot be read (403).
|
|
83
|
-
|
|
85
|
+
// With `expect` ({ dev, ino } of the file a check passed, as BigInt), another file at the path is none:
|
|
86
|
+
// the path changed between that check and this open. BigInt, since an NTFS file id is 64 bits and a
|
|
87
|
+
// Number keeps only 53: two files could read as one.
|
|
88
|
+
function openRegular(fp, expect) {
|
|
84
89
|
let fd;
|
|
85
90
|
try { fd = fs.openSync(fp, READ_FLAGS); } catch (e) { return { status: ['ENOENT', 'ENOTDIR', 'EISDIR'].includes(e.code) ? 404 : 403 }; }
|
|
86
91
|
let st = null;
|
|
87
|
-
try { st = fs.fstatSync(fd); } catch { /* a status the system cannot give (EIO on a network drive) */ }
|
|
88
|
-
if (st && st.isFile()) return { fd, size: st.size };
|
|
92
|
+
try { st = fs.fstatSync(fd, { bigint: true }); } catch { /* a status the system cannot give (EIO on a network drive) */ }
|
|
93
|
+
if (st && st.isFile() && sameFile(st, expect)) return { fd, size: Number(st.size) };
|
|
89
94
|
fs.closeSync(fd);
|
|
90
95
|
return { status: st ? 404 : 403 };
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
const sameFile = (st, expect) => !expect || (st.dev === expect.dev && st.ino === expect.ino);
|
|
99
|
+
|
|
93
100
|
// Sends the bytes `start`..`end` of an open file (what the headers declared, even if the file grows
|
|
94
101
|
// meanwhile) and closes it, also when the browser drops the download midway (a pipe would keep the
|
|
95
102
|
// file open). Nothing to send closes the file and ends the response.
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
// A HEAD request gets the headers only: the file is closed unread.
|
|
104
|
+
function stream(res, fp, { fd, start, end, head }) {
|
|
105
|
+
if (head || end < start) {
|
|
98
106
|
fs.closeSync(fd);
|
|
99
107
|
return void res.end();
|
|
100
108
|
}
|
|
101
|
-
pipeline(fs.createReadStream(fp, { fd, start, end }), res, () => {});
|
|
109
|
+
pipeline(fs.createReadStream(fp, { fd, start, end }), declaredLength(end - start + 1), res, () => {});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Passes the bytes on and fails at the end when they were fewer than the headers declared (the file
|
|
113
|
+
// shrank since): the pipeline then destroys the response, which closes the connection. Ended as
|
|
114
|
+
// usual, it would leave the browser waiting on a kept-alive connection for bytes that never come.
|
|
115
|
+
function declaredLength(want) {
|
|
116
|
+
let sent = 0;
|
|
117
|
+
return new Transform({
|
|
118
|
+
transform(chunk, _enc, done) { sent += chunk.length; done(null, chunk); },
|
|
119
|
+
flush(done) { done(sent < want ? new Error('the file is shorter than its declared length') : null); },
|
|
120
|
+
});
|
|
102
121
|
}
|
|
103
122
|
|
|
104
123
|
// A file, with byte ranges for video (smoother scrubbing). Never cached: a rebuilt page is fresh.
|
|
105
124
|
// The file is opened before any header goes out, so one that cannot be read gets 403 instead of
|
|
106
|
-
// ending the server.
|
|
107
|
-
export function sendFile(req, res, fp, headers = {}) {
|
|
108
|
-
const f = openRegular(fp);
|
|
125
|
+
// ending the server. `expect` ({ dev, ino }) names the only file that may go out (404 otherwise).
|
|
126
|
+
export function sendFile(req, res, fp, headers = {}, { expect } = {}) {
|
|
127
|
+
const f = openRegular(fp, expect);
|
|
109
128
|
if (f.status) return send(res, f.status, String(f.status), headers);
|
|
129
|
+
f.head = req.method === 'HEAD';
|
|
110
130
|
const type = MIME[path.extname(fp).toLowerCase()] || 'application/octet-stream';
|
|
111
131
|
const base = { ...headers, 'Content-Type': type, 'Accept-Ranges': 'bytes', 'Cache-Control': 'no-store' };
|
|
112
132
|
const range = type.startsWith('video/') ? /bytes=(\d+)-(\d*)/.exec(req.headers.range || '') : null;
|
|
113
133
|
if (range) return sendRange(res, fp, f, range, base, headers);
|
|
114
134
|
res.writeHead(200, { ...base, 'Content-Length': f.size });
|
|
115
|
-
stream(res, fp, { fd: f.fd, start: 0, end: f.size - 1 });
|
|
135
|
+
stream(res, fp, { fd: f.fd, start: 0, end: f.size - 1, head: f.head });
|
|
116
136
|
}
|
|
117
137
|
|
|
118
138
|
function sendRange(res, fp, f, range, base, headers) {
|
|
@@ -124,7 +144,7 @@ function sendRange(res, fp, f, range, base, headers) {
|
|
|
124
144
|
return void res.end();
|
|
125
145
|
}
|
|
126
146
|
res.writeHead(206, { ...base, 'Content-Range': `bytes ${start}-${end}/${f.size}`, 'Content-Length': end - start + 1 });
|
|
127
|
-
stream(res, fp, { fd: f.fd, start, end });
|
|
147
|
+
stream(res, fp, { fd: f.fd, start, end, head: f.head });
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
// True when the request was the PID question, which this answers.
|
|
@@ -169,8 +189,12 @@ export const portMatches = (st, port) => !port || st.port === port || st.request
|
|
|
169
189
|
// The state file of a server for `dir`: beside the folder, so a deployed folder never carries it.
|
|
170
190
|
export const siblingState = (dir) => path.join(path.dirname(dir), `${path.basename(dir)}.server.json`);
|
|
171
191
|
|
|
192
|
+
// Through a fresh random name opened exclusively, then renamed over the old file: a link at the state
|
|
193
|
+
// file's name (one a repository can bring) is replaced, never written through.
|
|
172
194
|
export function writeState(file, info) {
|
|
173
|
-
|
|
195
|
+
const tmp = `${file}.${crypto.randomBytes(8).toString('hex')}.tmp`;
|
|
196
|
+
fs.writeFileSync(tmp, JSON.stringify(info, null, 2) + '\n', { flag: 'wx' });
|
|
197
|
+
try { fs.renameSync(tmp, file); } catch (e) { fs.rmSync(tmp, { force: true }); throw e; }
|
|
174
198
|
}
|
|
175
199
|
|
|
176
200
|
export function readState(file) {
|
|
@@ -220,10 +244,22 @@ async function waitClosed(port, timeoutMs, host) {
|
|
|
220
244
|
// A PID a state file may name: a whole number above 1 (never a process group, never init).
|
|
221
245
|
const validPid = (pid) => Number.isInteger(pid) && pid > 1;
|
|
222
246
|
|
|
247
|
+
// A host a state file may name: this machine (loopback, a wildcard, an address of its own). A state file
|
|
248
|
+
// can come with a repository: its host must not send a request elsewhere, nor pick the PID a signal goes to.
|
|
249
|
+
// A record a stop or a start may act on: a PID that is a process id, on this machine.
|
|
250
|
+
const recordedHere = (st) => validPid(st.pid) && thisMachine(st.host);
|
|
251
|
+
|
|
252
|
+
// No host is the default loopback one. A loopback address counts only as an IPv4 address of 127/8: `127.evil.test`
|
|
253
|
+
// or `127.256.0.1` is a DNS name, resolved anywhere (a resolver's search domain may answer it).
|
|
254
|
+
export function thisMachine(host) {
|
|
255
|
+
if (!host || ['0.0.0.0', '::', '::1', 'localhost'].includes(host) || (net.isIPv4(String(host)) && String(host).startsWith('127.'))) return true;
|
|
256
|
+
return Object.values(os.networkInterfaces()).flat().some((a) => a && a.address === host);
|
|
257
|
+
}
|
|
258
|
+
|
|
223
259
|
/** True when the server a state file records still answers on its port with its PID. */
|
|
224
260
|
export async function isRunning(stateFile) {
|
|
225
261
|
const st = readState(stateFile);
|
|
226
|
-
return Boolean(st && st.port) &&
|
|
262
|
+
return Boolean(st && st.port) && recordedHere(st) && (await askPid(st.port, 1500, st.host)) === st.pid;
|
|
227
263
|
}
|
|
228
264
|
|
|
229
265
|
function signal(pid, sig) {
|
|
@@ -236,7 +272,7 @@ function signal(pid, sig) {
|
|
|
236
272
|
export async function stopServer(stateFile, { timeoutMs = 3000 } = {}) {
|
|
237
273
|
const st = readState(stateFile);
|
|
238
274
|
if (!st || !st.port) return { stopped: false, reason: 'no-state' };
|
|
239
|
-
if (!
|
|
275
|
+
if (!recordedHere(st)) return { stopped: false, reason: 'invalid-state' };
|
|
240
276
|
const who = await askPid(st.port, Math.min(timeoutMs, 1500), st.host);
|
|
241
277
|
if (who === NONE) {
|
|
242
278
|
dropState(stateFile);
|
|
@@ -80,7 +80,7 @@ Subject: [SCENE.subject — 미니어처 장면을 묘사: 건물/공간, 일하
|
|
|
80
80
|
|
|
81
81
|
- `size` = `ORIENTATION` (`720x1280` 세로 / `1280x720` 가로). Sora 크기와 반드시 일치시킨다.
|
|
82
82
|
- `quality` = `low`(미리보기·저렴) 또는 `high`(최종).
|
|
83
|
-
- **N3 주의:** v1 경로가 실패해 classic 폴백이 뜨면 반환 크기가 `1024x1536`(2:3)라 `720x1280` 이 아니다. `
|
|
83
|
+
- **N3 주의:** v1 경로가 실패해 classic 폴백이 뜨면 반환 크기가 `1024x1536`(2:3)라 `720x1280` 이 아니다. 영상 씨앗(`generateClip` 의 `firstFramePng`)으로 쓰기 전에 ORIENTATION 크기로 리사이즈(ffmpeg `scale`)하거나 재생성한다. v1 경로(`720x1280`)가 검증된 happy path 다.
|
|
84
84
|
|
|
85
85
|
### FLUX 코히전 옵트인 (`generateImageFlux` / FLUX.2-pro)
|
|
86
86
|
|
|
@@ -90,14 +90,14 @@ Subject: [SCENE.subject — 미니어처 장면을 묘사: 건물/공간, 일하
|
|
|
90
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
93
|
-
## 4. Fly-through — forward-chaining leg 프롬프트 (`Sora-2
|
|
93
|
+
## 4. Fly-through — forward-chaining leg 프롬프트 (`generateClip`: WAN 키 풀이 먼저, 쓸 수 있는 키가 없으면 `Sora-2`)
|
|
94
94
|
|
|
95
95
|
Azure `/videos` 는 `input_reference` 를 **딱 하나만** 받는다(두-이미지는 400).
|
|
96
96
|
그래서 연속성은 **forward-chaining** 으로 만든다: 이전 클립의 **실제 마지막 프레임**을 다음 클립의 `input_reference` 로 넣는다.
|
|
97
97
|
아래 굵은 절이 **모션 핸드오프 계약**이다 — 글자 그대로 유지하고, 씬별 표현은 가운데 MOVE 슬롯에 넣는다.
|
|
98
98
|
|
|
99
99
|
**Leg 0 (씨앗) — 씬 1 스틸에서 dive-in:**
|
|
100
|
-
`
|
|
100
|
+
`firstFramePng = 씬 1 스틸`.
|
|
101
101
|
|
|
102
102
|
```
|
|
103
103
|
Single continuous cinematic camera move, no cuts. Begin high and far, looking down at the
|
|
@@ -142,7 +142,7 @@ tilt. It only travels straight and level, the world sliding past beneath the sam
|
|
|
142
142
|
잔잔한 전진 활공의 한 컷으로 읽혀야 한다(옆으로 흐르는 모션블러·완결 안 된 오빗 금지).
|
|
143
143
|
아니면 그 leg 를 재생성한다 — 나쁜 핸드오프 프레임 하나가 뒤의 모든 leg 를 오염시킨다.
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
`generateClip` 인자: `size: ORIENTATION`, `seconds`(기본 5. WAN 은 정수 2~30초, Sora 로 폴백되면 4, 8, 12초 중 가장 가까운 값), `firstFramePng`. 모델과 provider 순서는 키 파일(`WAN_MODEL`, `AZURE_SORA_DEPLOYMENT`, `VIDEO_PROVIDER_ORDER`)이 정한다. `createVideo` 를 직접 부르지 않는다. 키 풀, 폴백, 이중 과금 방지를 건너뛴다(SKILL.md Step 4).
|
|
146
146
|
|
|
147
147
|
---
|
|
148
148
|
|
|
@@ -95,6 +95,9 @@ async function stopMain(o, dir) {
|
|
|
95
95
|
const sameSettings = (st, o, dir) => path.resolve(String(st.dir || '')) === dir && (st.csp || null) === cspFrom(o.csp)
|
|
96
96
|
&& (st.host || LOOPBACK) === (process.env.PREVIEW_HOST || LOOPBACK) && portMatches(st, o.port);
|
|
97
97
|
|
|
98
|
+
// A value in one quoted piece, with "?" for what a reader takes for a line end or what changes how text shows.
|
|
99
|
+
const oneLine = (v) => JSON.stringify(String(v)).replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, '?');
|
|
100
|
+
|
|
98
101
|
// A server already running for this state file: the same settings only report its address; other
|
|
99
102
|
// settings (folder, CSP, host, port) never pass for it, so a check meant to run under a CSP cannot
|
|
100
103
|
// run without one, and a page meant for one folder or port is not checked on another.
|
|
@@ -105,7 +108,9 @@ async function reportRunning(recorded, o, dir) {
|
|
|
105
108
|
console.log(`PREVIEW ${st.url} (already running, pid ${st.pid})`);
|
|
106
109
|
return true;
|
|
107
110
|
}
|
|
108
|
-
|
|
111
|
+
// the folder in one quoted piece, and as "?" what a reader takes for a line end or what changes how text shows (a
|
|
112
|
+
// line or paragraph separator, NEL, a bidi override, a tag character): none of it adds a line to what an agent reads
|
|
113
|
+
console.error(`serve: the state file records a server running with different settings (folder: ${oneLine(st.dir)}, `
|
|
109
114
|
+ `csp: ${st.csp ? 'on' : 'off'}, host: ${st.host}, port: ${st.port}); stop it with --stop and start again`);
|
|
110
115
|
process.exit(1);
|
|
111
116
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* [--viewports 1280x800,390x844t] [--no-input] [--no-contrast] [--playwright-from-project]
|
|
19
19
|
* --viewports a trailing t marks a touch screen, r reduced motion (default: the six below)
|
|
20
20
|
* --header the site's fixed header (default: the engine's .sw-topbar)
|
|
21
|
-
* --playwright-from-project also look in the current folder's node_modules,
|
|
21
|
+
* --playwright-from-project also look in the current folder's node_modules, before the global npm root (your own project only)
|
|
22
22
|
* Exit 0 when nothing failed, 1 otherwise; the report (JSON) goes to --out or stdout.
|
|
23
23
|
*
|
|
24
24
|
* Needs Playwright with Chromium (setup-playwright skill: npx playwright install chromium), found
|
|
@@ -138,17 +138,24 @@ function globalRoot() {
|
|
|
138
138
|
return String(r.stdout || '').trim();
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// The playwright entry outside this script's folder: the project's when asked, else the global root's.
|
|
142
|
+
function outsideEntry(fromProject) {
|
|
143
|
+
const fromHere = fromProject ? resolveFrom(process.cwd()) : null;
|
|
144
|
+
if (fromHere) return fromHere;
|
|
145
|
+
const root = globalRoot();
|
|
146
|
+
return root ? resolveFrom(root) : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
141
149
|
/**
|
|
142
|
-
* The playwright package: beside this script, else in the global npm root.
|
|
143
|
-
* node_modules (the current folder) is read only with `fromProject`
|
|
144
|
-
* then
|
|
150
|
+
* The playwright package: beside this script (or in a folder above it), else in the global npm root.
|
|
151
|
+
* The project's own node_modules (the current folder) is read only with `fromProject`
|
|
152
|
+
* (--playwright-from-project), and then before the global root: a repository can ship a node_modules
|
|
153
|
+
* of its own, and loading it runs its code. `importBeside` replaces the first import (tests).
|
|
145
154
|
* `npx playwright install chromium` alone fetches the browser, not this package.
|
|
146
155
|
*/
|
|
147
|
-
export async function loadPlaywright({ fromProject = false } = {}) {
|
|
148
|
-
try { return await
|
|
149
|
-
const
|
|
150
|
-
const root = fromHere ? null : globalRoot();
|
|
151
|
-
const entry = fromHere || (root && resolveFrom(root));
|
|
156
|
+
export async function loadPlaywright({ fromProject = false, importBeside = () => import('playwright') } = {}) {
|
|
157
|
+
try { return await importBeside(); } catch { /* not beside this script */ }
|
|
158
|
+
const entry = outsideEntry(fromProject);
|
|
152
159
|
if (entry) {
|
|
153
160
|
const mod = await import(pathToFileURL(entry).href);
|
|
154
161
|
return mod.chromium ? mod : mod.default;
|