@toa.io/operations 1.0.0-alpha.291 → 1.0.0-alpha.293

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/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.0.0-alpha.293](https://github.com/toa-io/toa/compare/v1.0.0-alpha.292...v1.0.0-alpha.293) (2026-09-07)
7
+
8
+ **Note:** Version bump only for package @toa.io/operations
9
+
10
+
11
+
12
+
13
+
14
+ # [1.0.0-alpha.292](https://github.com/toa-io/toa/compare/v1.0.0-alpha.291...v1.0.0-alpha.292) (2026-09-07)
15
+
16
+ ### Features
17
+
18
+ * **cli:** the deployment library is a peer, and the binary is the CLI's ([a923f81](https://github.com/toa-io/toa/commit/a923f8173a55ed1c091213a7c7db1863ca1d0ba6))
19
+ * **norm:** a package's definition is read by one resolver ([bffd41c](https://github.com/toa-io/toa/commit/bffd41c8d18c42786b0bf321623088b9dabc4724))
20
+
21
+
6
22
  # [1.0.0-alpha.291](https://github.com/toa-io/toa/compare/v1.0.0-alpha.290...v1.0.0-alpha.291) (2026-09-07)
7
23
 
8
24
  **Note:** Version bump only for package @toa.io/operations
@@ -1,4 +1,18 @@
1
1
  FROM node:24.14.0-alpine3.22
2
2
 
3
3
  ARG VERSION
4
- RUN npm i -g @toa.io/runtime@${VERSION} --omit=dev
4
+
5
+ # installed locally rather than globally: the binary is @toa.io/cli's, which the runtime depends
6
+ # on, and npm links a dependency's binaries only into a local `node_modules/.bin`
7
+ WORKDIR /toa
8
+
9
+ # mounted rather than written: npm's cache is a hundred megabytes, and a layer keeps whatever
10
+ # the command left behind.
11
+ # `--legacy-peer-deps` installs no peer this does not ask for: `promex`, `reretry` and
12
+ # `matchacho` declare `typescript` a peer without marking it optional, and nothing here imports
13
+ # it. Every other peer in the closure is a dependency of the runtime's already. Drop the flag
14
+ # once those three are published with `peerDependenciesMeta`.
15
+ RUN --mount=type=cache,target=/root/.npm,sharing=locked \
16
+ npm i @toa.io/runtime@${VERSION} --omit=dev --legacy-peer-deps
17
+
18
+ ENV PATH=/toa/node_modules/.bin:$PATH
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/operations",
3
- "version": "1.0.0-alpha.291",
3
+ "version": "1.0.0-alpha.293",
4
4
  "type": "module",
5
5
  "description": "Toa Deployment",
6
6
  "homepage": "https://toa.io",
@@ -28,9 +28,11 @@
28
28
  "test": "echo \"Error: run tests from root\" && exit 1"
29
29
  },
30
30
  "dependencies": {
31
- "@toa.io/generic": "1.0.0-alpha.291",
32
- "@toa.io/norm": "1.0.0-alpha.291",
33
- "js-yaml": "5.4.1"
31
+ "@toa.io/generic": "1.0.0-alpha.292",
32
+ "@toa.io/kubernetes": "1.0.0-alpha.293",
33
+ "@toa.io/norm": "1.0.0-alpha.293",
34
+ "oxc-parser": "0.149.0",
35
+ "paseto": "4.0.0"
34
36
  },
35
- "gitHead": "bcd8d8d07bd3e64fc826af8719472404662cc37f"
37
+ "gitHead": "1e05b3ab64758324c58c425f1f8e52e9f35f1da8"
36
38
  }
package/readme.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Toa Operations
2
2
 
3
+ ## Installing
4
+
5
+ `toa deploy`, `toa build`, `toa push`, `toa env`, `toa export` and `toa conceal` need this package
6
+ beside the CLI; the runtime does not carry it, and a container that runs a composition cannot
7
+ deploy one. An application that deploys lists it with the runtime:
8
+
9
+ ```shell
10
+ $ npm i -D @toa.io/runtime @toa.io/operations
11
+ ```
12
+
13
+ A machine that only deploys needs no runtime and no extension:
14
+
15
+ ```shell
16
+ $ npm i @toa.io/cli @toa.io/operations
17
+ $ npx toa deploy production -p application
18
+ ```
19
+
20
+ What the extensions declare is read from `@toa.io/definitions`, which the CLI brings; what they run
21
+ is not needed to render a chart. `registry.services: build` is the exception: it builds an
22
+ extension's service image from the installed package, so it needs the runtime installed beside
23
+ this one. `published` does not.
24
+
3
25
  ## Compositions
