@mohou/host 1.0.20 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/host/config.ts +6 -0
- package/src/http/bind.ts +4 -3
- package/src/http/owner.ts +2 -1
- package/src/http/ports.ts +3 -1
- package/src/http/reply.ts +5 -1
- package/src/http/updates.ts +62 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mohou/host",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
"react-dom": "^19.2.0",
|
|
50
50
|
"tailwindcss": "^4.3.3",
|
|
51
51
|
"tw-animate-css": "^1.4.0",
|
|
52
|
-
"@mohou/app-view": "^1.0.
|
|
53
|
-
"@mohou/contract": "^1.0.
|
|
54
|
-
"@mohou/runtime-provider": "^1.0.
|
|
55
|
-
"@mohou/
|
|
56
|
-
"@mohou/
|
|
52
|
+
"@mohou/app-view": "^1.0.22",
|
|
53
|
+
"@mohou/contract": "^1.0.22",
|
|
54
|
+
"@mohou/runtime-provider": "^1.0.22",
|
|
55
|
+
"@mohou/mcp-client": "^1.0.22",
|
|
56
|
+
"@mohou/ui": "^1.0.22"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/better-sqlite3": "^9.6.0",
|
package/src/host/config.ts
CHANGED
|
@@ -25,6 +25,12 @@ export interface HostPolicy {
|
|
|
25
25
|
}
|
|
26
26
|
/** Absent, or `default`, means the builtin library. Not required to boot. */
|
|
27
27
|
defaultWorkbenchId?: string
|
|
28
|
+
/**
|
|
29
|
+
* Registry for this product's own update check and install, when the owner wants a mirror.
|
|
30
|
+
* Absent or empty follows the install prefix's build-time value. Our own npm children only:
|
|
31
|
+
* `~/.npmrc` and an app's own dependency installs are untouched.
|
|
32
|
+
*/
|
|
33
|
+
updateRegistry?: string
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/** Seed numbers and names. Host policy, not a locked product value. */
|
package/src/http/bind.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { readVendorFile } from './ports.ts'
|
|
|
15
15
|
import type { LoopbackPorts } from './ports.ts'
|
|
16
16
|
import { authoringTokenMatches } from './guard.ts'
|
|
17
17
|
import { probeBrain, type HostPolicy } from '../host/config.ts'
|
|
18
|
-
import { checkPackageUpdate, discardUpdateResult, readUpdateResult, stagePackageUpdate } from './updates.ts'
|
|
18
|
+
import { checkPackageUpdate, discardUpdateResult, readUpdateResult, readUpdateSource, stagePackageUpdate, updateEnv } from './updates.ts'
|
|
19
19
|
import { McpError } from '@mohou/mcp-client'
|
|
20
20
|
|
|
21
21
|
import { admitMcpText } from '../host/mcp-import.ts'
|
|
@@ -147,15 +147,16 @@ export function bindLoopback(input: {
|
|
|
147
147
|
subscribeHost: listener => input.author.hostEvents.subscribe(listener),
|
|
148
148
|
subscribeFrames: listener => forwardFrames(input.author.appEvents, listener),
|
|
149
149
|
subscribeApp: (appId, since, listener) => input.author.appEvents.subscribe(appId, since, listener),
|
|
150
|
+
updateSource: () => Promise.resolve(readUpdateSource(updateEnv(input.policy()))),
|
|
150
151
|
checkUpdate: async () => {
|
|
151
152
|
const attempt = readUpdateResult(input.runtimeRoot)
|
|
152
153
|
return {
|
|
153
|
-
...await checkPackageUpdate(),
|
|
154
|
+
...await checkPackageUpdate(undefined, updateEnv(input.policy())),
|
|
154
155
|
...attempt === undefined ? {} : { lastAttempt: attempt },
|
|
155
156
|
}
|
|
156
157
|
},
|
|
157
158
|
installUpdate: (version) => {
|
|
158
|
-
stagePackageUpdate(version)
|
|
159
|
+
stagePackageUpdate(version, updateEnv(input.policy()))
|
|
159
160
|
if (input.restart === undefined) return Promise.resolve()
|
|
160
161
|
const restart = input.restart
|
|
161
162
|
setTimeout(() => {
|
package/src/http/owner.ts
CHANGED
|
@@ -36,9 +36,10 @@ export function mountOwner(app: Hono<HostEnv>, ports: LoopbackPorts): void {
|
|
|
36
36
|
if (denied !== undefined) return denied
|
|
37
37
|
return writeConfig(c, ports)
|
|
38
38
|
})
|
|
39
|
-
app.get(httpLayout.about, c => c.json({
|
|
39
|
+
app.get(httpLayout.about, async c => c.json({
|
|
40
40
|
ok: true,
|
|
41
41
|
...aboutInfo(),
|
|
42
|
+
source: await ports.updateSource(),
|
|
42
43
|
authoring: {
|
|
43
44
|
url: `http://127.0.0.1:${ports.readPolicy().hostPort}${httpLayout.mcp}`,
|
|
44
45
|
token: ports.authoringToken,
|
package/src/http/ports.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { CredentialListing } from '../credentials/provider.ts'
|
|
|
11
11
|
import type { McpCheck, McpEditorServer } from '../host/mcp-editor.ts'
|
|
12
12
|
import type { McpLoadFailure } from '../host/mcp.ts'
|
|
13
13
|
import type { AppPin } from '../theme/pin.ts'
|
|
14
|
-
import type { UpdateCheck } from './updates.ts'
|
|
14
|
+
import type { UpdateCheck, UpdateSource } from './updates.ts'
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url)
|
|
17
17
|
|
|
@@ -67,6 +67,8 @@ export interface LoopbackPorts {
|
|
|
67
67
|
subscribeFrames(listener: (event: unknown) => void): () => void
|
|
68
68
|
subscribeApp(appId: string, since: number, listener: (event: unknown) => void): () => void
|
|
69
69
|
checkUpdate(): Promise<UpdateCheck>
|
|
70
|
+
/** Where an update would come from. Reads the prefix; no network. */
|
|
71
|
+
updateSource(): Promise<UpdateSource>
|
|
70
72
|
installUpdate?(version: string): Promise<void>
|
|
71
73
|
/** The panel showed the launcher's last attempt. The record is dropped, not shown twice. */
|
|
72
74
|
ackUpdate?(at: number): Promise<void>
|
package/src/http/reply.ts
CHANGED
|
@@ -30,11 +30,15 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
30
30
|
export function mergePolicy(current: unknown, body: Record<string, unknown>): Record<string, unknown> {
|
|
31
31
|
const base = isRecord(current) ? current : {}
|
|
32
32
|
const runtimeRoot = typeof base.runtimeRoot === 'string' ? base.runtimeRoot : undefined
|
|
33
|
+
const { updateRegistry: rawRegistry, ...rest } = body
|
|
34
|
+
const updateRegistry = typeof rawRegistry === 'string' ? rawRegistry.trim() : undefined
|
|
33
35
|
const provider = isRecord(body.runtimeProvider) ? body.runtimeProvider : undefined
|
|
34
36
|
const previous = isRecord(base.runtimeProvider) ? base.runtimeProvider : undefined
|
|
35
37
|
return {
|
|
36
38
|
...base,
|
|
37
|
-
...
|
|
39
|
+
...rest,
|
|
40
|
+
// An empty string means "the packaged default". A non-string never clobbers the stored value.
|
|
41
|
+
...updateRegistry === undefined ? {} : { updateRegistry },
|
|
38
42
|
...runtimeRoot === undefined ? {} : { runtimeRoot },
|
|
39
43
|
...provider === undefined || previous === undefined ? {} : {
|
|
40
44
|
runtimeProvider: {
|
package/src/http/updates.ts
CHANGED
|
@@ -5,6 +5,12 @@ import path from 'node:path'
|
|
|
5
5
|
import { homePackagesDir, hostUpdateResultPath } from '../host/layout.ts'
|
|
6
6
|
import { aboutInfo } from './ports.ts'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Ask the install channel once. A dev tree with no prefix still asks the registry and cannot install.
|
|
10
|
+
* The budget is host policy, not a locked number: a mirror's first response is often slow.
|
|
11
|
+
*/
|
|
12
|
+
export const updateCheckTimeoutMs = 8_000
|
|
13
|
+
|
|
8
14
|
/** Why an install did not finish. The panel owns the wording of each code. */
|
|
9
15
|
export const updateFailureCodes = ['timeout', 'exit', 'prepare', 'closed', 'leftover', 'verify', 'boot'] as const
|
|
10
16
|
|
|
@@ -32,6 +38,15 @@ export type UpdateAttempt =
|
|
|
32
38
|
readonly at: number
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Where this install updates from. `none` is a source tree with no install prefix,
|
|
43
|
+
* which cannot install anything.
|
|
44
|
+
*/
|
|
45
|
+
export type UpdateSource =
|
|
46
|
+
| { readonly channel: 'registry'; readonly registry: string }
|
|
47
|
+
| { readonly channel: 'tarball'; readonly tarballDir: string }
|
|
48
|
+
| { readonly channel: 'none' }
|
|
49
|
+
|
|
35
50
|
export interface UpdateCheck {
|
|
36
51
|
readonly name: string
|
|
37
52
|
readonly current: string
|
|
@@ -39,6 +54,9 @@ export interface UpdateCheck {
|
|
|
39
54
|
readonly updateAvailable: boolean
|
|
40
55
|
readonly channel?: 'registry' | 'tarball'
|
|
41
56
|
readonly installable?: boolean
|
|
57
|
+
/** Set when the install prefix names one, so a panel can say where an update comes from. */
|
|
58
|
+
readonly registry?: string
|
|
59
|
+
readonly tarballDir?: string
|
|
42
60
|
readonly error?: string
|
|
43
61
|
/** The last install this machine recorded, when one is on disk. */
|
|
44
62
|
readonly lastAttempt?: UpdateAttempt
|
|
@@ -62,12 +80,14 @@ export async function checkPackageUpdate(
|
|
|
62
80
|
if (about.name.length === 0) return { ...empty, error: 'package name is missing' }
|
|
63
81
|
const prefix = readPrefixUpdate(cwd, env)
|
|
64
82
|
if (prefix?.channel === 'tarball') {
|
|
65
|
-
const
|
|
83
|
+
const tarballDir = prefix.tarballDir ?? homePackagesDir(homedir())
|
|
84
|
+
const latest = newestTarball(tarballDir, 'shell')
|
|
66
85
|
return {
|
|
67
86
|
name: '@mohou/shell',
|
|
68
87
|
current: about.current,
|
|
69
88
|
latest,
|
|
70
89
|
channel: 'tarball',
|
|
90
|
+
tarballDir,
|
|
71
91
|
installable: true,
|
|
72
92
|
updateAvailable: latest !== null && compareVersion(latest, about.current) > 0,
|
|
73
93
|
}
|
|
@@ -77,9 +97,15 @@ export async function checkPackageUpdate(
|
|
|
77
97
|
const name = prefix === undefined ? about.name : '@mohou/shell'
|
|
78
98
|
try {
|
|
79
99
|
const response = await fetch(`${registry.replace(/\/$/, '')}/${name}/latest`, {
|
|
80
|
-
signal: AbortSignal.timeout(
|
|
100
|
+
signal: AbortSignal.timeout(updateCheckTimeoutMs),
|
|
81
101
|
})
|
|
82
|
-
if (!response.ok) return {
|
|
102
|
+
if (!response.ok) return {
|
|
103
|
+
...empty,
|
|
104
|
+
name,
|
|
105
|
+
...prefix === undefined ? {} : { channel: prefix.channel, registry: prefix.registry ?? 'https://registry.npmjs.org' },
|
|
106
|
+
installable: prefix !== undefined,
|
|
107
|
+
error: `package registry returned ${response.status}`,
|
|
108
|
+
}
|
|
83
109
|
const body = await response.json() as { version?: unknown }
|
|
84
110
|
const latest = typeof body.version === 'string' ? body.version : null
|
|
85
111
|
return {
|
|
@@ -87,14 +113,41 @@ export async function checkPackageUpdate(
|
|
|
87
113
|
current: about.current,
|
|
88
114
|
latest,
|
|
89
115
|
channel: prefix?.channel ?? 'registry',
|
|
116
|
+
...prefix === undefined ? {} : { registry: prefix.registry ?? 'https://registry.npmjs.org' },
|
|
90
117
|
installable: prefix !== undefined,
|
|
91
|
-
|
|
118
|
+
// Newer only. A mirror syncs late, and an older `latest` is not an update.
|
|
119
|
+
updateAvailable: latest !== null && compareVersion(latest, about.current) > 0,
|
|
92
120
|
}
|
|
93
121
|
} catch (error) {
|
|
94
|
-
return {
|
|
122
|
+
return {
|
|
123
|
+
...empty,
|
|
124
|
+
name,
|
|
125
|
+
...prefix === undefined ? {} : { channel: prefix.channel, registry: prefix.registry ?? 'https://registry.npmjs.org' },
|
|
126
|
+
installable: prefix !== undefined,
|
|
127
|
+
error: error instanceof Error ? error.message : 'update check failed',
|
|
128
|
+
}
|
|
95
129
|
}
|
|
96
130
|
}
|
|
97
131
|
|
|
132
|
+
export function updateEnv(policy: { readonly updateRegistry?: string }, env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
133
|
+
const registry = policy.updateRegistry?.trim()
|
|
134
|
+
return registry === undefined || registry.length === 0 ? env : { ...env, MINI_APP_NPM_REGISTRY: registry }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Where this install would update from. Reads the install prefix; asks no registry, so a
|
|
139
|
+
* panel can show the source while the network is slow or down.
|
|
140
|
+
* @param env - process environment; `MINI_APP_TARBALL_DIR` overrides the package folder, and
|
|
141
|
+
* `MINI_APP_NPM_REGISTRY` overrides the prefix's registry (that is how the policy reaches us)
|
|
142
|
+
* @param cwd - directory the sidecar runs in
|
|
143
|
+
*/
|
|
144
|
+
export function readUpdateSource(env: NodeJS.ProcessEnv = process.env, cwd = process.cwd()): UpdateSource {
|
|
145
|
+
const prefix = readPrefixUpdate(cwd, env)
|
|
146
|
+
if (prefix === undefined) return { channel: 'none' }
|
|
147
|
+
if (prefix.channel === 'tarball') return { channel: 'tarball', tarballDir: prefix.tarballDir ?? homePackagesDir(homedir()) }
|
|
148
|
+
return { channel: 'registry', registry: prefix.registry ?? 'https://registry.npmjs.org' }
|
|
149
|
+
}
|
|
150
|
+
|
|
98
151
|
/**
|
|
99
152
|
* Read the launcher's record of the last install. A missing, unreadable, or malformed file reads as
|
|
100
153
|
* no attempt: the panel shows nothing rather than a guessed outcome.
|
|
@@ -232,10 +285,13 @@ function readPrefixUpdate(cwd: string, env: NodeJS.ProcessEnv): PrefixUpdate | u
|
|
|
232
285
|
const packages = Object.keys(parsed.dependencies ?? {}).filter(name => name.startsWith('@mohou/'))
|
|
233
286
|
const override = env.MINI_APP_TARBALL_DIR
|
|
234
287
|
const tarballDir = override !== undefined && override.length > 0 ? override : parsed.mohou?.tarballDir
|
|
288
|
+
const registry = env.MINI_APP_NPM_REGISTRY?.trim()
|
|
235
289
|
return {
|
|
236
290
|
dir,
|
|
237
291
|
channel,
|
|
238
|
-
...
|
|
292
|
+
...registry !== undefined && registry.length > 0
|
|
293
|
+
? { registry }
|
|
294
|
+
: typeof parsed.mohou?.registry === 'string' ? { registry: parsed.mohou.registry } : {},
|
|
239
295
|
...tarballDir === undefined ? {} : { tarballDir },
|
|
240
296
|
packages: packages.length === 0 ? ['@mohou/shell'] : packages,
|
|
241
297
|
}
|