@design.estate/dees-catalog 9.8.0 → 11.0.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/dist_bundle/bundle.js +5626 -5343
- package/dist_bundle/bundle.js.map +1 -1
- package/dist_bundle/third-party-notices.md +783 -10
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/dist_ts_web/elements/00actionerror.d.ts +32 -0
- package/dist_ts_web/elements/00actionerror.js +45 -0
- package/dist_ts_web/elements/00group-appui/dees-appui-tabs/dees-appui-tabs.d.ts +16 -0
- package/dist_ts_web/elements/00group-appui/dees-appui-tabs/dees-appui-tabs.js +40 -3
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.d.ts +7 -0
- package/dist_ts_web/elements/00group-dataview/dees-table/dees-table.js +17 -17
- package/dist_ts_web/elements/00group-feedback/dees-hint/dees-hint.d.ts +14 -3
- package/dist_ts_web/elements/00group-feedback/dees-hint/dees-hint.demo.js +23 -4
- package/dist_ts_web/elements/00group-feedback/dees-hint/dees-hint.js +77 -5
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.d.ts +62 -48
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.js +89 -51
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.js +528 -662
- package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.d.ts +45 -6
- package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.js +184 -27
- package/dist_ts_web/elements/00group-harness/interfaces.d.ts +55 -20
- package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.d.ts +23 -3
- package/dist_ts_web/elements/00group-input/dees-input-text/dees-input-text.js +71 -11
- package/dist_ts_web/elements/00group-input/index.d.ts +0 -1
- package/dist_ts_web/elements/00group-input/index.js +1 -2
- package/dist_ts_web/elements/00group-overlay/dees-modal/dees-modal.d.ts +7 -0
- package/dist_ts_web/elements/00group-overlay/dees-modal/dees-modal.js +13 -3
- package/dist_ts_web/elements/index.d.ts +1 -2
- package/dist_ts_web/elements/index.js +2 -4
- package/dist_ts_web/services/DeesServiceLibLoader.d.ts +7 -0
- package/dist_ts_web/services/DeesServiceLibLoader.js +45 -22
- package/package.json +3 -4
- package/readme.md +105 -18
- package/scripts/check-third-party-notices.mjs +261 -0
- package/third-party-notices.md +783 -10
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00actionerror.ts +66 -0
- package/ts_web/elements/00group-appui/dees-appui-tabs/dees-appui-tabs.ts +38 -2
- package/ts_web/elements/00group-dataview/dees-table/dees-table.ts +18 -16
- package/ts_web/elements/00group-feedback/dees-hint/dees-hint.demo.ts +22 -3
- package/ts_web/elements/00group-feedback/dees-hint/dees-hint.ts +71 -3
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.ts +99 -49
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts +607 -752
- package/ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.ts +196 -24
- package/ts_web/elements/00group-harness/interfaces.ts +59 -19
- package/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts +73 -13
- package/ts_web/elements/00group-input/index.ts +0 -1
- package/ts_web/elements/00group-overlay/dees-modal/dees-modal.ts +13 -2
- package/ts_web/elements/index.ts +1 -4
- package/ts_web/services/DeesServiceLibLoader.ts +150 -102
- package/.smartconfig.json +0 -86
- package/dist_ts_web/elements/00group-input/dees-input-searchselect/dees-input-searchselect.d.ts +0 -1
- package/dist_ts_web/elements/00group-input/dees-input-searchselect/dees-input-searchselect.js +0 -2
- package/dist_ts_web/elements/00group-input/dees-input-searchselect/index.d.ts +0 -1
- package/dist_ts_web/elements/00group-input/dees-input-searchselect/index.js +0 -2
- package/dist_ts_web/elements/dees-search/dees-search.d.ts +0 -1
- package/dist_ts_web/elements/dees-search/dees-search.js +0 -2
- package/dist_ts_web/elements/dees-search/index.d.ts +0 -1
- package/dist_ts_web/elements/dees-search/index.js +0 -2
- package/dist_ts_web/elements/dees-tooltip/dees-tooltip.d.ts +0 -1
- package/dist_ts_web/elements/dees-tooltip/dees-tooltip.js +0 -2
- package/dist_ts_web/elements/dees-tooltip/index.d.ts +0 -1
- package/dist_ts_web/elements/dees-tooltip/index.js +0 -2
- package/ts_web/elements/00group-input/dees-input-searchselect/dees-input-searchselect.ts +0 -0
- package/ts_web/elements/00group-input/dees-input-searchselect/index.ts +0 -1
- package/ts_web/elements/dees-search/dees-search.ts +0 -0
- package/ts_web/elements/dees-search/index.ts +0 -1
- package/ts_web/elements/dees-tooltip/dees-tooltip.ts +0 -1
- package/ts_web/elements/dees-tooltip/index.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@design.estate/dees-catalog",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
|
6
6
|
"main": "dist_ts_web/index.js",
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
"@design.estate/dees-wcctools": "^3.9.4",
|
|
48
48
|
"@git.zone/tsbuild": "^4.4.3",
|
|
49
49
|
"@git.zone/tsbundle": "^2.13.0",
|
|
50
|
-
"@git.zone/tstest": "^6.
|
|
50
|
+
"@git.zone/tstest": "^6.1.0",
|
|
51
51
|
"@git.zone/tswatch": "^3.3.5",
|
|
52
52
|
"@push.rocks/projectinfo": "^5.1.0",
|
|
53
|
-
"@types/node": "^26.
|
|
53
|
+
"@types/node": "^26.5.0"
|
|
54
54
|
},
|
|
55
55
|
"files": [
|
|
56
56
|
"ts/**/*",
|
|
@@ -62,7 +62,6 @@
|
|
|
62
62
|
"assets/**/*",
|
|
63
63
|
"scripts/**/*",
|
|
64
64
|
"cli.js",
|
|
65
|
-
".smartconfig.json",
|
|
66
65
|
"readme.md",
|
|
67
66
|
"third-party-notices.md"
|
|
68
67
|
],
|
package/readme.md
CHANGED
|
@@ -229,7 +229,7 @@ task guide and examples deliberately; the generated table does not replace them.
|
|
|
229
229
|
| `dees-storage-keys` | dataview | — | [Source](ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-keys.ts) |
|
|
230
230
|
| `dees-storage-preview` | dataview | — | [Source](ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-preview.ts) |
|
|
231
231
|
| `dees-table` | dataview | [DeesTable](#deestable) | [Source](ts_web/elements/00group-dataview/dees-table/dees-table.ts) |
|
|
232
|
-
| `dees-terminal-view` | harness |
|
|
232
|
+
| `dees-terminal-view` | harness | [DeesTerminalView](#deesterminalview) | [Source](ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.ts) |
|
|
233
233
|
| `dees-theme` | utility | [DeesTheme](#deestheme) | [Source](ts_web/elements/00group-utility/dees-theme/dees-theme.ts) |
|
|
234
234
|
| `dees-thumbnail-audio` | media | [DeesThumbnailAudio](#deesthumbnailaudio) | [Source](ts_web/elements/00group-media/dees-thumbnail-audio/component.ts) |
|
|
235
235
|
| `dees-thumbnail-folder` | media | [DeesThumbnailFolder](#deesthumbnailfolder) | [Source](ts_web/elements/00group-media/dees-thumbnail-folder/component.ts) |
|
|
@@ -257,6 +257,28 @@ task guide and examples deliberately; the generated table does not replace them.
|
|
|
257
257
|
|
|
258
258
|
## Component API
|
|
259
259
|
|
|
260
|
+
### Failed actions
|
|
261
|
+
|
|
262
|
+
Components that run an application callback — `dees-modal`'s `menuOptions[].action`
|
|
263
|
+
and `dees-table`'s `dataActions[].actionFunc` — await it. A rejected callback is
|
|
264
|
+
never dropped: the component dispatches the cancelable `dees-action-error` event
|
|
265
|
+
(bubbling and composed) and, unless a listener cancels it, shows the error through
|
|
266
|
+
`dees-toast`. Successful callbacks behave exactly as before and dispatch nothing.
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
element.addEventListener('dees-action-error', (event: CustomEvent<IActionErrorDetail>) => {
|
|
270
|
+
const { error, action, source } = event.detail;
|
|
271
|
+
// source: 'modal-menu' | 'table-header' | 'table-footer' | 'table-row'
|
|
272
|
+
// | 'table-rowmenu' | 'table-contextmenu' | 'table-doubleclick'
|
|
273
|
+
event.preventDefault(); // render your own error surface instead of the toast
|
|
274
|
+
reportToCrashService(error);
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`IActionErrorDetail`, `actionErrorEventName`, `actionErrorMessage`,
|
|
279
|
+
`reportActionError` and `runAction` are exported from the package root, so a
|
|
280
|
+
component outside the catalog can announce failures the same way.
|
|
281
|
+
|
|
260
282
|
### Core UI Components
|
|
261
283
|
|
|
262
284
|
#### `DeesButton`
|
|
@@ -446,14 +468,21 @@ Consistent heading component with level and styling options.
|
|
|
446
468
|
```
|
|
447
469
|
|
|
448
470
|
#### `DeesHint`
|
|
449
|
-
|
|
471
|
+
An inline advisory note next to the thing it explains. Use `text` for a plain
|
|
472
|
+
sentence and the default slot for markup; both render when both are set. The
|
|
473
|
+
`type` chooses the icon and the accent color. A hint with neither text nor
|
|
474
|
+
slotted content renders nothing and reserves no space, so a conditional note
|
|
475
|
+
needs no wrapper element.
|
|
450
476
|
|
|
451
477
|
```typescript
|
|
452
478
|
<dees-hint
|
|
453
|
-
text="
|
|
454
|
-
type="info" // Options: info, warning, error
|
|
455
|
-
position="top" // Options: top, bottom, left, right
|
|
479
|
+
text="Changing the URL breaks existing links."
|
|
480
|
+
type="info" // Options: info, success, warning, error (default: info)
|
|
456
481
|
></dees-hint>
|
|
482
|
+
|
|
483
|
+
<dees-hint type="warning">
|
|
484
|
+
Keys older than 90 days <strong>stop working</strong>.
|
|
485
|
+
</dees-hint>
|
|
457
486
|
```
|
|
458
487
|
|
|
459
488
|
#### `DeesPanel`
|
|
@@ -567,6 +596,7 @@ Text input field with validation, info tooltips, description text, and context m
|
|
|
567
596
|
<dees-input-text
|
|
568
597
|
key="email" // Unique identifier for form data
|
|
569
598
|
label="Email" // Input label
|
|
599
|
+
type="email" // Native input type: text (default), number, date, email, url, tel, password
|
|
570
600
|
value="initial@value.com" // Initial value
|
|
571
601
|
required // Makes the field required
|
|
572
602
|
disabled // Disables the input
|
|
@@ -585,6 +615,15 @@ Text input field with validation, info tooltips, description text, and context m
|
|
|
585
615
|
></dees-input-text>
|
|
586
616
|
```
|
|
587
617
|
|
|
618
|
+
- `type` maps to the native input type and picks the matching keyboard hint; an
|
|
619
|
+
explicit `inputmode` on the element still wins. `value` is a string for every
|
|
620
|
+
type except `number`, where it is a `number`, or `null` while the field is
|
|
621
|
+
empty — so a form collects a number rather than a numeric string. A value bound
|
|
622
|
+
as the wrong shape is converted to the one the type promises.
|
|
623
|
+
- `type="password"` masks the value. `isPasswordBool` masks it as well and adds
|
|
624
|
+
the reveal button that `showPasswordBool` toggles.
|
|
625
|
+
- `validationFunction` always receives the value as text, whatever the type.
|
|
626
|
+
|
|
588
627
|
> 💡 **All input components** share these common properties from `DeesInputBase`: `key`, `label`, `required`, `disabled`, `infoText`, `description`, `layoutMode`, `labelPosition`.
|
|
589
628
|
|
|
590
629
|
#### `DeesInputTextarea`
|
|
@@ -1505,6 +1544,10 @@ appui.setBottomBarVisible(false);
|
|
|
1505
1544
|
Content-panel navigation with native tab buttons, separate close controls and
|
|
1506
1545
|
arrow/Home/End focus movement. Enter/Space select; Delete closes a closeable tab.
|
|
1507
1546
|
`IMenuItem.key` remains the identity; optional `label` supplies display text.
|
|
1547
|
+
The selection follows that key, so a host that binds a literal `.tabs=${[...]}`
|
|
1548
|
+
keeps its open tab across rerenders: descriptors with the same key are adopted
|
|
1549
|
+
without re-running the tab's `action`. Only a key that is no longer present falls
|
|
1550
|
+
back to the first tab. Descriptors without a key fall back to their `label`.
|
|
1508
1551
|
Use multitoggle for a view mode within the current panel.
|
|
1509
1552
|
Both horizontal and vertical layouts use an inset selection surface.
|
|
1510
1553
|
`showTabIndicator=false` hides that surface while retaining selected text and ARIA
|
|
@@ -1564,6 +1607,7 @@ html`<dees-table
|
|
|
1564
1607
|
- Action `type` accepts `header`, `inRow`, `contextmenu`, `footer`, `doubleClick`, `preview`, and `keyCombination`. Use `actionRelevancyCheckFunc` for row-specific availability. Desktop rows show up to three relevant primary actions plus overflow; below 700px of table width, one Actions menu contains the row actions. Coarse pointers receive 44px targets independently of width. The opaque pinned column shows an edge shadow only while it covers scrolling columns.
|
|
1565
1608
|
- `fixedHeight` uses internal scrolling with sticky headers and `--table-max-height`; the default uses a floating header while the page scrolls. `virtualized` limits rendered rows and retains spacer geometry.
|
|
1566
1609
|
- Set a column's `editor` to `text`, `number`, `checkbox`, `dropdown`, `date`, or `tags`; `editable: true` defaults to text. Use `editorOptions`, `format`, `parse`, and `validate` for value handling. Double-click edits a cell; Enter/Tab commit and move, Escape cancels. `cellEdit` reports `{ row, key, oldValue, newValue }`; `cellEditError` reports validation failures.
|
|
1610
|
+
- Every `actionFunc` is awaited. A rejected action dispatches the cancelable `dees-action-error` event (see [Failed actions](#failed-actions)) instead of disappearing.
|
|
1567
1611
|
- File drop onto rows and `highlightUpdates: 'flash'` remain available. Flashing respects reduced motion and stable row identity.
|
|
1568
1612
|
|
|
1569
1613
|
#### `DeesDataviewCodebox`
|
|
@@ -2154,6 +2198,9 @@ runs before closing. Rejected operations leave the dialog open with their error,
|
|
|
2154
2198
|
and repeated submission and dismissal are blocked while confirmation is pending.
|
|
2155
2199
|
The modal emits `modal-close` when removed.
|
|
2156
2200
|
|
|
2201
|
+
Every `menuOptions` action is awaited. A rejected action keeps the dialog open and
|
|
2202
|
+
reports the failure through `dees-action-error` (see [Failed actions](#failed-actions)).
|
|
2203
|
+
|
|
2157
2204
|
The modal gallery includes a working settings draft: values survive navigation,
|
|
2158
2205
|
Save returns the draft, and Cancel discards it. The example lives in
|
|
2159
2206
|
`ts_web/elements/00group-overlay/dees-modal/dees-modal.demo.ts`.
|
|
@@ -2704,6 +2751,14 @@ Idle/busy/error line with animated dots and `aria-live="polite"`. Renders nothin
|
|
|
2704
2751
|
|
|
2705
2752
|
Token usage chip (`in · out · total`, `compact` shows the total only); cache read/write detail in the tooltip.
|
|
2706
2753
|
|
|
2754
|
+
#### `DeesTerminalView`
|
|
2755
|
+
|
|
2756
|
+
Transport-agnostic xterm.js surface. The host pipes remote PTY output in with `write(data)` and receives keystrokes as `terminal-input` (`{ data }`); there is no process, runtime or socket coupling. `terminal-resize` (`{ rows, cols }`) reports the grid after every refit so the host can negotiate a PTY size, and `currentSize` reads it back (`undefined` while no terminal is live). `fontSize` sets the grid font, `disabled` stops `terminal-input` from being emitted, and `focus()` focuses the grid. xterm is loaded through `DeesServiceLibLoader`; CSP-locked or offline hosts call `provideXtermModules()` first to supply their own bundled copy instead of the CDN.
|
|
2757
|
+
|
|
2758
|
+
`write()` is safe before the terminal exists and while the element is detached: output is buffered and replayed in order on the next connection, bounded by the exported `maxPendingWriteBytes` (8 MiB — `Uint8Array` chunks count their `byteLength`, strings their UTF-16 memory). Past that bound the oldest chunks are dropped and the replay emits RIS first, so a truncated prefix is never rendered as if it were contiguous. `clear()` resets screen and scrollback through the write stream (RIS, `ESC c`) rather than around it, so it is ordered against output already queued in xterm — write, `clear()`, write leaves only the later output on screen.
|
|
2759
|
+
|
|
2760
|
+
`ready` resolves once the terminal of the current connection is live; a fresh promise is created per connection, so a reattached element can be awaited again. Every connection builds a terminal and every disconnection disposes it, so an element moved between containers, or returned to, is a live surface rather than a dead one; buffered output survives the move. A failed xterm load is not an unhandled rejection: it dispatches the bubbling, composed `terminal-error` (`ITerminalErrorDetail { error }`), rejects `ready` with the same error, leaves the element writable, and is retried on the next connection. xterm's own flow-control refusal is reported the same way. Sizing comes from the host element — give it a bounded height, as the demo does.
|
|
2761
|
+
|
|
2707
2762
|
#### `DeesHarnessSessionSidebar`
|
|
2708
2763
|
|
|
2709
2764
|
Generic session-detail panel that composes Tasks, Tools & Subagents, Tokens, Session Scratchpad, and Session Intelligence sections. Headings stay fixed in a single-open accordion; Session Intelligence splits its expanded body into independently scrolling history and a docked question composer. Set `.toolMessages` to top-level tool messages from the currently retained transcript window and `.toolRegistry` for matching labels/icons; active subagents use authoritative child status, and removed messages disappear without a second history cache. The host also supplies `IHarnessSessionMetrics`, `IHarnessScratchpad`, and `IHarnessIntelligenceExchange[]`; the component never loads or persists data itself. Context percentage and its progress bar render only when both `currentContextTokens` and a positive `maxContextTokens` are present.
|
|
@@ -2722,13 +2777,25 @@ Each card exposes an Actions button. Actions, right-click, and ContextMenu/Shift
|
|
|
2722
2777
|
|
|
2723
2778
|
Attention and error rows retain a colored edge even when selected. Group headings count both states, including when collapsed. `working` controls the small activity pulse on normal/working rows, defaulting to `state === 'working'`; attention, error and finished indicators remain stationary. Reduced-motion preferences disable the pulse. State changes preserve row height, selection, expansion and host-provided order.
|
|
2724
2779
|
|
|
2725
|
-
|
|
2780
|
+
**Sessions and resources are peers in one ordered list.** Every sortable row — conversation or resource — carries a focusable grip: drag it anywhere the other kind can go, including between sessions and into or out of groups, or reorder it with ArrowUp, ArrowDown, Home, and End. Pointer dragging moves a preview the size of the dragged row, opens a matching drop slot, and animates surrounding rows. Reordering does not require `enableGrouping`, which now only adds group chrome; an active search query renders one flat, non-sortable, recency-ordered section and cancels a drag in flight.
|
|
2781
|
+
|
|
2782
|
+
Every move emits `harness-item-move` with `IHarnessItemMoveDetail`: `{ item, fromGroupId, toGroupId, beforeItem, source }`, where `item` and `beforeItem` are `IHarnessSessionListItemRef` values (`{ kind: 'session' | 'resource'; id }`) and a null `beforeItem` means append. Groups hold `itemIds: IHarnessSessionListItemRef[]`, and `ungroupedItemIds` controls the order outside groups; omitted or unknown entries of either kind fall back to `updatedAt ?? createdAt` recency. Set `draggable: false` on a session or a resource for pinned entries such as terminals; pinned entries sort first in the ungrouped section and stay outside the sortable order.
|
|
2726
2783
|
|
|
2727
2784
|
The compact **New group** button sits beside search whenever `enableGrouping` is true, including while filtering, and emits `harness-group-create-request`. The host owns creation; the list keeps its current search until the host chooses to clear it. The demo clears search after adding the group. Both search and the group action use 44px targets for coarse pointers.
|
|
2728
2785
|
|
|
2729
2786
|
The Sessions demo pairs the list with a selected-session workspace. Its controls step through starting, working, feedback, approval, completion and failure without timers or backend operations. Question and permission responses update the selected session; declining approval pauses it. The narrow layout switches between the preserved list and detail panes with an **All sessions** control that restores row focus.
|
|
2730
2787
|
|
|
2731
|
-
Generic host resources use the exported `IHarnessResourceMeta` and `IHarnessResourceAssociation` contracts. Set `.resources`, `.resourceAssociations`, `.selectedResourceId`, and `.
|
|
2788
|
+
Generic host resources use the exported `IHarnessResourceMeta` and `IHarnessResourceAssociation` contracts. Set `.resources`, `.resourceAssociations`, `.selectedResourceId`, and `.eligibleTargetsByResourceId`. Resource rows render as top-level rows in the ordered list, never as children of a session. Resource metadata is presentation-only: the list never mounts a terminal, browser, file viewer, or other runtime.
|
|
2789
|
+
|
|
2790
|
+
**Attaching is a separate explicit action, not a drop.** An association is an access grant: it shows as a decorative marker on the resource row (a link icon plus the target's title, resolved from `sessions` or `resources` regardless of search or collapse) and never moves the row. Dropping is purely positional and never attaches. The association stays single-valued — a resource has at most one target — and the target is any other item, so a resource may be attached to a session **or** to another resource such as a terminal:
|
|
2791
|
+
|
|
2792
|
+
```typescript
|
|
2793
|
+
list.resourceAssociations = [
|
|
2794
|
+
{ resourceId: 'file:notes', target: { kind: 'resource', id: 'terminal:build' } },
|
|
2795
|
+
];
|
|
2796
|
+
```
|
|
2797
|
+
|
|
2798
|
+
Association records are projected only when the resource exists, the target exists, the target is not the resource itself, and no earlier record claimed that resource. `eligibleTargetsByResourceId` restricts attach/reassign targets for host menus: a missing entry leaves the resource unrestricted, an empty array means no eligible target. Which resource kinds may host an attachment is the host's rule, not the component's.
|
|
2732
2799
|
|
|
2733
2800
|
```typescript
|
|
2734
2801
|
import type {
|
|
@@ -2743,17 +2810,17 @@ list.resources = [
|
|
|
2743
2810
|
{ id: 'file:notes', kind: 'file', title: 'Release notes', preview: '6.7.0 draft' },
|
|
2744
2811
|
];
|
|
2745
2812
|
list.resourceAssociations = [
|
|
2746
|
-
{ resourceId: 'shell:build',
|
|
2813
|
+
{ resourceId: 'shell:build', target: { kind: 'session', id: 'session-1' } },
|
|
2747
2814
|
];
|
|
2748
|
-
list.
|
|
2749
|
-
'shell:build': ['session-1', 'session-2'],
|
|
2750
|
-
'file:notes': ['session-2'],
|
|
2815
|
+
list.eligibleTargetsByResourceId = {
|
|
2816
|
+
'shell:build': [{ kind: 'session', id: 'session-1' }, { kind: 'session', id: 'session-2' }],
|
|
2817
|
+
'file:notes': [{ kind: 'session', id: 'session-2' }, { kind: 'resource', id: 'shell:build' }],
|
|
2751
2818
|
};
|
|
2752
2819
|
|
|
2753
2820
|
list.addEventListener('harness-resource-association-request', async (event) => {
|
|
2754
2821
|
const resourceEvent = event as CustomEvent<THarnessResourceAssociationRequestDetail>;
|
|
2755
|
-
const { resource,
|
|
2756
|
-
await persistAssociation(resource.id,
|
|
2822
|
+
const { resource, toTarget } = resourceEvent.detail;
|
|
2823
|
+
await persistAssociation(resource.id, toTarget);
|
|
2757
2824
|
list.resourceAssociations = await loadAssociations();
|
|
2758
2825
|
});
|
|
2759
2826
|
|
|
@@ -2762,20 +2829,20 @@ list.addEventListener('harness-resource-context', (event) => {
|
|
|
2762
2829
|
openHostMenu({
|
|
2763
2830
|
x: resourceEvent.detail.clientX,
|
|
2764
2831
|
y: resourceEvent.detail.clientY,
|
|
2765
|
-
attach: (
|
|
2832
|
+
attach: (target) => resourceEvent.detail.requestAssociation(target),
|
|
2766
2833
|
detach: () => resourceEvent.detail.requestAssociation(null),
|
|
2767
2834
|
onClose: resourceEvent.detail.close,
|
|
2768
2835
|
});
|
|
2769
2836
|
});
|
|
2770
2837
|
```
|
|
2771
2838
|
|
|
2772
|
-
`harness-resource-select` and `harness-resource-open` carry `{ resource,
|
|
2839
|
+
`harness-resource-select` and `harness-resource-open` carry `{ resource, target }`, where `target` is the controlled association ref or null. `harness-resource-context` adds viewport coordinates, the pointer/Actions/keyboard source, the original event, `requestAssociation(toTarget)`, and `close()` for focus restoration. The visible Actions button has menu disclosure semantics; native `ContextMenu` and `Shift+F10` invoke the same host-owned path. The component does not require `DeesContextmenu` and does not own menu choices.
|
|
2773
2840
|
|
|
2774
|
-
All component-originated association request paths converge on the public `requestResourceAssociation(resourceId,
|
|
2841
|
+
All component-originated association request paths converge on the public `requestResourceAssociation(resourceId, toTarget, source?)` method. A valid change emits one bubbling/composed `harness-resource-association-request` discriminated by `intent: 'attach' | 'detach' | 'reassign'` and carrying `resource`, `fromTarget`, `toTarget`, and `source: 'action' | 'keyboard'`. The method returns `false` and emits nothing for unknown resources, unknown or ineligible targets, a target that is the resource itself, detach from an unattached resource, or the current target. It never mutates resources, associations, selection, groups, or order.
|
|
2775
2842
|
|
|
2776
|
-
Association projection is deterministic and non-destructive: resource records without a non-empty ID, kind, or title, unknown resource/
|
|
2843
|
+
Association projection is deterministic and non-destructive: resource records without a non-empty ID, kind, or title, unknown resource/target references, self-targets, and later duplicate records for a resource are ignored; the first valid association wins. Duplicate resource metadata IDs likewise use the first resource. Eligibility does not hide an existing authoritative association, which remains visible and detachable. Requested changes are announced separately from completion; completion is announced only after the controlled association input reflects the requested target.
|
|
2777
2844
|
|
|
2778
|
-
Search covers resource ID, kind, title, and preview
|
|
2845
|
+
Search covers resource ID, kind, title, and preview, and matches sessions and resources as peers — a matching session no longer reveals its attached resources, and a matching resource no longer reveals its target. Resources render with zero sessions. Resource dragging edge-scrolls the list exactly like session dragging and emits only `harness-item-move`. Use the Actions/context path for keyboard and mobile association workflows; coarse-pointer grip and Actions targets are at least 44px.
|
|
2779
2846
|
|
|
2780
2847
|
---
|
|
2781
2848
|
|
|
@@ -2949,9 +3016,19 @@ and selecting a kit with `dees-input-multitoggle`. Its Review bag action places
|
|
|
2949
3016
|
The library exports unified interfaces for consistent API patterns:
|
|
2950
3017
|
|
|
2951
3018
|
```typescript
|
|
3019
|
+
// Detail of the cancelable `dees-action-error` event (see Failed actions)
|
|
3020
|
+
interface IActionErrorDetail<TAction = unknown> {
|
|
3021
|
+
error: unknown; // whatever the action callback rejected with
|
|
3022
|
+
action: TAction; // the action descriptor whose callback rejected
|
|
3023
|
+
source: string; // where it ran, e.g. 'modal-menu' or 'table-row'
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2952
3026
|
// Base menu item interface (used by tabs, menus, etc.)
|
|
3027
|
+
// `key` is the stable identity: tab selection follows it across rerenders.
|
|
3028
|
+
// Give every tab a unique `key`: when it is absent the `label` is used, so two keyless tabs with the same label are the same tab.
|
|
2953
3029
|
interface IMenuItem {
|
|
2954
3030
|
key: string;
|
|
3031
|
+
label?: string; // display text; falls back to `key`
|
|
2955
3032
|
iconName?: string;
|
|
2956
3033
|
action: () => void;
|
|
2957
3034
|
badge?: string | number;
|
|
@@ -3045,6 +3122,16 @@ module worker under the Apache License 2.0. The attribution and complete license
|
|
|
3045
3122
|
provided in [third-party-notices.md](./third-party-notices.md) and copied into
|
|
3046
3123
|
`dist_bundle/` for standalone bundle distributions.
|
|
3047
3124
|
|
|
3125
|
+
[third-party-notices.md](./third-party-notices.md) lists every library the package
|
|
3126
|
+
embeds in `dist_bundle/bundle.js` and every library it loads into the host page at
|
|
3127
|
+
runtime (ECharts, highlight.js, Lightweight Charts, Monaco, xterm). That list is
|
|
3128
|
+
generated from the built artifact and the installed packages — a dependency added,
|
|
3129
|
+
removed, or re-pinned without regenerating fails `pnpm test`:
|
|
3130
|
+
|
|
3131
|
+
```sh
|
|
3132
|
+
node scripts/check-third-party-notices.mjs --write
|
|
3133
|
+
```
|
|
3134
|
+
|
|
3048
3135
|
## License and Legal Information
|
|
3049
3136
|
|
|
3050
3137
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Third-party notices for everything this package ships or loads.
|
|
4
|
+
*
|
|
5
|
+
* The embedded list is derived from dist_bundle itself: esbuild keeps a
|
|
6
|
+
* `// node_modules/...` comment per embedded module, so the set of embedded
|
|
7
|
+
* packages is read off the built artifact instead of a hand-kept list that
|
|
8
|
+
* silently rots when a dependency is added, removed, or made lazy. License
|
|
9
|
+
* metadata is read from the installed packages, never transcribed here.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node scripts/check-third-party-notices.mjs # verify (also covered by tests)
|
|
13
|
+
* node scripts/check-third-party-notices.mjs --write # regenerate the managed section
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
export const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
export const noticesPath = path.join(projectRoot, 'third-party-notices.md');
|
|
21
|
+
export const bundleDirectory = path.join(projectRoot, 'dist_bundle');
|
|
22
|
+
export const startMarker = '<!-- bundled-notices:start -->';
|
|
23
|
+
export const endMarker = '<!-- bundled-notices:end -->';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Libraries the catalog does not embed but pins and loads into the host page at
|
|
27
|
+
* runtime (the CDN modules behind DeesServiceLibLoader and dees-workspace-monaco).
|
|
28
|
+
* Nothing in the built artifact carries their code, so they are named here; their
|
|
29
|
+
* license metadata still comes from the installed package where one exists.
|
|
30
|
+
* Mirrors ts_web/services/versions.ts.
|
|
31
|
+
*/
|
|
32
|
+
export const runtimeLoadedLibraries = [
|
|
33
|
+
{ name: 'echarts' },
|
|
34
|
+
{ name: 'highlight.js' },
|
|
35
|
+
{ name: 'lightweight-charts' },
|
|
36
|
+
{ name: 'monaco-editor' },
|
|
37
|
+
{ name: 'xterm' },
|
|
38
|
+
{ name: 'xterm-addon-fit' },
|
|
39
|
+
// Loaded by URL only; the catalog does not depend on it for types.
|
|
40
|
+
{ name: 'xterm-addon-search', version: '0.13.0', license: 'MIT' },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const listJsFiles = (directoryArg) => {
|
|
44
|
+
const files = [];
|
|
45
|
+
for (const entry of fs.readdirSync(directoryArg, { withFileTypes: true })) {
|
|
46
|
+
const entryPath = path.join(directoryArg, entry.name);
|
|
47
|
+
if (entry.isDirectory()) files.push(...listJsFiles(entryPath));
|
|
48
|
+
else if (entry.name.endsWith('.js')) files.push(entryPath);
|
|
49
|
+
}
|
|
50
|
+
return files;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** The package directory a bundled module path belongs to. */
|
|
54
|
+
const packageDirectoryOf = (modulePathArg) => {
|
|
55
|
+
const marker = '/node_modules/';
|
|
56
|
+
const index = modulePathArg.lastIndexOf(marker);
|
|
57
|
+
if (index < 0) return null;
|
|
58
|
+
const rest = modulePathArg.slice(index + marker.length).split('/');
|
|
59
|
+
const name = rest[0].startsWith('@') ? `${rest[0]}/${rest[1]}` : rest[0];
|
|
60
|
+
if (!name || name.startsWith('.')) return null;
|
|
61
|
+
return modulePathArg.slice(0, index + marker.length) + name;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const readIfPresent = (directoryArg, matcherArg) => {
|
|
65
|
+
const entry = fs.readdirSync(directoryArg).find(matcherArg);
|
|
66
|
+
if (!entry) return null;
|
|
67
|
+
const text = fs.readFileSync(path.join(directoryArg, entry), 'utf8').replace(/\r\n/g, '\n').trim();
|
|
68
|
+
return text || null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** The license file that matches the elected license id, else the generic one. */
|
|
72
|
+
const readLicenseText = (packageDirectoryArg, licenseIdArg) => {
|
|
73
|
+
const entries = fs.readdirSync(packageDirectoryArg);
|
|
74
|
+
const exact = entries.find((entry) => entry.toLowerCase() === `license.${licenseIdArg.toLowerCase()}`);
|
|
75
|
+
const generic = entries.find((entry) => /^(licen[cs]e|copying)(\.(md|txt))?$/i.test(entry));
|
|
76
|
+
const chosen = exact ?? generic;
|
|
77
|
+
if (!chosen) return null;
|
|
78
|
+
const text = fs.readFileSync(path.join(packageDirectoryArg, chosen), 'utf8').replace(/\r\n/g, '\n').trim();
|
|
79
|
+
// A stub that only points at a sibling file is not the license text.
|
|
80
|
+
return text.length < 200 ? null : text;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A copyright notice, not prose that happens to contain the word: license bodies
|
|
85
|
+
* wrap sentences onto lines starting with a lowercase "copyright", and the Apache
|
|
86
|
+
* appendix carries a bracketed placeholder.
|
|
87
|
+
*/
|
|
88
|
+
const isCopyrightLine = (lineArg) => /^\s*Copyright\b/.test(lineArg)
|
|
89
|
+
&& !/\[yyyy\]|\[name of copyright owner\]/i.test(lineArg);
|
|
90
|
+
|
|
91
|
+
const copyrightLinesOf = (...textsArg) => [...new Set(
|
|
92
|
+
textsArg.filter(Boolean).flatMap((text) => text.split('\n'))
|
|
93
|
+
.filter(isCopyrightLine)
|
|
94
|
+
.map((line) => line.replace(/^\s*[-*#>]*\s*/, '').trim())
|
|
95
|
+
.filter((line) => line.length > 4)
|
|
96
|
+
)];
|
|
97
|
+
|
|
98
|
+
/** A dual license is consumed under its first option; that election is recorded. */
|
|
99
|
+
const electedLicense = (rawArg) => {
|
|
100
|
+
const raw = typeof rawArg === 'string' ? rawArg : rawArg?.type ?? 'UNKNOWN';
|
|
101
|
+
const options = raw.split(/\s+(?:or|OR)\s+|\//).map((option) => option.replace(/[()]/g, '').trim()).filter(Boolean);
|
|
102
|
+
return { elected: options[0] || 'UNKNOWN', raw, options };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const describePackage = (packageDirectoryArg, distributionArg) => {
|
|
106
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(packageDirectoryArg, 'package.json'), 'utf8'));
|
|
107
|
+
const { elected, raw, options } = electedLicense(manifest.license ?? manifest.licenses?.[0]?.type);
|
|
108
|
+
const licenseText = readLicenseText(packageDirectoryArg, elected);
|
|
109
|
+
// Apache-2.0 section 4(d): a NOTICE file travels with the distribution.
|
|
110
|
+
const noticeText = readIfPresent(packageDirectoryArg, (entry) => /^notice(\.(md|txt))?$/i.test(entry));
|
|
111
|
+
return {
|
|
112
|
+
name: manifest.name,
|
|
113
|
+
version: manifest.version,
|
|
114
|
+
license: elected,
|
|
115
|
+
licenseRaw: raw,
|
|
116
|
+
dualLicensed: options.length > 1,
|
|
117
|
+
licenseText,
|
|
118
|
+
noticeText,
|
|
119
|
+
distribution: distributionArg,
|
|
120
|
+
copyrights: copyrightLinesOf(licenseText, noticeText),
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const sortPackages = (entriesArg) => [...entriesArg].sort((a, b) =>
|
|
125
|
+
a.name.localeCompare(b.name, 'en') || a.version.localeCompare(b.version, 'en'));
|
|
126
|
+
|
|
127
|
+
/** Every npm package whose code is embedded in the built bundle. */
|
|
128
|
+
export const collectBundledPackages = () => {
|
|
129
|
+
if (!fs.existsSync(bundleDirectory)) {
|
|
130
|
+
throw new Error('dist_bundle is missing. Run pnpm build before checking third-party notices.');
|
|
131
|
+
}
|
|
132
|
+
const directories = new Set();
|
|
133
|
+
for (const file of listJsFiles(bundleDirectory)) {
|
|
134
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
135
|
+
for (const match of content.matchAll(/^\/\/ (node_modules\/\S+)/gm)) {
|
|
136
|
+
const directory = packageDirectoryOf(match[1]);
|
|
137
|
+
if (directory) directories.add(directory);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const packages = new Map();
|
|
141
|
+
for (const directory of directories) {
|
|
142
|
+
const absolute = path.join(projectRoot, directory);
|
|
143
|
+
if (!fs.existsSync(path.join(absolute, 'package.json'))) {
|
|
144
|
+
throw new Error(`Bundled package without a manifest: ${directory}`);
|
|
145
|
+
}
|
|
146
|
+
const entry = describePackage(absolute, 'bundled');
|
|
147
|
+
packages.set(`${entry.name}@${entry.version}`, entry);
|
|
148
|
+
}
|
|
149
|
+
return sortPackages(packages.values());
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** The pinned libraries the catalog loads into the host page at runtime. */
|
|
153
|
+
export const collectRuntimePackages = () => runtimeLoadedLibraries.map((library) => {
|
|
154
|
+
const directory = path.join(projectRoot, 'node_modules', library.name);
|
|
155
|
+
if (fs.existsSync(path.join(directory, 'package.json'))) {
|
|
156
|
+
return describePackage(directory, 'runtime');
|
|
157
|
+
}
|
|
158
|
+
if (!library.version || !library.license) {
|
|
159
|
+
throw new Error(`Runtime library ${library.name} is not installed and declares no version or license`);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
name: library.name, version: library.version, license: library.license, licenseRaw: library.license,
|
|
163
|
+
dualLicensed: false, licenseText: null, noticeText: null, distribution: 'runtime', copyrights: [],
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export const collectPackages = () => [...collectBundledPackages(), ...collectRuntimePackages()];
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Identical license wording is printed once, with every copyright notice it
|
|
171
|
+
* covers. Two texts are the same terms when their operative part matches; the
|
|
172
|
+
* Apache appendix is instructions for applying the license, not a term, and
|
|
173
|
+
* packages ship it filled in, blank, or not at all. The longest text in a group
|
|
174
|
+
* is printed verbatim, so nothing is summarized away.
|
|
175
|
+
*/
|
|
176
|
+
const groupByLicenseText = (packagesArg) => {
|
|
177
|
+
const groups = new Map();
|
|
178
|
+
for (const entry of packagesArg) {
|
|
179
|
+
const normalized = (entry.licenseText ?? '')
|
|
180
|
+
.split('\n').filter((line) => !isCopyrightLine(line)).join('\n')
|
|
181
|
+
.replace(/\s+/g, ' ').trim();
|
|
182
|
+
const terminator = normalized.indexOf('END OF TERMS AND CONDITIONS');
|
|
183
|
+
const key = `${entry.license} ${terminator < 0 ? normalized : normalized.slice(0, terminator)}`;
|
|
184
|
+
if (!groups.has(key)) groups.set(key, { license: entry.license, text: entry.licenseText, packages: [] });
|
|
185
|
+
const group = groups.get(key);
|
|
186
|
+
group.packages.push(entry);
|
|
187
|
+
if ((entry.licenseText?.length ?? 0) > (group.text?.length ?? 0)) group.text = entry.licenseText;
|
|
188
|
+
}
|
|
189
|
+
return [...groups.values()].sort((a, b) =>
|
|
190
|
+
a.license.localeCompare(b.license, 'en') || a.packages[0].name.localeCompare(b.packages[0].name, 'en'));
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const listEntry = (entryArg) => `- ${entryArg.name} ${entryArg.version} — `
|
|
194
|
+
+ (entryArg.dualLicensed ? `${entryArg.licenseRaw}, used here under ${entryArg.license}` : entryArg.license);
|
|
195
|
+
|
|
196
|
+
export const renderManagedSection = (packagesArg) => {
|
|
197
|
+
const bundled = packagesArg.filter((entry) => entry.distribution === 'bundled');
|
|
198
|
+
const runtime = packagesArg.filter((entry) => entry.distribution === 'runtime');
|
|
199
|
+
const lines = [startMarker, '', '## Third-party libraries', ''];
|
|
200
|
+
lines.push('This list is generated from the built artifact and the installed packages by');
|
|
201
|
+
lines.push('`scripts/check-third-party-notices.mjs`. License terms follow below, with identical');
|
|
202
|
+
lines.push('wording reproduced once and every copyright notice it covers listed with it.', '');
|
|
203
|
+
lines.push('### Embedded in the bundle', '');
|
|
204
|
+
lines.push('`dist_bundle/bundle.js` and its chunks contain the code of these packages.', '');
|
|
205
|
+
for (const entry of bundled) lines.push(listEntry(entry));
|
|
206
|
+
lines.push('', '### Loaded at runtime', '');
|
|
207
|
+
lines.push('These are not embedded. The catalog pins their versions and loads them into the host');
|
|
208
|
+
lines.push('page on demand, so their notices are reproduced here as well.', '');
|
|
209
|
+
for (const entry of runtime) lines.push(listEntry(entry));
|
|
210
|
+
lines.push('', '## License terms', '');
|
|
211
|
+
for (const group of groupByLicenseText(packagesArg)) {
|
|
212
|
+
const names = group.packages.map((entry) => `${entry.name} ${entry.version}`).join(', ');
|
|
213
|
+
const copyrights = [...new Set(group.packages.flatMap((entry) => entry.copyrights))].sort();
|
|
214
|
+
const notices = [...new Set(group.packages.filter((entry) => entry.noticeText).map((entry) => entry.noticeText))];
|
|
215
|
+
lines.push(`### ${group.license} — ${group.packages.length === 1 ? group.packages[0].name : `${group.packages.length} packages`}`, '');
|
|
216
|
+
lines.push(`Applies to: ${names}.`, '');
|
|
217
|
+
if (copyrights.length) {
|
|
218
|
+
lines.push('Copyright notices:', '');
|
|
219
|
+
for (const copyright of copyrights) lines.push(`- ${copyright}`);
|
|
220
|
+
lines.push('');
|
|
221
|
+
}
|
|
222
|
+
for (const notice of notices) lines.push('Attribution notice:', '', '```text', notice, '```', '');
|
|
223
|
+
if (group.text) {
|
|
224
|
+
// Verbatim, as published by the package: a license text is not ours to edit.
|
|
225
|
+
lines.push('```text', group.text, '```', '');
|
|
226
|
+
} else {
|
|
227
|
+
lines.push(`These packages publish no license file; they are distributed under ${group.license}.`, '');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
lines.push(endMarker);
|
|
231
|
+
return lines.join('\n');
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export const readNotices = () => fs.readFileSync(noticesPath, 'utf8');
|
|
235
|
+
|
|
236
|
+
export const renderNotices = (currentArg, packagesArg) => {
|
|
237
|
+
const first = currentArg.indexOf(startMarker);
|
|
238
|
+
const last = currentArg.indexOf(endMarker);
|
|
239
|
+
if (first < 0 || last < first) {
|
|
240
|
+
throw new Error('third-party-notices.md is missing the bundled-notices markers');
|
|
241
|
+
}
|
|
242
|
+
return currentArg.slice(0, first) + renderManagedSection(packagesArg) + currentArg.slice(last + endMarker.length);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
246
|
+
if (isCli) {
|
|
247
|
+
const packages = collectPackages();
|
|
248
|
+
const current = readNotices();
|
|
249
|
+
const generated = renderNotices(current, packages);
|
|
250
|
+
const counts = `${packages.filter((entry) => entry.distribution === 'bundled').length} bundled`
|
|
251
|
+
+ ` and ${packages.filter((entry) => entry.distribution === 'runtime').length} runtime libraries`;
|
|
252
|
+
if (process.argv.includes('--write')) {
|
|
253
|
+
fs.writeFileSync(noticesPath, generated);
|
|
254
|
+
console.log(`[third-party-notices] wrote ${counts}`);
|
|
255
|
+
} else if (current !== generated) {
|
|
256
|
+
console.error('[third-party-notices] FAIL — the notices are stale. Run node scripts/check-third-party-notices.mjs --write');
|
|
257
|
+
process.exitCode = 1;
|
|
258
|
+
} else {
|
|
259
|
+
console.log(`[third-party-notices] OK — ${counts} listed`);
|
|
260
|
+
}
|
|
261
|
+
}
|