kennel 0.2.6 → 0.2.7
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/README.md +9 -6
- package/dist/cli/src/plugins/external.d.ts +3 -0
- package/dist/cli/src/plugins/external.js +22 -10
- package/dist/cli/src/site/hot-viewer.js +45 -38
- package/dist/cli/src/site/viewer.js +13 -8
- package/dist/plugins/external.d.ts +3 -0
- package/dist/plugins/external.js +22 -10
- package/dist/site/hot-viewer.js +45 -38
- package/dist/site/viewer.js +13 -8
- package/kernel/apps/web/scripts/dev.mjs +4 -0
- package/kernel/apps/web/src/api/events.ts +60 -24
- package/kernel/apps/web/vite.config.ts +2 -0
- package/kernel/packages/notes/src/pages/notes-page.tsx +34 -1
- package/kernel/packages/notes/src/ui/folder-notes-list.tsx +10 -74
- package/kernel/packages/plugins/package.json +1 -1
- package/package.json +2 -2
- package/web/assets/{abnfDiagram-VRR7QNED-xd_g1uK3.js → abnfDiagram-VRR7QNED-CDr-U31F.js} +1 -1
- package/web/assets/app-DHGorUqZ.js +2 -0
- package/web/assets/app-options-CtkiC8GR.js +25 -0
- package/web/assets/{app-view-header-CNk5KnA-.js → app-view-header-DbQuD6_F.js} +1 -1
- package/web/assets/{arc-B0u5yBJA.js → arc-Cwz5OeLQ.js} +1 -1
- package/web/assets/{architectureDiagram-ZJ3FMSHR-DfxOAo8P.js → architectureDiagram-ZJ3FMSHR-igGC0b-J.js} +1 -1
- package/web/assets/{ask-tray-SscJecDY.js → ask-tray-y8nkSKNG.js} +1 -1
- package/web/assets/{blockDiagram-677ZJIJ3-Ci3Usxh6.js → blockDiagram-677ZJIJ3-D4SDltQ_.js} +1 -1
- package/web/assets/{c4Diagram-LMCZKHZV-BqgwIVgh.js → c4Diagram-LMCZKHZV-Br15uN-s.js} +1 -1
- package/web/assets/channel-DRnAhH31.js +1 -0
- package/web/assets/{chunk-2Q5K7J3B-CfX69l2U.js → chunk-2Q5K7J3B-B0RqrvJA.js} +1 -1
- package/web/assets/{chunk-32BRIVSS-CuFlzZkl.js → chunk-32BRIVSS-De9VBEyE.js} +1 -1
- package/web/assets/{chunk-5VM5RSS4-DidCMlJa.js → chunk-5VM5RSS4-YIMlZtW3.js} +1 -1
- package/web/assets/{chunk-EX3LRPZG-B5CDm1dS.js → chunk-EX3LRPZG-y9kGcrYi.js} +1 -1
- package/web/assets/{chunk-JWPE2WC7-D5fGV_sH.js → chunk-JWPE2WC7-D0A13HAu.js} +1 -1
- package/web/assets/{chunk-MOJQB5TN-ayhH5Q-H.js → chunk-MOJQB5TN-8v4jL8iu.js} +1 -1
- package/web/assets/{chunk-RYQCIY6F-C4BfOOZn.js → chunk-RYQCIY6F-Ri_iNmIp.js} +1 -1
- package/web/assets/{chunk-V7JOEXUC-DpUEIM9V.js → chunk-V7JOEXUC-b0mIbG_X.js} +1 -1
- package/web/assets/{chunk-VR4S4FIN-DoQuFFRh.js → chunk-VR4S4FIN-BkTZWt1n.js} +1 -1
- package/web/assets/{chunk-XXDRQBXY-CI9eiLwn.js → chunk-XXDRQBXY-D7muOXcd.js} +1 -1
- package/web/assets/classDiagram-OUVF2IWQ-BPkIpFlc.js +1 -0
- package/web/assets/classDiagram-v2-EOCWNBFH-BPkIpFlc.js +1 -0
- package/web/assets/{client-Dk-NBvJR.js → client-BMso7lHb.js} +2 -2
- package/web/assets/client-Bkl3gwZ5.js +17 -0
- package/web/assets/client-soK_bLWT.js +5 -0
- package/web/assets/{command-zkzH4hVz.js → command-CsN8TfJg.js} +1 -1
- package/web/assets/{copy-context-D5oiWrYC.js → copy-context-DmBNpffM.js} +1 -1
- package/web/assets/{cose-bilkent-JH36ORCC-BHRk1mO3.js → cose-bilkent-JH36ORCC-7hnQZylB.js} +1 -1
- package/web/assets/{cynefin-VYW2F7L2-C1B09TRE.js → cynefin-VYW2F7L2-CTbqXuvk.js} +1 -1
- package/web/assets/{cynefinDiagram-TSTJHNR4-B27qXQbv.js → cynefinDiagram-TSTJHNR4-CmIJtocX.js} +1 -1
- package/web/assets/{dagre-VKFMJZFB-B1qEi32g.js → dagre-VKFMJZFB-BLPHNs2Y.js} +1 -1
- package/web/assets/{diagram-FQU43EPY-BMkkgADD.js → diagram-FQU43EPY-L6FVWAuj.js} +1 -1
- package/web/assets/{diagram-G47NLZAW-Cb8_g1A5.js → diagram-G47NLZAW-Bb26dbIM.js} +1 -1
- package/web/assets/{diagram-NH7WQ7WH-B51wuc5m.js → diagram-NH7WQ7WH-BBauwdU-.js} +1 -1
- package/web/assets/{diagram-OA4YK3LP-xnsecbbI.js → diagram-OA4YK3LP-Dj_9FE7R.js} +1 -1
- package/web/assets/{diagram-WEI45ONY-DSZdyaAV.js → diagram-WEI45ONY-Co1MS5BO.js} +1 -1
- package/web/assets/{document-Dc2dftTW.js → document-_ETc5epn.js} +3 -3
- package/web/assets/{ebnfDiagram-CCIWWBDH-aTHmcUGy.js → ebnfDiagram-CCIWWBDH-COGAgW5z.js} +1 -1
- package/web/assets/{entities-CCUtpskY.js → entities-ByGFSWnQ.js} +1 -1
- package/web/assets/{erDiagram-Q63AITRT-CUUPBcvo.js → erDiagram-Q63AITRT-CRMKCr0m.js} +1 -1
- package/web/assets/{file-viewer-DJ3rsmef.js → file-viewer-Dqx7DbTw.js} +1 -1
- package/web/assets/{flowDiagram-23GEKE2U-aTQXx2Mh.js → flowDiagram-23GEKE2U-Drv72N5g.js} +1 -1
- package/web/assets/folder-filter-bar-Bs_uyU0g.js +4 -0
- package/web/assets/folder-image-BBNRcu1j.js +1 -0
- package/web/assets/{folder-landing-DCnzYXXY.js → folder-landing-0EXAzEpD.js} +3 -3
- package/web/assets/{folder-list-DvVkVjiR.js → folder-list-Do8Kl8uk.js} +1 -1
- package/web/assets/{folder-metadata-C9hMMzIH.js → folder-metadata-DncP3Vx2.js} +1 -1
- package/web/assets/folder-mobile-bar-D-XXKrip.js +1 -0
- package/web/assets/folder-plugin-view-DoRQa8Dz.js +3 -0
- package/web/assets/{ganttDiagram-NO4QXBWP-DiwdUp7u.js → ganttDiagram-NO4QXBWP-DHEt2_9K.js} +1 -1
- package/web/assets/{gitGraphDiagram-IHSO6WYX-BL2gG_IS.js → gitGraphDiagram-IHSO6WYX-Bt6XsYTq.js} +1 -1
- package/web/assets/{index-CUTtMSyJ.js → index-7JCizcu7.js} +2 -2
- package/web/assets/index-DY0VGBht.js +1 -0
- package/web/assets/{infoDiagram-FWYZ7A6U-B3n0zZpt.js → infoDiagram-FWYZ7A6U-961DCJAH.js} +1 -1
- package/web/assets/{ishikawaDiagram-FXEZZL3T-D7YytbEv.js → ishikawaDiagram-FXEZZL3T-ibSYK50l.js} +1 -1
- package/web/assets/{journeyDiagram-5HDEW3XC-BYwqEl3C.js → journeyDiagram-5HDEW3XC-Cd0ZPqw2.js} +1 -1
- package/web/assets/{kanban-definition-HUTT4EX6-BQHNJgXZ.js → kanban-definition-HUTT4EX6-Bu5skY8G.js} +1 -1
- package/web/assets/{linear-BPiQCLNk.js → linear-Cf8Xflqn.js} +1 -1
- package/web/assets/{mermaid.core-CD-KvBjV.js → mermaid.core-C9iHkrxr.js} +4 -4
- package/web/assets/{metadata-COJ7fZul.js → metadata-CLq-ZGmh.js} +1 -1
- package/web/assets/{mindmap-definition-LN4V7U3C-C1L_gyoj.js → mindmap-definition-LN4V7U3C-C5bK3O7k.js} +1 -1
- package/web/assets/{pegDiagram-2B236MQR-BDAfItqS.js → pegDiagram-2B236MQR-5rN2nkjz.js} +1 -1
- package/web/assets/{pieDiagram-ENE6RG2P-CkZYH3Vz.js → pieDiagram-ENE6RG2P-DnSBFuIb.js} +1 -1
- package/web/assets/popover-D7MTK-sz.js +9 -0
- package/web/assets/{project-search-dialog-B2oqMT6h.js → project-search-dialog-Bo1JWdqR.js} +1 -1
- package/web/assets/{quadrantDiagram-ABIIQ3AL-Dy8Al3YZ.js → quadrantDiagram-ABIIQ3AL-xinqVpVM.js} +1 -1
- package/web/assets/{railroadDiagram-RFXS5EU6-Dd9_yo7B.js → railroadDiagram-RFXS5EU6-GsASFEVa.js} +1 -1
- package/web/assets/{requirementDiagram-TGXJPOKE-DPhIUsbs.js → requirementDiagram-TGXJPOKE-DS5CqNNo.js} +1 -1
- package/web/assets/{sankeyDiagram-HTMAVEWB-B2QkYtub.js → sankeyDiagram-HTMAVEWB-DsnvjCXC.js} +1 -1
- package/web/assets/{search-C1J0-xiB.js → search-BA4r_Yoa.js} +1 -1
- package/web/assets/select-D4TpCgnU.js +30 -0
- package/web/assets/{selection-popup-qhlUFzGq.js → selection-popup-DIME-60e.js} +3 -3
- package/web/assets/{sequenceDiagram-DBY2YBRQ-Bx7fcZDx.js → sequenceDiagram-DBY2YBRQ-PI0WgJrf.js} +1 -1
- package/web/assets/{sizeCapture-X5ZJPWSS-CIXIhl_A.js → sizeCapture-X5ZJPWSS-Bpul3MSt.js} +1 -1
- package/web/assets/{stateDiagram-2N3HPSRC-DCrdAflB.js → stateDiagram-2N3HPSRC-DZEEP9Z2.js} +1 -1
- package/web/assets/stateDiagram-v2-6OUMAXLB-WAp7-BWX.js +1 -0
- package/web/assets/{swimlanes-5IMT3BWC-DA2vc7iZ.js → swimlanes-5IMT3BWC-_qVjNiCh.js} +2 -2
- package/web/assets/swimlanesDiagram-G3AALYLV-Dy4wBDSi.js +8 -0
- package/web/assets/{timeline-definition-FHXFAJF6-lKfJiV9V.js → timeline-definition-FHXFAJF6-CJev7rIB.js} +1 -1
- package/web/assets/use-folder-filter-hA6fa7nm.js +1 -0
- package/web/assets/{vennDiagram-L72KCM5P-DO1q2KzG.js → vennDiagram-L72KCM5P-BrTVvREy.js} +1 -1
- package/web/assets/{view-context-DXKM3-l1.js → view-context-Co01ON81.js} +35 -35
- package/web/assets/{viewer-capabilities-Cig_xM9u.js → viewer-capabilities-C_kjcYcl.js} +1 -1
- package/web/assets/{wardleyDiagram-EHGQE667-Dal9LkuZ.js → wardleyDiagram-EHGQE667-XwMSgrT5.js} +1 -1
- package/web/assets/{xychartDiagram-FW5EYKEG-B3dPLPII.js → xychartDiagram-FW5EYKEG-Be1kFxeC.js} +1 -1
- package/web/index.html +1 -1
- package/web/assets/app-CX20nNIa.js +0 -2
- package/web/assets/app-options-CkPJE5ep.js +0 -25
- package/web/assets/channel-X7WHyxxT.js +0 -1
- package/web/assets/classDiagram-OUVF2IWQ-BjsLQ3dz.js +0 -1
- package/web/assets/classDiagram-v2-EOCWNBFH-BjsLQ3dz.js +0 -1
- package/web/assets/client-7OWw5kW2.js +0 -6
- package/web/assets/client-DYj_td4Q.js +0 -17
- package/web/assets/folder-image-Bnv43tzP.js +0 -1
- package/web/assets/folder-mobile-bar-VoTL1GS0.js +0 -1
- package/web/assets/folder-plugin-view-Cvd804ZU.js +0 -6
- package/web/assets/popover-0oA2lLUV.js +0 -9
- package/web/assets/select-CtXC_PLC.js +0 -30
- package/web/assets/stateDiagram-v2-6OUMAXLB-0KCFLYeQ.js +0 -1
- package/web/assets/swimlanesDiagram-G3AALYLV-CAw3AV84.js +0 -8
package/README.md
CHANGED
|
@@ -116,14 +116,17 @@ diagnostics come from the declared schema and Markdown frontmatter. Pass
|
|
|
116
116
|
list. See [Core folders and metadata indexing](../docs/core-folders.md) for
|
|
117
117
|
patterns, parser support, navigation labels and refresh semantics.
|
|
118
118
|
|
|
119
|
-
`kennel view` starts the source Vite viewer from a Kennel checkout when
|
|
119
|
+
`kennel view` starts the source Vite viewer from a Kennel checkout, also when
|
|
120
120
|
external plugins are installed. React and CSS changes use HMR. Changes to
|
|
121
121
|
schema and content files cause a metadata refetch without a full page reload.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
uses the current directory when no folder is given. It
|
|
126
|
-
server that
|
|
122
|
+
The Vite viewer loads the installed external plugins and reports a
|
|
123
|
+
runtime-independent plugin-set key from `/api/health`. `kennel serve` starts
|
|
124
|
+
the packaged static viewer with its own runtime-specific cache key. The hot
|
|
125
|
+
viewer uses the current directory when no folder is given. It reuses a Kennel Vite
|
|
126
|
+
server on the selected port only when that server has the same kernel key. If
|
|
127
|
+
the plugin set changed, stop the running viewer and run the command again. A
|
|
128
|
+
local Vite viewer can open any local folder; the static viewer is reused only
|
|
129
|
+
for its own root or a folder below it.
|
|
127
130
|
|
|
128
131
|
`kennel publish init cloudflare` is the exception for deploy wiring: it scaffolds
|
|
129
132
|
a Cloudflare Worker email-code gate for an existing docs workspace by writing
|
|
@@ -26,6 +26,9 @@ export declare function readExternalPlugins(): ExternalPluginEntry[];
|
|
|
26
26
|
export declare function installExternalPlugin(url: string, options?: {
|
|
27
27
|
directory?: string;
|
|
28
28
|
}): Promise<ExternalPluginEntry>;
|
|
29
|
+
/** Identifies the installed plugin set, independent of the Node runtime. */
|
|
30
|
+
export declare function hotKernelKey(plugins?: ExternalPluginEntry[]): string;
|
|
31
|
+
/** Identifies a runtime-specific static kernel cache entry. */
|
|
29
32
|
export declare function kernelKey(plugins?: ExternalPluginEntry[]): string;
|
|
30
33
|
export type ActiveKernel = {
|
|
31
34
|
buildIdentity: string;
|
|
@@ -373,21 +373,33 @@ export async function installExternalPlugin(url, options = {}) {
|
|
|
373
373
|
function cliRoot() {
|
|
374
374
|
return resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
375
375
|
}
|
|
376
|
+
function kernelPlugins(plugins) {
|
|
377
|
+
return plugins.map((plugin) => ({
|
|
378
|
+
url: plugin.url,
|
|
379
|
+
requestedRef: plugin.requestedRef ?? null,
|
|
380
|
+
packageName: plugin.packageName,
|
|
381
|
+
id: plugin.id,
|
|
382
|
+
directory: plugin.directory ?? null,
|
|
383
|
+
commit: plugin.commit,
|
|
384
|
+
trigger: plugin.trigger,
|
|
385
|
+
entries: plugin.entries,
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
/** Identifies the installed plugin set, independent of the Node runtime. */
|
|
389
|
+
export function hotKernelKey(plugins = readExternalPlugins()) {
|
|
390
|
+
return hash(JSON.stringify({
|
|
391
|
+
buildIdentity: BUILD_IDENTITY,
|
|
392
|
+
schema: KENNEL_KERNEL_SCHEMA,
|
|
393
|
+
plugins: kernelPlugins(plugins),
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
/** Identifies a runtime-specific static kernel cache entry. */
|
|
376
397
|
export function kernelKey(plugins = readExternalPlugins()) {
|
|
377
398
|
return hash(JSON.stringify({
|
|
378
399
|
buildIdentity: BUILD_IDENTITY,
|
|
379
400
|
schema: KENNEL_KERNEL_SCHEMA,
|
|
380
401
|
runtime: BUILD_RUNTIME,
|
|
381
|
-
plugins: plugins
|
|
382
|
-
url: plugin.url,
|
|
383
|
-
requestedRef: plugin.requestedRef ?? null,
|
|
384
|
-
packageName: plugin.packageName,
|
|
385
|
-
id: plugin.id,
|
|
386
|
-
directory: plugin.directory ?? null,
|
|
387
|
-
commit: plugin.commit,
|
|
388
|
-
trigger: plugin.trigger,
|
|
389
|
-
entries: plugin.entries,
|
|
390
|
-
})),
|
|
402
|
+
plugins: kernelPlugins(plugins),
|
|
391
403
|
}));
|
|
392
404
|
}
|
|
393
405
|
function kernelRoot(key) {
|
|
@@ -7,12 +7,15 @@ import { tmpdir } from 'node:os';
|
|
|
7
7
|
import { dirname, relative, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { createKennelClient } from '../core/api-client.js';
|
|
10
|
+
import { hotKernelKey, readExternalPlugins } from '../plugins/external.js';
|
|
10
11
|
function sourceRoot() {
|
|
11
12
|
return resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
12
13
|
}
|
|
13
14
|
async function viewerStatus(url) {
|
|
14
15
|
return createKennelClient({ baseUrl: url }).health({ signal: AbortSignal.timeout(1_000) })
|
|
15
|
-
.then((body) => body.viewer === 'kennel-vite' || body.viewer === 'kennel-core'
|
|
16
|
+
.then((body) => body.viewer === 'kennel-vite' || body.viewer === 'kennel-core'
|
|
17
|
+
? { viewer: body.viewer, kernelKey: body.kernelKey }
|
|
18
|
+
: 'other')
|
|
16
19
|
.catch((error) => {
|
|
17
20
|
// HTTP rejection means a different service owns the port; connection and
|
|
18
21
|
// timeout failures mean the viewer is unavailable, so startup may retry.
|
|
@@ -20,17 +23,17 @@ async function viewerStatus(url) {
|
|
|
20
23
|
? 'other' : 'unavailable';
|
|
21
24
|
});
|
|
22
25
|
}
|
|
23
|
-
async function
|
|
26
|
+
async function runningViewer(url, attempts = 1, waitOnOther = false) {
|
|
24
27
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
25
28
|
const status = await viewerStatus(url);
|
|
26
|
-
if (status === '
|
|
27
|
-
return
|
|
29
|
+
if (typeof status === 'object')
|
|
30
|
+
return status;
|
|
28
31
|
if (status === 'other' && !waitOnOther)
|
|
29
|
-
return
|
|
32
|
+
return undefined;
|
|
30
33
|
if (attempt + 1 < attempts)
|
|
31
34
|
await new Promise((done) => setTimeout(done, 250));
|
|
32
35
|
}
|
|
33
|
-
return
|
|
36
|
+
return undefined;
|
|
34
37
|
}
|
|
35
38
|
async function portOpen(host, port) {
|
|
36
39
|
return new Promise((done) => {
|
|
@@ -132,8 +135,20 @@ export async function hotViewer(options) {
|
|
|
132
135
|
console.log(`bun ${[...args, ...(options.open ? ['--open', openPath] : [])].join(' ')}`);
|
|
133
136
|
return;
|
|
134
137
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
138
|
+
const plugins = readExternalPlugins();
|
|
139
|
+
// Must match the key apps/web/scripts/dev.mjs gives Vite as KENNEL_KERNEL_KEY.
|
|
140
|
+
const expectedKey = plugins.length > 0 ? hotKernelKey(plugins) : 'core';
|
|
141
|
+
// Reuse only a Vite viewer that serves the same plugin set. Anything else on
|
|
142
|
+
// the port is stale for this kernel and must be restarted, never reused.
|
|
143
|
+
const reuse = (running) => {
|
|
144
|
+
const restart = 'Stop that viewer (Ctrl+C in its terminal) and run this command again, or select another port.';
|
|
145
|
+
if (running.viewer !== 'kennel-vite') {
|
|
146
|
+
throw new Error(`Port ${options.port} runs the static Kennel viewer, not the hot viewer. ${restart}`);
|
|
147
|
+
}
|
|
148
|
+
if (running.kernelKey !== expectedKey) {
|
|
149
|
+
throw new Error(`The Kennel hot viewer on port ${options.port} uses kernel ${running.kernelKey}, `
|
|
150
|
+
+ `but the installed plugins need kernel ${expectedKey}. ${restart}`);
|
|
151
|
+
}
|
|
137
152
|
if (options.open) {
|
|
138
153
|
// Do not stack another tab: the viewer is already open. Print the URL
|
|
139
154
|
// so the caller can still get to it deliberately.
|
|
@@ -142,21 +157,19 @@ export async function hotViewer(options) {
|
|
|
142
157
|
else {
|
|
143
158
|
console.log('Kennel hot viewer is already running. Reusing it.');
|
|
144
159
|
}
|
|
160
|
+
};
|
|
161
|
+
const status = await viewerStatus(url);
|
|
162
|
+
if (typeof status === 'object') {
|
|
163
|
+
reuse(status);
|
|
145
164
|
return;
|
|
146
165
|
}
|
|
147
166
|
if (status === 'other') {
|
|
148
167
|
throw new Error(`Port ${options.port} is used by another service. Stop it or select another port.`);
|
|
149
168
|
}
|
|
150
169
|
if (await portOpen(options.host, options.port)) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// so the caller can still get to it deliberately.
|
|
155
|
-
console.log(`Kennel hot viewer is already running at ${viewerUrl}`);
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
console.log('Kennel hot viewer is already running. Reusing it.');
|
|
159
|
-
}
|
|
170
|
+
const running = await runningViewer(url, 20, true);
|
|
171
|
+
if (running) {
|
|
172
|
+
reuse(running);
|
|
160
173
|
return;
|
|
161
174
|
}
|
|
162
175
|
throw new Error(`Port ${options.port} is used by another service. Stop it or select another port.`);
|
|
@@ -164,15 +177,9 @@ export async function hotViewer(options) {
|
|
|
164
177
|
const lock = startupLock(options.host, options.port);
|
|
165
178
|
let token = await acquireLock(lock);
|
|
166
179
|
if (!token) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// so the caller can still get to it deliberately.
|
|
171
|
-
console.log(`Kennel hot viewer is already running at ${viewerUrl}`);
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
console.log('Kennel hot viewer is already running. Reusing it.');
|
|
175
|
-
}
|
|
180
|
+
const running = await runningViewer(url, 40, true);
|
|
181
|
+
if (running) {
|
|
182
|
+
reuse(running);
|
|
176
183
|
return;
|
|
177
184
|
}
|
|
178
185
|
await rm(lock, { force: true, recursive: true });
|
|
@@ -180,16 +187,10 @@ export async function hotViewer(options) {
|
|
|
180
187
|
}
|
|
181
188
|
if (!token)
|
|
182
189
|
throw new Error(`Could not acquire the hot viewer startup lock for port ${options.port}.`);
|
|
183
|
-
|
|
190
|
+
const running = await runningViewer(url);
|
|
191
|
+
if (running) {
|
|
184
192
|
await releaseLock(lock, token);
|
|
185
|
-
|
|
186
|
-
// Do not stack another tab: the viewer is already open. Print the URL
|
|
187
|
-
// so the caller can still get to it deliberately.
|
|
188
|
-
console.log(`Kennel hot viewer is already running at ${viewerUrl}`);
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
console.log('Kennel hot viewer is already running. Reusing it.');
|
|
192
|
-
}
|
|
193
|
+
reuse(running);
|
|
193
194
|
return;
|
|
194
195
|
}
|
|
195
196
|
console.log(`bun ${args.join(' ')}`);
|
|
@@ -220,9 +221,15 @@ export async function hotViewer(options) {
|
|
|
220
221
|
done();
|
|
221
222
|
return;
|
|
222
223
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
const existing = await runningViewer(url, 20, true);
|
|
225
|
+
if (existing) {
|
|
226
|
+
try {
|
|
227
|
+
reuse(existing);
|
|
228
|
+
done();
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
fail(error);
|
|
232
|
+
}
|
|
226
233
|
return;
|
|
227
234
|
}
|
|
228
235
|
fail(new Error(`Kennel hot viewer stopped with exit code ${code ?? 'unknown'}`));
|
|
@@ -259,17 +259,15 @@ function tailscaleHost() {
|
|
|
259
259
|
export async function viewer(input) {
|
|
260
260
|
const portExplicit = viewerPortWasExplicit(input);
|
|
261
261
|
const options = splitViewerArgs(input);
|
|
262
|
-
|
|
263
|
-
const externalPlugins = readExternalPlugins();
|
|
264
|
-
if (options.hot && externalPlugins.length === 0) {
|
|
262
|
+
if (options.hot) {
|
|
265
263
|
const { hotViewer } = await import("./hot-viewer.js");
|
|
266
264
|
await hotViewer(options);
|
|
267
265
|
return;
|
|
268
266
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
267
|
+
const { ensureKernel, hotKernelKey, readExternalPlugins } = await import("../plugins/external.js");
|
|
268
|
+
const externalPlugins = readExternalPlugins();
|
|
272
269
|
const kernel = await ensureKernel(externalPlugins);
|
|
270
|
+
const viteKernelKey = externalPlugins.length > 0 ? hotKernelKey(externalPlugins) : "core";
|
|
273
271
|
const web = kernel.webRoot;
|
|
274
272
|
if (!existsSync(resolve(web, "index.html"))) {
|
|
275
273
|
throw new Error(`Kennel web app not found. Run the CLI build first.`);
|
|
@@ -298,9 +296,16 @@ export async function viewer(input) {
|
|
|
298
296
|
.join(",");
|
|
299
297
|
const initialViewerUrl = `http://${options.host}:${options.port}`;
|
|
300
298
|
const existing = await runningViewer(initialViewerUrl);
|
|
299
|
+
// A local Vite viewer is not root-restricted, so it can open any local
|
|
300
|
+
// folder. The static kennel-core viewer is root-scoped: reuse it only for
|
|
301
|
+
// its own root or a descendant.
|
|
301
302
|
if (existing &&
|
|
302
|
-
existing.
|
|
303
|
-
|
|
303
|
+
((existing.viewer === "kennel-vite" &&
|
|
304
|
+
!remote &&
|
|
305
|
+
existing.kernelKey === viteKernelKey) ||
|
|
306
|
+
(existing.viewer !== "kennel-vite" &&
|
|
307
|
+
existing.kernelKey === kernel.key &&
|
|
308
|
+
isSameOrDescendant(existing.root, options.root)))) {
|
|
304
309
|
const viewerUrl = popUrl(displayHost, options.port, options);
|
|
305
310
|
console.log(viewerUrl);
|
|
306
311
|
console.log(`Kennel is already running at ${viewerUrl}`);
|
|
@@ -26,6 +26,9 @@ export declare function readExternalPlugins(): ExternalPluginEntry[];
|
|
|
26
26
|
export declare function installExternalPlugin(url: string, options?: {
|
|
27
27
|
directory?: string;
|
|
28
28
|
}): Promise<ExternalPluginEntry>;
|
|
29
|
+
/** Identifies the installed plugin set, independent of the Node runtime. */
|
|
30
|
+
export declare function hotKernelKey(plugins?: ExternalPluginEntry[]): string;
|
|
31
|
+
/** Identifies a runtime-specific static kernel cache entry. */
|
|
29
32
|
export declare function kernelKey(plugins?: ExternalPluginEntry[]): string;
|
|
30
33
|
export type ActiveKernel = {
|
|
31
34
|
buildIdentity: string;
|
package/dist/plugins/external.js
CHANGED
|
@@ -373,21 +373,33 @@ export async function installExternalPlugin(url, options = {}) {
|
|
|
373
373
|
function cliRoot() {
|
|
374
374
|
return resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
375
375
|
}
|
|
376
|
+
function kernelPlugins(plugins) {
|
|
377
|
+
return plugins.map((plugin) => ({
|
|
378
|
+
url: plugin.url,
|
|
379
|
+
requestedRef: plugin.requestedRef ?? null,
|
|
380
|
+
packageName: plugin.packageName,
|
|
381
|
+
id: plugin.id,
|
|
382
|
+
directory: plugin.directory ?? null,
|
|
383
|
+
commit: plugin.commit,
|
|
384
|
+
trigger: plugin.trigger,
|
|
385
|
+
entries: plugin.entries,
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
/** Identifies the installed plugin set, independent of the Node runtime. */
|
|
389
|
+
export function hotKernelKey(plugins = readExternalPlugins()) {
|
|
390
|
+
return hash(JSON.stringify({
|
|
391
|
+
buildIdentity: BUILD_IDENTITY,
|
|
392
|
+
schema: KENNEL_KERNEL_SCHEMA,
|
|
393
|
+
plugins: kernelPlugins(plugins),
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
/** Identifies a runtime-specific static kernel cache entry. */
|
|
376
397
|
export function kernelKey(plugins = readExternalPlugins()) {
|
|
377
398
|
return hash(JSON.stringify({
|
|
378
399
|
buildIdentity: BUILD_IDENTITY,
|
|
379
400
|
schema: KENNEL_KERNEL_SCHEMA,
|
|
380
401
|
runtime: BUILD_RUNTIME,
|
|
381
|
-
plugins: plugins
|
|
382
|
-
url: plugin.url,
|
|
383
|
-
requestedRef: plugin.requestedRef ?? null,
|
|
384
|
-
packageName: plugin.packageName,
|
|
385
|
-
id: plugin.id,
|
|
386
|
-
directory: plugin.directory ?? null,
|
|
387
|
-
commit: plugin.commit,
|
|
388
|
-
trigger: plugin.trigger,
|
|
389
|
-
entries: plugin.entries,
|
|
390
|
-
})),
|
|
402
|
+
plugins: kernelPlugins(plugins),
|
|
391
403
|
}));
|
|
392
404
|
}
|
|
393
405
|
function kernelRoot(key) {
|
package/dist/site/hot-viewer.js
CHANGED
|
@@ -7,12 +7,15 @@ import { tmpdir } from 'node:os';
|
|
|
7
7
|
import { dirname, relative, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { createKennelClient } from '../core/api-client.js';
|
|
10
|
+
import { hotKernelKey, readExternalPlugins } from '../plugins/external.js';
|
|
10
11
|
function sourceRoot() {
|
|
11
12
|
return resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
12
13
|
}
|
|
13
14
|
async function viewerStatus(url) {
|
|
14
15
|
return createKennelClient({ baseUrl: url }).health({ signal: AbortSignal.timeout(1_000) })
|
|
15
|
-
.then((body) => body.viewer === 'kennel-vite' || body.viewer === 'kennel-core'
|
|
16
|
+
.then((body) => body.viewer === 'kennel-vite' || body.viewer === 'kennel-core'
|
|
17
|
+
? { viewer: body.viewer, kernelKey: body.kernelKey }
|
|
18
|
+
: 'other')
|
|
16
19
|
.catch((error) => {
|
|
17
20
|
// HTTP rejection means a different service owns the port; connection and
|
|
18
21
|
// timeout failures mean the viewer is unavailable, so startup may retry.
|
|
@@ -20,17 +23,17 @@ async function viewerStatus(url) {
|
|
|
20
23
|
? 'other' : 'unavailable';
|
|
21
24
|
});
|
|
22
25
|
}
|
|
23
|
-
async function
|
|
26
|
+
async function runningViewer(url, attempts = 1, waitOnOther = false) {
|
|
24
27
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
25
28
|
const status = await viewerStatus(url);
|
|
26
|
-
if (status === '
|
|
27
|
-
return
|
|
29
|
+
if (typeof status === 'object')
|
|
30
|
+
return status;
|
|
28
31
|
if (status === 'other' && !waitOnOther)
|
|
29
|
-
return
|
|
32
|
+
return undefined;
|
|
30
33
|
if (attempt + 1 < attempts)
|
|
31
34
|
await new Promise((done) => setTimeout(done, 250));
|
|
32
35
|
}
|
|
33
|
-
return
|
|
36
|
+
return undefined;
|
|
34
37
|
}
|
|
35
38
|
async function portOpen(host, port) {
|
|
36
39
|
return new Promise((done) => {
|
|
@@ -132,8 +135,20 @@ export async function hotViewer(options) {
|
|
|
132
135
|
console.log(`bun ${[...args, ...(options.open ? ['--open', openPath] : [])].join(' ')}`);
|
|
133
136
|
return;
|
|
134
137
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
138
|
+
const plugins = readExternalPlugins();
|
|
139
|
+
// Must match the key apps/web/scripts/dev.mjs gives Vite as KENNEL_KERNEL_KEY.
|
|
140
|
+
const expectedKey = plugins.length > 0 ? hotKernelKey(plugins) : 'core';
|
|
141
|
+
// Reuse only a Vite viewer that serves the same plugin set. Anything else on
|
|
142
|
+
// the port is stale for this kernel and must be restarted, never reused.
|
|
143
|
+
const reuse = (running) => {
|
|
144
|
+
const restart = 'Stop that viewer (Ctrl+C in its terminal) and run this command again, or select another port.';
|
|
145
|
+
if (running.viewer !== 'kennel-vite') {
|
|
146
|
+
throw new Error(`Port ${options.port} runs the static Kennel viewer, not the hot viewer. ${restart}`);
|
|
147
|
+
}
|
|
148
|
+
if (running.kernelKey !== expectedKey) {
|
|
149
|
+
throw new Error(`The Kennel hot viewer on port ${options.port} uses kernel ${running.kernelKey}, `
|
|
150
|
+
+ `but the installed plugins need kernel ${expectedKey}. ${restart}`);
|
|
151
|
+
}
|
|
137
152
|
if (options.open) {
|
|
138
153
|
// Do not stack another tab: the viewer is already open. Print the URL
|
|
139
154
|
// so the caller can still get to it deliberately.
|
|
@@ -142,21 +157,19 @@ export async function hotViewer(options) {
|
|
|
142
157
|
else {
|
|
143
158
|
console.log('Kennel hot viewer is already running. Reusing it.');
|
|
144
159
|
}
|
|
160
|
+
};
|
|
161
|
+
const status = await viewerStatus(url);
|
|
162
|
+
if (typeof status === 'object') {
|
|
163
|
+
reuse(status);
|
|
145
164
|
return;
|
|
146
165
|
}
|
|
147
166
|
if (status === 'other') {
|
|
148
167
|
throw new Error(`Port ${options.port} is used by another service. Stop it or select another port.`);
|
|
149
168
|
}
|
|
150
169
|
if (await portOpen(options.host, options.port)) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// so the caller can still get to it deliberately.
|
|
155
|
-
console.log(`Kennel hot viewer is already running at ${viewerUrl}`);
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
console.log('Kennel hot viewer is already running. Reusing it.');
|
|
159
|
-
}
|
|
170
|
+
const running = await runningViewer(url, 20, true);
|
|
171
|
+
if (running) {
|
|
172
|
+
reuse(running);
|
|
160
173
|
return;
|
|
161
174
|
}
|
|
162
175
|
throw new Error(`Port ${options.port} is used by another service. Stop it or select another port.`);
|
|
@@ -164,15 +177,9 @@ export async function hotViewer(options) {
|
|
|
164
177
|
const lock = startupLock(options.host, options.port);
|
|
165
178
|
let token = await acquireLock(lock);
|
|
166
179
|
if (!token) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// so the caller can still get to it deliberately.
|
|
171
|
-
console.log(`Kennel hot viewer is already running at ${viewerUrl}`);
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
console.log('Kennel hot viewer is already running. Reusing it.');
|
|
175
|
-
}
|
|
180
|
+
const running = await runningViewer(url, 40, true);
|
|
181
|
+
if (running) {
|
|
182
|
+
reuse(running);
|
|
176
183
|
return;
|
|
177
184
|
}
|
|
178
185
|
await rm(lock, { force: true, recursive: true });
|
|
@@ -180,16 +187,10 @@ export async function hotViewer(options) {
|
|
|
180
187
|
}
|
|
181
188
|
if (!token)
|
|
182
189
|
throw new Error(`Could not acquire the hot viewer startup lock for port ${options.port}.`);
|
|
183
|
-
|
|
190
|
+
const running = await runningViewer(url);
|
|
191
|
+
if (running) {
|
|
184
192
|
await releaseLock(lock, token);
|
|
185
|
-
|
|
186
|
-
// Do not stack another tab: the viewer is already open. Print the URL
|
|
187
|
-
// so the caller can still get to it deliberately.
|
|
188
|
-
console.log(`Kennel hot viewer is already running at ${viewerUrl}`);
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
console.log('Kennel hot viewer is already running. Reusing it.');
|
|
192
|
-
}
|
|
193
|
+
reuse(running);
|
|
193
194
|
return;
|
|
194
195
|
}
|
|
195
196
|
console.log(`bun ${args.join(' ')}`);
|
|
@@ -220,9 +221,15 @@ export async function hotViewer(options) {
|
|
|
220
221
|
done();
|
|
221
222
|
return;
|
|
222
223
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
const existing = await runningViewer(url, 20, true);
|
|
225
|
+
if (existing) {
|
|
226
|
+
try {
|
|
227
|
+
reuse(existing);
|
|
228
|
+
done();
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
fail(error);
|
|
232
|
+
}
|
|
226
233
|
return;
|
|
227
234
|
}
|
|
228
235
|
fail(new Error(`Kennel hot viewer stopped with exit code ${code ?? 'unknown'}`));
|
package/dist/site/viewer.js
CHANGED
|
@@ -259,17 +259,15 @@ function tailscaleHost() {
|
|
|
259
259
|
export async function viewer(input) {
|
|
260
260
|
const portExplicit = viewerPortWasExplicit(input);
|
|
261
261
|
const options = splitViewerArgs(input);
|
|
262
|
-
|
|
263
|
-
const externalPlugins = readExternalPlugins();
|
|
264
|
-
if (options.hot && externalPlugins.length === 0) {
|
|
262
|
+
if (options.hot) {
|
|
265
263
|
const { hotViewer } = await import("./hot-viewer.js");
|
|
266
264
|
await hotViewer(options);
|
|
267
265
|
return;
|
|
268
266
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
267
|
+
const { ensureKernel, hotKernelKey, readExternalPlugins } = await import("../plugins/external.js");
|
|
268
|
+
const externalPlugins = readExternalPlugins();
|
|
272
269
|
const kernel = await ensureKernel(externalPlugins);
|
|
270
|
+
const viteKernelKey = externalPlugins.length > 0 ? hotKernelKey(externalPlugins) : "core";
|
|
273
271
|
const web = kernel.webRoot;
|
|
274
272
|
if (!existsSync(resolve(web, "index.html"))) {
|
|
275
273
|
throw new Error(`Kennel web app not found. Run the CLI build first.`);
|
|
@@ -298,9 +296,16 @@ export async function viewer(input) {
|
|
|
298
296
|
.join(",");
|
|
299
297
|
const initialViewerUrl = `http://${options.host}:${options.port}`;
|
|
300
298
|
const existing = await runningViewer(initialViewerUrl);
|
|
299
|
+
// A local Vite viewer is not root-restricted, so it can open any local
|
|
300
|
+
// folder. The static kennel-core viewer is root-scoped: reuse it only for
|
|
301
|
+
// its own root or a descendant.
|
|
301
302
|
if (existing &&
|
|
302
|
-
existing.
|
|
303
|
-
|
|
303
|
+
((existing.viewer === "kennel-vite" &&
|
|
304
|
+
!remote &&
|
|
305
|
+
existing.kernelKey === viteKernelKey) ||
|
|
306
|
+
(existing.viewer !== "kennel-vite" &&
|
|
307
|
+
existing.kernelKey === kernel.key &&
|
|
308
|
+
isSameOrDescendant(existing.root, options.root)))) {
|
|
304
309
|
const viewerUrl = popUrl(displayHost, options.port, options);
|
|
305
310
|
console.log(viewerUrl);
|
|
306
311
|
console.log(`Kennel is already running at ${viewerUrl}`);
|
|
@@ -5,6 +5,7 @@ import { dirname, join, resolve } from "node:path"
|
|
|
5
5
|
import { fileURLToPath } from "node:url"
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
+
hotKernelKey,
|
|
8
9
|
prepareExternalPluginModules,
|
|
9
10
|
readExternalPlugins,
|
|
10
11
|
} from "../../../cli/src/plugins/external.ts"
|
|
@@ -30,6 +31,9 @@ try {
|
|
|
30
31
|
stdio: "inherit",
|
|
31
32
|
env: {
|
|
32
33
|
...process.env,
|
|
34
|
+
// Reported by /api/health so the CLI reuses this viewer only for the
|
|
35
|
+
// same plugin set; "core" matches the CLI kernel without plugins.
|
|
36
|
+
KENNEL_KERNEL_KEY: plugins.length > 0 ? hotKernelKey(plugins) : "core",
|
|
33
37
|
KENNEL_KERNEL_SOURCE_ROOT: workspace,
|
|
34
38
|
KENNEL_HOST_NODE_MODULES: resolve(workspace, "cli/node_modules"),
|
|
35
39
|
KENNEL_EXTERNAL_CLIENT_MODULE: modules.clientModule,
|
|
@@ -4,39 +4,75 @@ export function subscribeFolderEvent(
|
|
|
4
4
|
folder: string | undefined,
|
|
5
5
|
listener: () => void,
|
|
6
6
|
) {
|
|
7
|
-
return subscribeApiEvent(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
data
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
7
|
+
return subscribeApiEvent(
|
|
8
|
+
event,
|
|
9
|
+
(message) => {
|
|
10
|
+
let data: unknown
|
|
11
|
+
try {
|
|
12
|
+
data = JSON.parse((message as MessageEvent<string>).data)
|
|
13
|
+
} catch {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
if (
|
|
17
|
+
data &&
|
|
18
|
+
typeof data === "object" &&
|
|
19
|
+
"path" in data &&
|
|
20
|
+
data.path === folder
|
|
21
|
+
)
|
|
22
|
+
listener()
|
|
23
|
+
},
|
|
24
|
+
listener,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Subscription = {
|
|
29
|
+
event: string
|
|
30
|
+
listener: (event: Event) => void
|
|
31
|
+
resume: () => void
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
let source: EventSource | undefined
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
const subscriptions = new Set<Subscription>()
|
|
36
|
+
|
|
37
|
+
function connect() {
|
|
38
|
+
if (source || document.visibilityState !== "visible") return
|
|
39
|
+
source = new EventSource("/api/events")
|
|
40
|
+
for (const { event, listener } of subscriptions) {
|
|
41
|
+
source.addEventListener(event, listener)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function visibilityChanged() {
|
|
46
|
+
if (document.visibilityState !== "visible") {
|
|
47
|
+
source?.close()
|
|
48
|
+
source = undefined
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
connect()
|
|
52
|
+
// Hidden tabs missed notifications. Refresh each subscriber on return.
|
|
53
|
+
for (const subscription of subscriptions) subscription.resume()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Hidden tabs must not consume the browser's per-origin HTTP/1 connection limit. */
|
|
27
57
|
export function subscribeApiEvent(
|
|
28
58
|
event: string,
|
|
29
59
|
listener: (event: Event) => void,
|
|
60
|
+
resume = () => listener(new Event(event)),
|
|
30
61
|
) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
62
|
+
const subscription = { event, listener, resume }
|
|
63
|
+
subscriptions.add(subscription)
|
|
64
|
+
if (subscriptions.size === 1) {
|
|
65
|
+
document.addEventListener("visibilitychange", visibilityChanged)
|
|
66
|
+
}
|
|
67
|
+
if (source) source.addEventListener(event, listener)
|
|
68
|
+
else connect()
|
|
35
69
|
return () => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
70
|
+
if (!subscriptions.delete(subscription)) return
|
|
71
|
+
source?.removeEventListener(event, listener)
|
|
72
|
+
if (subscriptions.size === 0) {
|
|
73
|
+
source?.close()
|
|
39
74
|
source = undefined
|
|
75
|
+
document.removeEventListener("visibilitychange", visibilityChanged)
|
|
40
76
|
}
|
|
41
77
|
}
|
|
42
78
|
}
|
|
@@ -215,6 +215,8 @@ export default defineConfig({
|
|
|
215
215
|
corePlugin({
|
|
216
216
|
remoteAccess,
|
|
217
217
|
restrictToRoot: remoteAccess,
|
|
218
|
+
// /api/health reports this name so the CLI reuses only a hot viewer.
|
|
219
|
+
viewerName: "kennel-vite",
|
|
218
220
|
root: process.env.KENNEL_VIEWER_ROOT || root,
|
|
219
221
|
// `kennel pop <file>` (hot) locks document reads to exactly this file;
|
|
220
222
|
// the static viewer applies the same lock in-process.
|