walover-line-harness-gui 0.1.0 → 0.2.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/package.json +1 -1
- package/src/server/guide.js +54 -10
- package/src/server/pty.js +8 -1
- package/src/server/state.js +8 -2
- package/src/web/index.html +105 -18
package/package.json
CHANGED
package/src/server/guide.js
CHANGED
|
@@ -33,6 +33,27 @@ const CF_R2 = "https://dash.cloudflare.com/?to=/:account/r2/overview";
|
|
|
33
33
|
* @property {{url: string, label: string}} [manual] 手順書の該当箇所
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* R2 の有効化。
|
|
38
|
+
*
|
|
39
|
+
* 実測で**最初のブロッカーであり、最大の脱落ポイント**だった箇所。
|
|
40
|
+
* 始める前の前提条件({@link PREFLIGHT})としても、本家が実行中に訊いてくる
|
|
41
|
+
* `r2Enabled` の場面でも、同じものを見せる。
|
|
42
|
+
*
|
|
43
|
+
* @type {Guide}
|
|
44
|
+
*/
|
|
45
|
+
const R2_GUIDE = {
|
|
46
|
+
steps: [
|
|
47
|
+
"Cloudflare にログインする",
|
|
48
|
+
"支払い情報(クレジットカード)を登録する",
|
|
49
|
+
"R2 の画面で「Purchase R2」などのボタンを押し、利用を開始する",
|
|
50
|
+
"もう一度 R2 の画面を開き、バケットの一覧(空でよい)が出ることを確かめる",
|
|
51
|
+
],
|
|
52
|
+
warn: "「カード登録済み」と「R2 有効化済み」は別です。カードを登録しただけで進むと、あとの手順で必ず失敗します(実測でも、カード登録済みのアカウントが code: 10042 で止まりました)。",
|
|
53
|
+
links: [{ label: "Cloudflare の R2 を開く", url: CF_R2 }],
|
|
54
|
+
manual: { url: `${KIT}/cloudflare.html#step3`, label: "1. Cloudflare の準備 — Step 3:R2 を有効化する" },
|
|
55
|
+
};
|
|
56
|
+
|
|
36
57
|
/**
|
|
37
58
|
* プロンプト id をキーにする。フォームの欄も、実行中に人が答える場面も同じ扱いにできる。
|
|
38
59
|
* @type {Record<string, Guide>}
|
|
@@ -154,16 +175,7 @@ export const GUIDE = {
|
|
|
154
175
|
},
|
|
155
176
|
|
|
156
177
|
// ── 実行中に人が答える場面 ───────────────────────────────────────
|
|
157
|
-
r2Enabled:
|
|
158
|
-
steps: [
|
|
159
|
-
"Cloudflare ダッシュボードの R2 を開く",
|
|
160
|
-
"支払い情報(クレジットカード)を登録する",
|
|
161
|
-
"R2 の利用を開始する",
|
|
162
|
-
],
|
|
163
|
-
warn: "「カード登録済み」と「R2 有効化済み」は別です。カードを登録しただけで進むと、あとの手順で必ず失敗します。",
|
|
164
|
-
links: [{ label: "Cloudflare の R2 を開く", url: CF_R2 }],
|
|
165
|
-
manual: { url: `${KIT}/cloudflare.html`, label: "1. Cloudflare の準備" },
|
|
166
|
-
},
|
|
178
|
+
r2Enabled: R2_GUIDE,
|
|
167
179
|
|
|
168
180
|
subdomainManualCheck: {
|
|
169
181
|
steps: [
|
|
@@ -175,6 +187,38 @@ export const GUIDE = {
|
|
|
175
187
|
},
|
|
176
188
|
};
|
|
177
189
|
|
|
190
|
+
/**
|
|
191
|
+
* 始める前に済ませておくこと。
|
|
192
|
+
*
|
|
193
|
+
* R2 の有効化は、本家が実行の途中で「R2 の有効化は終わりましたか?」と訊く。
|
|
194
|
+
* だが実機では、**そこで初めて気づいて手が止まる参加者が多かった**。
|
|
195
|
+
* 有効化には別タブでの作業とクレジットカードの登録が要るので、
|
|
196
|
+
* 入力を全部済ませたあとに知らされるのでは遅い。
|
|
197
|
+
*
|
|
198
|
+
* フォームより前に出し、**開始の前提条件**として扱う。
|
|
199
|
+
* 実行中の `r2Enabled` の場面は残す(そこで初めて出すのをやめるだけ)。
|
|
200
|
+
*
|
|
201
|
+
* @typedef {object} Prerequisite
|
|
202
|
+
* @property {string} id 対応するプロンプト id
|
|
203
|
+
* @property {string} title 画面の見出し
|
|
204
|
+
* @property {string} body なぜ先にやるのか
|
|
205
|
+
* @property {string} confirmLabel 参加者に確認させる文
|
|
206
|
+
* @property {Guide} guide 作業の手順
|
|
207
|
+
*
|
|
208
|
+
* @type {Prerequisite[]}
|
|
209
|
+
*/
|
|
210
|
+
export const PREFLIGHT = [
|
|
211
|
+
{
|
|
212
|
+
id: "r2Enabled",
|
|
213
|
+
title: "始める前に:Cloudflare の R2 を有効化してください",
|
|
214
|
+
body:
|
|
215
|
+
"R2 は画像などの保管場所です(10GB まで無料)。有効化しないまま始めると、" +
|
|
216
|
+
"LINE の値をすべて入力し終えたあと、終盤の保管場所づくりで必ず失敗します。",
|
|
217
|
+
confirmLabel: "R2 を有効化しました(R2 の画面にバケットの一覧が出ています)",
|
|
218
|
+
guide: R2_GUIDE,
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
|
|
178
222
|
/** @param {string} promptId */
|
|
179
223
|
export function guideFor(promptId) {
|
|
180
224
|
return GUIDE[promptId] ?? null;
|
package/src/server/pty.js
CHANGED
|
@@ -201,8 +201,15 @@ function resolveLaunch(command, args) {
|
|
|
201
201
|
if (command !== "npx" && command !== "npm") return { command, args };
|
|
202
202
|
|
|
203
203
|
// /d = AutoRun をスキップ、/s = 引用符の扱いを素直にする
|
|
204
|
+
//
|
|
205
|
+
// chcp 65001 はコンソールのコードページを UTF-8 にするため。
|
|
206
|
+
// 日本語環境の既定は CP932 で、本家が出す日本語がそのままだと文字化けする
|
|
207
|
+
// (勉強会の実機で確認)。
|
|
204
208
|
const shell = process.env.ComSpec || "cmd.exe";
|
|
205
|
-
return {
|
|
209
|
+
return {
|
|
210
|
+
command: shell,
|
|
211
|
+
args: ["/d", "/s", "/c", `chcp 65001 >nul && ${[command, ...args].join(" ")}`],
|
|
212
|
+
};
|
|
206
213
|
}
|
|
207
214
|
|
|
208
215
|
/**
|
package/src/server/state.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
validateAnswer,
|
|
20
20
|
crossCheckAnswers,
|
|
21
21
|
} from "./prompt-contract.js";
|
|
22
|
-
import { guideFor, GUIDE } from "./guide.js";
|
|
22
|
+
import { guideFor, GUIDE, PREFLIGHT } from "./guide.js";
|
|
23
23
|
|
|
24
24
|
/** @type {{ answers: Record<string, string|boolean>, run: SetupRun|null }} */
|
|
25
25
|
const state = {
|
|
@@ -75,10 +75,16 @@ export function filledFields() {
|
|
|
75
75
|
.map(([k]) => k);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* #2 のフォームが参照する、入力欄の定義と**始める前の前提条件**。値は一切含まない。
|
|
80
|
+
*
|
|
81
|
+
* 前提条件をここに載せるのは、画面がフォームを組み立てる前に読む唯一の定義だから。
|
|
82
|
+
* R2 の有効化は、入力を始める前に見えていないと意味がない。
|
|
83
|
+
*/
|
|
79
84
|
export function formSchema() {
|
|
80
85
|
return {
|
|
81
86
|
cliVersion: CONTRACT_CLI_VERSION,
|
|
87
|
+
prerequisites: PREFLIGHT,
|
|
82
88
|
groups: FORM_GROUPS,
|
|
83
89
|
fields: FORM_FIELDS.map((f) => {
|
|
84
90
|
const spec = PROMPTS.find((p) => p.id === f.promptId);
|
package/src/web/index.html
CHANGED
|
@@ -69,6 +69,25 @@
|
|
|
69
69
|
.dot.ok { background: var(--brand); }
|
|
70
70
|
.dot.bad { background: var(--err); }
|
|
71
71
|
|
|
72
|
+
/*
|
|
73
|
+
* 始める前の前提条件(R2 の有効化)。
|
|
74
|
+
*
|
|
75
|
+
* 実機では、有効化しないまま始めてしまう参加者が多かった。フォームより前に、
|
|
76
|
+
* 見落としようのない形で置く。囲みの上の警告(.banner)と同じ黄色の面だが、
|
|
77
|
+
* こちらは**開始の条件**なので、枠を太くして見出しを立てる。
|
|
78
|
+
*/
|
|
79
|
+
.preflight { border: 2px solid var(--warn-line); background: var(--warn-bg);
|
|
80
|
+
border-radius: .6rem; padding: 1.1rem 1.25rem; margin-bottom: 1.75rem; }
|
|
81
|
+
.preflight h2 { font-size: 1.05rem; margin: 0 0 .35rem; }
|
|
82
|
+
.preflight .why { margin: 0 0 .75rem; font-size: .9rem; }
|
|
83
|
+
.preflight .guide-body { padding: 0; font-size: .88rem; }
|
|
84
|
+
/* 黄色の面の上で、いちばん効かせたい一文が沈まないようにする */
|
|
85
|
+
.preflight .guide-warn { background: var(--bg); }
|
|
86
|
+
.gate { display: flex; align-items: flex-start; gap: .6rem; font-weight: 600; font-size: .92rem;
|
|
87
|
+
margin-top: .9rem; padding-top: .85rem; border-top: 1px solid var(--warn-line); }
|
|
88
|
+
.gate input { margin-top: .4rem; flex: none; }
|
|
89
|
+
.gate-note { font-size: .85rem; color: var(--muted); }
|
|
90
|
+
|
|
72
91
|
fieldset { border: 1px solid var(--line); border-radius: .6rem; padding: 1.25rem;
|
|
73
92
|
margin: 0 0 1.25rem; border-left-width: 4px; }
|
|
74
93
|
fieldset[data-group="project"] { border-left-color: var(--brand); }
|
|
@@ -205,6 +224,8 @@
|
|
|
205
224
|
<span class="dot" id="dot"></span><span id="status">確認中…</span>
|
|
206
225
|
</div>
|
|
207
226
|
|
|
227
|
+
<section id="preflight" hidden></section>
|
|
228
|
+
|
|
208
229
|
<form id="form" autocomplete="off" novalidate hidden></form>
|
|
209
230
|
|
|
210
231
|
<section id="run" hidden>
|
|
@@ -297,6 +318,37 @@
|
|
|
297
318
|
const form = $("form");
|
|
298
319
|
const inputs = new Map();
|
|
299
320
|
|
|
321
|
+
/**
|
|
322
|
+
* 始める前に済ませておくこと(R2 の有効化)。
|
|
323
|
+
*
|
|
324
|
+
* 実機では、有効化しないまま始めてしまう参加者が多かった。本家がそれを訊くのは
|
|
325
|
+
* 実行の途中で、そこまで来て初めて気づくと手が止まる。**フォームより前**に出し、
|
|
326
|
+
* 確認するまで「セットアップを開始する」を押せなくする。
|
|
327
|
+
*/
|
|
328
|
+
const gates = [];
|
|
329
|
+
for (const item of schema.prerequisites ?? []) {
|
|
330
|
+
const box = el("div", { className: "preflight" });
|
|
331
|
+
box.append(el("h2", { textContent: item.title }));
|
|
332
|
+
if (item.body) box.append(el("p", { className: "why", textContent: item.body }));
|
|
333
|
+
// ここは畳まない。開かないと読めない形では、素通りされる
|
|
334
|
+
if (item.guide) box.append(guideBody(item.guide));
|
|
335
|
+
|
|
336
|
+
const check = el("input", { type: "checkbox", id: `gate-${item.id}` });
|
|
337
|
+
check.addEventListener("change", syncGate);
|
|
338
|
+
gates.push(check);
|
|
339
|
+
box.append(
|
|
340
|
+
el("div", { className: "gate" }, [
|
|
341
|
+
check,
|
|
342
|
+
el("label", { htmlFor: `gate-${item.id}`, textContent: item.confirmLabel }),
|
|
343
|
+
]),
|
|
344
|
+
);
|
|
345
|
+
$("preflight").append(box);
|
|
346
|
+
}
|
|
347
|
+
$("preflight").hidden = gates.length === 0;
|
|
348
|
+
|
|
349
|
+
/** 前提がすべて確認済みか */
|
|
350
|
+
const gateReady = () => gates.every((g) => g.checked);
|
|
351
|
+
|
|
300
352
|
for (const group of schema.groups) {
|
|
301
353
|
const fields = schema.fields.filter((f) => f.group === group.id);
|
|
302
354
|
if (fields.length === 0) continue;
|
|
@@ -385,7 +437,17 @@
|
|
|
385
437
|
const box = el("details", { className: "guide" });
|
|
386
438
|
if (open) box.open = true;
|
|
387
439
|
box.append(el("summary", { textContent: "どこで取るか" }));
|
|
440
|
+
box.append(guideBody(guide, box));
|
|
441
|
+
return box;
|
|
442
|
+
}
|
|
388
443
|
|
|
444
|
+
/**
|
|
445
|
+
* 案内の中身。畳んだ中にも、前提条件のように畳まずに出す場所にも使う。
|
|
446
|
+
*
|
|
447
|
+
* @param guide 案内
|
|
448
|
+
* @param details 畳んで出す場合、その details。画像はそのとき初めて読む
|
|
449
|
+
*/
|
|
450
|
+
function guideBody(guide, details = null) {
|
|
389
451
|
const body = el("div", { className: "guide-body" });
|
|
390
452
|
|
|
391
453
|
if (guide.steps?.length) {
|
|
@@ -409,21 +471,21 @@
|
|
|
409
471
|
}
|
|
410
472
|
|
|
411
473
|
if (guide.images?.length) {
|
|
412
|
-
// 開いたときに初めて読みにいく。畳まれたままなら要求も出さない
|
|
413
474
|
const slot = el("div");
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
|
|
475
|
+
const load = () => {
|
|
476
|
+
for (const image of guide.images) {
|
|
477
|
+
const img = el("img", { src: image.url, alt: image.alt });
|
|
478
|
+
// ネットワークが無いと読めない。読めなくても手順は残す
|
|
479
|
+
img.addEventListener("error", () => img.remove());
|
|
480
|
+
slot.append(img);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
// 畳んで出すなら、開いたときに初めて読みにいく。畳まれたままなら要求も出さない
|
|
484
|
+
if (details && !details.open) {
|
|
485
|
+
details.addEventListener("toggle", () => details.open && load(), { once: true });
|
|
486
|
+
} else {
|
|
487
|
+
load();
|
|
488
|
+
}
|
|
427
489
|
body.append(slot);
|
|
428
490
|
}
|
|
429
491
|
|
|
@@ -436,8 +498,7 @@
|
|
|
436
498
|
);
|
|
437
499
|
}
|
|
438
500
|
|
|
439
|
-
|
|
440
|
-
return box;
|
|
501
|
+
return body;
|
|
441
502
|
}
|
|
442
503
|
|
|
443
504
|
/** サーバーから渡された規則で見る。画面側で独自の規則を作らない */
|
|
@@ -477,15 +538,39 @@
|
|
|
477
538
|
const askBox = $("run-ask");
|
|
478
539
|
const screen = $("run-screen");
|
|
479
540
|
let startButton = null;
|
|
541
|
+
let gateNote = null;
|
|
542
|
+
/** 開始を投げている最中。この間にチェックを外されても、押し直せるようにはしない */
|
|
543
|
+
let starting = false;
|
|
480
544
|
|
|
481
545
|
function showStart() {
|
|
482
546
|
if (startButton) return;
|
|
483
547
|
startButton = el("button", { className: "primary", type: "button", textContent: "セットアップを開始する" });
|
|
484
548
|
startButton.addEventListener("click", startRun);
|
|
485
|
-
|
|
549
|
+
gateNote = el("span", { className: "gate-note" });
|
|
550
|
+
form.querySelector(".actions").append(startButton, gateNote);
|
|
551
|
+
syncGate();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** 前提を確認するまで開始させない。押せない理由は、押せないボタンの隣に置く */
|
|
555
|
+
function syncGate() {
|
|
556
|
+
if (!startButton || starting) return;
|
|
557
|
+
const ready = gateReady();
|
|
558
|
+
startButton.disabled = !ready;
|
|
559
|
+
gateNote.replaceChildren();
|
|
560
|
+
if (!ready) {
|
|
561
|
+
gateNote.append(document.createTextNode("開始するには、"));
|
|
562
|
+
gateNote.append(el("a", { href: "#preflight", textContent: "ページ上部の前提条件" }));
|
|
563
|
+
gateNote.append(document.createTextNode("を確認してください。"));
|
|
564
|
+
}
|
|
486
565
|
}
|
|
487
566
|
|
|
488
567
|
async function startRun() {
|
|
568
|
+
// 画面の状態がずれていても、前提が未確認のまま走り出さない
|
|
569
|
+
if (!gateReady()) {
|
|
570
|
+
syncGate();
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
starting = true;
|
|
489
574
|
startButton.disabled = true;
|
|
490
575
|
const res = await fetch("/api/run/start", {
|
|
491
576
|
method: "POST",
|
|
@@ -495,11 +580,13 @@
|
|
|
495
580
|
if (!res.ok) {
|
|
496
581
|
result.textContent = (await res.json()).error ?? "開始できませんでした。";
|
|
497
582
|
result.className = "result bad";
|
|
498
|
-
|
|
583
|
+
starting = false;
|
|
584
|
+
syncGate();
|
|
499
585
|
return;
|
|
500
586
|
}
|
|
501
587
|
// 入力はサーバーに渡した。ここから先は実行の画面
|
|
502
588
|
for (const input of inputs.values()) input.disabled = true;
|
|
589
|
+
for (const gate of gates) gate.disabled = true;
|
|
503
590
|
submit.disabled = true;
|
|
504
591
|
runBox.hidden = false;
|
|
505
592
|
runBox.scrollIntoView({ behavior: "smooth", block: "start" });
|