@memohai/cloud-runtime 0.1.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/LICENSE +661 -0
- package/README.md +35 -0
- package/dist/bridge.proto +188 -0
- package/dist/cli.mjs +29978 -0
- package/package.json +37 -0
- package/scripts/build.mjs +191 -0
- package/src/bootstrap.ts +29 -0
- package/src/build-info.ts +40 -0
- package/src/cli.ts +7 -0
- package/src/cloud.ts +62 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@memohai/cloud-runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Memoh Cloud Remote Runtime CLI — connects a local machine to Memoh Cloud",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/memohai/Memoh-Cloud.git",
|
|
8
|
+
"directory": "packages/cloud-runtime"
|
|
9
|
+
},
|
|
10
|
+
"license": "AGPL-3.0-only",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"memoh-cloud-runtime": "dist/cli.mjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"scripts"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^25.9.5",
|
|
28
|
+
"esbuild": "0.27.2",
|
|
29
|
+
"typescript": "^6.0.3",
|
|
30
|
+
"vitest": "^4.1.10"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "node scripts/build.mjs",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"typecheck": "tsc --noEmit"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { access, chmod, copyFile, mkdir, readFile, rm } from 'node:fs/promises'
|
|
3
|
+
import { dirname, resolve } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
import { build } from 'esbuild'
|
|
7
|
+
|
|
8
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
+
const submodule = resolve(root, '../memoh')
|
|
10
|
+
const upstreamRuntime = resolve(submodule, 'packages/runtime')
|
|
11
|
+
const protoSource = resolve(submodule, 'internal/workspace/bridgepb/bridge.proto')
|
|
12
|
+
const outdir = resolve(root, 'dist')
|
|
13
|
+
|
|
14
|
+
// This package ships no runtime dependencies: the entire implementation is
|
|
15
|
+
// bundled from the memoh submodule. Node resolves the upstream sources' own
|
|
16
|
+
// imports (ws, @grpc/*) against packages/memoh's node_modules — resolution
|
|
17
|
+
// walks up from the importing file, not from this package — so the submodule
|
|
18
|
+
// has to be both checked out and installed.
|
|
19
|
+
await requirePath(
|
|
20
|
+
resolve(upstreamRuntime, 'src/cli.ts'),
|
|
21
|
+
'packages/memoh submodule is not checked out — run `git submodule update --init --recursive`',
|
|
22
|
+
)
|
|
23
|
+
await requirePath(
|
|
24
|
+
resolve(upstreamRuntime, 'node_modules/ws'),
|
|
25
|
+
'packages/memoh is not installed — run `pnpm install --filter @memohai/runtime...` from packages/memoh',
|
|
26
|
+
)
|
|
27
|
+
await requirePath(
|
|
28
|
+
protoSource,
|
|
29
|
+
'canonical bridge.proto is missing from the memoh submodule',
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// Which upstream runtime ends up inside the bundle is decided by the submodule
|
|
33
|
+
// gitlink, and nothing in the published tarball would otherwise record it.
|
|
34
|
+
// Stamp it in so a build in the wild can identify itself — see src/build-info.ts.
|
|
35
|
+
const cloudVersion = await readVersion(resolve(root, 'package.json'))
|
|
36
|
+
const upstreamVersion = await readVersion(resolve(upstreamRuntime, 'package.json'))
|
|
37
|
+
const upstreamCommit = await readSubmoduleCommit()
|
|
38
|
+
|
|
39
|
+
await rm(outdir, { recursive: true, force: true })
|
|
40
|
+
await mkdir(outdir, { recursive: true })
|
|
41
|
+
|
|
42
|
+
await build({
|
|
43
|
+
entryPoints: { cli: resolve(root, 'src/cli.ts') },
|
|
44
|
+
bundle: true,
|
|
45
|
+
platform: 'node',
|
|
46
|
+
format: 'esm',
|
|
47
|
+
target: 'node20',
|
|
48
|
+
packages: 'bundle',
|
|
49
|
+
define: {
|
|
50
|
+
__CLOUD_RUNTIME_VERSION__: JSON.stringify(cloudVersion),
|
|
51
|
+
__UPSTREAM_RUNTIME_VERSION__: JSON.stringify(upstreamVersion),
|
|
52
|
+
__UPSTREAM_RUNTIME_COMMIT__: JSON.stringify(upstreamCommit),
|
|
53
|
+
},
|
|
54
|
+
// grpc-js and ws are CommonJS. The bundled ESM artifact needs a require shim
|
|
55
|
+
// to load them — the same bridge the upstream build installs. esbuild emits
|
|
56
|
+
// the entry's shebang above this banner, so the bin stays executable.
|
|
57
|
+
banner: {
|
|
58
|
+
js: [
|
|
59
|
+
`// @memohai/cloud-runtime ${cloudVersion} — bundles @memohai/runtime ${upstreamVersion} (memoh ${upstreamCommit})`,
|
|
60
|
+
"import { createRequire as __memohCreateRequire } from 'node:module'",
|
|
61
|
+
'const require = __memohCreateRequire(import.meta.url)',
|
|
62
|
+
].join('\n'),
|
|
63
|
+
},
|
|
64
|
+
outdir,
|
|
65
|
+
outExtension: { '.js': '.mjs' },
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await chmod(resolve(outdir, 'cli.mjs'), 0o755)
|
|
69
|
+
|
|
70
|
+
// service.ts resolves the descriptor next to the built artifact first, so the
|
|
71
|
+
// canonical proto has to travel with the bundle.
|
|
72
|
+
await copyFile(protoSource, resolve(outdir, 'bridge.proto'))
|
|
73
|
+
|
|
74
|
+
await verifyBundleLoads()
|
|
75
|
+
await verifyVersionStamp()
|
|
76
|
+
await verifyHelpBranding()
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Runs the bundle far enough to prove its whole module graph evaluates under
|
|
80
|
+
* ESM — the CommonJS grpc-js/ws bridge above is the fragile part, and it only
|
|
81
|
+
* fails at load time. The upstream CLI validates arguments *after* that graph
|
|
82
|
+
* is live, so its own error is the success signal here.
|
|
83
|
+
*/
|
|
84
|
+
async function verifyBundleLoads() {
|
|
85
|
+
const { code, stderr, stdout } = await run(process.execPath, [resolve(outdir, 'cli.mjs')], {
|
|
86
|
+
// Scrub the developer's own credentials: with a key present the CLI would
|
|
87
|
+
// skip validation and try to connect, hanging the build.
|
|
88
|
+
...process.env,
|
|
89
|
+
MEMOH_RUNTIME_SERVER: '',
|
|
90
|
+
MEMOH_RUNTIME_KEY: '',
|
|
91
|
+
MEMOH_RUNTIME_TEAM_ID: '',
|
|
92
|
+
})
|
|
93
|
+
if (!stderr.includes('--server and --key are required')) {
|
|
94
|
+
throw new Error(`bundle smoke test failed (exit ${code}):\n${stderr || stdout}`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Guards the `define` wiring: an un-substituted stamp silently degrades to
|
|
100
|
+
* "unknown" at runtime rather than failing the build, which would leave
|
|
101
|
+
* published artifacts unable to identify themselves.
|
|
102
|
+
*/
|
|
103
|
+
async function verifyVersionStamp() {
|
|
104
|
+
const { code, stdout, stderr } = await run(
|
|
105
|
+
process.execPath,
|
|
106
|
+
[resolve(outdir, 'cli.mjs'), '--version'],
|
|
107
|
+
process.env,
|
|
108
|
+
)
|
|
109
|
+
if (!stdout.includes(upstreamVersion) || !stdout.includes(upstreamCommit)) {
|
|
110
|
+
throw new Error(`version stamp missing from the bundle (exit ${code}):\n${stdout || stderr}`)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Guards the import order in src/cli.ts — the one thing this package's design
|
|
116
|
+
* hinges on. If the upstream entry ever evaluates first, --help prints
|
|
117
|
+
* upstream's `memoh-runtime` usage (or worse, starts a session) before
|
|
118
|
+
* bootstrap can intercept. The branding is the observable proof that
|
|
119
|
+
* bootstrap still runs first.
|
|
120
|
+
*/
|
|
121
|
+
async function verifyHelpBranding() {
|
|
122
|
+
const { stderr, stdout } = await run(
|
|
123
|
+
process.execPath,
|
|
124
|
+
[resolve(outdir, 'cli.mjs'), '--help'],
|
|
125
|
+
process.env,
|
|
126
|
+
)
|
|
127
|
+
if (!stderr.includes('memoh-cloud-runtime') || stderr.includes('Usage: memoh-runtime ')) {
|
|
128
|
+
throw new Error(`--help is not Cloud-branded — bootstrap did not run first:\n${stderr || stdout}`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function readVersion(packageJsonPath) {
|
|
133
|
+
const { version } = JSON.parse(await readFile(packageJsonPath, 'utf8'))
|
|
134
|
+
if (!version) {
|
|
135
|
+
throw new Error(`no version field in ${packageJsonPath}`)
|
|
136
|
+
}
|
|
137
|
+
return version
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Identifies the exact submodule tree that was bundled. Falls back to "unknown"
|
|
142
|
+
* rather than failing: builds from a source tarball have no git metadata, and
|
|
143
|
+
* that is not a reason to block a release.
|
|
144
|
+
*/
|
|
145
|
+
async function readSubmoduleCommit() {
|
|
146
|
+
// A missing git *binary* rejects the spawn promise outright — same
|
|
147
|
+
// "no git metadata" situation as a failing rev-parse, same fallback.
|
|
148
|
+
try {
|
|
149
|
+
const head = await run('git', ['-C', submodule, 'rev-parse', '--short=8', 'HEAD'], process.env)
|
|
150
|
+
if (head.code !== 0) {
|
|
151
|
+
return 'unknown'
|
|
152
|
+
}
|
|
153
|
+
const commit = head.stdout.trim()
|
|
154
|
+
// A dirty submodule means the bundle contains code that exists in no
|
|
155
|
+
// commit; saying so is the whole point of the stamp.
|
|
156
|
+
const status = await run('git', ['-C', submodule, 'status', '--porcelain'], process.env)
|
|
157
|
+
return status.code === 0 && status.stdout.trim() ? `${commit}-dirty` : commit
|
|
158
|
+
} catch {
|
|
159
|
+
return 'unknown'
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function run(command, args, env) {
|
|
164
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
165
|
+
const child = spawn(command, args, { env, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
166
|
+
const timer = setTimeout(() => {
|
|
167
|
+
child.kill('SIGKILL')
|
|
168
|
+
rejectPromise(new Error('bundle smoke test timed out after 30s'))
|
|
169
|
+
}, 30_000)
|
|
170
|
+
let stdout = ''
|
|
171
|
+
let stderr = ''
|
|
172
|
+
child.stdout.on('data', chunk => (stdout += chunk))
|
|
173
|
+
child.stderr.on('data', chunk => (stderr += chunk))
|
|
174
|
+
child.on('error', error => {
|
|
175
|
+
clearTimeout(timer)
|
|
176
|
+
rejectPromise(error)
|
|
177
|
+
})
|
|
178
|
+
child.on('close', code => {
|
|
179
|
+
clearTimeout(timer)
|
|
180
|
+
resolvePromise({ code, stdout, stderr })
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function requirePath(path, message) {
|
|
186
|
+
try {
|
|
187
|
+
await access(path)
|
|
188
|
+
} catch {
|
|
189
|
+
throw new Error(message)
|
|
190
|
+
}
|
|
191
|
+
}
|
package/src/bootstrap.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-effect module: adjusts process state before the upstream CLI runs.
|
|
3
|
+
*
|
|
4
|
+
* cli.ts imports this ahead of the upstream entry point, and ES modules
|
|
5
|
+
* evaluate their imports depth-first in source order, so this body is
|
|
6
|
+
* guaranteed to finish first — which matters because the upstream module
|
|
7
|
+
* starts a session as soon as *its* body evaluates. That ordering is a
|
|
8
|
+
* language guarantee, not a bundler behaviour, so it survives esbuild.
|
|
9
|
+
*/
|
|
10
|
+
import { versionReport } from './build-info'
|
|
11
|
+
import { applyCloudDefaults, cloudUsage } from './cloud'
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2)
|
|
14
|
+
|
|
15
|
+
// Both flags are handled here rather than upstream: the usage string there
|
|
16
|
+
// names `memoh-runtime` and cannot know the Cloud default endpoint, and
|
|
17
|
+
// upstream has no --version at all.
|
|
18
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
19
|
+
// stderr and exit 0, matching upstream's usage() contract.
|
|
20
|
+
console.error(cloudUsage())
|
|
21
|
+
process.exit(0)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
25
|
+
console.log(versionReport())
|
|
26
|
+
process.exit(0)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
applyCloudDefaults(args, process.env)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build stamps injected by scripts/build.mjs through esbuild `define`.
|
|
3
|
+
*
|
|
4
|
+
* The artifact is the only place that can answer which upstream runtime it
|
|
5
|
+
* contains. This package bundles its implementation from the packages/memoh
|
|
6
|
+
* submodule at build time, so the upstream version is pinned by a gitlink
|
|
7
|
+
* rather than by a semver range in package.json — nothing in the published
|
|
8
|
+
* tarball would otherwise record it. Stamping it here is what lets a build in
|
|
9
|
+
* the wild identify itself during an incident.
|
|
10
|
+
*
|
|
11
|
+
* Drop this module when the package moves to a plain npm dependency on
|
|
12
|
+
* @memohai/runtime: at that point the lockfile records the version instead.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const unknownStamp = 'unknown'
|
|
16
|
+
|
|
17
|
+
declare const __CLOUD_RUNTIME_VERSION__: string
|
|
18
|
+
declare const __UPSTREAM_RUNTIME_VERSION__: string
|
|
19
|
+
declare const __UPSTREAM_RUNTIME_COMMIT__: string
|
|
20
|
+
|
|
21
|
+
// The `typeof` guards keep this module loadable outside the bundle, where the
|
|
22
|
+
// identifiers are never declared — tests and `tsc` import it directly. Reading
|
|
23
|
+
// an undeclared identifier throws; `typeof` on one does not. Inside the bundle
|
|
24
|
+
// esbuild substitutes literals and these fold away.
|
|
25
|
+
export const cloudRuntimeVersion
|
|
26
|
+
= typeof __CLOUD_RUNTIME_VERSION__ === 'string' ? __CLOUD_RUNTIME_VERSION__ : unknownStamp
|
|
27
|
+
|
|
28
|
+
export const upstreamRuntimeVersion
|
|
29
|
+
= typeof __UPSTREAM_RUNTIME_VERSION__ === 'string' ? __UPSTREAM_RUNTIME_VERSION__ : unknownStamp
|
|
30
|
+
|
|
31
|
+
/** Short commit of the packages/memoh submodule, `-dirty` when built from edits. */
|
|
32
|
+
export const upstreamRuntimeCommit
|
|
33
|
+
= typeof __UPSTREAM_RUNTIME_COMMIT__ === 'string' ? __UPSTREAM_RUNTIME_COMMIT__ : unknownStamp
|
|
34
|
+
|
|
35
|
+
export function versionReport(): string {
|
|
36
|
+
return [
|
|
37
|
+
`@memohai/cloud-runtime ${cloudRuntimeVersion}`,
|
|
38
|
+
`bundles @memohai/runtime ${upstreamRuntimeVersion} (memoh ${upstreamRuntimeCommit})`,
|
|
39
|
+
].join('\n')
|
|
40
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Import order is load-bearing. ./bootstrap seeds the Cloud endpoint and may
|
|
4
|
+
// exit for --help; the upstream module below connects the moment it evaluates,
|
|
5
|
+
// so it must come second. See src/bootstrap.ts.
|
|
6
|
+
import './bootstrap'
|
|
7
|
+
import '../../memoh/packages/runtime/src/cli'
|
package/src/cloud.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything Memoh Cloud adds on top of the upstream runtime CLI.
|
|
3
|
+
*
|
|
4
|
+
* The rest of this package *is* @memohai/runtime, bundled straight from the
|
|
5
|
+
* packages/memoh submodule at build time (see scripts/build.mjs). Keeping the
|
|
6
|
+
* Cloud behaviour confined to this file is what makes the divergence from open
|
|
7
|
+
* source auditable: if this file is empty, the two CLIs are identical.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Where the CLI dials when the operator does not say otherwise.
|
|
12
|
+
*
|
|
13
|
+
* `app.memoh.net` is the Cloud public ingress (deploy/AGENTS.md) and the BFF
|
|
14
|
+
* mounts the shared Memoh data plane under `/api/memoh`. The CLI appends
|
|
15
|
+
* `/runtimes/connect` itself, so this value stays an origin plus prefix — see
|
|
16
|
+
* packages/bff/internal/handler/runtime_connect.go.
|
|
17
|
+
*/
|
|
18
|
+
export const cloudDefaultServerUrl = 'https://app.memoh.net/api/memoh'
|
|
19
|
+
|
|
20
|
+
/** The environment variable the upstream CLI reads as its `--server` fallback. */
|
|
21
|
+
export const serverUrlEnvVar = 'MEMOH_RUNTIME_SERVER'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Seeds the Cloud endpoint without taking precedence away from the operator.
|
|
25
|
+
*
|
|
26
|
+
* Resolution order stays `--server` > `MEMOH_RUNTIME_SERVER` > Cloud default.
|
|
27
|
+
* The upstream CLI already prefers the flag over the environment variable, so
|
|
28
|
+
* seeding the variable is enough to add a default — and it must never overwrite
|
|
29
|
+
* a value the caller set deliberately (staging, a tunnel, a self-hosted
|
|
30
|
+
* install), which is why both escape hatches are checked first.
|
|
31
|
+
*/
|
|
32
|
+
export function applyCloudDefaults(args: readonly string[], env: NodeJS.ProcessEnv): void {
|
|
33
|
+
// The prefix match also catches `--server=url`, a spelling upstream does NOT
|
|
34
|
+
// parse. Seeding the default there would silently redirect a mistyped-but-
|
|
35
|
+
// explicit endpoint to Cloud production; deferring lets upstream's own
|
|
36
|
+
// usage error surface so the caller fixes the spelling instead.
|
|
37
|
+
if (args.some(arg => arg === '--server' || arg.startsWith('--server='))) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
if (env[serverUrlEnvVar]?.trim()) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
env[serverUrlEnvVar] = cloudDefaultServerUrl
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Cloud-branded help text. The upstream string names `memoh-runtime` and omits
|
|
48
|
+
* the default endpoint, so this package prints its own and never reaches it.
|
|
49
|
+
*/
|
|
50
|
+
export function cloudUsage(): string {
|
|
51
|
+
return [
|
|
52
|
+
'Usage: memoh-cloud-runtime --key <key> [--team-id <uuid>] [--server <url>] [--insecure-localhost]',
|
|
53
|
+
'',
|
|
54
|
+
'Connects this machine to Memoh Cloud as a remote runtime.',
|
|
55
|
+
'',
|
|
56
|
+
` --server defaults to ${cloudDefaultServerUrl}`,
|
|
57
|
+
' (or $MEMOH_RUNTIME_SERVER when exported)',
|
|
58
|
+
' --key team-scoped runtime key, mrk_...',
|
|
59
|
+
' --team-id tenant UUID issued alongside the key',
|
|
60
|
+
' --version this build, and the upstream runtime bundled into it',
|
|
61
|
+
].join('\n')
|
|
62
|
+
}
|