@c6fc/spellcraft 0.0.6 → 0.0.8
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/README.md +180 -8
- package/bin/spellcraft.js +12 -4
- package/package.json +1 -1
- package/src/index.js +44 -25
package/README.md
CHANGED
@@ -1,17 +1,189 @@
|
|
1
|
-
# SpellCraft
|
1
|
+
# ✨ SpellCraft ✨
|
2
2
|
|
3
|
-
|
3
|
+
**The Sorcerer's Toolkit for Unified Configuration Management.**
|
4
4
|
|
5
|
-
|
5
|
+
SpellCraft is a powerful framework for generating and managing configurations across a diverse toolchain. It was born from the challenge of orchestrating tools like Terraform, Packer, Kubernetes, and Ansible, each with its own fragmented and isolated configuration format (YAML, JSON, HCL, etc.).
|
6
6
|
|
7
|
-
|
7
|
+
SpellCraft provides a single, unified source of truth, allowing you to generate tightly-integrated, context-aware configurations for any tool, all from one place.
|
8
|
+
|
9
|
+
[](https://www.npmjs.com/package/@c6fc/spellcraft)
|
10
|
+
[](https://github.com/your-repo/spellcraft/blob/main/LICENSE)
|
11
|
+
|
12
|
+
---
|
13
|
+
|
14
|
+
## The SpellCraft Philosophy
|
15
|
+
|
16
|
+
SpellCraft is built on three core principles to provide a superior Infrastructure-as-Code experience:
|
17
|
+
|
18
|
+
1. **Declarative Power (Jsonnet):** Configurations are written in [Jsonnet](https://jsonnet.org/), a superset of JSON. This gives you the power of variables, functions, conditionals, loops, and inheritance, eliminating the endless copy-pasting and structural limitations of plain YAML or JSON. Define a component once, and reuse it everywhere.
|
19
|
+
|
20
|
+
2. **Seamless Extensibility (Node.js):** Sometimes, declarative logic isn't enough. SpellCraft allows you to "escape" the confines of Jsonnet by writing custom logic in Node.js. Need to fetch a secret from a vault, call a third-party API, or perform complex data manipulation? Simply write a JavaScript function and expose it directly to your Jsonnet code as a `std.native()` function.
|
21
|
+
|
22
|
+
3. **Robust Modularity (NPM):** SpellCraft's module system is built on the battle-tested foundation of NPM. This means you can version, share, and manage your infrastructure modules just like any other software dependency. Leverage public or private NPM registries to build a reusable, maintainable, and collaborative infrastructure codebase.
|
23
|
+
|
24
|
+
## Quick Start
|
25
|
+
|
26
|
+
Get up and running with SpellCraft in minutes.
|
27
|
+
|
28
|
+
### 1. Installation
|
29
|
+
|
30
|
+
Install the SpellCraft CLI and core library into your project.
|
31
|
+
|
32
|
+
```sh
|
33
|
+
npm install --save @c6fc/spellcraft
|
34
|
+
```
|
35
|
+
|
36
|
+
### 2. Import a Module
|
37
|
+
|
38
|
+
Modules are the building blocks of SpellCraft. Let's import a module for interacting with AWS. The `importModule` command will install the package from NPM and link it into your project.
|
39
|
+
|
40
|
+
```sh
|
41
|
+
npx spellcraft importModule @c6fc/spellcraft-aws-auth
|
42
|
+
|
43
|
+
# Expected Output:
|
44
|
+
# [*] Attempting to install @c6fc/spellcraft-aws-auth...
|
45
|
+
# [+] Successfully installed @c6fc/spellcraft-aws-auth.
|
46
|
+
# [+] Linked @c6fc/spellcraft-aws-auth as SpellCraft module 'awsauth'
|
47
|
+
```
|
48
|
+
This makes the `@c6fc/spellcraft-aws-auth` package available in your Jsonnet files under the name `awsauth`.
|
49
|
+
|
50
|
+
### 3. Create Your First Spell
|
51
|
+
|
52
|
+
A "Spell" is a `.jsonnet` file that defines the files you want to create. The top-level keys of the output object become filenames.
|
53
|
+
|
54
|
+
Create a file named `manifest.jsonnet`:
|
55
|
+
```jsonnet
|
56
|
+
// manifest.jsonnet
|
57
|
+
local modules = import 'modules';
|
58
|
+
|
59
|
+
{
|
60
|
+
// The 'awsauth' module provides a native function `getCallerIdentity()`.
|
61
|
+
// We call it here and direct its output to a file named 'aws-identity.json'.
|
62
|
+
'aws-identity.json': modules.awsauth.getCallerIdentity(),
|
63
|
+
|
64
|
+
// We can also create YAML files. SpellCraft has built-in handlers for common types.
|
65
|
+
'config.yaml': {
|
66
|
+
apiVersion: 'v1',
|
67
|
+
kind: 'ConfigMap',
|
68
|
+
metadata: {
|
69
|
+
name: 'my-app-config',
|
70
|
+
},
|
71
|
+
data: {
|
72
|
+
region: std.native('envvar')('AWS_REGION') || 'us-east-1',
|
73
|
+
// The result of the native function is just data. We can reuse it!
|
74
|
+
callerArn: modules.awsauth.getCallerIdentity().Arn,
|
75
|
+
},
|
76
|
+
},
|
77
|
+
}
|
78
|
+
```
|
79
|
+
|
80
|
+
### 4. Generate the Artifacts
|
81
|
+
|
82
|
+
Use the `generate` command to render your `.jsonnet` file into the `render/` directory.
|
8
83
|
|
9
84
|
```sh
|
10
|
-
|
11
|
-
|
85
|
+
npx spellcraft generate manifest.jsonnet
|
86
|
+
|
87
|
+
# Expected Output:
|
88
|
+
# [+] Linked @c6fc/spellcraft-aws-auth as awsauth.libsonnet
|
89
|
+
# ...
|
90
|
+
# [+] Registered native functions [getCallerIdentity, ...] to modules.awsauth
|
91
|
+
# [+] Evaluating Jsonnet file manifest.jsonnet
|
92
|
+
# [+] Writing files to: render
|
93
|
+
# -> aws-identity.json
|
94
|
+
# -> config.yaml
|
95
|
+
# [+] Generation complete.
|
96
|
+
```
|
97
|
+
|
98
|
+
Check your `render/` directory. You will find two files created from your single source of truth!
|
99
|
+
|
100
|
+
```
|
101
|
+
.
|
102
|
+
├── manifest.jsonnet
|
103
|
+
├── node_modules/
|
104
|
+
├── package.json
|
105
|
+
└── render/
|
106
|
+
├── aws-identity.json
|
107
|
+
└── config.yaml
|
108
|
+
```
|
109
|
+
|
110
|
+
## The SpellCraft CLI
|
111
|
+
|
112
|
+
The `spellcraft` CLI is your primary interface for managing modules and generating files.
|
113
|
+
|
114
|
+
### Core Commands
|
115
|
+
|
116
|
+
- `generate <filename>`: Renders a `.jsonnet` file and writes the output to the `render/` directory.
|
117
|
+
- `importModule <npmPackage> [name]`: Installs an NPM package and links it as a SpellCraft module. If `[name]` is omitted, it uses the default name defined by the module.
|
118
|
+
|
119
|
+
### Extensible CLI
|
120
|
+
|
121
|
+
Modules can extend the SpellCraft CLI with their own custom commands. For example, after importing `@c6fc/spellcraft-aws-auth`, you gain new AWS-related commands.
|
122
|
+
|
123
|
+
Run `npx spellcraft --help` to see all available commands, including those added by modules.
|
124
|
+
|
125
|
+
```sh
|
126
|
+
$ npx spellcraft --help
|
127
|
+
|
128
|
+
# ... output showing core commands and module-added commands ...
|
129
|
+
Commands:
|
130
|
+
spellcraft generate <filename> Generates files from a configuration
|
131
|
+
spellcraft importModule <npmPackage> [name] Configures the current project to use a SpellCraft module
|
132
|
+
spellcraft aws-identity Display the AWS IAM identity of the SpellCraft context
|
133
|
+
spellcraft aws-exportcredentials Export the current credentials as environment variables
|
12
134
|
```
|
13
135
|
|
14
|
-
|
136
|
+
## Programmatic Usage (API)
|
137
|
+
|
138
|
+
For more advanced workflows, such as integration into larger automation scripts, you can use the `SpellFrame` class directly in your Node.js code.
|
139
|
+
|
140
|
+
The typical flow is:
|
141
|
+
1. Instantiate `SpellFrame`.
|
142
|
+
2. Load necessary modules.
|
143
|
+
3. (Optional) Run module initializers with `init()`.
|
144
|
+
4. Render the Jsonnet file with `render()`.
|
145
|
+
5. Write the resulting object to disk with `write()`.
|
146
|
+
|
147
|
+
```javascript
|
148
|
+
// my-automation-script.js
|
149
|
+
const { SpellFrame } = require('@c6fc/spellcraft');
|
150
|
+
const path = require('path');
|
151
|
+
|
152
|
+
// 1. Instantiate the SpellFrame
|
153
|
+
// Options allow you to customize output paths, cleaning behavior, etc.
|
154
|
+
const frame = new SpellFrame({
|
155
|
+
renderPath: "dist", // Output to 'dist/' instead of 'render/'
|
156
|
+
cleanBeforeRender: true,
|
157
|
+
});
|
158
|
+
|
159
|
+
(async () => {
|
160
|
+
try {
|
161
|
+
// 2. Load modules programmatically (this assumes they are in package.json)
|
162
|
+
// This loads modules listed in 'spellcraft_modules/packages.json'
|
163
|
+
// and from the local 'spellcraft_modules/' directory.
|
164
|
+
// frame.loadModuleByName('my-module-key', 'my-npm-package');
|
165
|
+
|
166
|
+
// 3. Initialize modules (if any modules registered an init function)
|
167
|
+
await frame.init();
|
168
|
+
|
169
|
+
// 4. Render the master Jsonnet file
|
170
|
+
const manifest = await frame.render(path.resolve('./manifest.jsonnet'));
|
171
|
+
|
172
|
+
// The result is available in memory
|
173
|
+
console.log('Rendered Manifest:', JSON.stringify(manifest, null, 2));
|
174
|
+
|
175
|
+
// 5. Write the manifest object to the filesystem
|
176
|
+
frame.write(manifest);
|
177
|
+
|
178
|
+
console.log('Successfully wrote files to the dist/ directory!');
|
179
|
+
|
180
|
+
} catch (error) {
|
181
|
+
console.error('An error occurred during the SpellCraft process:', error);
|
182
|
+
process.exit(1);
|
183
|
+
}
|
184
|
+
})();
|
185
|
+
```
|
15
186
|
|
187
|
+
## Creating Your Own Spells (Modules)
|
16
188
|
|
17
|
-
|
189
|
+
When you're ready to start writing your own modules and unleashing the true power of SpellCraft, check out **[create-spellcraft-module](https://www.npmjs.com/package/@c6fc/spellcraft)**
|
package/bin/spellcraft.js
CHANGED
@@ -78,18 +78,26 @@ const spellframe = new SpellFrame();
|
|
78
78
|
describe: 'Jsonnet configuration file to consume',
|
79
79
|
type: 'string',
|
80
80
|
demandOption: true,
|
81
|
+
}).option('skip-module-cleanup', {
|
82
|
+
alias: 's',
|
83
|
+
type: 'boolean',
|
84
|
+
description: 'Leave temporary modules intact after rendering'
|
81
85
|
});
|
82
86
|
},
|
83
87
|
async (argv) => { // No JSDoc for internal handler
|
84
|
-
try {
|
88
|
+
// try {
|
89
|
+
if (argv['s']) {
|
90
|
+
sfInstance.cleanModulesAfterRender = false;
|
91
|
+
}
|
92
|
+
|
85
93
|
await sfInstance.init();
|
86
94
|
await sfInstance.render(argv.filename);
|
87
95
|
await sfInstance.write();
|
88
96
|
console.log("[+] Generation complete.");
|
89
|
-
} catch (error) {
|
97
|
+
/*} catch (error) {
|
90
98
|
console.error(`[!] Error during generation: ${error.message.red}`);
|
91
99
|
process.exit(1);
|
92
|
-
}
|
100
|
+
}*/
|
93
101
|
})
|
94
102
|
|
95
103
|
.command("importModule <npmPackage> [name]", "Configures the current project to use a SpellCraft plugin as an import", (yargsInstance) => {
|
@@ -107,7 +115,7 @@ const spellframe = new SpellFrame();
|
|
107
115
|
},
|
108
116
|
async (argv) => {
|
109
117
|
await sfInstance.importSpellCraftModuleFromNpm(argv.npmPackage, argv.name);
|
110
|
-
console.log(`[+] Module '${argv.npmPackage.green}' ${argv.name ? `(aliased as ${argv.name.green})` : ''}
|
118
|
+
console.log(`[+] Module '${argv.npmPackage.green}' ${argv.name ? `(aliased as ${argv.name.green}) ` : ''}linked successfully.`);
|
111
119
|
});
|
112
120
|
|
113
121
|
// No JSDoc for CLI extensions loop if considered internal detail
|
package/package.json
CHANGED
package/src/index.js
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
// ./src/index.js
|
2
1
|
'use strict';
|
3
2
|
|
4
3
|
const fs = require("fs");
|
@@ -23,9 +22,10 @@ exports.SpellFrame = class SpellFrame {
|
|
23
22
|
|
24
23
|
constructor(options = {}) {
|
25
24
|
const defaults = {
|
26
|
-
renderPath: "
|
27
|
-
|
25
|
+
renderPath: "render",
|
26
|
+
spellcraftModuleRelativePath: ".spellcraft_linked_modules",
|
28
27
|
cleanBeforeRender: true,
|
28
|
+
cleanModulesAfterRender: true,
|
29
29
|
useDefaultFileHandlers: true
|
30
30
|
};
|
31
31
|
|
@@ -42,26 +42,31 @@ exports.SpellFrame = class SpellFrame {
|
|
42
42
|
this.lastRender = null;
|
43
43
|
this.activePath = null;
|
44
44
|
this.loadedModules = [];
|
45
|
-
this.modulePath = path.resolve(path.join(process.cwd(), this.modulePath));
|
46
45
|
this.magicContent = {}; // { modulefile: [...snippets] }
|
47
46
|
this.registeredFunctions = {}; // { modulefile: [...functionNames] }
|
48
47
|
|
49
48
|
this.renderPath = path.resolve(this.currentPackagePath, this.renderPath);
|
50
|
-
this.modulePath = path.resolve(this.currentPackagePath, this.
|
49
|
+
this.modulePath = path.resolve(this.currentPackagePath, this.spellcraftModuleRelativePath);
|
51
50
|
|
52
|
-
this.jsonnet = new Jsonnet()
|
53
|
-
.addJpath(path.join(__dirname, '../lib')) // For core SpellCraft libsonnet files
|
54
|
-
.addJpath(this.modulePath); // For dynamically generated module imports
|
51
|
+
this.jsonnet = new Jsonnet();
|
55
52
|
|
56
|
-
//
|
57
|
-
|
58
|
-
|
53
|
+
this.addJpath(path.join(__dirname, '../lib')) // For core SpellCraft libsonnet files
|
54
|
+
.addJpath(path.join(this.modulePath)) // For dynamically generated module imports
|
55
|
+
.addNativeFunction("envvar", (name) => process.env[name] || false, "name")
|
56
|
+
.addNativeFunction("path", () => this.activePath || process.cwd()) // Use activePath if available
|
57
|
+
.cleanModulePath();
|
59
58
|
|
59
|
+
this.loadedModules = this.loadModulesFromPackageList();
|
60
|
+
this.loadModulesFromModuleDirectory();
|
61
|
+
|
62
|
+
return this;
|
63
|
+
}
|
64
|
+
|
65
|
+
cleanModulePath() {
|
60
66
|
if (!fs.existsSync(this.modulePath)) {
|
61
67
|
fs.mkdirSync(this.modulePath, { recursive: true });
|
62
68
|
}
|
63
69
|
|
64
|
-
// Clean up the modules on init
|
65
70
|
try {
|
66
71
|
fs.readdirSync(this.modulePath)
|
67
72
|
.map(e => path.join(this.modulePath, e))
|
@@ -71,9 +76,6 @@ exports.SpellFrame = class SpellFrame {
|
|
71
76
|
throw new Error(`[!] Could not create/clean up temporary module folder ${path.dirname(this.modulePath).green}: ${e.message.red}`);
|
72
77
|
}
|
73
78
|
|
74
|
-
this.loadedModules = this.loadModulesFromPackageList();
|
75
|
-
this.loadModulesFromModuleDirectory();
|
76
|
-
|
77
79
|
return this;
|
78
80
|
}
|
79
81
|
|
@@ -126,7 +128,8 @@ exports.SpellFrame = class SpellFrame {
|
|
126
128
|
}
|
127
129
|
|
128
130
|
addJpath(jpath) {
|
129
|
-
|
131
|
+
// console.log(`[*] Adding Jpath ${jpath}`);
|
132
|
+
this.jsonnet.addJpath(jpath);
|
130
133
|
return this;
|
131
134
|
}
|
132
135
|
|
@@ -266,7 +269,7 @@ exports.SpellFrame = class SpellFrame {
|
|
266
269
|
|
267
270
|
jsModuleFiles.forEach(file => {
|
268
271
|
this.loadFunctionsFromFile(file, as);
|
269
|
-
console.log(`[+] Loaded [${this.registeredFunctions.join(', ').cyan}] from ${path.basename(file).green} into
|
272
|
+
console.log(`[+] Loaded [${this.registeredFunctions[as].join(', ').cyan}] from ${path.basename(file).green} into modules.${as.green}`);
|
270
273
|
});
|
271
274
|
|
272
275
|
return this;
|
@@ -275,12 +278,10 @@ exports.SpellFrame = class SpellFrame {
|
|
275
278
|
loadModulesFromModuleDirectory() {
|
276
279
|
const spellcraftModulesPath = path.join(this.currentPackagePath, 'spellcraft_modules');
|
277
280
|
if (!fs.existsSync(spellcraftModulesPath)) {
|
278
|
-
return
|
281
|
+
return this;
|
279
282
|
}
|
280
283
|
|
281
|
-
|
282
|
-
|
283
|
-
if (!!spellcraftConfig?.spellcraft_module_default_name) {
|
284
|
+
if (!!this.currentPackage?.config?.spellcraft_module_default_name) {
|
284
285
|
console.log("[-] This package is a SpellCraft module. Skipping directory-based module import.");
|
285
286
|
return { registeredFunctions: [], magicContent: [] };
|
286
287
|
}
|
@@ -308,11 +309,10 @@ exports.SpellFrame = class SpellFrame {
|
|
308
309
|
console.log(`[+] Successfully installed ${npmPackage.blue}.`);
|
309
310
|
}
|
310
311
|
|
311
|
-
const importModuleConfig = this.getModulePackage(npmPackage).config;
|
312
|
+
const importModuleConfig = this.getModulePackage(`${npmPackage}/package.json`).config;
|
312
313
|
const currentPackageConfig = this.currentPackage.config;
|
313
314
|
|
314
315
|
if (!name && !!!importModuleConfig?.spellcraft_module_default_name) {
|
315
|
-
// console.log("Package config:", moduleJson);
|
316
316
|
throw new Error(`[!] No import name specified for ${npmPackage.blue}, and it has no 'spellcraft_module_default_name' in its package.json config.`.red);
|
317
317
|
}
|
318
318
|
|
@@ -360,13 +360,32 @@ exports.SpellFrame = class SpellFrame {
|
|
360
360
|
|
361
361
|
this.activePath = path.dirname(absoluteFilePath); // Set active path for relative 'path()' calls
|
362
362
|
|
363
|
+
this.magicContent.modules.push(this.loadedModules.flatMap(e => {
|
364
|
+
return `\t${e}:: import '${e}.libsonnet'`;
|
365
|
+
}));
|
366
|
+
|
367
|
+
if (this.registeredFunctions.modules.length > 0) {
|
368
|
+
fs.writeFileSync(path.join(this.modulePath, `modules`), `{\n${this.magicContent.modules.join(",\n")}\n}`, 'utf-8');
|
369
|
+
console.log(`[+] Registered native functions [${this.registeredFunctions.modules.join(', ').cyan}] to modules.${'modules'.green}`);
|
370
|
+
}
|
371
|
+
|
372
|
+
delete this.magicContent.modules;
|
373
|
+
|
363
374
|
Object.keys(this.magicContent).forEach(e => {
|
364
|
-
fs.
|
375
|
+
fs.appendFileSync(path.join(this.modulePath, `${e}.libsonnet`), ` + {\n${this.magicContent[e].join(",\n")}\n}`, 'utf-8');
|
365
376
|
console.log(`[+] Registered native functions [${this.registeredFunctions[e].join(', ').cyan}] to modules.${e.green} `);
|
366
377
|
});
|
367
|
-
|
378
|
+
|
368
379
|
console.log(`[+] Evaluating Jsonnet file ${path.basename(absoluteFilePath).green}`);
|
369
380
|
this.lastRender = JSON.parse(await this.jsonnet.evaluateFile(absoluteFilePath));
|
381
|
+
|
382
|
+
if (this.cleanModulesAfterRender) {
|
383
|
+
this.cleanModulePath();
|
384
|
+
|
385
|
+
fs.rmdirSync(this.modulePath);
|
386
|
+
} else {
|
387
|
+
console.log(`[*] Leaving ${this.spellcraftModuleRelativePath} in place.`.magenta);
|
388
|
+
}
|
370
389
|
|
371
390
|
return this.lastRender;
|
372
391
|
}
|