toolroll 0.9.87 → 0.9.89
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 +48 -0
- package/dist/acp-check.d.ts +9 -0
- package/dist/acp-check.js +40 -28
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +23 -23
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +32 -32
- package/dist/chat-channel.js +11 -1
- package/dist/design/kit.css +87 -50
- package/dist/design/tokens.css +12 -0
- package/dist/egress.js +5 -0
- package/dist/flow-starters.js +3 -0
- package/dist/flow-triggers.js +4 -0
- package/dist/folder-gate.d.ts +32 -0
- package/dist/folder-gate.js +43 -0
- package/dist/project-access.js +15 -1
- package/dist/project-admission.d.ts +6 -1
- package/dist/project-admission.js +44 -7
- package/dist/project-identity.d.ts +8 -0
- package/dist/project-identity.js +43 -0
- package/dist/project.d.ts +1 -0
- package/dist/project.js +6 -0
- package/dist/proposal.js +11 -1
- package/dist/recipes.js +12 -1
- package/dist/release.d.ts +4 -2
- package/dist/release.js +9 -3
- package/dist/serve.js +28 -6
- package/dist/server/console.css +22 -10
- package/dist/server/kit-standalone.d.ts +1 -1
- package/dist/server/kit-standalone.js +1 -0
- package/dist/server/kit.d.ts +17 -3
- package/dist/server/kit.js +15 -7
- package/dist/server/pages.js +2 -2
- package/dist/server/render-new-task.js +3 -3
- package/dist/server/render-tasks.js +4 -2
- package/dist/setup-guide.js +6 -0
- package/dist/teardown-git.js +16 -1
- package/dist/youiyoux-intake.js +11 -1
- package/package.json +1 -1
package/dist/chat-channel.js
CHANGED
|
@@ -12,11 +12,21 @@ import { canonicalProject } from "./project.js";
|
|
|
12
12
|
import { phoneText, projectLabel } from "./telegram-status.js";
|
|
13
13
|
import { CHAT_CONTROLS, chatControlHref, isChatControl, } from "./chat-controls.js";
|
|
14
14
|
import { CHAT_TASK_ACTIONS, isChatTaskAction } from "./chat-task-actions.js";
|
|
15
|
+
import { FolderUnavailable } from "./folder-gate.js";
|
|
15
16
|
export function channelRepos(store, approver, registry) {
|
|
16
17
|
const seen = new Set();
|
|
17
18
|
const repos = [];
|
|
18
19
|
for (const path of registry) {
|
|
19
|
-
|
|
20
|
+
let canonical;
|
|
21
|
+
// A folder the console's gate holds back (folder-gate.ts) is left out, never matched by its spelling.
|
|
22
|
+
try {
|
|
23
|
+
canonical = canonicalProject(path) ?? path;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof FolderUnavailable)
|
|
27
|
+
continue;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
20
30
|
if (seen.has(canonical) || !store.accountCanAccess(approver, canonical))
|
|
21
31
|
continue;
|
|
22
32
|
seen.add(canonical);
|
package/dist/design/kit.css
CHANGED
|
@@ -87,26 +87,32 @@
|
|
|
87
87
|
.k-tabs > [aria-selected="true"], .k-tabs > [aria-current] { color: var(--so-ink); border-bottom-color: var(--so-ink); }
|
|
88
88
|
.k-tabs .k-count { margin-left: 2px; }
|
|
89
89
|
|
|
90
|
-
/* ---------- Fields ----------
|
|
90
|
+
/* ---------- Fields: one family (KIT.md "Fields") ----------
|
|
91
|
+
Every text field, select and secondary text box is one filled surface (--so-field), no outline, radius 12px; the
|
|
92
|
+
label above names it, the hint and the error sit below it, focus draws the 2px ink ring. A transparent 1px border
|
|
93
|
+
keeps the box's edge in forced colours. Textareas grow with their words (field-sizing), no resize grip. */
|
|
91
94
|
.k-field { display: grid; gap: 6px; margin: 0; min-width: 0; }
|
|
92
|
-
.k-label { display: block; margin: 0; font: var(--so-
|
|
95
|
+
.k-label { display: block; margin: 0; font: 500 12.5px/1.35 var(--so-font-sans); color: var(--so-muted); }
|
|
93
96
|
.k-hint { margin: 0; font-size: 12px; color: var(--so-muted); line-height: 1.45; }
|
|
94
|
-
.k-input.k-input, .k-textarea.k-textarea { box-sizing: border-box; width: 100%; margin: 0; border: 1px solid
|
|
95
|
-
.k-input.k-input { height:
|
|
96
|
-
.k-textarea.k-textarea { min-height:
|
|
97
|
-
.k-input
|
|
97
|
+
.k-input.k-input, .k-textarea.k-textarea, .k-sel.k-sel { box-sizing: border-box; width: 100%; margin: 0; border: 1px solid transparent; border-radius: 12px; background: var(--so-field); color: var(--so-ink); font: 400 13.5px/1.4 var(--so-font-sans); box-shadow: none; transition: background-color var(--m-quick) var(--m-ease); }
|
|
98
|
+
.k-input.k-input { height: 36px; min-height: 0; padding: 0 12px; }
|
|
99
|
+
.k-textarea.k-textarea { min-height: 76px; max-height: 360px; padding: 10px 12px; resize: none; field-sizing: content; overflow-y: auto; max-width: 640px; line-height: 1.5; }
|
|
100
|
+
.k-input::placeholder, .k-textarea::placeholder { color: var(--so-muted); opacity: 1; }
|
|
101
|
+
.k-input:focus-visible, .k-textarea:focus-visible, .k-sel:focus-visible { outline: 2px solid var(--so-ring); outline-offset: 1px; box-shadow: none; }
|
|
98
102
|
.k-input[aria-invalid="true"], .k-textarea[aria-invalid="true"], .k-sel[aria-invalid="true"] { border-color: var(--so-danger); }
|
|
103
|
+
.k-input:disabled, .k-textarea:disabled, .k-sel:disabled { color: var(--so-muted); cursor: default; }
|
|
99
104
|
.k-err { display: block; margin: 0; font-size: 12px; color: var(--so-fail-ink); line-height: 1.45; }
|
|
100
105
|
.k-err:empty { display: none; }
|
|
101
|
-
/* A native select with the chevron:
|
|
106
|
+
/* A native select in the same family, with the chevron: 36px (sm 28px in a toolbar). */
|
|
102
107
|
.k-selw { position: relative; display: inline-block; min-width: 0; max-width: 100%; }
|
|
103
|
-
.k-selw::after { content: ""; position: absolute; right:
|
|
104
|
-
.k-sel.k-sel { appearance: none; -webkit-appearance: none;
|
|
105
|
-
.k-sel:
|
|
106
|
-
.k-
|
|
107
|
-
.k-selw.sm .k-sel { height: 28px; font: 500 12.5px/1 var(--so-font-sans); padding: 0 26px 0 9px; }
|
|
108
|
-
.k-selw.sm::after { right: 10px; }
|
|
108
|
+
.k-selw::after { content: ""; position: absolute; right: 13px; top: 50%; margin-top: -5px; width: 6px; height: 6px; border: solid var(--so-muted); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg); pointer-events: none; }
|
|
109
|
+
.k-sel.k-sel { appearance: none; -webkit-appearance: none; height: 36px; min-height: 0; padding: 0 32px 0 12px; line-height: 1; text-overflow: ellipsis; cursor: pointer; }
|
|
110
|
+
.k-selw.sm .k-sel { height: 28px; border-radius: 999px; font: 500 12.5px/1 var(--so-font-sans); padding: 0 26px 0 11px; }
|
|
111
|
+
.k-selw.sm::after { right: 11px; }
|
|
109
112
|
.k-selw.wide { display: block; width: 100%; }
|
|
113
|
+
/* A select drawn as a pill chip (a composer's project): filled, no box, works without script. */
|
|
114
|
+
.k-selw-chip::after { right: 12px; }
|
|
115
|
+
.k-selw-chip .k-sel.k-sel { height: 36px; border-radius: 999px; padding: 0 30px 0 14px; background: var(--so-composer-control); font: 500 13.5px/1 var(--so-font-sans); }
|
|
110
116
|
/* Switch: a button[role=switch] that says its state, or a checkbox[role=switch] for a form. */
|
|
111
117
|
.k-switch { position: relative; display: inline-flex; align-items: center; gap: 8px; min-height: 28px; margin: 0; padding: 0; border: 0; background: transparent; font: 500 12.5px/1 var(--so-font-sans); color: var(--so-muted); cursor: pointer; box-shadow: none; }
|
|
112
118
|
.k-trk { position: relative; width: 32px; height: 18px; border-radius: 9px; border: 1px solid var(--so-field-line); background: var(--so-paper); transition: background-color var(--m-quick) var(--m-ease), border-color var(--m-quick) var(--m-ease); flex: none; box-sizing: border-box; }
|
|
@@ -314,25 +320,49 @@
|
|
|
314
320
|
.k-swap-in { animation: k-swap var(--m-base) var(--m-ease) both; }
|
|
315
321
|
@keyframes k-swap { from { opacity: 0; transform: translateY(4px); } }
|
|
316
322
|
|
|
317
|
-
/* ---------- Composer: one
|
|
318
|
-
|
|
319
|
-
.k-composer
|
|
320
|
-
.k-composer
|
|
321
|
-
|
|
322
|
-
.k-
|
|
323
|
+
/* ---------- Composer: one filled rounded object (KIT.md "Composer"); the textarea grows 1 → 8 lines; one bottom row
|
|
324
|
+
of filled circles and pills inside; its note and refusal sit under it, outside the box ---------- */
|
|
325
|
+
.k-composer-field { display: grid; gap: 6px; min-width: 0; }
|
|
326
|
+
.k-composer { --c-r: 28px; box-sizing: border-box; border: 1px solid transparent; border-radius: var(--c-r); background: var(--so-composer); display: grid; padding: 8px 10px 10px; transition: border-color var(--m-quick) var(--m-ease), background-color var(--m-quick) var(--m-ease); }
|
|
327
|
+
/* Focus: the fill lifts a step (a tap's cue is that and the caret); keyboard focus also draws the 2px ink ring, 3:1 or
|
|
328
|
+
better in both themes (html.k-kbd from the kit behaviour; without script the ring always shows). The textarea draws
|
|
329
|
+
no ring of its own. */
|
|
330
|
+
.k-composer { outline: 2px solid transparent; outline-offset: 2px; }
|
|
331
|
+
.k-composer:focus-within { background: color-mix(in srgb, var(--so-composer) 97%, var(--so-ink)); }
|
|
332
|
+
html:not(.k-js) .k-composer:focus-within, html.k-kbd .k-composer:focus-within { outline-color: var(--so-ring); }
|
|
333
|
+
.k-composer.off { background: var(--so-raised); color: var(--so-muted); }
|
|
334
|
+
.k-composer-ta.k-composer-ta { box-sizing: border-box; width: 100%; min-height: 44px; margin: 0; padding: 10px 10px 8px; border: 0; border-radius: 0; outline: none; resize: none; background: transparent; box-shadow: none; color: var(--so-ink); font: 400 17px/1.5 var(--so-font-sans); letter-spacing: -0.005em; overflow-y: hidden; scrollbar-width: thin; }
|
|
335
|
+
.k-composer-ta::placeholder { color: var(--so-muted); opacity: 1; }
|
|
323
336
|
.k-composer-ta:focus, .k-composer-ta:focus-visible { outline: none; box-shadow: none; }
|
|
324
|
-
.k-composer:has(.k-composer-ta[aria-invalid="true"]) { border-color: var(--so-danger);
|
|
337
|
+
.k-composer:has(.k-composer-ta[aria-invalid="true"]) { border-color: var(--so-danger); }
|
|
325
338
|
.k-composer + .k-err { margin-top: 6px; }
|
|
326
|
-
.k-composer-bar { display: flex; align-items: center; gap:
|
|
327
|
-
.k-composer-plus { box-sizing: border-box; width:
|
|
328
|
-
.k-composer-
|
|
339
|
+
.k-composer-bar { display: flex; align-items: center; gap: 8px; padding: 4px 0 0; flex-wrap: wrap; min-height: 40px; }
|
|
340
|
+
.k-composer-plus { box-sizing: border-box; width: 36px; height: 36px; min-height: 0; margin: 0; padding: 0; border-radius: 50%; display: grid; place-items: center; flex: none; color: var(--so-ink); border: 0; background: var(--so-composer-control); cursor: pointer; box-shadow: none; transition: background-color var(--m-quick) var(--m-ease), color var(--m-quick) var(--m-ease); }
|
|
341
|
+
.k-composer-plus .k-i { width: 18px; height: 18px; }
|
|
342
|
+
/* A pill chip: the primary word, then a dimmer secondary word (a project, then the agent). */
|
|
343
|
+
.k-composer-chip { box-sizing: border-box; height: 36px; min-height: 0; max-width: 100%; margin: 0; display: inline-flex; align-items: center; gap: 6px; padding: 0 14px; border: 0; border-radius: 999px; font: 500 13.5px/1 var(--so-font-sans); color: var(--so-ink); background: var(--so-composer-control); cursor: pointer; box-shadow: none; transition: background-color var(--m-quick) var(--m-ease), color var(--m-quick) var(--m-ease); }
|
|
344
|
+
.k-composer-chip > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
345
|
+
.k-composer-chip .k-chip-sub { color: var(--so-muted); font-weight: 400; }
|
|
329
346
|
.k-composer-plus:disabled, .k-composer-chip:disabled { opacity: .5; pointer-events: none; }
|
|
330
|
-
/*
|
|
331
|
-
.k-
|
|
332
|
-
.k-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
.k-
|
|
347
|
+
/* A small act inside the bar (Pause and send) is a filled pill like the chips, never an outlined box in the box. */
|
|
348
|
+
.k-composer-bar .k-btn:not(.k-send) { border-color: transparent; border-radius: 999px; background: var(--so-composer-control); }
|
|
349
|
+
.k-composer-plus:focus-visible, .k-composer-chip:focus-visible { outline: 2px solid var(--so-ring); outline-offset: 2px; }
|
|
350
|
+
/* A toggle chip (an on/off choice inside the box, as Read code before planning): a filled pill whose mark fills when
|
|
351
|
+
on; the checkbox inside is the control (role=switch), so it works without script. */
|
|
352
|
+
.k-tchip { position: relative; box-sizing: border-box; height: 36px; margin: 0; display: inline-flex; align-items: center; gap: 8px; padding: 0 14px 0 12px; border-radius: 999px; background: var(--so-composer-control); color: var(--so-muted); font: 500 13.5px/1 var(--so-font-sans); cursor: pointer; transition: color var(--m-quick) var(--m-ease); }
|
|
353
|
+
.k-tchip > input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; z-index: 1; }
|
|
354
|
+
.k-tchip-mark { box-sizing: border-box; width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; flex: none; display: grid; place-items: center; transition: background-color var(--m-quick) var(--m-ease), border-color var(--m-quick) var(--m-ease); }
|
|
355
|
+
.k-tchip:has(input:checked) { color: var(--so-ink); }
|
|
356
|
+
.k-tchip:has(input:checked) .k-tchip-mark { background: var(--so-ink); border-color: var(--so-ink); }
|
|
357
|
+
.k-tchip:has(input:checked) .k-tchip-mark::after { content: ""; width: 3px; height: 6px; margin-top: -2px; border: solid var(--so-paper); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg); }
|
|
358
|
+
.k-tchip:has(input:focus-visible) { outline: 2px solid var(--so-ring); outline-offset: 2px; }
|
|
359
|
+
/* The round send: solid ink with a paper arrow once there are words, the muted control fill until then; Stop takes
|
|
360
|
+
its place while the work runs. */
|
|
361
|
+
.k-send-slot { position: relative; display: inline-grid; width: 36px; height: 36px; flex: none; }
|
|
362
|
+
.k-send-slot .k-send { grid-area: 1 / 1; position: relative; box-sizing: border-box; width: 36px; height: 36px; min-height: 0; margin: 0; padding: 0; border-radius: 50%; display: grid; place-items: center; background: var(--so-ink); color: var(--so-paper); border: 0; cursor: pointer; box-shadow: none; transition: background-color var(--m-quick) var(--m-ease), color var(--m-quick) var(--m-ease), transform var(--m-base) var(--m-ease); }
|
|
363
|
+
.k-send::before { content: ""; position: absolute; inset: -4px; }
|
|
364
|
+
.k-send .k-i { width: 18px; height: 18px; stroke-width: 2.2; }
|
|
365
|
+
.k-send[aria-disabled="true"], .k-composer:not(.has-text) .k-send:not(.k-stop) { background: var(--so-composer-control); color: var(--so-muted); }
|
|
336
366
|
.k-send[aria-disabled="true"] { cursor: default; }
|
|
337
367
|
.k-send:not([aria-disabled="true"]):active { transform: scale(.94); }
|
|
338
368
|
.k-stop { visibility: hidden; }
|
|
@@ -344,17 +374,18 @@
|
|
|
344
374
|
.k-send:focus-visible { outline: 2px solid var(--so-ring); outline-offset: 2px; }
|
|
345
375
|
.k-composer-files { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 8px; }
|
|
346
376
|
.k-composer-files:empty { display: none; }
|
|
347
|
-
.k-composer-file { display: inline-flex; align-items: center; gap: 6px; height: 28px; margin-top: 6px; padding: 0 4px 0
|
|
377
|
+
.k-composer-file { display: inline-flex; align-items: center; gap: 6px; height: 28px; margin-top: 6px; padding: 0 4px 0 12px; border: 0; border-radius: 999px; background: var(--so-composer-control); font-size: 12px; }
|
|
348
378
|
.k-composer-file button { width: 22px; height: 22px; min-height: 0; margin: 0; padding: 0; display: grid; place-items: center; border: 0; border-radius: 50%; background: transparent; color: var(--so-muted); }
|
|
349
|
-
|
|
350
|
-
.k-composer-
|
|
379
|
+
/* Under the box, in small dim words: its note, then its refusal. */
|
|
380
|
+
.k-composer-note.k-composer-note { margin: 0; padding: 0 16px; font-size: 12px; color: var(--so-muted); line-height: 1.45; }
|
|
381
|
+
.k-composer-err { padding: 0 16px; }
|
|
351
382
|
.k-composer-err:empty { display: none; }
|
|
352
383
|
/* One line: the text and the send share a row. */
|
|
353
|
-
.k-composer.line { grid-template-columns: minmax(0, 1fr) auto; align-items: end; padding:
|
|
354
|
-
.k-composer.line .k-composer-ta { padding:
|
|
355
|
-
.k-composer.line .k-composer-bar { padding: 0 0
|
|
384
|
+
.k-composer.line { --c-r: 26px; grid-template-columns: minmax(0, 1fr) auto; align-items: end; padding: 5px 6px 5px 10px; }
|
|
385
|
+
.k-composer.line .k-composer-ta { padding: 9px 6px; min-height: 42px; font-size: 16px; }
|
|
386
|
+
.k-composer.line .k-composer-bar { padding: 0 0 3px; min-height: 0; }
|
|
356
387
|
.k-composer.line .k-composer-bar .k-grow { display: none; }
|
|
357
|
-
.k-composer.line .k-composer-files
|
|
388
|
+
.k-composer.line .k-composer-files { grid-column: 1 / -1; }
|
|
358
389
|
/* A composer docked to the bottom rides above the phone keyboard (kit behaviour sets html.kb-open and --kb). */
|
|
359
390
|
.k-composer-dock { position: sticky; bottom: 0; z-index: 4; padding: 12px 24px 14px; background: var(--so-paper); border-top: 1px solid var(--so-line); }
|
|
360
391
|
|
|
@@ -425,8 +456,10 @@
|
|
|
425
456
|
.k-card:hover { background: color-mix(in srgb, var(--so-raised) 60%, transparent); }
|
|
426
457
|
.k-backlink:hover { color: var(--so-ink); }
|
|
427
458
|
.k-menu > button:hover, .k-menu > a:hover { background: var(--so-soft); }
|
|
428
|
-
.k-composer-plus:hover { background: var(--so-
|
|
429
|
-
.k-
|
|
459
|
+
.k-composer-plus:hover, .k-composer-chip:hover, .k-tchip:hover { background: color-mix(in srgb, var(--so-composer-control) 88%, var(--so-ink)); }
|
|
460
|
+
.k-dock-needs:hover { background: color-mix(in srgb, var(--so-composer-control) 88%, var(--so-ink)); }
|
|
461
|
+
.k-input:hover, .k-textarea:hover, .k-sel:hover { background: color-mix(in srgb, var(--so-field) 94%, var(--so-ink)); }
|
|
462
|
+
.k-selw-chip .k-sel:hover { background: color-mix(in srgb, var(--so-composer-control) 88%, var(--so-ink)); }
|
|
430
463
|
.k-nav:hover { background: var(--so-nav-hover); color: var(--so-ink); }
|
|
431
464
|
.k-crumbs a:hover { color: var(--so-ink); }
|
|
432
465
|
}
|
|
@@ -437,7 +470,7 @@
|
|
|
437
470
|
/* ---------- Phone dock: the Needs-you button and the screen's one primary ---------- */
|
|
438
471
|
.k-dock { display: none; }
|
|
439
472
|
/* The Needs-you button (the app shell's dock): a dot, the words, the ink count, and which projects it covers. */
|
|
440
|
-
.k-dock-needs { display: grid; align-content: center; gap: 1px; box-sizing: border-box; width: 136px; min-height: 48px; margin: 0; padding: 0
|
|
473
|
+
.k-dock-needs { display: grid; align-content: center; gap: 1px; box-sizing: border-box; width: 136px; min-height: 48px; margin: 0; padding: 0 14px; border-radius: 14px; border: 1px solid transparent; background: var(--so-composer-control); color: var(--so-ink); text-align: left; white-space: nowrap; cursor: pointer; box-shadow: none; }
|
|
441
474
|
.k-dock-needs-l { display: inline-flex; align-items: center; gap: 7px; font: 500 14px/1.2 var(--so-font-sans); }
|
|
442
475
|
.k-dock-needs-s { overflow: hidden; text-overflow: ellipsis; font-size: 11.5px; color: var(--so-muted); padding-left: 14px; }
|
|
443
476
|
.k-dock-needs:focus-visible { outline: 2px solid var(--so-ring); outline-offset: 2px; }
|
|
@@ -509,16 +542,17 @@
|
|
|
509
542
|
.k-frow .k-err { text-align: left; }
|
|
510
543
|
.k-frow-c .k-seg { width: 100%; }
|
|
511
544
|
.k-frow-c .k-seg > button { flex: 1; justify-content: center; }
|
|
512
|
-
.k-composer-ta.k-composer-ta { font-size:
|
|
545
|
+
.k-composer-ta.k-composer-ta { font-size: 17px; }
|
|
513
546
|
.k-composer-bar .k-btn-sm { height: 44px; }
|
|
514
|
-
/* Every composer control is a real 44px target
|
|
547
|
+
/* Every composer control is a real, filled 44px target. */
|
|
548
|
+
.k-composer { --c-r: 26px; padding: 6px 8px 8px; }
|
|
549
|
+
.k-composer-ta.k-composer-ta { padding: 10px 10px 6px; }
|
|
515
550
|
.k-send-slot, .k-send-slot .k-send { width: 44px; height: 44px; }
|
|
516
|
-
.k-send-slot .k-send { background: radial-gradient(circle, var(--so-ink) 17.5px, transparent 18px); }
|
|
517
|
-
.k-send[aria-disabled="true"], .k-composer:not(.has-text) .k-send:not(.k-stop) { background: radial-gradient(circle, var(--so-raised) 17.5px, transparent 18px); }
|
|
518
551
|
.k-send::before { content: none; }
|
|
519
|
-
.k-composer-plus { width: 44px; height: 44px;
|
|
520
|
-
.k-composer-chip { height: 44px; }
|
|
521
|
-
.k-
|
|
552
|
+
.k-composer-plus { width: 44px; height: 44px; }
|
|
553
|
+
.k-composer-chip, .k-tchip, .k-selw-chip .k-sel.k-sel { height: 44px; }
|
|
554
|
+
.k-selw-chip .k-sel.k-sel { font-size: 16px; }
|
|
555
|
+
.k-composer-bar { gap: 6px; }
|
|
522
556
|
.k-composer-file button { width: 32px; height: 32px; }
|
|
523
557
|
.k-composer-dock { padding: 12px 16px 16px; }
|
|
524
558
|
html.kb-open .k-composer-dock { position: fixed; left: 0; right: 0; bottom: var(--kb, 0px); z-index: 13; }
|
|
@@ -535,6 +569,9 @@
|
|
|
535
569
|
.k-dock { display: flex; gap: 8px; align-items: stretch; position: fixed; left: 0; right: 0; bottom: 0; z-index: 12; padding: 10px 12px calc(10px + env(safe-area-inset-bottom)); background: var(--so-paper); border-top: 1px solid var(--so-line); }
|
|
536
570
|
.k-dock-primary { flex: 1; display: flex; }
|
|
537
571
|
.k-dock-primary .k-btn { width: 100%; }
|
|
572
|
+
/* The dock is filled controls only: a secondary act there is a filled control, never an outlined box. */
|
|
573
|
+
.k-dock .k-btn-outline { border-color: transparent; background: var(--so-composer-control); }
|
|
574
|
+
.k-dock .k-btn { min-height: 48px; border-radius: 14px; }
|
|
538
575
|
}
|
|
539
576
|
@media (min-width: 761px) { .k-phone-only { display: none !important; } }
|
|
540
577
|
|
|
@@ -563,9 +600,9 @@ main:has(> .k-out) { max-width: none; margin: 0; padding: 0; }
|
|
|
563
600
|
.k-refusal .k-empty-t::first-letter, .k-refusal .k-notice > span::first-letter { text-transform: uppercase; }
|
|
564
601
|
|
|
565
602
|
/* A composer's note can follow its switch: k-on-note while it is on, k-off-note while it is off (no script). */
|
|
566
|
-
.k-composer .k-off-note { display: none; }
|
|
567
|
-
.k-composer:has(.k-switch-input):not(:has(.k-switch-input:checked)) .k-on-note { display: none; }
|
|
568
|
-
.k-composer:has(.k-switch-input):not(:has(.k-switch-input:checked)) .k-off-note { display: inline; }
|
|
603
|
+
.k-composer-field .k-off-note { display: none; }
|
|
604
|
+
.k-composer-field:has(.k-switch-input):not(:has(.k-switch-input:checked)) .k-on-note { display: none; }
|
|
605
|
+
.k-composer-field:has(.k-switch-input):not(:has(.k-switch-input:checked)) .k-off-note { display: inline; }
|
|
569
606
|
|
|
570
607
|
/* The composer's focus is the box's (KIT.md): the workspace's own :focus-visible ring never lands on its textarea. */
|
|
571
608
|
.k-composer .k-composer-ta.k-composer-ta:focus-visible { outline: none; box-shadow: none; }
|
|
@@ -614,7 +651,7 @@ main:has(> .k-out) { max-width: none; margin: 0; padding: 0; }
|
|
|
614
651
|
|
|
615
652
|
/* ---------- Reduced motion changes presentation only ---------- */
|
|
616
653
|
@media (prefers-reduced-motion: reduce) {
|
|
617
|
-
.k-btn, .k-send, .k-composer, .k-composer-plus, .k-composer-chip, .k-trk, .k-trk::after, .k-tabs > *, .k-slots, .k-crow-bar { transition: none; }
|
|
654
|
+
.k-btn, .k-send, .k-composer, .k-composer-plus, .k-composer-chip, .k-tchip, .k-tchip-mark, .k-input, .k-textarea, .k-sel, .k-trk, .k-trk::after, .k-tabs > *, .k-slots, .k-crow-bar { transition: none; }
|
|
618
655
|
.k-steps-fill { transition: none; }
|
|
619
656
|
.k-grow-in, .k-msg-in, .k-swap-in, .k-scrim, .k-btn.swap .k-face, .k-check-draw path, .k-composer .k-i { animation: none !important; }
|
|
620
657
|
.k-check-draw path { stroke-dashoffset: 0; }
|
package/dist/design/tokens.css
CHANGED
|
@@ -8,10 +8,16 @@
|
|
|
8
8
|
--so-attention: var(--so-signal);
|
|
9
9
|
--so-attention-soft: var(--so-signal-soft);
|
|
10
10
|
--so-code-bg: #fafafa;
|
|
11
|
+
/* Composer: the filled surface of the message box, one step off the page. */
|
|
12
|
+
--so-composer: #f2f2f2;
|
|
13
|
+
/* Composer Control: the filled circle and pill chips inside the composer (+, project, toggle chips) and the empty send. */
|
|
14
|
+
--so-composer-control: #dcdcdc;
|
|
11
15
|
/* Vermilion: failure and irreversible acts. */
|
|
12
16
|
--so-danger: #c4320a;
|
|
13
17
|
--so-danger-soft: #feebe7;
|
|
14
18
|
--so-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
|
19
|
+
/* Field: the filled surface of a text field, select or secondary text box (the field family: no outline; the label names it, focus draws the ink ring). */
|
|
20
|
+
--so-field: #ececec;
|
|
15
21
|
/* Field Line: the boundary that identifies a control (fields, outline buttons, switches, radio cards), 3:1 or better on paper. */
|
|
16
22
|
--so-field-line: #8f8f8f;
|
|
17
23
|
--so-font-mono: "Geist Mono", ui-monospace, "SF Mono", "SFMono-Regular", "Menlo", "Consolas", monospace;
|
|
@@ -162,8 +168,11 @@
|
|
|
162
168
|
--so-accent-hover: #ffffff;
|
|
163
169
|
--so-accent-text: #ededed;
|
|
164
170
|
--so-code-bg: #111111;
|
|
171
|
+
--so-composer: #242424;
|
|
172
|
+
--so-composer-control: #333333;
|
|
165
173
|
--so-danger: #ff977d;
|
|
166
174
|
--so-danger-soft: rgb(255 151 125 / .12);
|
|
175
|
+
--so-field: #232323;
|
|
167
176
|
--so-field-line: #6b6b6b;
|
|
168
177
|
--so-ground: #0b0b0b;
|
|
169
178
|
--so-info: #70b8ff;
|
|
@@ -210,8 +219,11 @@
|
|
|
210
219
|
--so-accent-hover: #ffffff;
|
|
211
220
|
--so-accent-text: #ededed;
|
|
212
221
|
--so-code-bg: #111111;
|
|
222
|
+
--so-composer: #242424;
|
|
223
|
+
--so-composer-control: #333333;
|
|
213
224
|
--so-danger: #ff977d;
|
|
214
225
|
--so-danger-soft: rgb(255 151 125 / .12);
|
|
226
|
+
--so-field: #232323;
|
|
215
227
|
--so-field-line: #6b6b6b;
|
|
216
228
|
--so-ground: #0b0b0b;
|
|
217
229
|
--so-info: #70b8ff;
|
package/dist/egress.js
CHANGED
|
@@ -18,6 +18,7 @@ import { readdirSync } from "node:fs";
|
|
|
18
18
|
import { basename, join } from "node:path";
|
|
19
19
|
import { connectFirst, isPublicAddress } from "./scout-net.js";
|
|
20
20
|
import { lookup as dnsLookup } from "node:dns/promises";
|
|
21
|
+
import { folderAnswers } from "./folder-gate.js";
|
|
21
22
|
/** The policy's shape version: a worker enforces the versions it knows and refuses any other. */
|
|
22
23
|
export const NETWORK_POLICY_VERSION = 1;
|
|
23
24
|
export const PROJECT_NETWORK_DEFAULT = { mode: "allowlist", hosts: [], loopback: "proxy-only" };
|
|
@@ -114,6 +115,10 @@ const WALK_SKIP = new Set(["node_modules", "vendor", "target", "dist", "build",
|
|
|
114
115
|
* project's own hosts only.
|
|
115
116
|
*/
|
|
116
117
|
export function repositoryEcosystems(repo) {
|
|
118
|
+
// In the console, a folder that hasn't answered a look lately is not walked on its thread (folder-gate.ts): it names
|
|
119
|
+
// none, as an unreadable folder does.
|
|
120
|
+
if (!folderAnswers(repo))
|
|
121
|
+
return [];
|
|
117
122
|
const paths = [];
|
|
118
123
|
const walk = (folder, depth) => {
|
|
119
124
|
let entries;
|
package/dist/flow-starters.js
CHANGED
|
@@ -10,6 +10,7 @@ import { flowDefinitionOf } from "./flow-engine.js";
|
|
|
10
10
|
import { addFlowTriggerTo, githubRepoOf } from "./flow-triggers.js";
|
|
11
11
|
import { flowFromSteps, ISSUE_LABEL } from "./flows.js";
|
|
12
12
|
import { publishingOf } from "./pull-request-flow.js";
|
|
13
|
+
import { folderAnswers } from "./folder-gate.js";
|
|
13
14
|
export const STARTER_IDS = ["ci-fix", "issue-task", "overnight", "plane-review"];
|
|
14
15
|
export const STARTER_FLOWS = [
|
|
15
16
|
{
|
|
@@ -87,6 +88,8 @@ export function planeReviewSteps(own) {
|
|
|
87
88
|
}
|
|
88
89
|
/** Whether a project is Toolroll's own source. */
|
|
89
90
|
export function isToolrollRepo(repo) {
|
|
91
|
+
if (!folderAnswers(repo))
|
|
92
|
+
return false;
|
|
90
93
|
try {
|
|
91
94
|
return JSON.parse(readFileSync(join(repo, "package.json"), "utf8")).name === "toolroll";
|
|
92
95
|
}
|
package/dist/flow-triggers.js
CHANGED
|
@@ -52,6 +52,7 @@ import { envValue } from "./names.js";
|
|
|
52
52
|
import { recordBranchCheckRun, unprovenNoticeRuns } from "./tidy.js";
|
|
53
53
|
import { linearLabelNames, readFormSubmission, readGithubEvent, readGithubIssue, readGithubIssueEventList, readGithubIssueList, readGithubRun, readGithubRunList, readInboundMail, readLinearAnswer, readLinearEvent, readLinearIssue, readWebhookPayload } from "./contracts/trigger-payloads.js";
|
|
54
54
|
import { isRecord } from "./is-record.js";
|
|
55
|
+
import { folderAnswers } from "./folder-gate.js";
|
|
55
56
|
export { FLOW_TRIGGER_KINDS };
|
|
56
57
|
export const CHAT_APP_NAMES = { slack: "Slack", discord: "Discord", teams: "Teams", telegram: "Telegram" };
|
|
57
58
|
/** What each kind is called on the canvas. */
|
|
@@ -124,6 +125,9 @@ export function scheduleFromWords(text) {
|
|
|
124
125
|
}
|
|
125
126
|
/** A GitHub repository as owner/name, read from a project's origin remote when it is on GitHub. */
|
|
126
127
|
export function githubRepoOf(path) {
|
|
128
|
+
// In the console, a folder that hasn't answered a look lately is not read on its thread (folder-gate.ts).
|
|
129
|
+
if (!folderAnswers(path))
|
|
130
|
+
return null;
|
|
127
131
|
try {
|
|
128
132
|
const dot = join(path, ".git");
|
|
129
133
|
const config = statSync(dot).isDirectory() ? readFileSync(join(dot, "config"), "utf8") : null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the console may read a folder synchronously right now.
|
|
3
|
+
*
|
|
4
|
+
* The console answers every page on one thread, so one synchronous read of a folder that has stopped answering (a
|
|
5
|
+
* drive that hangs, a folder macOS holds in iCloud) freezes the console with it: it listens and answers nothing, not
|
|
6
|
+
* even /healthz (the 0.9.82 and 0.9.87 deploys). So the console's own work (its request handling and its background
|
|
7
|
+
* pass, serve.ts) runs inside a folder scope: there, a synchronous read of a person's folder goes ahead only when that
|
|
8
|
+
* folder answered an off-thread look lately (project-identity.ts `answering`), or it lies in the console's own data.
|
|
9
|
+
* Otherwise the read isn't made and its caller refuses, says the folder is unavailable, or throws FolderUnavailable.
|
|
10
|
+
*
|
|
11
|
+
* Availability never decides authorization: a held-back canonicalization throws FolderUnavailable (project.ts), never
|
|
12
|
+
* answers null, so no caller falls back to a path's spelling because a folder didn't answer.
|
|
13
|
+
*
|
|
14
|
+
* Work outside a console scope (the worker, even in the same process as the console; `up`; the CLI) reads as it
|
|
15
|
+
* always did.
|
|
16
|
+
*/
|
|
17
|
+
/** One console's answer for a resolved path. `pointer`: the path came from a file a build can write (a checkout's
|
|
18
|
+
* `.git` or `commondir`), so the console's own folders (build checkouts among them) don't vouch for it by spelling. */
|
|
19
|
+
export type FolderGate = (path: string, pointer: boolean) => boolean;
|
|
20
|
+
/** What a caller says, and throws, when the gate holds a read back. */
|
|
21
|
+
export declare const FOLDER_DIDNT_ANSWER = "the folder didn't answer";
|
|
22
|
+
/** A synchronous read the console's folder gate held back: the folder is unavailable now, which is never a "no". */
|
|
23
|
+
export declare class FolderUnavailable extends Error {
|
|
24
|
+
readonly path: string;
|
|
25
|
+
constructor(path: string);
|
|
26
|
+
}
|
|
27
|
+
/** Runs `work` (and everything it starts) inside the console's folder scope. */
|
|
28
|
+
export declare function inFolderScope<T>(gate: FolderGate, work: () => T): T;
|
|
29
|
+
/** Whether a synchronous read of `path` may happen now. Outside a console's folder scope: always. */
|
|
30
|
+
export declare function folderAnswers(path: string, pointer?: boolean): boolean;
|
|
31
|
+
/** `path` is `folder` or inside it (both resolved). */
|
|
32
|
+
export declare function insideFolder(path: string, folder: string): boolean;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the console may read a folder synchronously right now.
|
|
3
|
+
*
|
|
4
|
+
* The console answers every page on one thread, so one synchronous read of a folder that has stopped answering (a
|
|
5
|
+
* drive that hangs, a folder macOS holds in iCloud) freezes the console with it: it listens and answers nothing, not
|
|
6
|
+
* even /healthz (the 0.9.82 and 0.9.87 deploys). So the console's own work (its request handling and its background
|
|
7
|
+
* pass, serve.ts) runs inside a folder scope: there, a synchronous read of a person's folder goes ahead only when that
|
|
8
|
+
* folder answered an off-thread look lately (project-identity.ts `answering`), or it lies in the console's own data.
|
|
9
|
+
* Otherwise the read isn't made and its caller refuses, says the folder is unavailable, or throws FolderUnavailable.
|
|
10
|
+
*
|
|
11
|
+
* Availability never decides authorization: a held-back canonicalization throws FolderUnavailable (project.ts), never
|
|
12
|
+
* answers null, so no caller falls back to a path's spelling because a folder didn't answer.
|
|
13
|
+
*
|
|
14
|
+
* Work outside a console scope (the worker, even in the same process as the console; `up`; the CLI) reads as it
|
|
15
|
+
* always did.
|
|
16
|
+
*/
|
|
17
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
|
+
import { resolve, sep } from "node:path";
|
|
19
|
+
const scope = new AsyncLocalStorage();
|
|
20
|
+
/** What a caller says, and throws, when the gate holds a read back. */
|
|
21
|
+
export const FOLDER_DIDNT_ANSWER = "the folder didn't answer";
|
|
22
|
+
/** A synchronous read the console's folder gate held back: the folder is unavailable now, which is never a "no". */
|
|
23
|
+
export class FolderUnavailable extends Error {
|
|
24
|
+
path;
|
|
25
|
+
constructor(path) {
|
|
26
|
+
super(FOLDER_DIDNT_ANSWER);
|
|
27
|
+
this.path = path;
|
|
28
|
+
this.name = "FolderUnavailable";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Runs `work` (and everything it starts) inside the console's folder scope. */
|
|
32
|
+
export function inFolderScope(gate, work) {
|
|
33
|
+
return scope.run(gate, work);
|
|
34
|
+
}
|
|
35
|
+
/** Whether a synchronous read of `path` may happen now. Outside a console's folder scope: always. */
|
|
36
|
+
export function folderAnswers(path, pointer = false) {
|
|
37
|
+
const gate = scope.getStore();
|
|
38
|
+
return gate === undefined || gate(resolve(path), pointer);
|
|
39
|
+
}
|
|
40
|
+
/** `path` is `folder` or inside it (both resolved). */
|
|
41
|
+
export function insideFolder(path, folder) {
|
|
42
|
+
return path === folder || path.startsWith(folder.endsWith(sep) ? folder : folder + sep);
|
|
43
|
+
}
|
package/dist/project-access.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { isAbsolute, resolve } from "node:path";
|
|
3
3
|
import { canonicalProject } from "./project.js";
|
|
4
|
+
import { FolderUnavailable } from "./folder-gate.js";
|
|
4
5
|
export function normalizeProjectAccess(value) {
|
|
5
6
|
if (value === null)
|
|
6
7
|
return null;
|
|
@@ -26,7 +27,20 @@ export function readProjectAccess(value) {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
export function projectAccessAllows(access, repo) {
|
|
29
|
-
|
|
30
|
+
if (access === null)
|
|
31
|
+
return true;
|
|
32
|
+
if (repo === null)
|
|
33
|
+
return false;
|
|
34
|
+
// A folder the console's gate holds back (folder-gate.ts) is unavailable: access to it is denied, never decided by
|
|
35
|
+
// its spelling.
|
|
36
|
+
try {
|
|
37
|
+
return access.includes(canonicalProject(repo) ?? resolve(repo));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof FolderUnavailable)
|
|
41
|
+
return false;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
30
44
|
}
|
|
31
45
|
/** SQL: projectAccessAllows for an `approver` row (`alias`) and a project path expression, for the dispatch belts. A
|
|
32
46
|
* null list reaches every project; an unreadable one reaches none. */
|
|
@@ -42,9 +42,11 @@ export type Released = {
|
|
|
42
42
|
removed: boolean;
|
|
43
43
|
};
|
|
44
44
|
export declare const PROJECT_ADDED = "project added";
|
|
45
|
+
/** A folder's real path (a folder), null (not one), or "no-answer" (it didn't answer in time). */
|
|
46
|
+
export type FolderResolver = (path: string) => Promise<string | null | "no-answer">;
|
|
45
47
|
export declare const PROJECT_REMOVED = "project removed";
|
|
46
48
|
/** The checkout root a folder belongs to, canonical; never a shell. */
|
|
47
|
-
export declare function proveProjectRoot(path: string, exec?: ProjectExec): Promise<{
|
|
49
|
+
export declare function proveProjectRoot(path: string, exec?: ProjectExec, resolveFolder?: FolderResolver): Promise<{
|
|
48
50
|
ok: true;
|
|
49
51
|
repo: string;
|
|
50
52
|
} | AdmissionFailure>;
|
|
@@ -63,6 +65,9 @@ export declare function admitProject(store: Store, input: {
|
|
|
63
65
|
/** The caller's authority, asked again about the proved repo inside the locked registry update and inside the
|
|
64
66
|
* database transaction (the console: nothing it proved before an await moved, and the repo is still visible). */
|
|
65
67
|
stillAllowed?: (repo: string) => boolean;
|
|
68
|
+
/** How the folder and its checkout root are resolved (the console: off its thread, under a deadline). Absent:
|
|
69
|
+
* canonicalProject. */
|
|
70
|
+
resolveFolder?: FolderResolver;
|
|
66
71
|
}): Promise<Admitted | AdmissionFailure>;
|
|
67
72
|
/**
|
|
68
73
|
* Remove one project from the lists and the builder. Its tasks, results and
|
|
@@ -17,6 +17,7 @@ import { run } from "./exec.js";
|
|
|
17
17
|
import { canonicalProject, projectName, sameRepo } from "./project.js";
|
|
18
18
|
import { remoteUrlProblem } from "./remote-main.js";
|
|
19
19
|
import { addRepos, loadProjectRegistry, removeRepos, updateRepos } from "./repos.js";
|
|
20
|
+
import { FolderUnavailable } from "./folder-gate.js";
|
|
20
21
|
const ADMISSION_GIT = {
|
|
21
22
|
envAllowlist: ["PATH", "HOME", "TMPDIR", "LANG", "LC_ALL"],
|
|
22
23
|
env: { GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_NO_REPLACE_OBJECTS: "1" },
|
|
@@ -24,15 +25,23 @@ const ADMISSION_GIT = {
|
|
|
24
25
|
export const PROJECT_ADDED = "project added";
|
|
25
26
|
export const PROJECT_REMOVED = "project removed";
|
|
26
27
|
/** The checkout root a folder belongs to, canonical; never a shell. */
|
|
27
|
-
export async function proveProjectRoot(path, exec = run) {
|
|
28
|
+
export async function proveProjectRoot(path, exec = run, resolveFolder) {
|
|
28
29
|
if (path.trim() === "" || path.length > 4096 || /[\x00-\x1f]/.test(path)) {
|
|
29
30
|
return { ok: false, reason: "unavailable", message: "Choose a valid project folder." };
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
+
// The console resolves both folders off its thread (project-identity.ts resolveNow): its thread never waits on one.
|
|
33
|
+
const canon = async (one) => resolveFolder === undefined ? canonicalProject(one) : resolveFolder(one);
|
|
34
|
+
const didntAnswer = (one) => ({ ok: false, reason: "unavailable", message: `${one} didn't answer. It may be on a drive that's disconnected, or waiting on macOS; try again in a moment.` });
|
|
35
|
+
const canonical = await canon(path);
|
|
36
|
+
if (canonical === "no-answer")
|
|
37
|
+
return didntAnswer(path);
|
|
32
38
|
if (canonical === null)
|
|
33
39
|
return { ok: false, reason: "unavailable", message: `${path} isn't a folder on this computer.` };
|
|
34
40
|
const top = await exec("git", ["rev-parse", "--show-toplevel"], { cwd: canonical, timeoutMs: 5_000, ...ADMISSION_GIT });
|
|
35
|
-
const
|
|
41
|
+
const topReal = top.code === 0 ? await canon(top.stdout.trim()) : null;
|
|
42
|
+
if (topReal === "no-answer")
|
|
43
|
+
return didntAnswer(top.stdout.trim());
|
|
44
|
+
const repo = topReal;
|
|
36
45
|
if (repo === null)
|
|
37
46
|
return { ok: false, reason: "not-git", message: `${path} isn't a Git repository. Run git init there first, or choose a folder inside one.` };
|
|
38
47
|
return { ok: true, repo };
|
|
@@ -48,7 +57,7 @@ async function originOf(repo, exec) {
|
|
|
48
57
|
}
|
|
49
58
|
/** Registry first, then the project and its audit records in one transaction. Re-adds preserve trust. */
|
|
50
59
|
export async function admitProject(store, input) {
|
|
51
|
-
const proved = await proveProjectRoot(input.path, input.exec);
|
|
60
|
+
const proved = await proveProjectRoot(input.path, input.exec, input.resolveFolder);
|
|
52
61
|
if (!proved.ok)
|
|
53
62
|
return proved;
|
|
54
63
|
const { repo } = proved;
|
|
@@ -110,12 +119,40 @@ export async function admitProject(store, input) {
|
|
|
110
119
|
* saved project changes nothing.
|
|
111
120
|
*/
|
|
112
121
|
export async function releaseProject(store, input) {
|
|
113
|
-
|
|
122
|
+
// Removing only narrows: a folder the console's gate holds back (folder-gate.ts) is removed by its exact saved path,
|
|
123
|
+
// and matches no other spelling.
|
|
124
|
+
// Latched for the whole removal: once its folder was unavailable, a later answer (during an await below) never
|
|
125
|
+
// widens it to other spellings.
|
|
126
|
+
const unavailable = (error) => error instanceof FolderUnavailable;
|
|
127
|
+
let asked, exactOnly = false;
|
|
128
|
+
try {
|
|
129
|
+
asked = canonicalProject(input.path) ?? resolve(input.path);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (!unavailable(error))
|
|
133
|
+
throw error;
|
|
134
|
+
asked = resolve(input.path);
|
|
135
|
+
exactOnly = true;
|
|
136
|
+
}
|
|
137
|
+
const same = (one) => {
|
|
138
|
+
if (exactOnly)
|
|
139
|
+
return false;
|
|
140
|
+
try {
|
|
141
|
+
return sameRepo(one, asked);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (unavailable(error)) {
|
|
145
|
+
exactOnly = true;
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
114
151
|
const loaded = input.registryFile === null ? { repos: [] } : await loadProjectRegistry(input.registryFile);
|
|
115
152
|
if ("error" in loaded)
|
|
116
153
|
return { ok: false, reason: "registry", message: loaded.error };
|
|
117
|
-
const entries = loaded.repos.filter(one => one === asked ||
|
|
118
|
-
const rows = store.listProjects().filter(one => one.path === asked ||
|
|
154
|
+
const entries = loaded.repos.filter(one => one === asked || same(one));
|
|
155
|
+
const rows = store.listProjects().filter(one => one.path === asked || same(one.path)).map(one => one.path);
|
|
119
156
|
const repo = entries[0] ?? rows[0] ?? asked;
|
|
120
157
|
if (entries.length === 0 && rows.length === 0)
|
|
121
158
|
return { ok: true, repo, name: projectName(repo), removed: false };
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* An embedder or test that passes no identities gets `syncProjectIdentities`: the old synchronous reads, unchanged.
|
|
18
18
|
*/
|
|
19
|
+
import { type FolderGate } from "./folder-gate.js";
|
|
19
20
|
import { type FolderLookOptions } from "./folder-look.js";
|
|
20
21
|
export type ProjectIdentities = {
|
|
21
22
|
/** For display and grouping only: the last known identity, else the path as saved (resolved lexically). Reads
|
|
@@ -36,9 +37,16 @@ export type ProjectIdentities = {
|
|
|
36
37
|
/** Moves whenever a known identity changes or goes: a decision made before an await is re-checked against it right
|
|
37
38
|
* before its write. */
|
|
38
39
|
generation: () => number;
|
|
40
|
+
/** Whether a synchronous read inside `path` may happen on the console's thread now (folder-gate.ts): the folder it is
|
|
41
|
+
* in answered a look within the last few minutes, and no look at it since went unanswered. Reads nothing. */
|
|
42
|
+
answering: (path: string) => boolean;
|
|
39
43
|
};
|
|
40
44
|
/** How many folders a pass looks at side by side when they're looked at one by one (within FOLDER_LOOK_CHILDREN). */
|
|
41
45
|
export declare const IDENTITY_LOOKS = 4;
|
|
42
46
|
export declare function projectIdentities(look?: FolderLookOptions, now?: () => number): ProjectIdentities;
|
|
43
47
|
/** The synchronous reads, as before: for embedders and tests that pass no identities. */
|
|
44
48
|
export declare function syncProjectIdentities(): ProjectIdentities;
|
|
49
|
+
/** The console's folder gate (folder-gate.ts): its own data folders (the database's, evidence, build checkouts) are read
|
|
50
|
+
* freely when the console names them; a path read from a file a build can write (`pointer`), and anything else, only
|
|
51
|
+
* while `answering` vouches for where it leads. */
|
|
52
|
+
export declare function consoleFolderGate(identities: ProjectIdentities, own: readonly (string | undefined)[]): FolderGate;
|