@mohou/host 1.0.20 → 1.0.21

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.20",
3
+ "version": "1.0.21",
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.20",
53
- "@mohou/contract": "^1.0.20",
54
- "@mohou/runtime-provider": "^1.0.20",
55
- "@mohou/ui": "^1.0.20",
56
- "@mohou/mcp-client": "^1.0.20"
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"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/better-sqlite3": "^9.6.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, stagePackageUpdate } from './updates.ts'
18
+ import { checkPackageUpdate, discardUpdateResult, readUpdateResult, readUpdateSource, stagePackageUpdate } from './updates.ts'
19
19
  import { McpError } from '@mohou/mcp-client'
20
20
 
21
21
  import { admitMcpText } from '../host/mcp-import.ts'
@@ -147,6 +147,7 @@ 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
151
  checkUpdate: async () => {
151
152
  const attempt = readUpdateResult(input.runtimeRoot)
152
153
  return {
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>
@@ -32,6 +32,15 @@ export type UpdateAttempt =
32
32
  readonly at: number
33
33
  }
34
34
 
35
+ /**
36
+ * Where this install updates from. `none` is a source tree with no install prefix,
37
+ * which cannot install anything.
38
+ */
39
+ export type UpdateSource =
40
+ | { readonly channel: 'registry'; readonly registry: string }
41
+ | { readonly channel: 'tarball'; readonly tarballDir: string }
42
+ | { readonly channel: 'none' }
43
+
35
44
  export interface UpdateCheck {
36
45
  readonly name: string
37
46
  readonly current: string
@@ -39,6 +48,9 @@ export interface UpdateCheck {
39
48
  readonly updateAvailable: boolean
40
49
  readonly channel?: 'registry' | 'tarball'
41
50
  readonly installable?: boolean
51
+ /** Set when the install prefix names one, so a panel can say where an update comes from. */
52
+ readonly registry?: string
53
+ readonly tarballDir?: string
42
54
  readonly error?: string
43
55
  /** The last install this machine recorded, when one is on disk. */
44
56
  readonly lastAttempt?: UpdateAttempt
@@ -62,12 +74,14 @@ export async function checkPackageUpdate(
62
74
  if (about.name.length === 0) return { ...empty, error: 'package name is missing' }
63
75
  const prefix = readPrefixUpdate(cwd, env)
64
76
  if (prefix?.channel === 'tarball') {
65
- const latest = newestTarball(prefix.tarballDir ?? homePackagesDir(homedir()), 'shell')
77
+ const tarballDir = prefix.tarballDir ?? homePackagesDir(homedir())
78
+ const latest = newestTarball(tarballDir, 'shell')
66
79
  return {
67
80
  name: '@mohou/shell',
68
81
  current: about.current,
69
82
  latest,
70
83
  channel: 'tarball',
84
+ tarballDir,
71
85
  installable: true,
72
86
  updateAvailable: latest !== null && compareVersion(latest, about.current) > 0,
73
87
  }
@@ -79,7 +93,13 @@ export async function checkPackageUpdate(
79
93
  const response = await fetch(`${registry.replace(/\/$/, '')}/${name}/latest`, {
80
94
  signal: AbortSignal.timeout(3_000),
81
95
  })
82
- if (!response.ok) return { ...empty, name, ...prefix === undefined ? {} : { channel: prefix.channel }, installable: prefix !== undefined, error: `package registry returned ${response.status}` }
96
+ if (!response.ok) return {
97
+ ...empty,
98
+ name,
99
+ ...prefix === undefined ? {} : { channel: prefix.channel, registry: prefix.registry ?? 'https://registry.npmjs.org' },
100
+ installable: prefix !== undefined,
101
+ error: `package registry returned ${response.status}`,
102
+ }
83
103
  const body = await response.json() as { version?: unknown }
84
104
  const latest = typeof body.version === 'string' ? body.version : null
85
105
  return {
@@ -87,14 +107,34 @@ export async function checkPackageUpdate(
87
107
  current: about.current,
88
108
  latest,
89
109
  channel: prefix?.channel ?? 'registry',
110
+ ...prefix === undefined ? {} : { registry: prefix.registry ?? 'https://registry.npmjs.org' },
90
111
  installable: prefix !== undefined,
91
112
  updateAvailable: latest !== null && latest !== about.current,
92
113
  }
93
114
  } catch (error) {
94
- return { ...empty, name, ...prefix === undefined ? {} : { channel: prefix.channel }, installable: prefix !== undefined, error: error instanceof Error ? error.message : 'update check failed' }
115
+ return {
116
+ ...empty,
117
+ name,
118
+ ...prefix === undefined ? {} : { channel: prefix.channel, registry: prefix.registry ?? 'https://registry.npmjs.org' },
119
+ installable: prefix !== undefined,
120
+ error: error instanceof Error ? error.message : 'update check failed',
121
+ }
95
122
  }
96
123
  }
97
124
 
125
+ /**
126
+ * Where this install would update from. Reads the install prefix; asks no registry, so a
127
+ * 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
129
+ * @param cwd - directory the sidecar runs in
130
+ */
131
+ export function readUpdateSource(env: NodeJS.ProcessEnv = process.env, cwd = process.cwd()): UpdateSource {
132
+ const prefix = readPrefixUpdate(cwd, env)
133
+ if (prefix === undefined) return { channel: 'none' }
134
+ if (prefix.channel === 'tarball') return { channel: 'tarball', tarballDir: prefix.tarballDir ?? homePackagesDir(homedir()) }
135
+ return { channel: 'registry', registry: prefix.registry ?? 'https://registry.npmjs.org' }
136
+ }
137
+
98
138
  /**
99
139
  * Read the launcher's record of the last install. A missing, unreadable, or malformed file reads as
100
140
  * no attempt: the panel shows nothing rather than a guessed outcome.