4
26
 
5
27
  A composition is deployed as one pod. Beside its components it may run extension services,
@@ -27,7 +49,8 @@ Deploy images default to `FROM ghcr.io/toa-io/runtime:<runtime.version>`
27
49
  installed; composition/service Dockerfiles only install component dependencies.
28
50
 
29
51
  To use a custom base image, set `registry.build.image` (or `composition.image`).
30
- That image must provide the `toa` CLI, or install it via `registry.build.run`:
52
+ That image must provide the `toa` CLI, or install it via `registry.build.run`. The binary is
53
+ `@toa.io/cli`'s, which the runtime depends on, so a local install puts it on the `PATH`:
31
54
 
32
55
  ```yaml
33
56
  # context.toa.yaml
@@ -35,25 +58,44 @@ That image must provide the `toa` CLI, or install it via `registry.build.run`:
35
58
  registry:
36
59
  build:
37
60
  image: node:24.14.0-alpine3.22
38
- run: npm i -g @toa.io/runtime --omit=dev
61
+ run: |
62
+ npm i --prefix /toa @toa.io/runtime --omit=dev
63
+ ln -s /toa/node_modules/.bin/toa /usr/local/bin/toa
39
64
  ```
40
65
 
66
+ `/toa` is where it goes: what an extension needs for what a component declares is installed
67
+ there, beside the extension that reads the declaration.
68
+
41
69
  #### Composition Images
42
70
 
43
71
  A composition is two images in one repository. `composition-<name>:deps-<hash>` is what its
44
72
  components depend on, installed on the base image: it is tagged by everything the install
45
- reads — the runtime version, the base image, the build options and each component's
46
- `package.json` (and `package-lock.json`, where there is one) and is built only when one of
47
- those changes. `composition-<name>:<hash>` is the sources laid over it in a single linked
73
+ reads — the runtime version, the base image, the build options, each component's
74
+ `package.json` (and `package-lock.json`, where there is one), and what the extensions install
75
+ for what the components declare — and is built only when one of those changes. `composition-<name>:<hash>` is the sources laid over it in a single linked
48
76
  layer, so a deploy that changes code alone builds and pushes that layer, and neither
49
77
  downloads nor uploads the dependencies again. The same holds for `mono`.
50
78
 
79
+ Beside each component's sources the layer carries `manifest.toa.json`, the manifest as this
80
+ build normalised it. A composition reads it instead of normalising again, so it loads no
81
+ bridge to read what a module declares; a workspace has no such file and is read as it always
82
+ was. What a build cannot express there — a prototype in a directory of the application's own
83
+ rather than in a package — fails the build, where before it failed the container.
84
+
51
85
  A pushed image builds on the `toa` container builder, whose registry exporter is what lays
52
86
  an image over a base it never pulled; `toa build` loads on the daemon's own.
53
87
 
54
88
  A dependency named by a moving git ref is installed when the dependencies image is built and
55
89
  not again until its manifest changes, so pin such a dependency to a commit and bump it there.
56
90
 
91
+ An extension's heavy dependency is not installed with the extension: `@toa.io/extensions.storages`
92
+ declares the AWS and Cloudinary SDKs as optional peers, so the base image carries neither. What a
93
+ component declares is what a deploy installs — the storages named in its `manifest.toa.yaml`,
94
+ resolved through the context's annotation to their providers' packages, installed into `/toa`
95
+ beside the extension that reads them. A composition whose components declare no `s3` storage
96
+ carries no AWS SDK, whatever the context declares for another composition. A workspace installs
97
+ the same set with `toa npm`.
98
+
57
99
  #### Extension Service Images
58
100
 
59
101
  `registry.services` says where an extension service's image comes from.
@@ -1,15 +1,9 @@
1
- import { readFileSync } from 'node:fs'
2
- import { join } from 'node:path'
3
- import { createRequire } from 'node:module'
4
- import { pathToFileURL } from 'node:url'
5
- import { context as load } from '@toa.io/norm'
1
+ import { context as load, definition } from '@toa.io/norm'
6
2
  import { Process } from '../process.js'
