@monster-orange/den 0.4.3 → 0.4.5
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/CHANGELOG.md +348 -0
- package/README.md +12 -11
- package/docs/distributions.md +438 -91
- package/docs/reference.md +8 -3
- package/package.json +1 -1
- package/src/shared/agents/catalog-runtime.ts +178 -0
- package/src/shared/agents/catalog.ts +343 -0
- package/src/shared/agents/den-claude.ts +14 -18
- package/src/shared/agents/runtime-search.ts +142 -15
- package/src/shared/agents/runtime.ts +18 -1
- package/src/shared/agents/runtimes.ts +23 -1
- package/src/shared/auth/sudo-vault.ts +131 -11
- package/src/shared/cli-command.ts +186 -0
- package/src/shared/confirm-code.ts +34 -0
- package/src/shared/den-config.ts +64 -3
- package/src/shared/dev-metrics/emit.ts +31 -1
- package/src/shared/edge/config.ts +39 -0
- package/src/shared/edge/render.ts +92 -18
- package/src/shared/edge/runtime.ts +57 -1
- package/src/shared/edge/status.ts +134 -0
- package/src/shared/edge/types.ts +11 -1
- package/src/shared/environment-manager/health.ts +82 -8
- package/src/shared/environment-manager/workspaces.ts +54 -0
- package/src/shared/github-app.ts +94 -0
- package/src/shared/github-device.ts +286 -0
- package/src/shared/health/not-checked.ts +42 -0
- package/src/shared/health/rollup.ts +4 -0
- package/src/shared/process/spawn-streaming.ts +8 -3
- package/src/shared/secrets/onepassword.ts +27 -4
- package/src/shared/sessions/client.ts +5 -1
- package/src/shared/sessions/host.ts +29 -1
- package/src/shared/sessions/protocol.ts +6 -1
- package/src/shared/sessions/types.ts +6 -0
- package/src/shared/stacks/status.ts +24 -0
- package/src/shared/strategies/registry.ts +5 -0
- package/src/shared/types/index.ts +6 -0
- package/src/shared/web-owner.ts +91 -0
- package/src/shared/web-session.ts +135 -1
- package/src/ui/cli/agent-ops.ts +210 -19
- package/src/ui/cli/agents-command.ts +50 -21
- package/src/ui/cli/edge-command.ts +16 -3
- package/src/ui/cli/flag-parser.ts +1 -1
- package/src/ui/cli/health-display.ts +4 -1
- package/src/ui/cli/web-command.ts +512 -33
- package/src/ui/mcp/scope.ts +23 -3
- package/src/ui/mcp/server.ts +31 -7
- package/src/ui/mcp/tools.ts +327 -20
- package/src/ui/mcp/views.ts +450 -0
- package/src/ui/server/agent-routes.ts +118 -28
- package/src/ui/server/device-flows.ts +274 -0
- package/src/ui/server/edge-exposure.ts +16 -4
- package/src/ui/server/github-routes.ts +321 -0
- package/src/ui/server/http-errors.ts +10 -0
- package/src/ui/server/index.ts +540 -33
- package/src/ui/server/login-waits.ts +203 -0
- package/src/ui/server/pair-requests.ts +237 -0
- package/src/ui/server/pair-routes.ts +196 -0
- package/src/ui/server/public/agent-start.js +46 -7
- package/src/ui/server/public/agents-page.js +232 -97
- package/src/ui/server/public/agents-section.js +3 -5
- package/src/ui/server/public/agents.html +12 -28
- package/src/ui/server/public/error-page.js +452 -40
- package/src/ui/server/public/error.html +59 -0
- package/src/ui/server/public/graph-model.js +2 -0
- package/src/ui/server/public/header.js +2 -3
- package/src/ui/server/public/health-rollup.js +20 -8
- package/src/ui/server/public/i18n/es.js +89 -21
- package/src/ui/server/public/i18n/ja.js +88 -21
- package/src/ui/server/public/index.html +17 -4
- package/src/ui/server/public/local-dev.html +44 -21
- package/src/ui/server/public/qr.js +317 -0
- package/src/ui/server/public/services.html +21 -5
- package/src/ui/server/public/settings-page.js +10 -2
- package/src/ui/server/public/settings-registry.js +617 -1
- package/src/ui/server/public/settings.html +90 -1
- package/src/ui/server/public/shell.js +151 -33
- package/src/ui/server/public/topbar.js +170 -206
- package/src/ui/server/public/workspace-picker.js +1 -1
- package/src/ui/server/readonly-routes.ts +29 -5
- package/src/ui/server/request-guard.ts +103 -36
- package/src/ui/server/route-classes.ts +116 -4
- package/src/ui/server/session-routes.ts +9 -1
- package/src/ui/server/sudo-store.ts +170 -6
- package/src/ui/server/ui-assets.ts +2 -0
- package/src/ui/server/workspace-routes.ts +10 -3
|
@@ -215,6 +215,13 @@
|
|
|
215
215
|
font-weight: 600;
|
|
216
216
|
background: color-mix(in srgb, var(--accent) 4%, var(--surface));
|
|
217
217
|
}
|
|
218
|
+
/* The CLI default (focus): a word, never a bare glyph. */
|
|
219
|
+
.ws-cli-default {
|
|
220
|
+
font-size: 10px;
|
|
221
|
+
color: var(--accent-text);
|
|
222
|
+
margin-left: 4px;
|
|
223
|
+
cursor: help;
|
|
224
|
+
}
|
|
218
225
|
.ws-item-right {
|
|
219
226
|
display: flex;
|
|
220
227
|
align-items: center;
|
|
@@ -1487,8 +1494,8 @@
|
|
|
1487
1494
|
initChangelog,
|
|
1488
1495
|
copyToClipboard,
|
|
1489
1496
|
computePrimaryByRepo,
|
|
1490
|
-
requireUnlock,
|
|
1491
1497
|
setServiceRunner,
|
|
1498
|
+
sudoRun,
|
|
1492
1499
|
takePendingAction,
|
|
1493
1500
|
problemCards,
|
|
1494
1501
|
problemFix,
|
|
@@ -1544,15 +1551,18 @@
|
|
|
1544
1551
|
.replace(/</g, "<")
|
|
1545
1552
|
.replace(/>/g, ">")
|
|
1546
1553
|
.replace(/"/g, """);
|
|
1554
|
+
/** A shell string, with the English when the shell isn't up yet. */
|
|
1555
|
+
const wsT = (key, fallback) => {
|
|
1556
|
+
const v = window.denShell?.t?.(key);
|
|
1557
|
+
return v && v !== key ? v : fallback;
|
|
1558
|
+
};
|
|
1547
1559
|
|
|
1548
|
-
// ── The
|
|
1549
|
-
//
|
|
1550
|
-
//
|
|
1551
|
-
//
|
|
1552
|
-
//
|
|
1553
|
-
|
|
1554
|
-
return requireUnlock();
|
|
1555
|
-
}
|
|
1560
|
+
// ── The admin password (R8, LK-0) ─────────────────────────────────────────
|
|
1561
|
+
// It is for sudo, and nothing else: no action here asks for it up
|
|
1562
|
+
// front. A run whose command is about to run sudo without it gets a
|
|
1563
|
+
// needs-sudo event from the server (sudoRun().see); the shell's
|
|
1564
|
+
// prompt then says what needs it, and after the password the action
|
|
1565
|
+
// runs again (sudoRun().done).
|
|
1556
1566
|
|
|
1557
1567
|
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
|
1558
1568
|
// Systems with auto-start toggled OFF in the viewed workspace (`autostart:
|
|
@@ -1572,7 +1582,7 @@
|
|
|
1572
1582
|
]);
|
|
1573
1583
|
initLogBadge();
|
|
1574
1584
|
// Start and Restart from the status drawer and the ⌘K palette run
|
|
1575
|
-
// here, with this page's
|
|
1585
|
+
// here, with this page's output (R15, R17).
|
|
1576
1586
|
setServiceRunner((cmd, s) =>
|
|
1577
1587
|
window.runCmd(s.address, s.repo, s.service, cmd),
|
|
1578
1588
|
);
|
|
@@ -1603,7 +1613,7 @@
|
|
|
1603
1613
|
? `<button class="danger" data-action="delete">Delete</button>`
|
|
1604
1614
|
: "";
|
|
1605
1615
|
return `<div class="ws-item ${n === activeWs ? "active" : ""}" data-name="${escHtml(n)}" data-action="select">
|
|
1606
|
-
<span>${escHtml(n)}${isWsActive(n) ? ' <span style="font-size:10px;color:var(--muted)">(active)</span>' : ""}${n === focusName() ?
|
|
1616
|
+
<span>${escHtml(n)}${isWsActive(n) ? ' <span style="font-size:10px;color:var(--muted)">(active)</span>' : ""}${n === focusName() ? ` <span class="ws-cli-default" title="${escHtml(wsT("ws.focusTip", "CLI commands run here when you don't name a workspace. Picking a workspace in the top bar only changes what pages show."))}">${escHtml(wsT("ws.focus", "CLI default"))}</span>` : ""}</span>
|
|
1607
1617
|
<span class="ws-item-right">
|
|
1608
1618
|
<button class="ws-term-btn" title="Open in terminal" data-open-term="${escHtml(dirPath)}">>_</button>
|
|
1609
1619
|
<button class="ws-open-btn" title="Open in editor" data-open-path="${escHtml(wsData.codeWorkspacePaths?.[n] || dirPath)}">↗</button>
|
|
@@ -2023,11 +2033,11 @@
|
|
|
2023
2033
|
<h2>${escHtml(wsName)}</h2>
|
|
2024
2034
|
${
|
|
2025
2035
|
isActive
|
|
2026
|
-
? `<span class="badge active-ws">active${isFocus ?
|
|
2027
|
-
${!isFocus ? `<button class="switch-btn" data-action="focus-ws">
|
|
2036
|
+
? `<span class="badge active-ws"${isFocus ? ` title="${escHtml(wsT("ws.focusTip", ""))}"` : ""}>active${isFocus ? ` · ${escHtml(wsT("ws.focus", "CLI default"))}` : ""}</span>
|
|
2037
|
+
${!isFocus ? `<button class="switch-btn" data-action="focus-ws" title="${escHtml(wsT("ws.setFocusTip", ""))}">Make CLI default</button>` : ""}
|
|
2028
2038
|
<button class="switch-btn" data-action="deactivate-ws">deactivate</button>`
|
|
2029
2039
|
: `<button class="switch-btn" data-action="activate-ws">activate</button>
|
|
2030
|
-
<button class="switch-btn" data-action="switch-ws">activate +
|
|
2040
|
+
<button class="switch-btn" data-action="switch-ws">activate + make CLI default</button>`
|
|
2031
2041
|
}
|
|
2032
2042
|
${stackNsChip}
|
|
2033
2043
|
<span class="badge ws-agents" id="ws-agents-count" hidden></span>
|
|
@@ -3051,9 +3061,9 @@
|
|
|
3051
3061
|
// ── Workspace-level command (SSE) ─────────────────────────────────────────
|
|
3052
3062
|
// Routes output to each service's own tab as `label` events arrive.
|
|
3053
3063
|
window.runWorkspaceSync = async function () {
|
|
3054
|
-
if (!(await requireSudo())) return;
|
|
3055
3064
|
setAllButtons(true);
|
|
3056
3065
|
setWsStatus("syncing…");
|
|
3066
|
+
const sudo = sudoRun();
|
|
3057
3067
|
const es = new EventSource("/api/workspace/sync");
|
|
3058
3068
|
// Clear previous sync output then open/activate the tab (explicit action).
|
|
3059
3069
|
clearDismissed("⟳ sync");
|
|
@@ -3062,6 +3072,7 @@
|
|
|
3062
3072
|
setBusy("⟳ sync", true);
|
|
3063
3073
|
es.onmessage = (e) => {
|
|
3064
3074
|
const msg = JSON.parse(e.data);
|
|
3075
|
+
sudo.see(msg);
|
|
3065
3076
|
if (msg.type === "line") appendToTab("⟳ sync", msg.text);
|
|
3066
3077
|
if (msg.type === "error")
|
|
3067
3078
|
appendToTab("⟳ sync", "Error: " + msg.text, "err");
|
|
@@ -3073,6 +3084,7 @@
|
|
|
3073
3084
|
setTimeout(() => setWsStatus(""), 3000);
|
|
3074
3085
|
// See refreshAfterSync — a sync that clones brings new commands with it.
|
|
3075
3086
|
refreshAfterSync();
|
|
3087
|
+
void sudo.done(() => window.runWorkspaceSync());
|
|
3076
3088
|
}
|
|
3077
3089
|
};
|
|
3078
3090
|
es.onerror = () => {
|
|
@@ -3103,9 +3115,9 @@
|
|
|
3103
3115
|
// system. Buttons are not globally disabled: it is one repo.
|
|
3104
3116
|
const _syncing = new Set();
|
|
3105
3117
|
window.runRepoSync = async function (repo) {
|
|
3106
|
-
if (!(await requireSudo())) return;
|
|
3107
3118
|
if (_syncing.has(repo)) return; // already in flight — ignore re-entry
|
|
3108
3119
|
_syncing.add(repo);
|
|
3120
|
+
const sudo = sudoRun();
|
|
3109
3121
|
|
|
3110
3122
|
const route = tabRouteFor(repo);
|
|
3111
3123
|
clearDismissed(route); // explicit action — undo any prior dismissal
|
|
@@ -3130,12 +3142,14 @@
|
|
|
3130
3142
|
);
|
|
3131
3143
|
es.onmessage = (e) => {
|
|
3132
3144
|
const msg = JSON.parse(e.data);
|
|
3145
|
+
sudo.see(msg);
|
|
3133
3146
|
if (msg.type === "line") appendToTab(route, msg.text);
|
|
3134
3147
|
if (msg.type === "error")
|
|
3135
3148
|
appendToTab(route, "Error: " + msg.text, "err");
|
|
3136
3149
|
if (msg.type === "done") {
|
|
3137
3150
|
es.close();
|
|
3138
3151
|
finish("sync complete");
|
|
3152
|
+
void sudo.done(() => window.runRepoSync(repo));
|
|
3139
3153
|
// A pull can replace this repo's health probes on disk. The health
|
|
3140
3154
|
// response is cached ~8s, so force a fresh poll rather than leaving
|
|
3141
3155
|
// dots that reflect the code we just replaced.
|
|
@@ -3156,7 +3170,7 @@
|
|
|
3156
3170
|
};
|
|
3157
3171
|
|
|
3158
3172
|
window.runWorkspaceCmd = async function (cmd, addresses) {
|
|
3159
|
-
|
|
3173
|
+
const sudo = sudoRun();
|
|
3160
3174
|
const only =
|
|
3161
3175
|
Array.isArray(addresses) && addresses.length
|
|
3162
3176
|
? `&addresses=${encodeURIComponent(addresses.join(","))}`
|
|
@@ -3174,6 +3188,7 @@
|
|
|
3174
3188
|
|
|
3175
3189
|
esWs.onmessage = (e) => {
|
|
3176
3190
|
const msg = JSON.parse(e.data);
|
|
3191
|
+
sudo.see(msg);
|
|
3177
3192
|
if (msg.type === "label") {
|
|
3178
3193
|
// New service section — route to the SYSTEM's primary tab (one tab
|
|
3179
3194
|
// per system; sidecar output lands in its repo's tab with a header).
|
|
@@ -3239,6 +3254,7 @@
|
|
|
3239
3254
|
idle();
|
|
3240
3255
|
// Lifecycle changed run-state — sync dots now, don't wait for the poll.
|
|
3241
3256
|
syncHealthNow();
|
|
3257
|
+
void sudo.done(() => window.runWorkspaceCmd(cmd, addresses));
|
|
3242
3258
|
}
|
|
3243
3259
|
};
|
|
3244
3260
|
esWs.onerror = () => {
|
|
@@ -3270,7 +3286,6 @@
|
|
|
3270
3286
|
// its rules are load-bearing, and the already-healthy skip it relies on is
|
|
3271
3287
|
// suppressed whenever an explicit address list is sent.
|
|
3272
3288
|
window.runUpWithDeps = async function (repo) {
|
|
3273
|
-
if (!(await requireSudo())) return;
|
|
3274
3289
|
void runWorkspaceCmdAsync("up", null, {
|
|
3275
3290
|
seed: repo,
|
|
3276
3291
|
undismiss: true,
|
|
@@ -3282,7 +3297,6 @@
|
|
|
3282
3297
|
// same reason as its counterpart: the already-stopped skip is suppressed
|
|
3283
3298
|
// when an explicit address list is sent.
|
|
3284
3299
|
window.runDownWithDeps = async function (repo) {
|
|
3285
|
-
if (!(await requireSudo())) return;
|
|
3286
3300
|
void runWorkspaceCmdAsync("down", null, {
|
|
3287
3301
|
seed: repo,
|
|
3288
3302
|
undismiss: true,
|
|
@@ -3290,7 +3304,6 @@
|
|
|
3290
3304
|
};
|
|
3291
3305
|
|
|
3292
3306
|
window.runCmd = async function (address, repo, service, cmd) {
|
|
3293
|
-
if (!(await requireSudo())) return;
|
|
3294
3307
|
// Explicit single-service action — undo any prior dismissal so its pane returns.
|
|
3295
3308
|
clearDismissed(address);
|
|
3296
3309
|
// up/restart/down go through the workspace-exec runner: it uses the full
|
|
@@ -3319,9 +3332,12 @@
|
|
|
3319
3332
|
setTabStatus(address, { es: esCmd });
|
|
3320
3333
|
const _cmdTab = _tabs.get(address);
|
|
3321
3334
|
if (_cmdTab) _cmdTab.esKind = "exec";
|
|
3335
|
+
const sudo = sudoRun();
|
|
3336
|
+
const again = () => window.runCmd(address, repo, service, cmd);
|
|
3322
3337
|
|
|
3323
3338
|
esCmd.onmessage = (e) => {
|
|
3324
3339
|
const msg = JSON.parse(e.data);
|
|
3340
|
+
sudo.see(msg);
|
|
3325
3341
|
if (msg.type === "cmd") {
|
|
3326
3342
|
appendToTab(address, msg.text, "cmd");
|
|
3327
3343
|
} else if (msg.type === "started") {
|
|
@@ -3349,6 +3365,7 @@
|
|
|
3349
3365
|
"done-fail",
|
|
3350
3366
|
);
|
|
3351
3367
|
setTabStatus(address, { live: false, errored: true, es: null });
|
|
3368
|
+
void sudo.done(again);
|
|
3352
3369
|
}
|
|
3353
3370
|
} else if (msg.type === "line") {
|
|
3354
3371
|
appendToTab(address, msg.text, "");
|
|
@@ -3369,6 +3386,7 @@
|
|
|
3369
3386
|
);
|
|
3370
3387
|
_tabs.get(address) && (_tabs.get(address).busy = false);
|
|
3371
3388
|
setTabStatus(address, { live: false, errored: !ok, es: null });
|
|
3389
|
+
void sudo.done(again);
|
|
3372
3390
|
}
|
|
3373
3391
|
};
|
|
3374
3392
|
esCmd.onerror = () => {
|
|
@@ -3853,9 +3871,11 @@
|
|
|
3853
3871
|
// address filter — the filter suppresses the already-healthy skip and
|
|
3854
3872
|
// the auto-start-off handling that make "with deps" behave.
|
|
3855
3873
|
if (opts.seed) params.set("seed", opts.seed);
|
|
3874
|
+
const sudo = sudoRun();
|
|
3856
3875
|
const es = new EventSource(`/api/workspace/exec?${params}`);
|
|
3857
3876
|
es.onmessage = (e) => {
|
|
3858
3877
|
const msg = JSON.parse(e.data);
|
|
3878
|
+
sudo.see(msg);
|
|
3859
3879
|
if (msg.type === "label") {
|
|
3860
3880
|
currentAddress = msg.text;
|
|
3861
3881
|
// Route to the system's primary tab; skip dismissed panes on
|
|
@@ -3924,7 +3944,10 @@
|
|
|
3924
3944
|
idle();
|
|
3925
3945
|
// Lifecycle changed run-state — sync dots now, don't wait for the poll.
|
|
3926
3946
|
syncHealthNow();
|
|
3927
|
-
|
|
3947
|
+
// LK-0: after the admin password, the same run again.
|
|
3948
|
+
void sudo
|
|
3949
|
+
.done(() => runWorkspaceCmdAsync(cmd, addresses, opts))
|
|
3950
|
+
.then((again) => resolve(again ?? ok));
|
|
3928
3951
|
}
|
|
3929
3952
|
};
|
|
3930
3953
|
es.onerror = () => {
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny QR code encoder (SEC-2a, "Pair a device"): byte mode, error
|
|
3
|
+
* correction level M, versions 1 to 10 (up to 213 bytes, enough for a
|
|
4
|
+
* sign-in link). Written for Den from ISO/IEC 18004; no dependency, no CDN.
|
|
5
|
+
* Its matrices are checked against an independent encoder's in
|
|
6
|
+
* tests/unit/server/qr.test.ts.
|
|
7
|
+
*
|
|
8
|
+
* qrMatrix(text, { mask }?) rows of booleans (true: dark), no quiet zone
|
|
9
|
+
* qrSvg(text, { scale, margin }?) an SVG string, dark on light
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Level M, versions 1..10: ECC codewords per block, and blocks.
|
|
13
|
+
const ECC_PER_BLOCK = [0, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26];
|
|
14
|
+
const ECC_BLOCKS = [0, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5];
|
|
15
|
+
const MAX_VERSION = 10;
|
|
16
|
+
const FORMAT_BITS_M = 0;
|
|
17
|
+
|
|
18
|
+
function gfMul(x, y) {
|
|
19
|
+
let z = 0;
|
|
20
|
+
for (let i = 7; i >= 0; i--) {
|
|
21
|
+
z = (z << 1) ^ ((z >>> 7) * 0x11d);
|
|
22
|
+
z ^= ((y >>> i) & 1) * x;
|
|
23
|
+
}
|
|
24
|
+
return z & 0xff;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function rsDivisor(degree) {
|
|
28
|
+
const out = new Array(degree).fill(0);
|
|
29
|
+
out[degree - 1] = 1;
|
|
30
|
+
let root = 1;
|
|
31
|
+
for (let i = 0; i < degree; i++) {
|
|
32
|
+
for (let j = 0; j < out.length; j++) {
|
|
33
|
+
out[j] = gfMul(out[j], root);
|
|
34
|
+
if (j + 1 < out.length) out[j] ^= out[j + 1];
|
|
35
|
+
}
|
|
36
|
+
root = gfMul(root, 0x02);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rsRemainder(data, divisor) {
|
|
42
|
+
const out = divisor.map(() => 0);
|
|
43
|
+
for (const b of data) {
|
|
44
|
+
const factor = b ^ out.shift();
|
|
45
|
+
out.push(0);
|
|
46
|
+
divisor.forEach((c, i) => (out[i] ^= gfMul(c, factor)));
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function alignPositions(ver) {
|
|
52
|
+
if (ver === 1) return [];
|
|
53
|
+
const size = ver * 4 + 17;
|
|
54
|
+
const n = Math.floor(ver / 7) + 2;
|
|
55
|
+
const step = Math.floor((ver * 8 + n * 3 + 5) / (n * 4 - 4)) * 2;
|
|
56
|
+
const out = [6];
|
|
57
|
+
for (let pos = size - 7; out.length < n; pos -= step) out.splice(1, 0, pos);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rawModules(ver) {
|
|
62
|
+
let r = (16 * ver + 128) * ver + 64;
|
|
63
|
+
if (ver >= 2) {
|
|
64
|
+
const n = Math.floor(ver / 7) + 2;
|
|
65
|
+
r -= (25 * n - 10) * n - 55;
|
|
66
|
+
if (ver >= 7) r -= 36;
|
|
67
|
+
}
|
|
68
|
+
return r;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function dataCodewords(ver) {
|
|
72
|
+
return Math.floor(rawModules(ver) / 8) - ECC_PER_BLOCK[ver] * ECC_BLOCKS[ver];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function utf8(text) {
|
|
76
|
+
return Array.from(new TextEncoder().encode(String(text)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The data codewords for `bytes` at version `ver` (mode, count, pad). */
|
|
80
|
+
function encodeData(bytes, ver) {
|
|
81
|
+
const bits = [];
|
|
82
|
+
const put = (v, n) => {
|
|
83
|
+
for (let i = n - 1; i >= 0; i--) bits.push((v >>> i) & 1);
|
|
84
|
+
};
|
|
85
|
+
put(0b0100, 4);
|
|
86
|
+
put(bytes.length, ver <= 9 ? 8 : 16);
|
|
87
|
+
for (const b of bytes) put(b, 8);
|
|
88
|
+
const cap = dataCodewords(ver) * 8;
|
|
89
|
+
put(0, Math.min(4, cap - bits.length));
|
|
90
|
+
put(0, (8 - (bits.length % 8)) % 8);
|
|
91
|
+
for (let pad = 0xec; bits.length < cap; pad ^= 0xec ^ 0x11) put(pad, 8);
|
|
92
|
+
const out = [];
|
|
93
|
+
for (let i = 0; i < bits.length; i += 8)
|
|
94
|
+
out.push(bits.slice(i, i + 8).reduce((a, b) => (a << 1) | b, 0));
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function withEcc(data, ver) {
|
|
99
|
+
const nBlocks = ECC_BLOCKS[ver];
|
|
100
|
+
const eccLen = ECC_PER_BLOCK[ver];
|
|
101
|
+
const raw = Math.floor(rawModules(ver) / 8);
|
|
102
|
+
const nShort = nBlocks - (raw % nBlocks);
|
|
103
|
+
const shortLen = Math.floor(raw / nBlocks);
|
|
104
|
+
const div = rsDivisor(eccLen);
|
|
105
|
+
const blocks = [];
|
|
106
|
+
for (let i = 0, k = 0; i < nBlocks; i++) {
|
|
107
|
+
const dat = data.slice(k, k + shortLen - eccLen + (i < nShort ? 0 : 1));
|
|
108
|
+
k += dat.length;
|
|
109
|
+
const ecc = rsRemainder(dat, div);
|
|
110
|
+
if (i < nShort) dat.push(0);
|
|
111
|
+
blocks.push(dat.concat(ecc));
|
|
112
|
+
}
|
|
113
|
+
const out = [];
|
|
114
|
+
for (let i = 0; i < blocks[0].length; i++)
|
|
115
|
+
blocks.forEach((b, j) => {
|
|
116
|
+
if (i !== shortLen - eccLen || j >= nShort) out.push(b[i]);
|
|
117
|
+
});
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function makeGrid(ver) {
|
|
122
|
+
const size = ver * 4 + 17;
|
|
123
|
+
const mods = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
124
|
+
const fn = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
125
|
+
const set = (x, y, dark) => {
|
|
126
|
+
mods[y][x] = dark;
|
|
127
|
+
fn[y][x] = true;
|
|
128
|
+
};
|
|
129
|
+
for (let i = 0; i < size; i++) {
|
|
130
|
+
set(6, i, i % 2 === 0);
|
|
131
|
+
set(i, 6, i % 2 === 0);
|
|
132
|
+
}
|
|
133
|
+
for (const [cx, cy] of [
|
|
134
|
+
[3, 3],
|
|
135
|
+
[size - 4, 3],
|
|
136
|
+
[3, size - 4],
|
|
137
|
+
])
|
|
138
|
+
for (let dy = -4; dy <= 4; dy++)
|
|
139
|
+
for (let dx = -4; dx <= 4; dx++) {
|
|
140
|
+
const d = Math.max(Math.abs(dx), Math.abs(dy));
|
|
141
|
+
const x = cx + dx;
|
|
142
|
+
const y = cy + dy;
|
|
143
|
+
if (x >= 0 && x < size && y >= 0 && y < size)
|
|
144
|
+
set(x, y, d !== 2 && d !== 4);
|
|
145
|
+
}
|
|
146
|
+
const al = alignPositions(ver);
|
|
147
|
+
const last = al.length - 1;
|
|
148
|
+
al.forEach((ax, i) =>
|
|
149
|
+
al.forEach((ay, j) => {
|
|
150
|
+
if ((i === 0 && j === 0) || (i === 0 && j === last)) return;
|
|
151
|
+
if (i === last && j === 0) return;
|
|
152
|
+
for (let dy = -2; dy <= 2; dy++)
|
|
153
|
+
for (let dx = -2; dx <= 2; dx++)
|
|
154
|
+
set(ax + dx, ay + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
drawFormat(set, size, 0);
|
|
158
|
+
if (ver >= 7) {
|
|
159
|
+
let rem = ver;
|
|
160
|
+
for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
|
|
161
|
+
const bits = (ver << 12) | rem;
|
|
162
|
+
for (let i = 0; i < 18; i++) {
|
|
163
|
+
const dark = ((bits >>> i) & 1) === 1;
|
|
164
|
+
const a = size - 11 + (i % 3);
|
|
165
|
+
const b = Math.floor(i / 3);
|
|
166
|
+
set(a, b, dark);
|
|
167
|
+
set(b, a, dark);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { size, mods, fn, set };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function drawFormat(set, size, mask) {
|
|
174
|
+
const data = (FORMAT_BITS_M << 3) | mask;
|
|
175
|
+
let rem = data;
|
|
176
|
+
for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
|
|
177
|
+
const bits = ((data << 10) | rem) ^ 0x5412;
|
|
178
|
+
const bit = (i) => ((bits >>> i) & 1) === 1;
|
|
179
|
+
for (let i = 0; i <= 5; i++) set(8, i, bit(i));
|
|
180
|
+
set(8, 7, bit(6));
|
|
181
|
+
set(8, 8, bit(7));
|
|
182
|
+
set(7, 8, bit(8));
|
|
183
|
+
for (let i = 9; i < 15; i++) set(14 - i, 8, bit(i));
|
|
184
|
+
for (let i = 0; i < 8; i++) set(size - 1 - i, 8, bit(i));
|
|
185
|
+
for (let i = 8; i < 15; i++) set(8, size - 15 + i, bit(i));
|
|
186
|
+
set(8, size - 8, true);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const MASKS = [
|
|
190
|
+
(x, y) => (x + y) % 2 === 0,
|
|
191
|
+
(x, y) => y % 2 === 0,
|
|
192
|
+
(x) => x % 3 === 0,
|
|
193
|
+
(x, y) => (x + y) % 3 === 0,
|
|
194
|
+
(x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0,
|
|
195
|
+
(x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
|
|
196
|
+
(x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
197
|
+
(x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
function penalty(m) {
|
|
201
|
+
const n = m.length;
|
|
202
|
+
let score = 0;
|
|
203
|
+
const lines = [];
|
|
204
|
+
for (let i = 0; i < n; i++) {
|
|
205
|
+
lines.push(m[i]);
|
|
206
|
+
lines.push(m.map((row) => row[i]));
|
|
207
|
+
}
|
|
208
|
+
const finder = [true, false, true, true, true, false, true];
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
for (let i = 0; i < n; ) {
|
|
211
|
+
let j = i;
|
|
212
|
+
while (j < n && line[j] === line[i]) j++;
|
|
213
|
+
if (j - i >= 5) score += 3 + (j - i - 5);
|
|
214
|
+
i = j;
|
|
215
|
+
}
|
|
216
|
+
for (let i = 0; i + 7 <= n; i++) {
|
|
217
|
+
if (!finder.every((v, k) => line[i + k] === v)) continue;
|
|
218
|
+
const light = (a, b) => {
|
|
219
|
+
for (let k = a; k < b; k++)
|
|
220
|
+
if (k >= 0 && k < n && line[k]) return false;
|
|
221
|
+
return true;
|
|
222
|
+
};
|
|
223
|
+
if (light(i - 4, i) || light(i + 7, i + 11)) score += 40;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
let dark = 0;
|
|
227
|
+
for (let y = 0; y < n; y++)
|
|
228
|
+
for (let x = 0; x < n; x++) {
|
|
229
|
+
if (m[y][x]) dark++;
|
|
230
|
+
if (
|
|
231
|
+
x + 1 < n &&
|
|
232
|
+
y + 1 < n &&
|
|
233
|
+
m[y][x] === m[y][x + 1] &&
|
|
234
|
+
m[y][x] === m[y + 1][x] &&
|
|
235
|
+
m[y][x] === m[y + 1][x + 1]
|
|
236
|
+
)
|
|
237
|
+
score += 3;
|
|
238
|
+
}
|
|
239
|
+
const total = n * n;
|
|
240
|
+
score += 10 * Math.floor(Math.abs(dark * 20 - total * 10) / total);
|
|
241
|
+
return score;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The QR code for `text` (UTF-8, byte mode, level M), as rows of booleans
|
|
246
|
+
* (true: dark), no quiet zone. `mask` 0..7 fixes the mask; by default the
|
|
247
|
+
* one with the lowest penalty. Throws when `text` is longer than version
|
|
248
|
+
* 10 holds (213 bytes).
|
|
249
|
+
*/
|
|
250
|
+
export function qrMatrix(text, opts = {}) {
|
|
251
|
+
const bytes = utf8(text);
|
|
252
|
+
let ver = 1;
|
|
253
|
+
const need = (v) => 4 + (v <= 9 ? 8 : 16) + bytes.length * 8;
|
|
254
|
+
while (ver <= MAX_VERSION && need(ver) > dataCodewords(ver) * 8) ver++;
|
|
255
|
+
if (ver > MAX_VERSION)
|
|
256
|
+
throw new Error(`QR: ${bytes.length} bytes is too long (213 at most)`);
|
|
257
|
+
const code = withEcc(encodeData(bytes, ver), ver);
|
|
258
|
+
const { size, mods, fn } = makeGrid(ver);
|
|
259
|
+
let i = 0;
|
|
260
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
261
|
+
if (right === 6) right = 5;
|
|
262
|
+
for (let vert = 0; vert < size; vert++)
|
|
263
|
+
for (let j = 0; j < 2; j++) {
|
|
264
|
+
const x = right - j;
|
|
265
|
+
const up = ((right + 1) & 2) === 0;
|
|
266
|
+
const y = up ? size - 1 - vert : vert;
|
|
267
|
+
if (!fn[y][x] && i < code.length * 8) {
|
|
268
|
+
mods[y][x] = ((code[i >>> 3] >>> (7 - (i & 7))) & 1) === 1;
|
|
269
|
+
i++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const masked = (mask) => {
|
|
274
|
+
const m = mods.map((row) => row.slice());
|
|
275
|
+
const put = (x, y, dark) => (m[y][x] = dark);
|
|
276
|
+
for (let y = 0; y < size; y++)
|
|
277
|
+
for (let x = 0; x < size; x++)
|
|
278
|
+
if (!fn[y][x] && MASKS[mask](x, y)) m[y][x] = !m[y][x];
|
|
279
|
+
drawFormat(put, size, mask);
|
|
280
|
+
return m;
|
|
281
|
+
};
|
|
282
|
+
if (Number.isInteger(opts.mask) && opts.mask >= 0 && opts.mask <= 7)
|
|
283
|
+
return masked(opts.mask);
|
|
284
|
+
let best = null;
|
|
285
|
+
let bestScore = Infinity;
|
|
286
|
+
for (let k = 0; k < 8; k++) {
|
|
287
|
+
const m = masked(k);
|
|
288
|
+
const s = penalty(m);
|
|
289
|
+
if (s < bestScore) {
|
|
290
|
+
best = m;
|
|
291
|
+
bestScore = s;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return best;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** `text`'s QR code as an SVG (one path), with a quiet zone of `margin` modules. */
|
|
298
|
+
export function qrSvg(text, opts = {}) {
|
|
299
|
+
const m = qrMatrix(text);
|
|
300
|
+
const scale = opts.scale ?? 4;
|
|
301
|
+
const margin = opts.margin ?? 4;
|
|
302
|
+
const n = m.length + margin * 2;
|
|
303
|
+
let d = "";
|
|
304
|
+
m.forEach((row, y) =>
|
|
305
|
+
row.forEach((dark, x) => {
|
|
306
|
+
if (dark) d += `M${x + margin} ${y + margin}h1v1h-1z`;
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
return (
|
|
310
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n} ${n}" ` +
|
|
311
|
+
`width="${n * scale}" height="${n * scale}" shape-rendering="crispEdges" role="img">` +
|
|
312
|
+
// Black on white whatever the theme: a scanner needs that contrast
|
|
313
|
+
// (an inverted code fails on many phones), so no theme token here.
|
|
314
|
+
`<rect width="${n}" height="${n}" fill="white"/>` +
|
|
315
|
+
`<path d="${d}" fill="black"/></svg>`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
@@ -333,6 +333,7 @@
|
|
|
333
333
|
initChangelog,
|
|
334
334
|
computePrimaryByRepo,
|
|
335
335
|
envReady,
|
|
336
|
+
sudoRun,
|
|
336
337
|
} from "/header.js";
|
|
337
338
|
import { ownersLabel } from "/workspace-picker.js";
|
|
338
339
|
|
|
@@ -613,10 +614,16 @@
|
|
|
613
614
|
const usableCmds = isSub
|
|
614
615
|
? s.commands.filter((cmd) => !LIFECYCLE_CMDS.has(cmd))
|
|
615
616
|
: s.commands;
|
|
617
|
+
// The workspace the row was built from (the first in view that
|
|
618
|
+
// defines it, focus first): its command runs there, not in
|
|
619
|
+
// whichever workspace is focused.
|
|
620
|
+
const wsAttr = s.inWorkspaces?.[0]
|
|
621
|
+
? ` data-workspace="${esc(s.inWorkspaces[0])}"`
|
|
622
|
+
: "";
|
|
616
623
|
const cmdButtons = usableCmds
|
|
617
624
|
.map(
|
|
618
625
|
(cmd) =>
|
|
619
|
-
`<button class="cmd-btn" data-repo="${s.repo}" data-service="${s.service}" data-cmd="${cmd}" data-type="exec">${cmd}</button>`,
|
|
626
|
+
`<button class="cmd-btn" data-repo="${s.repo}" data-service="${s.service}" data-cmd="${cmd}" data-type="exec"${wsAttr}>${cmd}</button>`,
|
|
620
627
|
)
|
|
621
628
|
.join("");
|
|
622
629
|
const logsButton = s.logs
|
|
@@ -743,8 +750,9 @@
|
|
|
743
750
|
// Attach click handlers
|
|
744
751
|
pane.querySelectorAll(".cmd-btn").forEach((btn) => {
|
|
745
752
|
btn.addEventListener("click", () => {
|
|
746
|
-
const { repo, service, cmd, type } = btn.dataset;
|
|
747
|
-
if (type === "exec")
|
|
753
|
+
const { repo, service, cmd, type, workspace } = btn.dataset;
|
|
754
|
+
if (type === "exec")
|
|
755
|
+
runExec(repo, service, cmd, btn, workspace);
|
|
748
756
|
else runLogs(repo, service, btn);
|
|
749
757
|
});
|
|
750
758
|
});
|
|
@@ -765,7 +773,7 @@
|
|
|
765
773
|
}
|
|
766
774
|
}
|
|
767
775
|
|
|
768
|
-
function runExec(repo, service, cmd, btn) {
|
|
776
|
+
function runExec(repo, service, cmd, btn, workspace) {
|
|
769
777
|
endStream();
|
|
770
778
|
const env = document.getElementById("env-sel").value;
|
|
771
779
|
|
|
@@ -781,8 +789,13 @@
|
|
|
781
789
|
appendLine(`\n▶ ${repo}/${service} · ${cmd} · ${env}\n`, "cmd");
|
|
782
790
|
|
|
783
791
|
const params = new URLSearchParams({ repo, service, cmd, env });
|
|
792
|
+
if (workspace) params.set("workspace", workspace);
|
|
784
793
|
es = new EventSource(`/api/service/exec?${params}`);
|
|
785
|
-
|
|
794
|
+
// LK-0: a command that needs sudo without the password asks,
|
|
795
|
+
// then runs again.
|
|
796
|
+
handleSSE(es, repo, service, {
|
|
797
|
+
again: () => runExec(repo, service, cmd, btn, workspace),
|
|
798
|
+
});
|
|
786
799
|
}
|
|
787
800
|
|
|
788
801
|
function runLogs(repo, service, btn) {
|
|
@@ -819,8 +832,10 @@
|
|
|
819
832
|
const isStream = opts.stream === true;
|
|
820
833
|
const id = runId;
|
|
821
834
|
const appendLine = (text, cls) => runs().append(id, text, cls);
|
|
835
|
+
const sudo = sudoRun();
|
|
822
836
|
evtSrc.onmessage = (e) => {
|
|
823
837
|
const msg = JSON.parse(e.data);
|
|
838
|
+
sudo.see(msg);
|
|
824
839
|
if (msg.type === "cmd") {
|
|
825
840
|
appendLine(msg.text, "cmd");
|
|
826
841
|
} else if (msg.type === "line") {
|
|
@@ -842,6 +857,7 @@
|
|
|
842
857
|
} else runs().set(id, { running: false });
|
|
843
858
|
evtSrc.close();
|
|
844
859
|
es = null;
|
|
860
|
+
if (opts.again) void sudo.done(opts.again);
|
|
845
861
|
}
|
|
846
862
|
};
|
|
847
863
|
evtSrc.onerror = () => {
|
|
@@ -293,6 +293,14 @@ export async function mountSettings(deps) {
|
|
|
293
293
|
const v = typeof o === "function" ? o(ctx) : o;
|
|
294
294
|
return Array.isArray(v) ? v : [];
|
|
295
295
|
}
|
|
296
|
+
// A custom control draws itself (it may act through its own API rather
|
|
297
|
+
// than save a value: Settings › Access), so it renders whether or not the
|
|
298
|
+
// row has a write.
|
|
299
|
+
function drawsItself(r) {
|
|
300
|
+
return (
|
|
301
|
+
r.control?.type === "custom" && typeof r.control.render === "function"
|
|
302
|
+
);
|
|
303
|
+
}
|
|
296
304
|
function drawRow(r) {
|
|
297
305
|
const id = slug(r.id);
|
|
298
306
|
const lid = `l-${id}`;
|
|
@@ -300,7 +308,7 @@ export async function mountSettings(deps) {
|
|
|
300
308
|
const did = `d-${id}`;
|
|
301
309
|
const v = value(r);
|
|
302
310
|
const t = r.control?.type || "readonly";
|
|
303
|
-
const ro = !r.write;
|
|
311
|
+
const ro = !r.write && !drawsItself(r);
|
|
304
312
|
const single = ["number", "select", "toggle", "text"].includes(t);
|
|
305
313
|
const label = single
|
|
306
314
|
? `<label class="l" id="${lid}" for="${cid}">${esc(r.label)}</label>`
|
|
@@ -499,7 +507,7 @@ export async function mountSettings(deps) {
|
|
|
499
507
|
}, 2000);
|
|
500
508
|
}
|
|
501
509
|
function wireRow(r) {
|
|
502
|
-
if (!r.write) return;
|
|
510
|
+
if (!r.write && !drawsItself(r)) return;
|
|
503
511
|
const row = $(`setting-${slug(r.id)}`);
|
|
504
512
|
if (!row) return;
|
|
505
513
|
const t = r.control?.type;
|