@mohou/host 1.0.21 → 1.0.23

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohou/host",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
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.21",
53
- "@mohou/mcp-client": "^1.0.21",
54
- "@mohou/runtime-provider": "^1.0.21",
55
- "@mohou/contract": "^1.0.21",
56
- "@mohou/ui": "^1.0.21"
52
+ "@mohou/contract": "^1.0.23",
53
+ "@mohou/app-view": "^1.0.23",
54
+ "@mohou/mcp-client": "^1.0.23",
55
+ "@mohou/ui": "^1.0.23",
56
+ "@mohou/runtime-provider": "^1.0.23"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/better-sqlite3": "^9.6.0",
@@ -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. */
@@ -44,7 +50,7 @@ export interface PortBound {
44
50
  export const DEFAULT_PORT_BOUND: PortBound = { min: 1024, max: 65535 }
45
51
 
46
52
  const PUBLIC_KEYS = ['runtimeRoot', 'hostPort', 'theme', 'palette', 'locale', 'chatLanguage', 'llm', 'runtimeProvider'] as const
47
- const OPTIONAL_KEYS = ['defaultWorkbenchId'] as const
53
+ const OPTIONAL_KEYS = ['defaultWorkbenchId', 'updateRegistry'] as const
48
54
 
49
55
  /**
50
56
  * Read `host.json`. A missing file writes a complete seed. A present bad file throws and is left as it was.
@@ -183,9 +189,18 @@ function admitPolicy(raw: unknown, runtimeRoot: string, ports: PortBound): HostP
183
189
  llm: admitLlm(raw.llm),
184
190
  runtimeProvider: admitProvider(raw.runtimeProvider),
185
191
  ...admitWorkbench(raw.defaultWorkbenchId),
192
+ ...admitUpdateRegistry(raw.updateRegistry),
186
193
  }
187
194
  }
188
195
 
196
+ /** An empty value means the packaged default, so it is not written at all. */
197
+ function admitUpdateRegistry(value: unknown): { updateRegistry: string } | Record<string, never> {
198
+ if (value === undefined) return {}
199
+ if (typeof value !== 'string') throw new ConfigError('config-invalid', 'host config updateRegistry is invalid')
200
+ const registry = value.trim()
201
+ return registry.length === 0 ? {} : { updateRegistry: registry }
202
+ }
203
+
189
204
  function admitWorkbench(value: unknown): { defaultWorkbenchId: string } | Record<string, never> {
190
205
  if (value === undefined || value === 'default') return {}
191
206
  if (typeof value !== 'string' || value.length === 0) {
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, readUpdateSource, 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,16 +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()),
150
+ updateSource: () => Promise.resolve(readUpdateSource(updateEnv(input.policy()))),
151
151
  checkUpdate: async () => {
152
152
  const attempt = readUpdateResult(input.runtimeRoot)
153
153
  return {
154
- ...await checkPackageUpdate(),
154
+ ...await checkPackageUpdate(undefined, updateEnv(input.policy())),
155
155
  ...attempt === undefined ? {} : { lastAttempt: attempt },
156
156
  }
157
157
  },
158
158
  installUpdate: (version) => {
159
- stagePackageUpdate(version)
159
+ stagePackageUpdate(version, updateEnv(input.policy()))
160
160
  if (input.restart === undefined) return Promise.resolve()
161
161
  const restart = input.restart
162
162
  setTimeout(() => {
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
- ...body,
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: {
@@ -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
 
@@ -91,7 +97,7 @@ export async function checkPackageUpdate(
91
97
  const name = prefix === undefined ? about.name : '@mohou/shell'
92
98
  try {
93
99
  const response = await fetch(`${registry.replace(/\/$/, '')}/${name}/latest`, {
94
- signal: AbortSignal.timeout(3_000),
100
+ signal: AbortSignal.timeout(updateCheckTimeoutMs),
95
101
  })
96
102
  if (!response.ok) return {
97
103
  ...empty,
@@ -109,7 +115,8 @@ export async function checkPackageUpdate(
109
115
  channel: prefix?.channel ?? 'registry',
110
116
  ...prefix === undefined ? {} : { registry: prefix.registry ?? 'https://registry.npmjs.org' },
111
117
  installable: prefix !== undefined,
112
- updateAvailable: latest !== null && latest !== about.current,
118
+ // Newer only. A mirror syncs late, and an older `latest` is not an update.
119
+ updateAvailable: latest !== null && compareVersion(latest, about.current) > 0,
113
120
  }
114
121
  } catch (error) {
115
122
  return {
@@ -122,10 +129,16 @@ export async function checkPackageUpdate(
122
129
  }
123
130
  }
124
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
+
125
137
  /**
126
138
  * Where this install would update from. Reads the install prefix; asks no registry, so a
127
139
  * panel can show the source while the network is slow or down.
128
- * @param env - process environment; `MINI_APP_TARBALL_DIR` overrides the package folder
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)
129
142
  * @param cwd - directory the sidecar runs in
130
143
  */
131
144
  export function readUpdateSource(env: NodeJS.ProcessEnv = process.env, cwd = process.cwd()): UpdateSource {
@@ -272,10 +285,13 @@ function readPrefixUpdate(cwd: string, env: NodeJS.ProcessEnv): PrefixUpdate | u
272
285
  const packages = Object.keys(parsed.dependencies ?? {}).filter(name => name.startsWith('@mohou/'))
273
286
  const override = env.MINI_APP_TARBALL_DIR
274
287
  const tarballDir = override !== undefined && override.length > 0 ? override : parsed.mohou?.tarballDir
288
+ const registry = env.MINI_APP_NPM_REGISTRY?.trim()
275
289
  return {
276
290
  dir,
277
291
  channel,
278
- ...typeof parsed.mohou?.registry === 'string' ? { registry: parsed.mohou.registry } : {},
292
+ ...registry !== undefined && registry.length > 0
293
+ ? { registry }
294
+ : typeof parsed.mohou?.registry === 'string' ? { registry: parsed.mohou.registry } : {},
279
295
  ...tarballDir === undefined ? {} : { tarballDir },
280
296
  packages: packages.length === 0 ? ['@mohou/shell'] : packages,
281
297
  }