7
3
  import { Operator } from './operator.js'
8
4
  import { Factory as ImagesFactory } from './images/index.js'
9
5
  import { Deployment } from './deployment.js'
10
6
 
11
- // a dependency is resolved the way a package is
12
- const require = createRequire(import.meta.url)
13
7
  import { Registry } from './registry.js'
14
8
  import { Composition } from './composition.js'
15
9
  import { Service } from './service.js'
@@ -47,7 +41,9 @@ export class Factory {
47
41
 
48
42
  if (this.#mono)
49
43
  this.#image = this.#registry.mono({
50
- components: context.components
44
+ components: context.components,
45
+ // mono runs every service, so it installs what every one of them brings
46
+ packages: context.packages
51
47
  })
52
48
  else
53
49
  this.#compositions = context.compositions.map((composition) =>
@@ -110,16 +106,11 @@ export class Factory {
110
106
  }
111
107
 
112
108
  async #getDependency(reference, instances) {
113
- // a dependency may be named the way a package is or written as a directory,
114
- // and a module is loaded by file
115
- const module = await import(pathToFileURL(require.resolve(reference)).href)
116
- const pkg = JSON.parse(
117
- readFileSync(require.resolve(join(reference, 'package.json')), 'utf8')
118
- )
109
+ const { name, module } = await definition(reference)
119
110
 
120
111
  if (module.deployment === undefined) return
121
112
 
122
- const annotation = this.#context.annotations?.[pkg.name]
113
+ const annotation = this.#context.annotations?.[name]
123
114
 
124
115
  /** @type {toa.deployment.dependency.Declaration} */
125
116
  const dependency = module.deployment(instances, annotation)
@@ -4,7 +4,7 @@ import { createHash } from 'node:crypto'
4
4
 
5
5
  import { Image } from './image.js'
6
6
  import { Dependencies } from './dependencies.js'
7
- import { declare } from './format.js'
7
+ import { declare, normalized } from './format.js'
8
8
 
9
9
  /**
10
10
  * Components in one image: their sources laid over their dependencies, which are an image
@@ -23,11 +23,16 @@ export class Bundle extends Image {
23
23
  /** @type {toa.norm.Component[]} */
24
24
  components
25
25
 
26
+ /** What the services this workload runs install, which no component of it declares.
27
+ * @type {Record<string, string> | undefined} */
28
+ packages
29
+
26
30
  constructor(scope, runtime, registry, composition) {
27
31
  super(scope, runtime, registry)
28
32
 
29
33
  this.image = composition.image
30
34
  this.components = composition.components
35
+ this.packages = composition.packages
31
36
  this.dependencies = new Dependencies(scope, runtime, registry, this)
32
37
  }
33
38
 
@@ -73,6 +78,7 @@ export class Bundle extends Image {
73
78
  // what was installed in the workspace is not what the image installs
74
79
  await cp(component.path, target, { recursive: true, filter: sources })
75
80
  await declare(component.path, target, component.locator.label)
81
+ await normalized(component, target)
76
82
  }
77
83
 
78
84
  return context
@@ -11,6 +11,13 @@ COPY --chown=node:node . /composition
11
11
 
12
12
  {{build.run}}
13
13
 
14
- # the context holds the manifests alone, so this is every dependency and none of the sources
14
+ # the context holds the manifests alone, so this is every dependency and none of the sources.
15
+ # `.packages` is a dotfile, so the glob does not reach it
15
16
  RUN --mount=type=cache,target=/root/.npm,sharing=locked \
16
17
  for entry in *; do if grep -qs '"dependencies"' "$entry/package.json"; then (cd $entry && npm i --omit=dev); fi; done
18
+
19
+ # what the extensions need for what these components declare: installed beside the extension
20
+ # that reads the declaration, in Toa's own install, because that is where Node resolves an
21
+ # extension's imports from — and one copy for the composition rather than one per component
22
+ RUN --mount=type=cache,target=/root/.npm,sharing=locked \
23
+ if [ -s .packages ]; then npm i --prefix /toa --omit=dev $(cat .packages); fi
@@ -1,6 +1,6 @@
1
1
  import { basename, join } from 'node:path'
