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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/README.txt +126 -0
  4. package/dist/bundle/install.d.ts +7 -0
  5. package/dist/bundle/install.js +99 -0
  6. package/dist/bundle/install.js.map +1 -0
  7. package/dist/bundle/manifest.d.ts +5 -0
  8. package/dist/bundle/manifest.js +267 -0
  9. package/dist/bundle/manifest.js.map +1 -0
  10. package/dist/bundle/store.d.ts +16 -0
  11. package/dist/bundle/store.js +129 -0
  12. package/dist/bundle/store.js.map +1 -0
  13. package/dist/checks/evaluate.d.ts +2 -0
  14. package/dist/checks/evaluate.js +107 -0
  15. package/dist/checks/evaluate.js.map +1 -0
  16. package/dist/cli/index.d.ts +2 -0
  17. package/dist/cli/index.js +144 -0
  18. package/dist/cli/index.js.map +1 -0
  19. package/dist/logging/executionLog.d.ts +3 -0
  20. package/dist/logging/executionLog.js +31 -0
  21. package/dist/logging/executionLog.js.map +1 -0
  22. package/dist/runner/cast.d.ts +7 -0
  23. package/dist/runner/cast.js +117 -0
  24. package/dist/runner/cast.js.map +1 -0
  25. package/dist/runner/hostRunner.d.ts +5 -0
  26. package/dist/runner/hostRunner.js +41 -0
  27. package/dist/runner/hostRunner.js.map +1 -0
  28. package/dist/runner/input.d.ts +2 -0
  29. package/dist/runner/input.js +42 -0
  30. package/dist/runner/input.js.map +1 -0
  31. package/dist/runner/summary.d.ts +2 -0
  32. package/dist/runner/summary.js +31 -0
  33. package/dist/runner/summary.js.map +1 -0
  34. package/dist/steps/httpStep.d.ts +7 -0
  35. package/dist/steps/httpStep.js +100 -0
  36. package/dist/steps/httpStep.js.map +1 -0
  37. package/dist/steps/shellStep.d.ts +7 -0
  38. package/dist/steps/shellStep.js +48 -0
  39. package/dist/steps/shellStep.js.map +1 -0
  40. package/dist/types.d.ts +105 -0
  41. package/dist/types.js +3 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist/util/errors.d.ts +3 -0
  44. package/dist/util/errors.js +11 -0
  45. package/dist/util/errors.js.map +1 -0
  46. package/dist/util/http.d.ts +6 -0
  47. package/dist/util/http.js +11 -0
  48. package/dist/util/http.js.map +1 -0
  49. package/dist/util/idKey.d.ts +2 -0
  50. package/dist/util/idKey.js +11 -0
  51. package/dist/util/idKey.js.map +1 -0
  52. package/dist/util/object.d.ts +7 -0
  53. package/dist/util/object.js +62 -0
  54. package/dist/util/object.js.map +1 -0
  55. package/dist/util/outputs.d.ts +1 -0
  56. package/dist/util/outputs.js +28 -0
  57. package/dist/util/outputs.js.map +1 -0
  58. package/dist/util/paths.d.ts +4 -0
  59. package/dist/util/paths.js +26 -0
  60. package/dist/util/paths.js.map +1 -0
  61. package/dist/util/platform.d.ts +1 -0
  62. package/dist/util/platform.js +7 -0
  63. package/dist/util/platform.js.map +1 -0
  64. package/dist/util/template.d.ts +1 -0
  65. package/dist/util/template.js +61 -0
  66. package/dist/util/template.js.map +1 -0
  67. package/dist/util/version.d.ts +2 -0
  68. package/dist/util/version.js +35 -0
  69. package/dist/util/version.js.map +1 -0
  70. 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,7 @@
1
+ export interface InstallResult {
2
+ id: string;
3
+ version: string;
4
+ idKey: string;
5
+ destination: string;
6
+ }
7
+ export declare function installBundle(localPath: string): Promise<InstallResult>;
@@ -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,5 @@
1
+ import { SpellBundleManifest } from "../types";
2
+ export declare function loadManifestFromDir(bundlePath: string): Promise<{
3
+ manifest: SpellBundleManifest;
4
+ schemaPath: string;
5
+ }>;
@@ -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