@c6fc/spellcraft-gcp-terraform 1.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Brad Woodward
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,199 @@
1
+ # SpellCraft AWS Integration
2
+
3
+ [![NPM version](https://img.shields.io/npm/v/@c6fc/spellcraft-gcp-terraform.svg?style=flat)](https://www.npmjs.com/package/@c6fc/spellcraft-gcp-terraform)
4
+ [![License](https://img.shields.io/npm/l/@c6fc/spellcraft-gcp-terraform.svg?style=flat)](https://opensource.org/licenses/MIT)
5
+
6
+ This module exposes common constructs for using [SpellCraft](https://github.com/@c6fc/spellcraft) SpellFrames to deploy infrastructure to AWS using Terraform.
7
+
8
+ ```sh
9
+ npm install --save @c6fc/spellcraft-gcp-terraform
10
+ ```
11
+
12
+ ## Features
13
+
14
+ This module exposes the concept of a bootstrap bucket (functionally a terraform backend), and artifacts which can contain arbitrary data and are stored alongside the terraform state in the bootstrap bucket. The former allows for dynamic configuration of Terraform providers within different environments, while the latter simplifies the storage and use of dynamic configuration details that might be environment dependent.
15
+
16
+ <!-- SPELLCRAFT_DOCS_CLI_START -->
17
+
18
+ <!-- SPELLCRAFT_DOCS_CLI_END -->
19
+
20
+ ## SpellFrame 'init()' features
21
+
22
+ This plugin does not perform any distinct 'init' operations, other than to initialize credentials within the dependant plugin `@c6fc/terraform-gcp-auth`.
23
+
24
+ ## JavaScript context features
25
+
26
+ Extends the JavaScript function context with an `awsterraform` object containing the following keys:
27
+
28
+ ```JSON
29
+ {
30
+ "projectName": "<contains the name of the project specified by bootstrap()>",
31
+ "bootstrapBucket": "<contains the ARN for the bootstrap bucket>",
32
+ "bootstrapLocation": "<contains the region where the bootstrap bucket is located>"
33
+ }
34
+ ```
35
+
36
+ <!-- SPELLCRAFT_DOCS_API_START -->
37
+ ## API Reference
38
+
39
+ ### `bootstrap(project)`
40
+
41
+ Creates a Terraform backend bucket if one doesn't already exist, then
42
+ returns a 'backend' object referencing this bucket and a unique path
43
+ for this project's state and artifacts.
44
+
45
+ - param {string} project
46
+ - returns {object} backend
47
+
48
+ **Examples:**
49
+
50
+ ```jsonnet
51
+ local gcp = import "@c6fc/spellcraft-gcp-terraform";
52
+
53
+ gcp.bootstrap("myBootstrapTest");
54
+
55
+ // Returns:
56
+ {
57
+ "terraform": {
58
+ "backend": {
59
+ "gcs": {
60
+ "bucket": "spellcraft-random-0123456789",
61
+ "key": "spellcraft/myBootstrapTest/terraform.tfstate",
62
+ }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ---
69
+ ### `getArtifact(name)`
70
+
71
+ Obtains the contents of a named artifact stored alongside this project in the bootstrap
72
+ bucket. This artifact is created with 'putArtifact';
73
+
74
+ - param {string} name
75
+ - returns {object} backend
76
+
77
+ **Examples:**
78
+
79
+ ```jsonnet
80
+ local gcp = import "@c6fc/spellcraft-gcp-terraform";
81
+
82
+ gcp.getArtifact("myArtifact");
83
+
84
+ // Returns:
85
+ <contents of your artifact>
86
+ ```
87
+
88
+ ---
89
+ ### `getBootstrapBucket()`
90
+
91
+ Attempts to discover the bucket created through bootstrap(), returning the
92
+ bucket name if present.
93
+
94
+ - returns {string} bucketArn
95
+
96
+ **Examples:**
97
+
98
+ ```jsonnet
99
+ local gcp = import "@c6fc/spellcraft-gcp-terraform";
100
+
101
+ gcp.getBootstrapBucket();
102
+
103
+ // Returns:
104
+ spellcraft-terraform-<project-id>
105
+ ```
106
+
107
+ ---
108
+ ### `getRemoteState(project)`
109
+
110
+ Read the Terraform state for an adjacent SpellCraft project in the same GCP account
111
+
112
+ - param {string} project
113
+ - returns {object} state
114
+
115
+ **Examples:**
116
+
117
+ ```jsonnet
118
+ local gcp = import "@c6fc/spellcraft-gcp-terraform";
119
+
120
+ gcp.getRemoteState("mySecondProject");
121
+
122
+ // Returns:
123
+ { full remote state object }
124
+ ```
125
+
126
+ ---
127
+ ### `putArtifact(name, content)`
128
+
129
+ Stores the JSON-encoded balue of 'contents' as a file in the GCS backend bucket using
130
+ the project prefix.
131
+
132
+ - param {string} name
133
+ - param {*} contents
134
+ - returns {boolean} true
135
+
136
+ **Examples:**
137
+
138
+ ```jsonnet
139
+ local gcp = import "@c6fc/spellcraft-gcp-terraform";
140
+
141
+ gcp.putArtifact("myArtifact", { someData: someValue });
142
+
143
+ // Returns:
144
+ true
145
+ ```
146
+
147
+ ---
148
+ ### `providerAliases(default, filter="")`
149
+
150
+ Stores the JSON-encoded value of 'contents' as a file in the GCS backend bucket using
151
+ the project prefix. If 'filter' is provided, only region names that string match
152
+ will be included.
153
+
154
+ - param {string} default
155
+ - param {string} filter
156
+
157
+ **Examples:**
158
+
159
+ ```jsonnet
160
+ local gcp = import "@c6fc/spellcraft-gcp-terraform";
161
+
162
+ gcp.providerAliases("us-west2");
163
+
164
+ // Returns:
165
+ [{ google: {
166
+ region: "us-west2"
167
+ }}, { google: {
168
+ region: "us-east1",
169
+ alias: "us-east1"
170
+ }}, ...]
171
+ ```
172
+
173
+ ---
174
+
175
+ <!-- SPELLCRAFT_DOCS_API_END -->
176
+
177
+
178
+ ## Installation
179
+
180
+ Install the plugin as a dependency in your SpellCraft project:
181
+
182
+ ```bash
183
+ npm install --save @c6fc/spellcraft-gcp-terraform
184
+ ```
185
+
186
+ Once installed, you can load the module into your JSonnet files.
187
+
188
+ ```jsonnet
189
+ local aws = import "@c6fc/spellcraft-gcp-terraform";
190
+
191
+ {
192
+ 'backend.tf.json': aws.bootstrap("myProjectName"),
193
+
194
+ // Generate provider list defaulting to 'us-west2' but including all 'us-' regions
195
+ 'provider.tf.json': {
196
+ provider: aws.providerAliases("us-west2", "us-")
197
+ }
198
+ }
199
+ ```
package/module.js ADDED
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE=1
4
+
5
+ const fs = require("fs");
6
+ const os = require("os");
7
+
8
+ // Nab the authenticated AWS instantiation from gcp-auth
9
+ const gcpauth = require("@c6fc/spellcraft-gcp-auth");
10
+ const { google } = gcpauth._spellcraft_metadata.functionContext;
11
+ const storage = google.storage('v1');
12
+ const compute = google.compute('v1');
13
+
14
+ let cachedProject = null;
15
+
16
+ // Initialize caches
17
+ const artifacts = {};
18
+ const gcpterraform = { projectName: null, bootstrapBucket: null };
19
+ const remoteStates = {};
20
+
21
+ exports._spellcraft_metadata = {
22
+ functionContext: { gcpterraform }
23
+ }
24
+
25
+ exports.bootstrap = [async function (project) {
26
+ return await bootstrap(project);
27
+ }, "project"];
28
+
29
+ exports.getArtifact = [async function (name) {
30
+ return await getArtifact(name);
31
+ }, "name"];
32
+
33
+ exports.getBootstrapBucket = [async function () {
34
+ return await getBootstrapBucket();
35
+ }];
36
+
37
+ exports.getRemoteState = [async function (project) {
38
+ return await getRemoteState(project);
39
+ }, "project"];
40
+
41
+ exports.putArtifact = [async function (name, content) {
42
+ return await putArtifact(name, content);
43
+ }, "name", "content"];
44
+
45
+ async function bootstrap(projectName) {
46
+ cachedProject = await gcpauth.getProjectId[0]();
47
+ const targetBucket = `spellcraft-terraform-${cachedProject}`;
48
+
49
+ if (!await getBootstrapBucket()) {
50
+ try {
51
+ console.log(`[+] Creating GCS Bootstrap Bucket: ${targetBucket}`);
52
+ await storage.buckets.insert({
53
+ project: cachedProject,
54
+ requestBody: {
55
+ name: targetBucket,
56
+ location: 'US', // Defaulting to US multi-region for high availability
57
+ storageClass: 'STANDARD',
58
+ versioning: { enabled: true },
59
+ iamConfiguration: {
60
+ uniformBucketLevelAccess: { enabled: true }
61
+ }
62
+ }
63
+ });
64
+ } catch(e) {
65
+ throw new Error(`Failed to discover/create GCS bucket: ${e.message}`);
66
+ }
67
+ }
68
+
69
+ gcpterraform.bootstrapBucket = targetBucket;
70
+ gcpterraform.projectName = projectName;
71
+
72
+ // Return the Terraform backend configuration object
73
+ return {
74
+ terraform: {
75
+ backend: {
76
+ gcs: {
77
+ bucket: gcpterraform.bootstrapBucket,
78
+ prefix: `spellcraft/${projectName}`
79
+ }
80
+ }
81
+ }
82
+ };
83
+ };
84
+
85
+ async function getBootstrapBucket() {
86
+
87
+
88
+ if (!!gcpterraform.bootstrapBucket) {
89
+ return gcpterraform.bootstrapBucket;
90
+ }
91
+
92
+ if (!cachedProject) {
93
+ cachedProject = await gcpauth.getProjectId[0]();
94
+ }
95
+
96
+
97
+ try {
98
+ // 1. Try to find existing bucket
99
+ await storage.buckets.get({ bucket: `spellcraft-terraform-${cachedProject}` });
100
+ gcpterraform.bootstrapBucket = `spellcraft-terraform-${cachedProject}`;
101
+
102
+ return gcpterraform.bootstrapBucket;
103
+ } catch (e) {
104
+ return false;
105
+ };
106
+
107
+ return false;
108
+ }
109
+
110
+ async function getRemoteState(project) {
111
+
112
+ if (!!!remoteStates[project]) {
113
+ if (!gcpterraform.bootstrapBucket) throw new Error("Module not bootstrapped. Call bootstrap() first.");
114
+
115
+ try {
116
+ const res = await storage.objects.get({
117
+ bucket: gcpterraform.bootstrapBucket,
118
+ object: `spellcraft/${project}/default.tfstate`,
119
+ alt: 'media'
120
+ });
121
+ return res.data;
122
+ } catch (e) {
123
+ throw new Error(`Could not find remote state for project: ${project}`);
124
+ }
125
+
126
+ const state = JSON.parse(stateJson.Body);
127
+
128
+ const resources = state.resources.reduce((a, c) => {
129
+ let path;
130
+
131
+ if (c.mode == "data") {
132
+ a.data = (!a.data) ? {} : a.data;
133
+ a.data[c.type] = (!a.data[c.type]) ? {} : a.data[c.type];
134
+
135
+ path = a.data[c.type];
136
+ } else {
137
+ a[c.type] = (!a[c.type]) ? {} : a[c.type];
138
+
139
+ path = a[c.type];
140
+ }
141
+
142
+ path[c.name] = (c.instances.length == 1) ? c.instances[0].attributes : c.instances.map(e => e.attributes);
143
+
144
+ return a;
145
+ }, {});
146
+
147
+ resources.outputs = Object.keys(state.outputs).reduce((a, c) => {
148
+ a[c] = state.outputs[c].value;
149
+
150
+ return a;
151
+ }, {});
152
+
153
+ // console.log(resources.outputs);
154
+
155
+ remoteStates[project] = resources;
156
+ }
157
+
158
+ return resources;
159
+ }
160
+
161
+ exports.getArtifact = async function(name) {
162
+ if (!gcpterraform.bootstrapBucket) throw new Error("Module not bootstrapped. Call bootstrap() first.");
163
+
164
+ try {
165
+ const res = await storage.objects.get({
166
+ bucket: gcpterraform.bootstrapBucket,
167
+ object: `spellcraft/${gcpterraform.projectName}/artifacts/${name}.json`,
168
+ alt: 'media'
169
+ });
170
+ return res.data;
171
+ } catch (e) {
172
+ return null;
173
+ }
174
+ };
175
+
176
+ exports.putArtifact = async function(name, content) {
177
+ if (!gcpterraform.bootstrapBucket) throw new Error("Module not bootstrapped. Call bootstrap() first.");
178
+
179
+ const res = await storage.objects.insert({
180
+ bucket: gcpterraform.bootstrapBucket,
181
+ name: `spellcraft/${gcpterraform.projectName}/artifacts/${name}.json`,
182
+ media: {
183
+ mimeType: 'application/json',
184
+ body: JSON.stringify(content, null, 2)
185
+ }
186
+ });
187
+
188
+ return !!res.data;
189
+ };
@@ -0,0 +1,132 @@
1
+ // Don't try to 'import' your spellcraft native functions here.
2
+ // Use std.native(function)(..args) instead
3
+
4
+ {
5
+ // JS Native functions are already documented in spellcraft_modules/foo.js
6
+ // but need to be specified here to expose them through the import
7
+
8
+ /**
9
+ * Creates a Terraform backend bucket if one doesn't already exist, then
10
+ * returns a 'backend' object referencing this bucket and a unique path
11
+ * for this project's state and artifacts.
12
+ *
13
+ * @param {string} project
14
+ * @returns {object} backend
15
+ * @example
16
+ * local gcp = import "@c6fc/spellcraft-gcp-terraform";
17
+ *
18
+ * gcp.bootstrap("myBootstrapTest");
19
+ *
20
+ * // Returns:
21
+ * {
22
+ * "terraform": {
23
+ * "backend": {
24
+ * "gcs": {
25
+ * "bucket": "spellcraft-random-0123456789",
26
+ * "key": "spellcraft/myBootstrapTest/terraform.tfstate",
27
+ * }
28
+ * }
29
+ * }
30
+ * }
31
+ */
32
+ bootstrap(project):: std.native("@c6fc/spellcraft-gcp-terraform:bootstrap")(project),
33
+
34
+ /**
35
+ * Obtains the contents of a named artifact stored alongside this project in the bootstrap
36
+ * bucket. This artifact is created with 'putArtifact';
37
+ *
38
+ * @param {string} name
39
+ * @returns {object} backend
40
+ * @example
41
+ * local gcp = import "@c6fc/spellcraft-gcp-terraform";
42
+ *
43
+ * gcp.getArtifact("myArtifact");
44
+ *
45
+ * // Returns:
46
+ * <contents of your artifact>
47
+ */
48
+ getArtifact(name):: std.native("@c6fc/spellcraft-gcp-terraform:getArtifact")(name),
49
+
50
+ /**
51
+ * Attempts to discover the bucket created through bootstrap(), returning the
52
+ * bucket name if present.
53
+ *
54
+ * @returns {string} bucketArn
55
+ * @example
56
+ * local gcp = import "@c6fc/spellcraft-gcp-terraform";
57
+ *
58
+ * gcp.getBootstrapBucket();
59
+ *
60
+ * // Returns:
61
+ * spellcraft-terraform-<project-id>
62
+ */
63
+ getBootstrapBucket():: std.native("@c6fc/spellcraft-gcp-terraform:getBootstrapBucket")(),
64
+
65
+ /**
66
+ * Read the Terraform state for an adjacent SpellCraft project in the same GCP account
67
+ *
68
+ * @param {string} project
69
+ * @returns {object} state
70
+ * @example
71
+ * local gcp = import "@c6fc/spellcraft-gcp-terraform";
72
+ *
73
+ * gcp.getRemoteState("mySecondProject");
74
+ *
75
+ * // Returns:
76
+ * { full remote state object }
77
+ */
78
+ getRemoteState(project):: std.native("@c6fc/spellcraft-gcp-terraform:getRemoteState")(project),
79
+
80
+ /**
81
+ * Stores the JSON-encoded balue of 'contents' as a file in the GCS backend bucket using
82
+ * the project prefix.
83
+ *
84
+ * @param {string} name
85
+ * @param {*} contents
86
+ * @returns {boolean} true
87
+ * @example
88
+ * local gcp = import "@c6fc/spellcraft-gcp-terraform";
89
+ *
90
+ * gcp.putArtifact("myArtifact", { someData: someValue });
91
+ *
92
+ * // Returns:
93
+ * true
94
+ */
95
+ putArtifact(name, content):: std.native("@c6fc/spellcraft-gcp-terraform:putArtifact")(name, content),
96
+
97
+ /**
98
+ * Stores the JSON-encoded value of 'contents' as a file in the GCS backend bucket using
99
+ * the project prefix. If 'filter' is provided, only region names that string match
100
+ * will be included.
101
+ *
102
+ * @param {string} default
103
+ * @param {string} filter
104
+ * @example
105
+ * local gcp = import "@c6fc/spellcraft-gcp-terraform";
106
+ *
107
+ * gcp.providerAliases("us-west2");
108
+ *
109
+ * // Returns:
110
+ * [{ google: {
111
+ * region: "us-west2"
112
+ * }}, { google: {
113
+ * region: "us-east1",
114
+ alias: "us-east1"
115
+ * }}, ...]
116
+ */
117
+ providerAliases(default, filter=""):: [{
118
+ google: {
119
+ alias: region,
120
+ region: region
121
+ }
122
+ } for region in std.filterMap(
123
+ function(x) std.length(filter) < 1 || std.length(std.findSubstr(filter, x.name)) > 0,
124
+ function(x) x.name,
125
+ std.native("@c6fc/spellcraft-gcp-auth:api")('compute.v1.regions.list', '{"project":"%s"}' % std.native("@c6fc/spellcraft-gcp-auth:getProjectId")()).items
126
+ )] + [{
127
+ google: {
128
+ region: default
129
+ }
130
+ }]
131
+
132
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@c6fc/spellcraft-gcp-terraform",
3
+ "version": "1.0.1",
4
+ "main": "module.js",
5
+ "scripts": {
6
+ "cli": "utils/cli-test.js",
7
+ "test": "node utils/test.js"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/c6fc/spellcraft-gcp-terraform.git"
12
+ },
13
+ "spellcraft": true,
14
+ "keywords": [
15
+ "spellcraft",
16
+ "aws",
17
+ "terraform"
18
+ ],
19
+ "author": "Brad Woodward (brad@bradwoodward.io)",
20
+ "license": "MIT",
21
+ "bugs": {
22
+ "url": "https://github.com/c6fc/spellcraft-gcp-terraform/issues"
23
+ },
24
+ "homepage": "https://github.com/c6fc/spellcraft-gcp-terraform#readme",
25
+ "description": "A plugin to empower @c6fc/spellcraft with GCP and Terraform",
26
+ "dependencies": {
27
+ "@c6fc/spellcraft-gcp-auth": "^1.0.1",
28
+ "@c6fc/spellcraft-terraform": "^1.1.3"
29
+ },
30
+ "devDependencies": {
31
+ "@c6fc/spellcraft": "~0.1.0",
32
+ "yargs": "^18.0.0"
33
+ },
34
+ "peerDependencies": {
35
+ "@c6fc/spellcraft": "~0.1.0"
36
+ }
37
+ }
package/test.jsonnet ADDED
@@ -0,0 +1,34 @@
1
+ /*
2
+ This file should manifest all the exposed features of your module
3
+ so users can see examples of how they are used, and the output they
4
+ generate.
5
+ */
6
+
7
+ local gcp = import "module.libsonnet";
8
+
9
+ {
10
+ "bootstrap.tf.json": gcp.bootstrap("spellcraft-gcp-terraform-module-test"),
11
+ "test.tf.json": {
12
+ output: {
13
+ putArtifact: {
14
+ value: gcp.putArtifact("putArtifactTest", "mytest2")
15
+ },
16
+ getBootstrapBucket: {
17
+ value: gcp.getBootstrapBucket()
18
+ },
19
+
20
+ /* These can only be used after the project is created and the contents populated.
21
+ getArtifact: {
22
+ value: gcp.getArtifact("putArtifactTest")
23
+ },
24
+ getRemoteState: {
25
+ value: std.parseJson(gcp.getRemoteState("spellcraft-gcp-terraform-module-test"))
26
+ }
27
+ */
28
+
29
+ }
30
+ },
31
+ 'providers.tf.json': {
32
+ provider: gcp.providerAliases("us-west2", "us-")
33
+ }
34
+ }
@@ -0,0 +1,78 @@
1
+ #! /usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const yargs = require('yargs');
8
+ const { hideBin } = require('yargs/helpers');
9
+ const { SpellFrame } = require('@c6fc/spellcraft');
10
+
11
+ const spellframe = new SpellFrame();
12
+
13
+ // 1. Resolve local package info to simulate a real plugin load
14
+ const packageDir = path.resolve(__dirname, '..'); // Assuming script is in /utils
15
+ const packageJsonPath = path.join(packageDir, 'package.json');
16
+
17
+ if (!fs.existsSync(packageJsonPath)) {
18
+ throw new Error(`[!] Could not find package.json at ${packageJsonPath}`);
19
+ }
20
+
21
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
22
+ const jsEntry = path.join(packageDir, pkg.main || 'index.js');
23
+
24
+ console.log(`[*] Loading local plugin context: ${pkg.name}`);
25
+
26
+ // 2. Load the plugin into SpellFrame
27
+ // This registers native functions (namespaced) and populates cliExtensions
28
+ // automatically via the _spellcraft_metadata export in module.js
29
+ spellframe.loadPlugin(pkg.name, jsEntry);
30
+
31
+ (async () => {
32
+
33
+ let cli = yargs(hideBin(process.argv))
34
+ .usage("Syntax: $0 <command> [options]")
35
+ .scriptName("spellcraft")
36
+
37
+ .command("generate <filename>", "Generates files from a configuration", (yargsInstance) => {
38
+ return yargsInstance.positional('filename', {
39
+ describe: 'Jsonnet configuration file to consume',
40
+ type: 'string',
41
+ demandOption: true,
42
+ });
43
+ },
44
+ async (argv) => {
45
+ try {
46
+ await spellframe.init();
47
+ console.log(`[+] Rendering configuration from: ${argv.filename}`);
48
+ await spellframe.render(argv.filename);
49
+ await spellframe.write();
50
+ console.log("[+] Generation complete.");
51
+ } catch (error) {
52
+ console.error(`[!] Error during generation: ${error.message}`);
53
+ process.exit(1);
54
+ }
55
+ });
56
+
57
+ // 3. Register CLI extensions found in the loaded plugin
58
+ // These were populated by loadPlugin() reading _spellcraft_metadata
59
+ if (spellframe.cliExtensions && spellframe.cliExtensions.length > 0) {
60
+ spellframe.cliExtensions.forEach((extensionFn) => {
61
+ if (typeof extensionFn === 'function') {
62
+ extensionFn(cli, spellframe);
63
+ }
64
+ });
65
+ }
66
+
67
+ cli
68
+ .demandCommand(1, 'You need to specify a command.')
69
+ .recommendCommands()
70
+ .strict()
71
+ .showHelpOnFail(true)
72
+ .help("help")
73
+ .alias('h', 'help')
74
+ .version()
75
+ .alias('v', 'version')
76
+ .epilogue('For more information, consult the SpellCraft documentation.')
77
+ .argv;
78
+ })();
package/utils/test.js ADDED
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ This is a pre-written test-case wrapper
5
+ for local module development. Populate
6
+ ../test.jsonnet with tests of your
7
+ module capabilities.
8
+
9
+ Run with `npm run test`
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { SpellFrame } = require('@c6fc/spellcraft');
15
+
16
+ const spellframe = new SpellFrame();
17
+
18
+ (async () => {
19
+ try {
20
+ // 1. Read the local package.json
21
+ const packageJsonPath = path.resolve('./package.json');
22
+ if (!fs.existsSync(packageJsonPath)) {
23
+ throw new Error("Could not find package.json in current directory.");
24
+ }
25
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
26
+
27
+ // 2. Identify the entry point
28
+ const jsEntry = path.resolve(pkg.main || 'index.js');
29
+
30
+ console.log(`[*] Manually loading plugin: ${pkg.name}`);
31
+
32
+ // 3. Register the plugin using the current package name.
33
+ // This ensures the native functions are registered as "@scope/pkg:func",
34
+ // matching the std.native() calls in your libsonnet.
35
+ spellframe.loadPlugin(pkg.name, jsEntry);
36
+
37
+ // 4. Initialize
38
+ await spellframe.init();
39
+
40
+ // 5. Render
41
+ // Note: Ensure your test.jsonnet imports "./module.libsonnet" directly
42
+ console.log("[*] Rendering test.jsonnet...");
43
+ await spellframe.render("test.jsonnet");
44
+
45
+ console.log(JSON.stringify(spellframe.lastRender, null, 4));
46
+
47
+ } catch (error) {
48
+ console.error(`[!] Test failed: ${error.message}`);
49
+ if (error.stack) console.error(error.stack);
50
+ process.exit(1);
51
+ }
52
+ })();