2
2
  import { existsSync, readFileSync } from 'node:fs'
3
- import { copyFile, mkdir } from 'node:fs/promises'
3
+ import { copyFile, mkdir, writeFile } from 'node:fs/promises'
4
4
  import { createHash } from 'node:crypto'
5
5
 
6
6
  import { Image } from './image.js'
@@ -68,6 +68,8 @@ export class Dependencies extends Image {
68
68
  for (const file of files)
69
69
  hash.update(label).update(basename(file)).update(readFileSync(file))
70
70
 
71
+ hash.update(this.#packages().join('\n'))
72
+
71
73
  this.#version = hash.digest('hex').slice(0, 8)
72
74
 
73
75
  return this.#version
@@ -112,9 +114,34 @@ export class Dependencies extends Image {
112
114
  for (const file of files) await copyFile(file, join(target, basename(file)))
113
115
  }
114
116
 
117
+ const packages = this.#packages()
118
+
119
+ // written whether or not there are any, so the file is one thing the Dockerfile reads
120
+ await writeFile(join(context, PACKAGES), packages.join('\n'))
121
+
115
122
  return context
116
123
  }
117
124
 
125
+ /**
126
+ * What the extensions install for what this workload runs, as `name@version`: what its
127
+ * components declare, and what the services it hosts bring. A component states its own
128
+ * dependencies in its manifest; this is what the packages that read its declaration need,
129
+ * which its manifest has no way to say.
130
+ *
131
+ * @returns {string[]}
132
+ */
133
+ #packages() {
134
+ /** @type {Record<string, string>} */
135
+ const packages = { ...this.#owner.packages }
136
+
137
+ for (const component of this.#owner.components)
138
+ Object.assign(packages, component.packages)
139
+
140
+ return Object.entries(packages)
141
+ .map(([name, version]) => `${name}@${version}`)
142
+ .sort()
143
+ }
144
+
118
145
  /**
119
146
  * The files the install reads, by component label, in a fixed order.
120
147
  *
@@ -139,3 +166,6 @@ const PREFIX = 'deps-'
139
166
  const DIRECTORY = 'dependencies'
140
167
 
141
168
  const MANIFESTS = ['package.json', 'package-lock.json']
169
+
170
+ /** Where the install reads what the extensions need, one `name@version` per line. */
171
+ const PACKAGES = '.packages'
@@ -90,6 +90,20 @@ describe('reference', () => {
90
90
  assert.notStrictEqual(create().dependencies.reference, run.dependencies.reference)
91
91
  })
92
92
 
93
+ it('should change with what an extension installs', () => {
94
+ const before = create()
95
+
96
+ composition.components[1].packages = { cloudinary: '2.11.0' }
97
+
98
+ const component = create()
99
+
100
+ assert.notStrictEqual(component.dependencies.reference, before.dependencies.reference)
101
+
102
+ composition.packages = { 'lru-cache': '11.5.2' }
103
+
104
+ assert.notStrictEqual(create().dependencies.reference, component.dependencies.reference)
105
+ })
106
+
93
107
  it('should change with the registry build settings', () => {
94
108
  const before = create()
95
109
 
@@ -128,6 +142,7 @@ describe('prepare', () => {
128
142
 
129
143
  assert.deepStrictEqual(entries, [
130
144
  '.dockerignore',
145
+ '.packages',
131
146
  'Dockerfile',
132
147
  'one',
133
148
  'one/package.json',
@@ -144,6 +159,44 @@ describe('prepare', () => {
144
159
  assert.doesNotMatch(dockerfile, /USER node|CMD /)
145
160
  })
146
161
 
162
+ it('should hold what the extensions install, deduplicated and ordered', async () => {
163
+ composition.components[0].packages = { cloudinary: '2.11.0' }
164
+ composition.components[1].packages = {
165
+ cloudinary: '2.11.0',
166
+ '@aws-sdk/client-s3': '3.1125.0'
167
+ }
168
+
169
+ const image = create()
170
+ const context = await image.dependencies.prepare(root)
171
+
172
+ assert.strictEqual(
173
+ await readFile(join(context, '.packages'), 'utf8'),
174
+ '@aws-sdk/client-s3@3.1125.0\ncloudinary@2.11.0'
175
+ )
176
+
177
+ const dockerfile = await readFile(join(context, 'Dockerfile'), 'utf8')
178
+
179
+ assert.ok(dockerfile.includes('npm i --prefix /toa --omit=dev $(cat .packages)'))
180
+ })
181
+
182
+ it('should hold what a service the composition runs brings', async () => {
183
+ composition.packages = { 'lru-cache': '11.5.2' }
184
+ composition.components[0].packages = { cloudinary: '2.11.0' }
185
+
186
+ const context = await create().dependencies.prepare(root)
187
+
188
+ assert.strictEqual(
189
+ await readFile(join(context, '.packages'), 'utf8'),
190
+ 'cloudinary@2.11.0\nlru-cache@11.5.2'
191
+ )
192
+ })
193
+
194
+ it('should hold an empty list where nothing is declared', async () => {
195
+ const context = await create().dependencies.prepare(root)
196
+
197
+ assert.strictEqual(await readFile(join(context, '.packages'), 'utf8'), '')
198
+ })
199
+
147
200
  it('should lay the sources over the dependencies', async () => {
148
201
  const image = create()
149
202
 
@@ -167,6 +220,7 @@ describe('prepare', () => {
167
220
 
168
221
  assert.ok(entries.includes('one/index.js'))
169
222
  assert.ok(entries.includes('two/package.json')) // declared for the component that had none
223
+ assert.ok(entries.includes('one/manifest.toa.json')) // what the process reads instead of normalizing
170
224
  assert.ok(!entries.some((entry) => entry.includes('node_modules')))
171
225
  })
172
226
 
@@ -2,6 +2,8 @@ import { existsSync, readFileSync } from 'node:fs'
2
2
  import { writeFile } from 'node:fs/promises'
3
3
  import { dirname, join, parse } from 'node:path'
4
4
 
5
+ import { NORMALIZED, plain } from '@toa.io/norm'
6
+
5
7
  /**
6
8
  * A component is copied into the image on its own, away from the package that
7
9
  * declared what its files are. Node reads a `.js` as CommonJS unless a manifest
@@ -23,6 +25,18 @@ export async function declare(source, target, name) {
23
25
  )
24
26
  }
25
27
 
28
+ /**
29
+ * The manifest as this build read it, written beside the component so that the process which
30
+ * runs it does not read it again. See `plain` in `@toa.io/norm` for what is left out and why
31
+ * a build's answer is the one to trust.
32
+ *
33
+ * @param {toa.norm.Component} component
34
+ * @param {string} target where it was copied
35
+ */
36
+ export async function normalized(component, target) {
37
+ await writeFile(join(target, NORMALIZED), JSON.stringify(plain(component)))
38
+ }
39
+
26
40
  /**
27
41
  * The module format a directory's files are read as, which the nearest manifest
28
42
  * above it decides.
@@ -4,7 +4,10 @@ import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'
4
4
  import { tmpdir } from 'node:os'
5
5
  import { join } from 'node:path'
6
6
 
7
- import { declare, format } from './format.js'
7
+ import { Locator } from '@toa.io/core'
8
+ import { revive } from '@toa.io/norm'
9
+
10
+ import { declare, format, normalized } from './format.js'
8
11
 
9
12
  let root
10
13
 
@@ -72,3 +75,39 @@ describe('declare', () => {
72
75
  assert.strictEqual(await readFile(join(target, 'package.json'), 'utf8'), own)
73
76
  })
74
77
  })
78
+
79
+ describe('normalized', () => {
80
+ it('should write what a process reads instead of normalizing', async () => {
81
+ const target = join(root, 'image', 'normalized')
82
+
83
+ await mkdir(target, { recursive: true })
84
+
85
+ const component = {
86
+ name: 'tasks',
87
+ namespace: 'todos',
88
+ version: 'abcdef12',
89
+ operations: { compute: { type: 'computation' } },
90
+ path: '/workspace/components/todos.tasks',
91
+ locator: new Locator('tasks', 'todos'),
92
+ packages: { cloudinary: '2.11.0' }
93
+ }
94
+
95
+ await normalized(component, target)
96
+
97
+ const carried = JSON.parse(
98
+ await readFile(join(target, 'manifest.toa.json'), 'utf8')
99
+ )
100
+
101
+ // where the manifest is, and what only a deploy reads, are not what it declares
102
+ assert.ok(!('path' in carried))
103
+ assert.ok(!('locator' in carried))
104
+ assert.ok(!('packages' in carried))
105
+
106
+ const read = revive(carried, '/composition/todos-tasks')
107
+
108
+ assert.deepStrictEqual(read.operations, component.operations)
109
+ assert.strictEqual(read.version, component.version)
110
+ assert.strictEqual(read.path, '/composition/todos-tasks')
111
+ assert.strictEqual(read.locator.id, 'todos.tasks')
112
+ })
113
+ })
@@ -2,7 +2,7 @@ import { mkdtemp, writeFile } from 'node:fs/promises'
2
2
  import { readFileSync } from 'node:fs'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join, resolve } from 'node:path'
5
- import { pathToFileURL } from 'node:url'
5
+ import { definition } from '@toa.io/norm'
6
6
 
7
7
  import { run } from '../../process.js'
8
8
  import { Service } from './service.js'
@@ -12,7 +12,7 @@ import { Service } from './service.js'
12
12
  * with `registry.services: published` takes it instead of building one.
13
13
  *
14
14
  * The image is built from the package as npm publishes it, not from the workspace: what
15
- * `.npmignore` leaves out is not in the tarball, and an application builds from the tarball.
15
+ * `files` leaves out is not in the tarball, and an application builds from the tarball.
16
16
  *
17
17
  * @param {string} workspace path to the extension's directory in this repository
18
18
  * @param {string} runtime the runtime version, which is the tag and the base image
@@ -23,9 +23,7 @@ export async function publish(workspace, runtime, platforms) {
23
23
  const directory = resolve(workspace)
24
24
  const manifest = read(join(directory, 'package.json'))
25
25
 
26
- // read from the workspace, where the extension's own dependencies resolve; what is
27
- // installed below carries the same constant but not everything it takes to load it
28
- const { image } = await import(pathToFileURL(join(directory, manifest.main)).href)
26
+ const { image } = (await definition(manifest.name)).module
29
27
 
30
28
  if (image === undefined)
31
29
  throw new Error(
@@ -10,6 +10,12 @@ COPY --chown=node:node . /service
10
10
  RUN --mount=type=cache,target=/root/.npm,sharing=locked \
11
11
  npm i --omit=dev
12
12
 
13
+ # a component of the extension's own declares what it imports, and it is installed beside the
14
+ # component rather than beside the extension: an application that runs none of them, and the
15
+ # runtime image every application is built on, carry nothing for them
16
+ RUN --mount=type=cache,target=/root/.npm,sharing=locked \
17
+ for entry in components/*; do if grep -qs '"dependencies"' "$entry/package.json"; then (cd $entry && npm i --omit=dev); fi; done
18
+
13
19
  # no USER: the runtime drops to `node` itself, and only a process that started as root can
14
20
  # close its environment under /proc — see runtime/runtime/bin/toa
15
21
  CMD toa serve .
@@ -66,9 +66,22 @@ export class Service extends Image {
66
66
  }
67
67
 
68
68
  /**
69
+ * Where the extension is installed, which is what its image is built from. A deploy install
70
+ * carries what an extension declares and not the extension, so a service is built only where
71
+ * the runtime is installed beside the deployment library; elsewhere its image is published.
72
+ *
69
73
  * @param {string} reference
70
74
  * @returns {string}
71
75
  */
72
76
  const find = (reference) => {
73
- return dirname(require.resolve(join(reference, 'package.json')))
77
+ try {
78
+ return dirname(require.resolve(join(reference, 'package.json')))
79
+ } catch (error) {
80
+ throw new Error(
81
+ `'${reference}' is not installed, and \`registry.services: build\` builds its image ` +
82
+ 'from where it is: install the runtime beside @toa.io/operations, or take the ' +
83
+ 'image the release publishes with `registry.services: published`',
84
+ { cause: error }
85
+ )
86
+ }
74
87
  }
package/src/index.js CHANGED
@@ -1 +1,4 @@
1
1
  export * as deployment from './deployment/index.js'
2
+
3
+ // what conceals a secret in the cluster a deployment goes to; the CLI needs no package of its own for it
4
+ export * as kubernetes from '@toa.io/kubernetes'