spell-runtime 1.0.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 +21 -0
- package/README.md +136 -0
- package/README.txt +126 -0
- package/dist/bundle/install.d.ts +7 -0
- package/dist/bundle/install.js +99 -0
- package/dist/bundle/install.js.map +1 -0
- package/dist/bundle/manifest.d.ts +5 -0
- package/dist/bundle/manifest.js +267 -0
- package/dist/bundle/manifest.js.map +1 -0
- package/dist/bundle/store.d.ts +16 -0
- package/dist/bundle/store.js +129 -0
- package/dist/bundle/store.js.map +1 -0
- package/dist/checks/evaluate.d.ts +2 -0
- package/dist/checks/evaluate.js +107 -0
- package/dist/checks/evaluate.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +144 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/logging/executionLog.d.ts +3 -0
- package/dist/logging/executionLog.js +31 -0
- package/dist/logging/executionLog.js.map +1 -0
- package/dist/runner/cast.d.ts +7 -0
- package/dist/runner/cast.js +117 -0
- package/dist/runner/cast.js.map +1 -0
- package/dist/runner/hostRunner.d.ts +5 -0
- package/dist/runner/hostRunner.js +41 -0
- package/dist/runner/hostRunner.js.map +1 -0
- package/dist/runner/input.d.ts +2 -0
- package/dist/runner/input.js +42 -0
- package/dist/runner/input.js.map +1 -0
- package/dist/runner/summary.d.ts +2 -0
- package/dist/runner/summary.js +31 -0
- package/dist/runner/summary.js.map +1 -0
- package/dist/steps/httpStep.d.ts +7 -0
- package/dist/steps/httpStep.js +100 -0
- package/dist/steps/httpStep.js.map +1 -0
- package/dist/steps/shellStep.d.ts +7 -0
- package/dist/steps/shellStep.js +48 -0
- package/dist/steps/shellStep.js.map +1 -0
- package/dist/types.d.ts +105 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/util/errors.d.ts +3 -0
- package/dist/util/errors.js +11 -0
- package/dist/util/errors.js.map +1 -0
- package/dist/util/http.d.ts +6 -0
- package/dist/util/http.js +11 -0
- package/dist/util/http.js.map +1 -0
- package/dist/util/idKey.d.ts +2 -0
- package/dist/util/idKey.js +11 -0
- package/dist/util/idKey.js.map +1 -0
- package/dist/util/object.d.ts +7 -0
- package/dist/util/object.js +62 -0
- package/dist/util/object.js.map +1 -0
- package/dist/util/outputs.d.ts +1 -0
- package/dist/util/outputs.js +28 -0
- package/dist/util/outputs.js.map +1 -0
- package/dist/util/paths.d.ts +4 -0
- package/dist/util/paths.js +26 -0
- package/dist/util/paths.js.map +1 -0
- package/dist/util/platform.d.ts +1 -0
- package/dist/util/platform.js +7 -0
- package/dist/util/platform.js.map +1 -0
- package/dist/util/template.d.ts +1 -0
- package/dist/util/template.js +61 -0
- package/dist/util/template.js.map +1 -0
- package/dist/util/version.d.ts +2 -0
- package/dist/util/version.js +35 -0
- package/dist/util/version.js.map +1 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Koichi Nishizuka
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Spell Runtime v1
|
|
2
|
+
|
|
3
|
+
Minimal CLI runtime for SpellBundle v1.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
- Node.js >= 20
|
|
8
|
+
- npm
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm i
|
|
12
|
+
npm run build
|
|
13
|
+
npm test
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Local dev:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm run dev -- --help
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Install as CLI
|
|
23
|
+
|
|
24
|
+
Global install:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm i -g spell-runtime
|
|
28
|
+
spell --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Run with npx:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx --yes --package spell-runtime spell --help
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Local package smoke checks:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm run smoke:link
|
|
41
|
+
npm run smoke:npx
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
- `spell install <local-path>`
|
|
47
|
+
- `spell list`
|
|
48
|
+
- `spell inspect <id> [--version x.y.z]`
|
|
49
|
+
- `spell cast <id> [--version x.y.z] [-p key=value ...] [--input input.json] [--dry-run] [--yes] [--allow-billing] [--verbose] [--profile <name>]`
|
|
50
|
+
- `spell log <execution-id>`
|
|
51
|
+
|
|
52
|
+
## Storage Layout
|
|
53
|
+
|
|
54
|
+
- Spells: `~/.spell/spells/<id_key>/<version>/`
|
|
55
|
+
- ID index: `~/.spell/spells/<id_key>/spell.id.txt`
|
|
56
|
+
- Logs: `~/.spell/logs/<timestamp>_<id>_<version>.json`
|
|
57
|
+
|
|
58
|
+
`id_key` is fixed as `base64url(utf8(id))`.
|
|
59
|
+
|
|
60
|
+
- `id` is the logical identifier (display, package identity).
|
|
61
|
+
- `id_key` is only for safe filesystem storage.
|
|
62
|
+
|
|
63
|
+
Consistency rule:
|
|
64
|
+
|
|
65
|
+
- `install` checks `spell.yaml` id against `spell.id.txt` when `spell.id.txt` already exists.
|
|
66
|
+
- mismatch is treated as an error.
|
|
67
|
+
|
|
68
|
+
## Cast Preflight
|
|
69
|
+
|
|
70
|
+
`cast` performs these checks before execution:
|
|
71
|
+
|
|
72
|
+
- bundle resolution by id (and optional version)
|
|
73
|
+
- input assembly (`--input` + `-p` overrides)
|
|
74
|
+
- JSON Schema validation by Ajv
|
|
75
|
+
- platform guard
|
|
76
|
+
- risk guard (`high`/`critical` requires `--yes`)
|
|
77
|
+
- billing guard (`billing.enabled` requires `--allow-billing`)
|
|
78
|
+
- connector token guard (`CONNECTOR_<NAME>_TOKEN`)
|
|
79
|
+
- execution summary output
|
|
80
|
+
|
|
81
|
+
If `--dry-run` is set, command exits after summary and validation.
|
|
82
|
+
|
|
83
|
+
## Runtime Model
|
|
84
|
+
|
|
85
|
+
v1 supports host execution only.
|
|
86
|
+
|
|
87
|
+
- host: steps run in order, shell/http supported.
|
|
88
|
+
- docker: explicitly unsupported in v1 and fails with a clear error.
|
|
89
|
+
|
|
90
|
+
Future docker direction:
|
|
91
|
+
|
|
92
|
+
- docker image contains `spell-runner` and executes bundle in container.
|
|
93
|
+
|
|
94
|
+
## Windows Policy
|
|
95
|
+
|
|
96
|
+
- host mode does not assume bash/sh.
|
|
97
|
+
- shell step expects executable files (`.js`/`.exe`/`.cmd`/`.ps1` etc).
|
|
98
|
+
- process spawn uses `shell=false`.
|
|
99
|
+
- for strict cross-OS reproducibility, docker mode is the long-term recommended path.
|
|
100
|
+
|
|
101
|
+
## Effects Vocabulary (Recommended)
|
|
102
|
+
|
|
103
|
+
Use these `effect.type` words where possible:
|
|
104
|
+
|
|
105
|
+
- `create`
|
|
106
|
+
- `update`
|
|
107
|
+
- `delete`
|
|
108
|
+
- `deploy`
|
|
109
|
+
- `notify`
|
|
110
|
+
|
|
111
|
+
## v1 Limitations (Intentionally Not Implemented)
|
|
112
|
+
|
|
113
|
+
- name search or ambiguous resolution (id only)
|
|
114
|
+
- registry/marketplace/signature enforcement/license verification
|
|
115
|
+
- real billing execution (Stripe)
|
|
116
|
+
- DAG/parallel/rollback/self-healing
|
|
117
|
+
- advanced templating language (only `{{INPUT.*}}` and `{{ENV.*}}`)
|
|
118
|
+
- docker step execution runtime
|
|
119
|
+
|
|
120
|
+
## Example Flow
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
spell install ./fixtures/spells/hello-host
|
|
124
|
+
spell list
|
|
125
|
+
spell inspect fixtures/hello-host
|
|
126
|
+
spell cast fixtures/hello-host --dry-run -p name=world
|
|
127
|
+
spell cast fixtures/hello-host -p name=world
|
|
128
|
+
spell log <execution-id>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## UI Connection Spec
|
|
132
|
+
|
|
133
|
+
- Decision-complete button integration spec:
|
|
134
|
+
- `/Users/koichinishizuka/spell-runtime/docs/ui-connection-spec-v1.md`
|
|
135
|
+
- Sample button registry:
|
|
136
|
+
- `/Users/koichinishizuka/spell-runtime/examples/button-registry.v1.json`
|
package/README.txt
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Spell Runtime v1
|
|
2
|
+
|
|
3
|
+
Minimal CLI runtime for SpellBundle v1.
|
|
4
|
+
|
|
5
|
+
1. Setup
|
|
6
|
+
- Node.js >= 20
|
|
7
|
+
- npm
|
|
8
|
+
|
|
9
|
+
Install dependencies:
|
|
10
|
+
npm i
|
|
11
|
+
|
|
12
|
+
Build:
|
|
13
|
+
npm run build
|
|
14
|
+
|
|
15
|
+
Test:
|
|
16
|
+
npm test
|
|
17
|
+
|
|
18
|
+
Local dev:
|
|
19
|
+
npm run dev -- --help
|
|
20
|
+
|
|
21
|
+
Binary smoke checks:
|
|
22
|
+
npm run smoke:link
|
|
23
|
+
npm run smoke:npx
|
|
24
|
+
|
|
25
|
+
Manual link:
|
|
26
|
+
npm link
|
|
27
|
+
spell --help
|
|
28
|
+
|
|
29
|
+
Manual npx (local package):
|
|
30
|
+
npx --yes --package file:. spell --help
|
|
31
|
+
|
|
32
|
+
2. CLI commands
|
|
33
|
+
- spell install <local-path>
|
|
34
|
+
- spell list
|
|
35
|
+
- spell inspect <id> [--version x.y.z]
|
|
36
|
+
- spell cast <id> [--version x.y.z] [-p key=value ...] [--input input.json] [--dry-run] [--yes] [--allow-billing] [--verbose] [--profile <name>]
|
|
37
|
+
- spell log <execution-id>
|
|
38
|
+
|
|
39
|
+
3. Storage layout
|
|
40
|
+
- Spells: ~/.spell/spells/<id_key>/<version>/
|
|
41
|
+
- ID index: ~/.spell/spells/<id_key>/spell.id.txt
|
|
42
|
+
- Logs: ~/.spell/logs/<timestamp>_<id>_<version>.json
|
|
43
|
+
|
|
44
|
+
id_key is fixed as base64url(utf8(id)).
|
|
45
|
+
- id is the logical identifier (display, package identity).
|
|
46
|
+
- id_key is only for safe filesystem storage.
|
|
47
|
+
|
|
48
|
+
Consistency rule:
|
|
49
|
+
- install checks spell.yaml id against spell.id.txt when spell.id.txt already exists.
|
|
50
|
+
- mismatch is treated as an error.
|
|
51
|
+
|
|
52
|
+
4. Cast preflight (always)
|
|
53
|
+
Cast performs these checks before execution:
|
|
54
|
+
- Bundle resolution by id (and optional version)
|
|
55
|
+
- Input assembly (--input + -p overrides)
|
|
56
|
+
- JSON Schema validation by Ajv
|
|
57
|
+
- Platform guard
|
|
58
|
+
- Risk guard (high/critical requires --yes)
|
|
59
|
+
- Billing guard (billing.enabled requires --allow-billing)
|
|
60
|
+
- Connector token guard (CONNECTOR_<NAME>_TOKEN)
|
|
61
|
+
- Execution summary output
|
|
62
|
+
|
|
63
|
+
If --dry-run is set, command exits after summary and validation.
|
|
64
|
+
|
|
65
|
+
5. Runtime model
|
|
66
|
+
v1 supports host execution only.
|
|
67
|
+
- host: steps run in order, shell/http supported.
|
|
68
|
+
- docker: explicitly unsupported in v1 and fails with a clear error.
|
|
69
|
+
|
|
70
|
+
Future docker direction:
|
|
71
|
+
- docker image contains spell-runner and executes bundle in container.
|
|
72
|
+
|
|
73
|
+
6. Windows policy
|
|
74
|
+
- host mode does not assume bash/sh.
|
|
75
|
+
- shell step expects executable files (.js/.exe/.cmd/.ps1 etc).
|
|
76
|
+
- process spawn uses shell=false.
|
|
77
|
+
- for strict cross-OS reproducibility, docker mode is the long-term recommended path.
|
|
78
|
+
|
|
79
|
+
7. Effects vocabulary (recommended)
|
|
80
|
+
Use these effect.type words where possible:
|
|
81
|
+
- create
|
|
82
|
+
- update
|
|
83
|
+
- delete
|
|
84
|
+
- deploy
|
|
85
|
+
- notify
|
|
86
|
+
|
|
87
|
+
8. v1 limitations (intentionally not implemented)
|
|
88
|
+
- name search or ambiguous resolution (id only)
|
|
89
|
+
- registry/marketplace/signature enforcement/license verification
|
|
90
|
+
- real billing execution (Stripe)
|
|
91
|
+
- DAG/parallel/rollback/self-healing
|
|
92
|
+
- advanced templating language (only {{INPUT.*}} and {{ENV.*}})
|
|
93
|
+
- docker step execution runtime
|
|
94
|
+
|
|
95
|
+
9. Example flow
|
|
96
|
+
1) Install a local fixture
|
|
97
|
+
spell install ./fixtures/spells/hello-host
|
|
98
|
+
|
|
99
|
+
2) List installed spells
|
|
100
|
+
spell list
|
|
101
|
+
|
|
102
|
+
3) Inspect by id
|
|
103
|
+
spell inspect fixtures/hello-host
|
|
104
|
+
|
|
105
|
+
4) Dry run cast
|
|
106
|
+
spell cast fixtures/hello-host --dry-run -p name=world
|
|
107
|
+
|
|
108
|
+
5) Execute cast
|
|
109
|
+
spell cast fixtures/hello-host -p name=world
|
|
110
|
+
|
|
111
|
+
6) Show execution log
|
|
112
|
+
spell log <execution-id>
|
|
113
|
+
|
|
114
|
+
10. UI connection spec
|
|
115
|
+
- Decision-complete button integration spec:
|
|
116
|
+
/Users/koichinishizuka/spell-runtime/docs/ui-connection-spec-v1.md
|
|
117
|
+
- Sample button registry:
|
|
118
|
+
/Users/koichinishizuka/spell-runtime/examples/button-registry.v1.json
|
|
119
|
+
|
|
120
|
+
11. Install from npm
|
|
121
|
+
Global install:
|
|
122
|
+
npm i -g spell-runtime
|
|
123
|
+
spell --help
|
|
124
|
+
|
|
125
|
+
Run with npx:
|
|
126
|
+
npx --yes --package spell-runtime spell --help
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.installBundle = installBundle;
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const idKey_1 = require("../util/idKey");
|
|
10
|
+
const paths_1 = require("../util/paths");
|
|
11
|
+
const errors_1 = require("../util/errors");
|
|
12
|
+
const manifest_1 = require("./manifest");
|
|
13
|
+
async function installBundle(localPath) {
|
|
14
|
+
const sourcePath = node_path_1.default.resolve(localPath);
|
|
15
|
+
const sourceRoot = await (0, promises_1.realpath)(sourcePath);
|
|
16
|
+
const sourceStat = await (0, promises_1.stat)(sourceRoot);
|
|
17
|
+
if (!sourceStat.isDirectory()) {
|
|
18
|
+
throw new errors_1.SpellError(`bundle path must be a directory: ${localPath}`);
|
|
19
|
+
}
|
|
20
|
+
const { manifest, schemaPath } = await (0, manifest_1.loadManifestFromDir)(sourceRoot);
|
|
21
|
+
const idKey = (0, idKey_1.toIdKey)(manifest.id);
|
|
22
|
+
await (0, paths_1.ensureSpellDirs)();
|
|
23
|
+
const targetRoot = node_path_1.default.join((0, paths_1.spellsRoot)(), idKey);
|
|
24
|
+
const targetVersionPath = node_path_1.default.join(targetRoot, manifest.version);
|
|
25
|
+
await (0, promises_1.mkdir)(targetRoot, { recursive: true });
|
|
26
|
+
const idFilePath = node_path_1.default.join(targetRoot, "spell.id.txt");
|
|
27
|
+
const idFileExists = await exists(idFilePath);
|
|
28
|
+
if (idFileExists) {
|
|
29
|
+
const existingId = (await (0, promises_1.readFile)(idFilePath, "utf8")).trim();
|
|
30
|
+
if (existingId !== manifest.id) {
|
|
31
|
+
throw new errors_1.SpellError(`spell.id.txt mismatch for ${idKey}: expected '${existingId}', got '${manifest.id}'`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
await (0, promises_1.writeFile)(idFilePath, `${manifest.id}\n`, "utf8");
|
|
36
|
+
}
|
|
37
|
+
if (await exists(targetVersionPath)) {
|
|
38
|
+
throw new errors_1.SpellError(`already installed: ${manifest.id}@${manifest.version}`);
|
|
39
|
+
}
|
|
40
|
+
await (0, promises_1.mkdir)(targetVersionPath, { recursive: false });
|
|
41
|
+
const srcManifestPath = node_path_1.default.join(sourceRoot, "spell.yaml");
|
|
42
|
+
const srcSchemaPath = schemaPath;
|
|
43
|
+
const srcStepsPath = node_path_1.default.join(sourceRoot, "steps");
|
|
44
|
+
await assertPathWithinSource(sourceRoot, srcManifestPath);
|
|
45
|
+
await assertPathWithinSource(sourceRoot, srcSchemaPath);
|
|
46
|
+
await assertPathWithinSource(sourceRoot, srcStepsPath);
|
|
47
|
+
await (0, promises_1.copyFile)(srcManifestPath, node_path_1.default.join(targetVersionPath, "spell.yaml"));
|
|
48
|
+
await (0, promises_1.copyFile)(srcSchemaPath, node_path_1.default.join(targetVersionPath, "schema.json"));
|
|
49
|
+
const targetStepsPath = node_path_1.default.join(targetVersionPath, "steps");
|
|
50
|
+
await copyDirectorySafe(srcStepsPath, targetStepsPath, sourceRoot);
|
|
51
|
+
await (0, promises_1.access)(node_path_1.default.join(targetVersionPath, "spell.yaml"));
|
|
52
|
+
await (0, promises_1.access)(node_path_1.default.join(targetVersionPath, "schema.json"));
|
|
53
|
+
await (0, promises_1.access)(node_path_1.default.join(targetVersionPath, "steps"));
|
|
54
|
+
return {
|
|
55
|
+
id: manifest.id,
|
|
56
|
+
version: manifest.version,
|
|
57
|
+
idKey,
|
|
58
|
+
destination: targetVersionPath
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function copyDirectorySafe(sourceDir, targetDir, sourceRoot) {
|
|
62
|
+
await (0, promises_1.mkdir)(targetDir, { recursive: true });
|
|
63
|
+
const entries = await (0, promises_1.readdir)(sourceDir, { withFileTypes: true });
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const srcPath = node_path_1.default.join(sourceDir, entry.name);
|
|
66
|
+
const dstPath = node_path_1.default.join(targetDir, entry.name);
|
|
67
|
+
await assertPathWithinSource(sourceRoot, srcPath);
|
|
68
|
+
const info = await (0, promises_1.lstat)(srcPath);
|
|
69
|
+
if (info.isSymbolicLink()) {
|
|
70
|
+
throw new errors_1.SpellError(`symlink is not allowed in steps/: ${srcPath}`);
|
|
71
|
+
}
|
|
72
|
+
if (info.isDirectory()) {
|
|
73
|
+
await copyDirectorySafe(srcPath, dstPath, sourceRoot);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (info.isFile()) {
|
|
77
|
+
await (0, promises_1.copyFile)(srcPath, dstPath);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
throw new errors_1.SpellError(`unsupported file type in steps/: ${srcPath}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function assertPathWithinSource(sourceRoot, candidatePath) {
|
|
84
|
+
const candidateReal = await (0, promises_1.realpath)(candidatePath);
|
|
85
|
+
const rel = node_path_1.default.relative(sourceRoot, candidateReal);
|
|
86
|
+
if (rel.startsWith("..") || node_path_1.default.isAbsolute(rel)) {
|
|
87
|
+
throw new errors_1.SpellError(`path escapes bundle root: ${candidatePath}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function exists(p) {
|
|
91
|
+
try {
|
|
92
|
+
await (0, promises_1.access)(p);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/bundle/install.ts"],"names":[],"mappings":";;;;;AAcA,sCA6DC;AA3ED,+CAAgH;AAChH,0DAA6B;AAC7B,yCAAwC;AACxC,yCAA4D;AAC5D,2CAA4C;AAC5C,yCAAiD;AAS1C,KAAK,UAAU,aAAa,CAAC,SAAiB;IACnD,MAAM,UAAU,GAAG,mBAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC3C,MAAM,UAAU,GAAG,MAAM,IAAA,mBAAQ,EAAC,UAAU,CAAC,CAAC;IAE9C,MAAM,UAAU,GAAG,MAAM,IAAA,eAAI,EAAC,UAAU,CAAC,CAAC;IAC1C,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,EAAE,CAAC;QAC9B,MAAM,IAAI,mBAAU,CAAC,oCAAoC,SAAS,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,MAAM,IAAA,8BAAmB,EAAC,UAAU,CAAC,CAAC;IACvE,MAAM,KAAK,GAAG,IAAA,eAAO,EAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAEnC,MAAM,IAAA,uBAAe,GAAE,CAAC;IAExB,MAAM,UAAU,GAAG,mBAAI,CAAC,IAAI,CAAC,IAAA,kBAAU,GAAE,EAAE,KAAK,CAAC,CAAC;IAClD,MAAM,iBAAiB,GAAG,mBAAI,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAClE,MAAM,IAAA,gBAAK,EAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE7C,MAAM,UAAU,GAAG,mBAAI,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;IACzD,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;IAC9C,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,UAAU,GAAG,CAAC,MAAM,IAAA,mBAAQ,EAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/D,IAAI,UAAU,KAAK,QAAQ,CAAC,EAAE,EAAE,CAAC;YAC/B,MAAM,IAAI,mBAAU,CAClB,6BAA6B,KAAK,eAAe,UAAU,WAAW,QAAQ,CAAC,EAAE,GAAG,CACrF,CAAC;QACJ,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,IAAA,oBAAS,EAAC,UAAU,EAAE,GAAG,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAC1D,CAAC;IAED,IAAI,MAAM,MAAM,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,mBAAU,CAAC,sBAAsB,QAAQ,CAAC,EAAE,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IAChF,CAAC;IAED,MAAM,IAAA,gBAAK,EAAC,iBAAiB,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;IAErD,MAAM,eAAe,GAAG,mBAAI,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAC5D,MAAM,aAAa,GAAG,UAAU,CAAC;IACjC,MAAM,YAAY,GAAG,mBAAI,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAEpD,MAAM,sBAAsB,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;IAC1D,MAAM,sBAAsB,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IACxD,MAAM,sBAAsB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAEvD,MAAM,IAAA,mBAAQ,EAAC,eAAe,EAAE,mBAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC,CAAC,CAAC;IAC5E,MAAM,IAAA,mBAAQ,EAAC,aAAa,EAAE,mBAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC,CAAC;IAE3E,MAAM,eAAe,GAAG,mBAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;IAC9D,MAAM,iBAAiB,CAAC,YAAY,EAAE,eAAe,EAAE,UAAU,CAAC,CAAC;IAEnE,MAAM,IAAA,iBAAM,EAAC,mBAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC,CAAC,CAAC;IACzD,MAAM,IAAA,iBAAM,EAAC,mBAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC,CAAC;IAC1D,MAAM,IAAA,iBAAM,EAAC,mBAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC,CAAC;IAEpD,OAAO;QACL,EAAE,EAAE,QAAQ,CAAC,EAAE;QACf,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,KAAK;QACL,WAAW,EAAE,iBAAiB;KAC/B,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,SAAiB,EAAE,SAAiB,EAAE,UAAkB;IACvF,MAAM,IAAA,gBAAK,EAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,MAAM,IAAA,kBAAO,EAAC,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAElE,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,mBAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,mBAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAEjD,MAAM,sBAAsB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAElD,MAAM,IAAI,GAAG,MAAM,IAAA,gBAAK,EAAC,OAAO,CAAC,CAAC;QAClC,IAAI,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;YAC1B,MAAM,IAAI,mBAAU,CAAC,qCAAqC,OAAO,EAAE,CAAC,CAAC;QACvE,CAAC;QAED,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YACvB,MAAM,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;YACtD,SAAS;QACX,CAAC;QAED,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YAClB,MAAM,IAAA,mBAAQ,EAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACjC,SAAS;QACX,CAAC;QAED,MAAM,IAAI,mBAAU,CAAC,oCAAoC,OAAO,EAAE,CAAC,CAAC;IACtE,CAAC;AACH,CAAC;AAED,KAAK,UAAU,sBAAsB,CAAC,UAAkB,EAAE,aAAqB;IAC7E,MAAM,aAAa,GAAG,MAAM,IAAA,mBAAQ,EAAC,aAAa,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,mBAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IACrD,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,mBAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACjD,MAAM,IAAI,mBAAU,CAAC,6BAA6B,aAAa,EAAE,CAAC,CAAC;IACrE,CAAC;AACH,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,CAAS;IAC7B,IAAI,CAAC;QACH,MAAM,IAAA,iBAAM,EAAC,CAAC,CAAC,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadManifestFromDir = loadManifestFromDir;
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const js_yaml_1 = require("js-yaml");
|
|
10
|
+
const errors_1 = require("../util/errors");
|
|
11
|
+
const RISK_VALUES = new Set(["low", "medium", "high", "critical"]);
|
|
12
|
+
const EXECUTION_VALUES = new Set(["host", "docker"]);
|
|
13
|
+
const STEP_VALUES = new Set(["shell", "http"]);
|
|
14
|
+
const CHECK_VALUES = new Set(["exit_code", "file_exists", "http_status", "jsonpath_equals"]);
|
|
15
|
+
const BILLING_MODES = new Set(["none", "upfront", "on_success", "subscription"]);
|
|
16
|
+
async function loadManifestFromDir(bundlePath) {
|
|
17
|
+
const manifestPath = node_path_1.default.join(bundlePath, "spell.yaml");
|
|
18
|
+
let rawYaml;
|
|
19
|
+
try {
|
|
20
|
+
rawYaml = await (0, promises_1.readFile)(manifestPath, "utf8");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new errors_1.SpellError(`spell.yaml not found: ${manifestPath}`);
|
|
24
|
+
}
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = (0, js_yaml_1.load)(rawYaml);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new errors_1.SpellError(`failed to parse spell.yaml: ${error.message}`);
|
|
31
|
+
}
|
|
32
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
33
|
+
throw new errors_1.SpellError("spell.yaml must be a mapping object");
|
|
34
|
+
}
|
|
35
|
+
const manifest = parsed;
|
|
36
|
+
const id = readRequiredString(manifest, "id");
|
|
37
|
+
const version = readRequiredString(manifest, "version");
|
|
38
|
+
const name = readRequiredString(manifest, "name");
|
|
39
|
+
const summary = readRequiredString(manifest, "summary");
|
|
40
|
+
const inputsSchema = readRequiredString(manifest, "inputs_schema");
|
|
41
|
+
validateId(id);
|
|
42
|
+
validateVersion(version);
|
|
43
|
+
const risk = readRequiredString(manifest, "risk");
|
|
44
|
+
if (!RISK_VALUES.has(risk)) {
|
|
45
|
+
throw new errors_1.SpellError(`invalid risk: ${risk}`);
|
|
46
|
+
}
|
|
47
|
+
const permissionsRaw = readRequiredArray(manifest, "permissions");
|
|
48
|
+
const permissions = permissionsRaw.map((entry, idx) => {
|
|
49
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
50
|
+
throw new errors_1.SpellError(`permissions[${idx}] must be an object`);
|
|
51
|
+
}
|
|
52
|
+
const obj = entry;
|
|
53
|
+
const connector = readRequiredString(obj, "connector");
|
|
54
|
+
const scopes = readRequiredArray(obj, "scopes").map((scope, scopeIdx) => {
|
|
55
|
+
if (typeof scope !== "string" || !scope.trim()) {
|
|
56
|
+
throw new errors_1.SpellError(`permissions[${idx}].scopes[${scopeIdx}] must be a non-empty string`);
|
|
57
|
+
}
|
|
58
|
+
return scope;
|
|
59
|
+
});
|
|
60
|
+
if (scopes.length === 0) {
|
|
61
|
+
throw new errors_1.SpellError(`permissions[${idx}].scopes must not be empty`);
|
|
62
|
+
}
|
|
63
|
+
return { connector, scopes };
|
|
64
|
+
});
|
|
65
|
+
const effectsRaw = readRequiredArray(manifest, "effects");
|
|
66
|
+
const effects = effectsRaw.map((entry, idx) => {
|
|
67
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
68
|
+
throw new errors_1.SpellError(`effects[${idx}] must be an object`);
|
|
69
|
+
}
|
|
70
|
+
const obj = entry;
|
|
71
|
+
const type = readRequiredString(obj, "type");
|
|
72
|
+
const target = readRequiredString(obj, "target");
|
|
73
|
+
const mutates = readRequiredBoolean(obj, "mutates");
|
|
74
|
+
return { type, target, mutates };
|
|
75
|
+
});
|
|
76
|
+
const billingRaw = readRequiredObject(manifest, "billing");
|
|
77
|
+
const billingEnabled = readRequiredBoolean(billingRaw, "enabled");
|
|
78
|
+
const billingMode = readRequiredString(billingRaw, "mode");
|
|
79
|
+
const currency = readRequiredString(billingRaw, "currency");
|
|
80
|
+
const maxAmount = readRequiredNumber(billingRaw, "max_amount");
|
|
81
|
+
if (!BILLING_MODES.has(billingMode)) {
|
|
82
|
+
throw new errors_1.SpellError(`invalid billing.mode: ${billingMode}`);
|
|
83
|
+
}
|
|
84
|
+
const runtimeRaw = readRequiredObject(manifest, "runtime");
|
|
85
|
+
const execution = readRequiredString(runtimeRaw, "execution");
|
|
86
|
+
if (!EXECUTION_VALUES.has(execution)) {
|
|
87
|
+
throw new errors_1.SpellError(`invalid runtime.execution: ${execution}`);
|
|
88
|
+
}
|
|
89
|
+
const platforms = readRequiredArray(runtimeRaw, "platforms").map((platformValue, idx) => {
|
|
90
|
+
if (typeof platformValue !== "string" || !platformValue.trim()) {
|
|
91
|
+
throw new errors_1.SpellError(`runtime.platforms[${idx}] must be a non-empty string`);
|
|
92
|
+
}
|
|
93
|
+
return platformValue;
|
|
94
|
+
});
|
|
95
|
+
const dockerImageRaw = runtimeRaw["docker_image"];
|
|
96
|
+
const dockerImage = typeof dockerImageRaw === "string" && dockerImageRaw.trim() ? dockerImageRaw : undefined;
|
|
97
|
+
if (execution === "docker" && !dockerImage) {
|
|
98
|
+
throw new errors_1.SpellError("runtime.docker_image is required when runtime.execution=docker");
|
|
99
|
+
}
|
|
100
|
+
const steps = parseSteps(readRequiredArray(manifest, "steps"));
|
|
101
|
+
const checks = parseChecks(readRequiredArray(manifest, "checks"));
|
|
102
|
+
const schemaPath = resolveInputsSchema(bundlePath, inputsSchema);
|
|
103
|
+
await (0, promises_1.access)(schemaPath);
|
|
104
|
+
await (0, promises_1.access)(node_path_1.default.join(bundlePath, "schema.json"));
|
|
105
|
+
const stepsDirPath = node_path_1.default.join(bundlePath, "steps");
|
|
106
|
+
const stepsDirStat = await (0, promises_1.stat)(stepsDirPath);
|
|
107
|
+
if (!stepsDirStat.isDirectory()) {
|
|
108
|
+
throw new errors_1.SpellError("steps/ must be a directory");
|
|
109
|
+
}
|
|
110
|
+
for (const step of steps) {
|
|
111
|
+
const runPath = node_path_1.default.resolve(bundlePath, step.run);
|
|
112
|
+
ensurePathWithin(bundlePath, runPath, `step '${step.name}' run path`);
|
|
113
|
+
await (0, promises_1.access)(runPath);
|
|
114
|
+
}
|
|
115
|
+
const typedManifest = {
|
|
116
|
+
id,
|
|
117
|
+
version,
|
|
118
|
+
name,
|
|
119
|
+
summary,
|
|
120
|
+
inputs_schema: inputsSchema,
|
|
121
|
+
risk: risk,
|
|
122
|
+
permissions,
|
|
123
|
+
effects,
|
|
124
|
+
billing: {
|
|
125
|
+
enabled: billingEnabled,
|
|
126
|
+
mode: billingMode,
|
|
127
|
+
currency,
|
|
128
|
+
max_amount: maxAmount
|
|
129
|
+
},
|
|
130
|
+
runtime: {
|
|
131
|
+
execution: execution,
|
|
132
|
+
platforms,
|
|
133
|
+
docker_image: dockerImage
|
|
134
|
+
},
|
|
135
|
+
steps,
|
|
136
|
+
checks
|
|
137
|
+
};
|
|
138
|
+
return { manifest: typedManifest, schemaPath };
|
|
139
|
+
}
|
|
140
|
+
function parseSteps(rawSteps) {
|
|
141
|
+
if (rawSteps.length === 0) {
|
|
142
|
+
throw new errors_1.SpellError("steps must not be empty");
|
|
143
|
+
}
|
|
144
|
+
const seenNames = new Set();
|
|
145
|
+
return rawSteps.map((entry, idx) => {
|
|
146
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
147
|
+
throw new errors_1.SpellError(`steps[${idx}] must be an object`);
|
|
148
|
+
}
|
|
149
|
+
const obj = entry;
|
|
150
|
+
const uses = readRequiredString(obj, "uses");
|
|
151
|
+
if (!STEP_VALUES.has(uses)) {
|
|
152
|
+
throw new errors_1.SpellError(`invalid steps[${idx}].uses: ${uses}`);
|
|
153
|
+
}
|
|
154
|
+
const name = readRequiredString(obj, "name");
|
|
155
|
+
if (seenNames.has(name)) {
|
|
156
|
+
throw new errors_1.SpellError(`duplicate step name: ${name}`);
|
|
157
|
+
}
|
|
158
|
+
seenNames.add(name);
|
|
159
|
+
const run = readRequiredString(obj, "run");
|
|
160
|
+
return {
|
|
161
|
+
uses: uses,
|
|
162
|
+
name,
|
|
163
|
+
run
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function parseChecks(rawChecks) {
|
|
168
|
+
if (rawChecks.length === 0) {
|
|
169
|
+
throw new errors_1.SpellError("checks must not be empty");
|
|
170
|
+
}
|
|
171
|
+
return rawChecks.map((entry, idx) => {
|
|
172
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
173
|
+
throw new errors_1.SpellError(`checks[${idx}] must be an object`);
|
|
174
|
+
}
|
|
175
|
+
const obj = entry;
|
|
176
|
+
const type = readRequiredString(obj, "type");
|
|
177
|
+
if (!CHECK_VALUES.has(type)) {
|
|
178
|
+
throw new errors_1.SpellError(`invalid checks[${idx}].type: ${type}`);
|
|
179
|
+
}
|
|
180
|
+
const paramsRaw = obj["params"];
|
|
181
|
+
const params = paramsRaw && typeof paramsRaw === "object" && !Array.isArray(paramsRaw)
|
|
182
|
+
? paramsRaw
|
|
183
|
+
: {};
|
|
184
|
+
return {
|
|
185
|
+
type: type,
|
|
186
|
+
params
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function resolveInputsSchema(bundlePath, inputSchemaPath) {
|
|
191
|
+
if (!inputSchemaPath.trim()) {
|
|
192
|
+
throw new errors_1.SpellError("inputs_schema must not be empty");
|
|
193
|
+
}
|
|
194
|
+
const resolved = node_path_1.default.resolve(bundlePath, inputSchemaPath);
|
|
195
|
+
ensurePathWithin(bundlePath, resolved, "inputs_schema");
|
|
196
|
+
if (node_path_1.default.basename(resolved) !== "schema.json") {
|
|
197
|
+
throw new errors_1.SpellError("inputs_schema must point to schema.json in v1");
|
|
198
|
+
}
|
|
199
|
+
return resolved;
|
|
200
|
+
}
|
|
201
|
+
function ensurePathWithin(root, target, label) {
|
|
202
|
+
const rel = node_path_1.default.relative(node_path_1.default.resolve(root), target);
|
|
203
|
+
if (rel.startsWith("..") || node_path_1.default.isAbsolute(rel)) {
|
|
204
|
+
throw new errors_1.SpellError(`${label} escapes bundle root`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function validateId(id) {
|
|
208
|
+
if (!id.trim()) {
|
|
209
|
+
throw new errors_1.SpellError("id must not be empty");
|
|
210
|
+
}
|
|
211
|
+
if (id.length > 200) {
|
|
212
|
+
throw new errors_1.SpellError("id must be <= 200 characters");
|
|
213
|
+
}
|
|
214
|
+
if (/[\x00-\x1F\x7F]/.test(id)) {
|
|
215
|
+
throw new errors_1.SpellError("id must not contain control characters");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function validateVersion(version) {
|
|
219
|
+
if (!version.trim()) {
|
|
220
|
+
throw new errors_1.SpellError("version must not be empty");
|
|
221
|
+
}
|
|
222
|
+
if (version.length > 50) {
|
|
223
|
+
throw new errors_1.SpellError("version must be <= 50 characters");
|
|
224
|
+
}
|
|
225
|
+
if (/[\x00-\x1F\x7F]/.test(version)) {
|
|
226
|
+
throw new errors_1.SpellError("version must not contain control characters");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function readRequiredString(obj, key) {
|
|
230
|
+
const value = obj[key];
|
|
231
|
+
if (typeof value !== "string") {
|
|
232
|
+
throw new errors_1.SpellError(`missing or invalid string field: ${key}`);
|
|
233
|
+
}
|
|
234
|
+
if (!value.trim()) {
|
|
235
|
+
throw new errors_1.SpellError(`field must not be empty: ${key}`);
|
|
236
|
+
}
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
function readRequiredNumber(obj, key) {
|
|
240
|
+
const value = obj[key];
|
|
241
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
242
|
+
throw new errors_1.SpellError(`missing or invalid number field: ${key}`);
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
function readRequiredBoolean(obj, key) {
|
|
247
|
+
const value = obj[key];
|
|
248
|
+
if (typeof value !== "boolean") {
|
|
249
|
+
throw new errors_1.SpellError(`missing or invalid boolean field: ${key}`);
|
|
250
|
+
}
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
function readRequiredArray(obj, key) {
|
|
254
|
+
const value = obj[key];
|
|
255
|
+
if (!Array.isArray(value)) {
|
|
256
|
+
throw new errors_1.SpellError(`missing or invalid array field: ${key}`);
|
|
257
|
+
}
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
function readRequiredObject(obj, key) {
|
|
261
|
+
const value = obj[key];
|
|
262
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
263
|
+
throw new errors_1.SpellError(`missing or invalid object field: ${key}`);
|
|
264
|
+
}
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=manifest.js.map
|